2
# Author:: Christopher Brown (<cb@opscode.com>)
3
# Author:: Christopher Walters (<cw@opscode.com>)
4
# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
5
# License:: Apache License, Version 2.0
7
# Licensed under the Apache License, Version 2.0 (the "License");
8
# you may not use this file except in compliance with the License.
9
# You may obtain a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS,
15
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
# See the License for the specific language governing permissions and
17
# limitations under the License.
22
require 'mixlib/authentication'
23
require 'mixlib/authentication/http_authentication_request'
24
require 'mixlib/authentication/signedheaderauth'
28
SignatureResponse = Struct.new(:name)
30
class SignatureVerification
33
def_delegator :@auth_request, :http_method
35
def_delegator :@auth_request, :path
37
def_delegator :auth_request, :signing_description
39
def_delegator :@auth_request, :user_id
41
def_delegator :@auth_request, :timestamp
43
def_delegator :@auth_request, :host
45
def_delegator :@auth_request, :request_signature
47
def_delegator :@auth_request, :content_hash
49
def_delegator :@auth_request, :request
51
include Mixlib::Authentication::SignedHeaderAuth
53
attr_reader :auth_request
55
def initialize(request=nil)
56
@auth_request = HTTPAuthenticationRequest.new(request) if request
58
@valid_signature, @valid_timestamp, @valid_content_hash = false, false, false
64
def authenticate_user_request(request, user_lookup, time_skew=(15*60))
65
@auth_request = HTTPAuthenticationRequest.new(request)
66
authenticate_request(user_lookup, time_skew)
68
# Takes the request, boils down the pieces we are interested in,
69
# looks up the user, generates a signature, and compares to
70
# the signature in the request
73
# X-Ops-Sign: algorithm=sha256;version=1.0;
74
# X-Ops-UserId: <user_id>
77
# X-Ops-Authorization-#{line_number}
78
def authenticate_request(user_secret, time_skew=(15*60))
79
Mixlib::Authentication::Log.debug "Initializing header auth : #{request.inspect}"
81
@user_secret = user_secret
82
@allowed_time_skew = time_skew # in seconds
87
#BUGBUG Not doing anything with the signing description yet [cb]
88
parse_signing_description
94
rescue StandardError=>se
95
raise AuthenticationError,"Failed to authenticate user request. Check your client key and clock: #{se.message}", se.backtrace
99
SignatureResponse.new(user_id)
113
def valid_content_hash?
118
valid_signature? && valid_timestamp? && valid_content_hash?
121
# The authorization header is a Base64-encoded version of an RSA signature.
122
# The client sent it on multiple header lines, starting at index 1 -
123
# X-Ops-Authorization-1, X-Ops-Authorization-2, etc. Pull them out and
126
@headers ||= request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo }
131
def assert_required_headers_present
132
MANDATORY_HEADERS.each do |header|
133
unless headers.key?(header)
134
raise MissingAuthenticationHeader, "required authentication header #{header.to_s.upcase} missing"
140
candidate_block = canonicalize_request
141
request_decrypted_block = @user_secret.public_decrypt(Base64.decode64(request_signature))
142
@valid_signature = (request_decrypted_block == candidate_block)
144
# Keep the debug messages lined up so it's easy to scan them
145
Mixlib::Authentication::Log.debug("Verifying request signature:")
146
Mixlib::Authentication::Log.debug(" Expected Block is: '#{candidate_block}'")
147
Mixlib::Authentication::Log.debug("Decrypted block is: '#{request_decrypted_block}'")
148
Mixlib::Authentication::Log.debug("Signatures match? : '#{@valid_signature}'")
152
Mixlib::Authentication::Log.debug("Failed to verify request signature: #{e.class.name}: #{e.message}")
153
@valid_signature = false
157
@valid_timestamp = timestamp_within_bounds?(Time.parse(timestamp), Time.now)
160
def verify_content_hash
161
@valid_content_hash = (content_hash == hashed_body)
163
# Keep the debug messages lined up so it's easy to scan them
164
Mixlib::Authentication::Log.debug("Expected content hash is: '#{hashed_body}'")
165
Mixlib::Authentication::Log.debug(" Request Content Hash is: '#{content_hash}'")
166
Mixlib::Authentication::Log.debug(" Hashes match?: #{@valid_content_hash}")
172
# The request signature is based on any file attached, if any. Otherwise
173
# it's based on the body of the request.
176
# TODO: tim: 2009-112-28: It'd be nice to remove this special case, and
177
# always hash the entire request body. In the file case it would just be
178
# expanded multipart text - the entire body of the POST.
180
# Pull out any file that was attached to this request, using multipart
182
# Depending on the server we're running in, multipart form uploads are
183
# handed to us differently.
184
# - In Passenger (Cookbooks Community Site), the File is handed to us
185
# directly in the params hash. The name is whatever the client used,
186
# its value is therefore a File or Tempfile.
187
# e.g. request['file_param'] = File
189
# - In Merb (Chef server), the File is wrapped. The original parameter
190
# name used for the file is used, but its value is a Hash. Within
191
# the hash is a name/value pair named 'file' which actually
192
# contains the Tempfile instance.
193
# e.g. request['file_param'] = { :file => Tempfile }
194
file_param = request.params.values.find { |value| value.respond_to?(:read) }
196
# No file_param; we're running in Merb, or it's just not there..
198
hash_param = request.params.values.find { |value| value.respond_to?(:has_key?) } # Hash responds to :has_key? .
200
file_param = hash_param.values.find { |value| value.respond_to?(:read) } # File/Tempfile responds to :read.
204
# Any file that's included in the request is hashed if it's there. Otherwise,
207
Mixlib::Authentication::Log.debug "Digesting file_param: '#{file_param.inspect}'"
208
@hashed_body = digester.hash_file(file_param)
210
body = request.raw_post
211
Mixlib::Authentication::Log.debug "Digesting body: '#{body}'"
212
@hashed_body = digester.hash_string(body)
218
# Compare the request timestamp with boundary time
222
# time1<Time>:: minuend
223
# time2<Time>:: subtrahend
225
def timestamp_within_bounds?(time1, time2)
226
time_diff = (time2-time1).abs
227
is_allowed = (time_diff < @allowed_time_skew)
228
Mixlib::Authentication::Log.debug "Request time difference: #{time_diff}, within #{@allowed_time_skew} seconds? : #{!!is_allowed}"