summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJay Mundrawala <jdmundrawala@gmail.com>2015-11-16 11:58:12 -0800
committerJay Mundrawala <jdmundrawala@gmail.com>2015-11-30 09:03:01 -0800
commit6ebe6bbdabd0c4da634b26deb00cafb7fa636bcc (patch)
tree8cdf9088ef12b811489d6eb5461fa6d59c14be2f
parent929231fb583816c76b5fc1c46a8436753848d981 (diff)
downloadmixlib-authentication-6ebe6bbdabd0c4da634b26deb00cafb7fa636bcc.tar.gz
Add signing algorithm v1.3
-rw-r--r--lib/mixlib/authentication/signedheaderauth.rb81
-rw-r--r--spec/mixlib/authentication/mixlib_authentication_spec.rb115
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)