diff options
-rw-r--r-- | lib/mixlib/authentication/digester.rb | 44 | ||||
-rw-r--r-- | lib/mixlib/authentication/signatureverification.rb | 4 | ||||
-rw-r--r-- | lib/mixlib/authentication/signedheaderauth.rb | 31 | ||||
-rw-r--r-- | spec/mixlib/authentication/mixlib_authentication_spec.rb | 34 |
4 files changed, 59 insertions, 54 deletions
diff --git a/lib/mixlib/authentication/digester.rb b/lib/mixlib/authentication/digester.rb index f3a3597..7dc6dd7 100644 --- a/lib/mixlib/authentication/digester.rb +++ b/lib/mixlib/authentication/digester.rb @@ -21,36 +21,28 @@ require 'mixlib/authentication' module Mixlib module Authentication class Digester - attr_reader :hashed_body - def initialize() - @hashed_body = nil - end + class << self + + def hash_file(f) + digester = Digest::SHA1.new + buf = "" + while f.read(16384, buf) + digester.update buf + end + ::Base64.encode64(digester.digest).chomp + end - # Compare the request timestamp with boundary time - # - # - # ====Parameters - # time1<Time>:: minuend - # time2<Time>:: subtrahend - # - def hash_file(f) - digester = Digest::SHA1.new - buf = "" - while f.read(16384, buf) - digester.update buf - end - @hashed_body ||= ::Base64.encode64(digester.digest).chomp + # Digests a string, base64's and chomps the end + # + # ====Parameters + # + def hash_string(str) + ::Base64.encode64(Digest::SHA1.digest(str)).chomp + end + end - # Digests the body, base64's and chomps the end - # - # - # ====Parameters - # - def hash_body(body) - @hashed_body ||= ::Base64.encode64(Digest::SHA1.digest(body)).chomp - end end end end diff --git a/lib/mixlib/authentication/signatureverification.rb b/lib/mixlib/authentication/signatureverification.rb index ee837ee..d067a2e 100644 --- a/lib/mixlib/authentication/signatureverification.rb +++ b/lib/mixlib/authentication/signatureverification.rb @@ -44,7 +44,7 @@ module Mixlib Mixlib::Authentication::Log.debug "Initializing header auth : #{request.inspect}" headers ||= request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo } - digester = Mixlib::Authentication::Digester.new + digester = Mixlib::Authentication::Digester begin @allowed_time_skew = time_skew # in seconds @@ -95,7 +95,7 @@ module Mixlib else body = request.raw_post Mixlib::Authentication::Log.debug "Digesting body: '#{body}'" - @hashed_body = digester.hash_body(body) + @hashed_body = digester.hash_string(body) end Mixlib::Authentication::Log.debug "Authenticating user : #{user_id}, User secret is : #{@user_secret}, Request signature is :\n#{@request_signature}, Hashed Body is : #{@hashed_body}" diff --git a/lib/mixlib/authentication/signedheaderauth.rb b/lib/mixlib/authentication/signedheaderauth.rb index 993d927..7e9aaa4 100644 --- a/lib/mixlib/authentication/signedheaderauth.rb +++ b/lib/mixlib/authentication/signedheaderauth.rb @@ -1,7 +1,7 @@ # # Author:: Christopher Brown (<cb@opscode.com>) # Author:: Christopher Walters (<cw@opscode.com>) -# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,28 +37,21 @@ module Mixlib OpenStruct.new(args).extend SignedHeaderAuth end end - + # Build the canonicalized request based on the method, other headers, etc. # compute the signature from the request, using the looked-up user secret # ====Parameters # private_key<OpenSSL::PKey::RSA>:: user's RSA private key. def sign(private_key) - digester = Mixlib::Authentication::Digester.new - @hashed_body = if self.file - digester.hash_file(self.file) - else - digester.hash_body(self.body) - end - + # Our multiline hash for authorization will be encoded in multiple header + # lines - X-Ops-Authorization-1, ... (starts at 1, not 0!) header_hash = { "X-Ops-Sign" => SIGNING_DESCRIPTION, "X-Ops-Userid" => user_id, "X-Ops-Timestamp" => canonical_time, - "X-Ops-Content-Hash" =>@hashed_body, + "X-Ops-Content-Hash" => hashed_body, } - # Our multiline hash for authorization will be encoded in multiple header - # lines - X-Ops-Authorization-1, ... (starts at 1, not 0!) string_to_sign = canonicalize_request signature = Base64.encode64(private_key.private_encrypt(string_to_sign)).chomp signature_lines = signature.split(/\n/) @@ -77,7 +70,7 @@ module Mixlib # ====Parameters # def canonical_time - Time.parse(timestamp).utc.iso8601 + Time.parse(timestamp).utc.iso8601 end # Build the canonicalized path, which collapses multiple slashes (/) and @@ -90,6 +83,10 @@ module Mixlib p.length > 1 ? p.chomp('/') : p end + def hashed_body + @hashed_body ||= self.file ? digester.hash_file(self.file) : digester.hash_string(self.body) + end + # Takes HTTP request method & headers and creates a canonical form # to create the signature # @@ -97,7 +94,7 @@ module Mixlib # # def canonicalize_request - "Method:#{http_method.to_s.upcase}\nPath:#{canonical_path}\nX-Ops-Content-Hash:#{@hashed_body}\nX-Ops-Timestamp:#{canonical_time}\nX-Ops-UserId:#{user_id}" + "Method:#{http_method.to_s.upcase}\nHashed Path:#{digester.hash_string(canonical_path)}\nX-Ops-Content-Hash:#{hashed_body}\nX-Ops-Timestamp:#{canonical_time}\nX-Ops-UserId:#{user_id}" end # Parses signature version information, algorithm used, etc. @@ -113,7 +110,11 @@ module Mixlib Mixlib::Authentication::Log.debug "Parsed signing description: #{parts.inspect}" end - private :canonical_time, :canonical_path, :canonicalize_request, :parse_signing_description + def digester + Mixlib::Authentication::Digester + end + + private :canonical_time, :canonical_path, :parse_signing_description, :digester end end diff --git a/spec/mixlib/authentication/mixlib_authentication_spec.rb b/spec/mixlib/authentication/mixlib_authentication_spec.rb index 1b94d9b..f371e54 100644 --- a/spec/mixlib/authentication/mixlib_authentication_spec.rb +++ b/spec/mixlib/authentication/mixlib_authentication_spec.rb @@ -1,6 +1,7 @@ # # Author:: Tim Hinderliter (<tim@opscode.com>) -# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +17,8 @@ # limitations under the License. # -$:.push File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "lib")) # lib in mixlib-authentication -$:.push File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "mixlib-log", "lib")) # mixlib-log/log +$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "lib")) # lib in mixlib-authentication +$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "mixlib-log", "lib")) # mixlib-log/log require 'rubygems' @@ -66,7 +67,7 @@ end #Mixlib::Authentication::Log.level :debug describe "Mixlib::Authentication::SignedHeaderAuth" do - it "should sign headers" do + it "should generate the correct string to sign and signature" do # fix the timestamp, private key and body so we get the same answer back # every time. args = { @@ -81,6 +82,15 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do private_key = OpenSSL::PKey::RSA.new(PRIVATE_KEY) signing_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(args) + + expected_string_to_sign = <<EOS +Method:POST +Hashed Path:#{HASHED_CANONICAL_PATH} +X-Ops-Content-Hash:#{HASHED_BODY} +X-Ops-Timestamp:#{TIMESTAMP_ISO8601} +X-Ops-UserId:#{USER_ID} +EOS + signing_obj.canonicalize_request.should == expected_string_to_sign.chomp # If you need to regenerate the constants in this test spec, print out # the results of res.inspect and copy them as appropriate into the @@ -102,7 +112,7 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do private_key = OpenSSL::PKey::RSA.new(PRIVATE_KEY) signing_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(args) - + lambda { signing_obj.sign(private_key) }.should_not raise_error end end @@ -163,21 +173,23 @@ end USER_ID = "spec-user" BODY = "Spec Body" +HASHED_BODY = "DFteJZPVv6WKdQmMqZUQUumUyRs=" # Base64.encode64(Digest::SHA1.digest("Spec Body")).chomp TIMESTAMP_ISO8601 = "2009-01-01T12:00:00Z" TIMESTAMP_OBJ = Time.parse("Thu Jan 01 12:00:00 -0000 2009") PATH = "/organizations/clownco" +HASHED_CANONICAL_PATH = "YtBWDn1blGGuFIuKksdwXzHU9oE=" # Base64.encode64(Digest::SHA1.digest("/organizations/clownco")).chomp REQUESTING_ACTOR_ID = "c0f8a68c52bffa1020222a56b23cccfa" # Content hash is ???TODO X_OPS_CONTENT_HASH = "DFteJZPVv6WKdQmMqZUQUumUyRs=" X_OPS_AUTHORIZATION_LINES = [ - "fXOBxpvfVpDFexP+Dl2k3RO/m4RLws8ii6+PsAY4pgwk6IxIFew7lL4ErWKA", - "80HVVx3lKALfGqx+XLZiX1ZwbHhMhfS2oUAbxssx+g3nlxSmN/c+cg2/hprj", - "0m+xwkpONuwVj06IKGe9WtMyDE0GNu7VFx/HWf4BCXMU3DZTsxD40Rm5Aqml", - "r8paPDo5ozoJRing2NOfj9uPpRCnjmUamwSVo/tgTOmWQxnp7YeI82YLfl6Z", - "i5qXzLMPzq/rA5Bt0dAZQxkCCvyVVXyn0pX0xHPgoeFcAJ/atc7hnIVxMzzM", - "xZn9HIwo9wRYbWdwvRLVdbz/jmlGbBcDbInFtVOjpA==" + "jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4", + "NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", + "3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O", + "IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy", + "9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", + "utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w==" ] # We expect Mixlib::Authentication::SignedHeaderAuth#sign to return this |