~ubuntu-branches/ubuntu/saucy/ruby-mixlib-authentication/saucy

« back to all changes in this revision

Viewing changes to lib/mixlib/authentication/signatureverification.rb

  • Committer: Package Import Robot
  • Author(s): Paul van Tilburg
  • Date: 2012-05-17 13:56:37 UTC
  • Revision ID: package-import@ubuntu.com-20120517135637-fo8zzak5h5z7e2ma
Tags: upstream-1.1.4
ImportĀ upstreamĀ versionĀ 1.1.4

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
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
 
6
#
 
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
 
10
 
11
#     http://www.apache.org/licenses/LICENSE-2.0
 
12
 
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.
 
18
#
 
19
 
 
20
require 'net/http'
 
21
require 'forwardable'
 
22
require 'mixlib/authentication'
 
23
require 'mixlib/authentication/http_authentication_request'
 
24
require 'mixlib/authentication/signedheaderauth'
 
25
 
 
26
module Mixlib
 
27
  module Authentication
 
28
    SignatureResponse = Struct.new(:name)
 
29
 
 
30
    class SignatureVerification
 
31
      extend Forwardable
 
32
 
 
33
      def_delegator :@auth_request, :http_method
 
34
 
 
35
      def_delegator :@auth_request, :path
 
36
 
 
37
      def_delegator :auth_request, :signing_description
 
38
 
 
39
      def_delegator :@auth_request, :user_id
 
40
 
 
41
      def_delegator :@auth_request, :timestamp
 
42
 
 
43
      def_delegator :@auth_request, :host
 
44
 
 
45
      def_delegator :@auth_request, :request_signature
 
46
 
 
47
      def_delegator :@auth_request, :content_hash
 
48
 
 
49
      def_delegator :@auth_request, :request
 
50
 
 
51
      include Mixlib::Authentication::SignedHeaderAuth
 
52
 
 
53
      attr_reader :auth_request
 
54
 
 
55
      def initialize(request=nil)
 
56
        @auth_request = HTTPAuthenticationRequest.new(request) if request
 
57
 
 
58
        @valid_signature, @valid_timestamp, @valid_content_hash = false, false, false
 
59
 
 
60
        @hashed_body = nil
 
61
      end
 
62
 
 
63
 
 
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)
 
67
      end
 
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
 
71
      # ====Headers
 
72
      #
 
73
      # X-Ops-Sign: algorithm=sha256;version=1.0;
 
74
      # X-Ops-UserId: <user_id>
 
75
      # X-Ops-Timestamp:
 
76
      # X-Ops-Content-Hash: 
 
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}"
 
80
 
 
81
        @user_secret       = user_secret
 
82
        @allowed_time_skew = time_skew # in seconds
 
83
 
 
84
        begin
 
85
          @auth_request
 
86
          
 
87
          #BUGBUG Not doing anything with the signing description yet [cb]          
 
88
          parse_signing_description
 
89
 
 
90
          verify_signature
 
91
          verify_timestamp
 
92
          verify_content_hash
 
93
 
 
94
        rescue StandardError=>se
 
95
          raise AuthenticationError,"Failed to authenticate user request. Check your client key and clock: #{se.message}", se.backtrace
 
96
        end
 
97
 
 
98
        if valid_request?
 
99
          SignatureResponse.new(user_id)
 
100
        else
 
101
          nil
 
102
        end
 
103
      end
 
104
 
 
105
      def valid_signature?
 
106
        @valid_signature
 
107
      end
 
108
 
 
109
      def valid_timestamp?
 
110
        @valid_timestamp
 
111
      end
 
112
 
 
113
      def valid_content_hash?
 
114
        @valid_content_hash
 
115
      end
 
116
 
 
117
      def valid_request?
 
118
        valid_signature? && valid_timestamp? && valid_content_hash?
 
119
      end
 
120
 
 
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
 
124
      # concatenate.
 
125
      def headers
 
126
        @headers ||= request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo }
 
127
      end
 
128
 
 
129
      private
 
130
 
 
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"
 
135
          end
 
136
        end
 
137
      end
 
138
 
 
139
      def verify_signature
 
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)
 
143
 
 
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}'")
 
149
 
 
150
        @valid_signature
 
151
      rescue => e
 
152
        Mixlib::Authentication::Log.debug("Failed to verify request signature: #{e.class.name}: #{e.message}")
 
153
        @valid_signature = false
 
154
      end
 
155
 
 
156
      def verify_timestamp
 
157
        @valid_timestamp = timestamp_within_bounds?(Time.parse(timestamp), Time.now)
 
158
      end
 
159
 
 
160
      def verify_content_hash
 
161
        @valid_content_hash = (content_hash == hashed_body)
 
162
 
 
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}")
 
167
 
 
168
        @valid_content_hash
 
169
      end
 
170
 
 
171
 
 
172
      # The request signature is based on any file attached, if any. Otherwise
 
173
      # it's based on the body of the request.
 
174
      def hashed_body
 
175
        unless @hashed_body
 
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.
 
179
          #
 
180
          # Pull out any file that was attached to this request, using multipart
 
181
          # form uploads.
 
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
 
188
          #   
 
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) }
 
195
 
 
196
          # No file_param; we're running in Merb, or it's just not there..
 
197
          if file_param.nil?
 
198
            hash_param = request.params.values.find { |value| value.respond_to?(:has_key?) }  # Hash responds to :has_key? .
 
199
            if !hash_param.nil?
 
200
              file_param = hash_param.values.find { |value| value.respond_to?(:read) } # File/Tempfile responds to :read.
 
201
            end
 
202
          end
 
203
 
 
204
          # Any file that's included in the request is hashed if it's there. Otherwise,
 
205
          # we hash the body.
 
206
          if file_param
 
207
            Mixlib::Authentication::Log.debug "Digesting file_param: '#{file_param.inspect}'"
 
208
            @hashed_body = digester.hash_file(file_param)
 
209
          else
 
210
            body = request.raw_post
 
211
            Mixlib::Authentication::Log.debug "Digesting body: '#{body}'"
 
212
            @hashed_body = digester.hash_string(body)
 
213
          end
 
214
        end
 
215
        @hashed_body
 
216
      end
 
217
 
 
218
      # Compare the request timestamp with boundary time
 
219
      # 
 
220
      # 
 
221
      # ====Parameters
 
222
      # time1<Time>:: minuend
 
223
      # time2<Time>:: subtrahend
 
224
      #
 
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}"
 
229
        is_allowed      
 
230
      end
 
231
    end
 
232
 
 
233
 
 
234
  end
 
235
end
 
236
 
 
237