diff options
author | Jay Mundrawala <jdmundrawala@gmail.com> | 2015-11-16 11:58:12 -0800 |
---|---|---|
committer | Jay Mundrawala <jdmundrawala@gmail.com> | 2015-11-30 09:03:01 -0800 |
commit | 6ebe6bbdabd0c4da634b26deb00cafb7fa636bcc (patch) | |
tree | 8cdf9088ef12b811489d6eb5461fa6d59c14be2f | |
parent | 929231fb583816c76b5fc1c46a8436753848d981 (diff) | |
download | mixlib-authentication-6ebe6bbdabd0c4da634b26deb00cafb7fa636bcc.tar.gz |
Add signing algorithm v1.3
-rw-r--r-- | lib/mixlib/authentication/signedheaderauth.rb | 81 | ||||
-rw-r--r-- | spec/mixlib/authentication/mixlib_authentication_spec.rb | 115 |
2 files changed, 174 insertions, 22 deletions
diff --git a/lib/mixlib/authentication/signedheaderauth.rb b/lib/mixlib/authentication/signedheaderauth.rb index 98923f8..7d4a775 100644 --- a/lib/mixlib/authentication/signedheaderauth.rb +++ b/lib/mixlib/authentication/signedheaderauth.rb @@ -19,7 +19,7 @@ require 'time' require 'base64' -require 'digest/sha1' +require 'openssl/digest' require 'mixlib/authentication' require 'mixlib/authentication/digester' @@ -36,6 +36,7 @@ module Mixlib ALGORITHMS_FOR_VERSION = { '1.0' => ['sha1'], '1.1' => ['sha1'], + '1.3' => ['sha256', 'sha1'], }.freeze() DEFAULT_SIGN_ALGORITHM = 'sha1'.freeze @@ -78,7 +79,9 @@ module Mixlib args[:timestamp], args[:user_id], args[:file], - args[:proto_version]) + args[:proto_version], + args[:signing_algorithm] + ) end def algorithm @@ -104,19 +107,29 @@ module Mixlib "X-Ops-Content-Hash" => hashed_body(digest), } - string_to_sign = canonicalize_request(sign_algorithm, sign_version) - signature = Base64.encode64(private_key.private_encrypt(string_to_sign)).chomp + signature = Base64.encode64(do_sign(private_key, digest, sign_algorithm, sign_version)).chomp signature_lines = signature.split(/\n/) signature_lines.each_index do |idx| key = "X-Ops-Authorization-#{idx + 1}" header_hash[key] = signature_lines[idx] end - Mixlib::Authentication::Log.debug "String to sign: '#{string_to_sign}'\nHeader hash: #{header_hash.inspect}" + Mixlib::Authentication::Log.debug "Header hash: #{header_hash.inspect}" header_hash end + def do_sign(private_key, digest, sign_algorithm, sign_version) + string_to_sign = canonicalize_request(sign_algorithm, sign_version) + Mixlib::Authentication::Log.debug "String to sign: '#{string_to_sign}'" + case sign_version + when '1.3' + private_key.sign(digest.new, string_to_sign) + else + private_key.private_encrypt(string_to_sign) + end + end + def validate_sign_version_digest!(sign_version, sign_algorithm) if ALGORITHMS_FOR_VERSION[sign_version].nil? raise AuthenticationError, @@ -130,9 +143,9 @@ module Mixlib case sign_algorithm when 'sha1' - Digest::SHA1 + OpenSSL::Digest::SHA1 when 'sha256' - Digest::SHA256 + OpenSSL::Digest::SHA256 else # This case should never happen raise "Unknown algorithm #{sign_algorithm}" @@ -157,7 +170,7 @@ module Mixlib p.length > 1 ? p.chomp('/') : p end - def hashed_body(digest=Digest::SHA1) + def hashed_body(digest=OpenSSL::Digest::SHA1) # This is weird. sign() is called with the digest type and signing # version. These are also expected to be properties of the object. # Hence, we're going to assume the one that is passed to sign is @@ -187,24 +200,33 @@ module Mixlib # # def canonicalize_request(sign_algorithm=algorithm, sign_version=proto_version) - digest = validate_sign_version_digest!(sign_version, sign_algorithm) canonical_x_ops_user_id = canonicalize_user_id(user_id, sign_version, digest) - [ - "Method:#{http_method.to_s.upcase}", - "Hashed Path:#{digester.hash_string(digest, canonical_path)}", - "X-Ops-Content-Hash:#{hashed_body(digest)}", - "X-Ops-Timestamp:#{canonical_time}", - "X-Ops-UserId:#{canonical_x_ops_user_id}" - ].join("\n") + case sign_version + when "1.3" + [ + "Method:#{http_method.to_s.upcase}", + "Hashed Path:#{digester.hash_string(digest, canonical_path)}", + "X-Ops-Content-Hash:#{hashed_body(digest)}", + "X-Ops-Sign:algorithm=#{sign_algorithm};version=#{sign_version}", + "X-Ops-Timestamp:#{canonical_time}", + "X-Ops-UserId:#{canonical_x_ops_user_id}" + ].join("\n") + else + [ + "Method:#{http_method.to_s.upcase}", + "Hashed Path:#{digester.hash_string(digest, canonical_path)}", + "X-Ops-Content-Hash:#{hashed_body(digest)}", + "X-Ops-Timestamp:#{canonical_time}", + "X-Ops-UserId:#{canonical_x_ops_user_id}" + ].join("\n") + end end - def canonicalize_user_id(user_id, proto_version, digest=Digest::SHA1) + def canonicalize_user_id(user_id, proto_version, digest=OpenSSL::Digest::SHA1) case proto_version - when "1.1" - digester.hash_string(Digest::SHA1, user_id) - when "1.0" - user_id + when "1.1", "1.3" + digester.hash_string(digest, user_id) else user_id end @@ -236,12 +258,27 @@ module Mixlib # A Struct-based value object that contains the necessary information to # generate a request signature. `SignedHeaderAuth.signing_object()` # provides a more convenient interface to the constructor. - class SigningObject < Struct.new(:http_method, :path, :body, :host, :timestamp, :user_id, :file, :proto_version) + class SigningObject < Struct.new(:http_method, :path, :body, :host, + :timestamp, :user_id, :file, :proto_version, + :signing_algorithm) include SignedHeaderAuth def proto_version (self[:proto_version] or DEFAULT_PROTO_VERSION).to_s end + + def algorithm + if self[:signing_algorithm] + self[:signing_algorithm] + else + case proto_version + when '1.3' + ALGORITHMS_FOR_VERSION[proto_version].first + else + DEFAULT_SIGN_ALGORITHM + end + end + end end end diff --git a/spec/mixlib/authentication/mixlib_authentication_spec.rb b/spec/mixlib/authentication/mixlib_authentication_spec.rb index 715e56c..1a18faa 100644 --- a/spec/mixlib/authentication/mixlib_authentication_spec.rb +++ b/spec/mixlib/authentication/mixlib_authentication_spec.rb @@ -90,6 +90,28 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do expect(V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_1) end + it "should generate the correct string to sign and signature for version 1.3 with SHA1" do + expect(V1_3_SHA1_SIGNING_OBJECT.proto_version).to eq("1.3") + expect(V1_3_SHA1_SIGNING_OBJECT.canonicalize_request).to eq(V1_3_SHA1_CANONICAL_REQUEST) + + # 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 + # the constants in this file. + expect(V1_3_SHA1_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_3_SHA1) + end + + it "should generate the correct string to sign and signature for version 1.3 with SHA256" do + expect(V1_3_SHA256_SIGNING_OBJECT.proto_version).to eq("1.3") + expect(V1_3_SHA256_SIGNING_OBJECT.algorithm).to eq("sha256") + expect(V1_3_SHA256_SIGNING_OBJECT.canonicalize_request).to eq(V1_3_SHA256_CANONICAL_REQUEST) + + # 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 + # the constants in this file. + expect(V1_3_SHA256_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_3_SHA256) + end + + it "should generate the correct string to sign and signature for non-default proto version when used as a mixin" do algorithm = 'sha1' version = '1.1' @@ -238,12 +260,15 @@ end USER_ID = "spec-user" DIGESTED_USER_ID = Base64.encode64(Digest::SHA1.new.digest(USER_ID)).chomp +DIGESTED_USER_ID_SHA256 = Base64.encode64(Digest::SHA256.new.digest(USER_ID)).chomp BODY = "Spec Body" HASHED_BODY = "DFteJZPVv6WKdQmMqZUQUumUyRs=" # Base64.encode64(Digest::SHA1.digest("Spec Body")).chomp +HASHED_BODY_SHA256 = "hDlKNZhIhgso3Fs0S0pZwJ0xyBWtR1RBaeHs1DrzOho=" 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 +HASHED_CANONICAL_PATH_SHA256 = "Z3EsTMw/UBNY9n+q+WBWTJmeVg8hQFbdFzVWRxW4dOA=" V1_0_ARGS = { :body => BODY, @@ -264,6 +289,28 @@ V1_1_ARGS = { :proto_version => 1.1 } +V1_3_ARGS_SHA1 = { + :body => BODY, + :user_id => USER_ID, + :http_method => :post, + :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. + :file => MockFile.new, + :path => PATH, + :proto_version => '1.3', + :signing_algorithm => 'sha1' +} + +V1_3_ARGS_SHA256 = { + :body => BODY, + :user_id => USER_ID, + :http_method => :post, + :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. + :file => MockFile.new, + :path => PATH, + :proto_version => '1.3' + # This defaults to sha256 +} + LONG_PATH_LONG_USER_ARGS = { :body => BODY, :user_id => "A" * 200, @@ -277,6 +324,8 @@ REQUESTING_ACTOR_ID = "c0f8a68c52bffa1020222a56b23cccfa" # Content hash is ???TODO X_OPS_CONTENT_HASH = "DFteJZPVv6WKdQmMqZUQUumUyRs=" +X_OPS_CONTENT_HASH_SHA256 = "hDlKNZhIhgso3Fs0S0pZwJ0xyBWtR1RBaeHs1DrzOho=" + X_OPS_AUTHORIZATION_LINES_V1_0 = [ "jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4", "NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", @@ -295,6 +344,23 @@ X_OPS_AUTHORIZATION_LINES = [ "FDlbAG7H8Dmvo+wBxmtNkszhzbBnEYtuwQqT8nM/8A==" ] +X_OPS_AUTHORIZATION_LINES_V1_3_SHA1 = [ + "wVDg3X99mxxQr1Ox/KJc+zy7b/mPX/M1+jsta5Qht43UhkNq3spRqup8vP26", + "TT/0pSSDnJ//wlYnxrEP+izgRGO3n4rwLQNM/ePB+dOXSiOwLDJOl7yChUde", + "qrxX6xsaIps6+Di/DRQ1jqLBO5KHkt8Ndc6KUeyV4Dbz/O4+8VIJw0j22Sne", + "6kWG648yVrS/ODeHfPGw2kLa1bQ1X7uEpNpOG2l1zzgm19wXZYllnjZphcll", + "lItQ/00hM7BgbSJWcqu8tShXlZUv6/ScClQZmkdN3mNojgmlt1fMv2LjejD1", + "8I8xfPcfBKelkz4bLHsB86pMvvE+g9tC+h2EvYU2Rw==" +] + +X_OPS_AUTHORIZATION_LINES_V1_3_SHA256 = [ + "Zo20R014Yt3IjMCsUVbOCoctwHOHaxsC3b6dPqmk3xIZo1zmOgVbsHzL72Bt", + "cRAeqm/gXHRNJlo4Fbh4jTJP3IBAm+mhga6aMZhRkrVUYfnZ1oEi8f/Z9WyY", + "uYPD8iygCEyFj2BshWsvo+lv3EvlmYlh5cKqjqaStLtGB118vfoTAf+XqK/y", + "tW4Ye6yn8KddnT/mLMqKJgy8PEbr+jVdLA7wDTZd64++IleNmgK72qneMEjk", + "4zuU5JWYfnT3MzyKR7sCsuEvxUNn/o4u5GEuuud2oP8dJUraNNUXpJYk9Us7", + "yZS3bUNsFPFVP3RAQadLX9gvC4eAZadTuly0wi0Edg==" +] # We expect Mixlib::Authentication::SignedHeaderAuth#sign to return this # if passed the BODY above, based on version @@ -324,6 +390,32 @@ EXPECTED_SIGN_RESULT_V1_1 = { "X-Ops-Timestamp"=>TIMESTAMP_ISO8601 } +EXPECTED_SIGN_RESULT_V1_3_SHA1 = { + "X-Ops-Content-Hash"=>X_OPS_CONTENT_HASH, + "X-Ops-Userid"=>USER_ID, + "X-Ops-Sign"=>"algorithm=sha1;version=1.3;", + "X-Ops-Authorization-1"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA1[0], + "X-Ops-Authorization-2"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA1[1], + "X-Ops-Authorization-3"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA1[2], + "X-Ops-Authorization-4"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA1[3], + "X-Ops-Authorization-5"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA1[4], + "X-Ops-Authorization-6"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA1[5], + "X-Ops-Timestamp"=>TIMESTAMP_ISO8601 +} + +EXPECTED_SIGN_RESULT_V1_3_SHA256 = { + "X-Ops-Content-Hash"=>X_OPS_CONTENT_HASH_SHA256, + "X-Ops-Userid"=>USER_ID, + "X-Ops-Sign"=>"algorithm=sha256;version=1.3;", + "X-Ops-Authorization-1"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[0], + "X-Ops-Authorization-2"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[1], + "X-Ops-Authorization-3"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[2], + "X-Ops-Authorization-4"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[3], + "X-Ops-Authorization-5"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[4], + "X-Ops-Authorization-6"=>X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[5], + "X-Ops-Timestamp"=>TIMESTAMP_ISO8601 +} + OTHER_HEADERS = { # An arbitrary sampling of non-HTTP_* headers are in here to # exercise that code path. @@ -478,6 +570,29 @@ X-Ops-UserId:#{DIGESTED_USER_ID} EOS V1_1_CANONICAL_REQUEST = V1_1_CANONICAL_REQUEST_DATA.chomp +V1_3_SHA1_CANONICAL_REQUEST_DATA = <<EOS +Method:POST +Hashed Path:#{HASHED_CANONICAL_PATH} +X-Ops-Content-Hash:#{HASHED_BODY} +X-Ops-Sign:algorithm=sha1;version=1.3 +X-Ops-Timestamp:#{TIMESTAMP_ISO8601} +X-Ops-UserId:#{DIGESTED_USER_ID} +EOS +V1_3_SHA1_CANONICAL_REQUEST = V1_3_SHA1_CANONICAL_REQUEST_DATA.chomp + +V1_3_SHA256_CANONICAL_REQUEST_DATA = <<EOS +Method:POST +Hashed Path:#{HASHED_CANONICAL_PATH_SHA256} +X-Ops-Content-Hash:#{HASHED_BODY_SHA256} +X-Ops-Sign:algorithm=sha256;version=1.3 +X-Ops-Timestamp:#{TIMESTAMP_ISO8601} +X-Ops-UserId:#{DIGESTED_USER_ID_SHA256} +EOS +V1_3_SHA256_CANONICAL_REQUEST = V1_3_SHA256_CANONICAL_REQUEST_DATA.chomp + + +V1_3_SHA256_SIGNING_OBJECT = Mixlib::Authentication::SignedHeaderAuth.signing_object(V1_3_ARGS_SHA256) +V1_3_SHA1_SIGNING_OBJECT = Mixlib::Authentication::SignedHeaderAuth.signing_object(V1_3_ARGS_SHA1) V1_1_SIGNING_OBJECT = Mixlib::Authentication::SignedHeaderAuth.signing_object(V1_1_ARGS) V1_0_SIGNING_OBJECT = Mixlib::Authentication::SignedHeaderAuth.signing_object(V1_0_ARGS) LONG_SIGNING_OBJECT = Mixlib::Authentication::SignedHeaderAuth.signing_object(LONG_PATH_LONG_USER_ARGS) |