@@ -130,8 +130,7 @@ class Signer
130130 # every other AWS service as of late 2016.
131131 #
132132 # @option options [Symbol] :signing_algorithm (:sigv4) The
133- # algorithm to use for signing. :sigv4a is only supported when
134- # `aws-crt` is available.
133+ # algorithm to use for signing.
135134 #
136135 # @option options [Boolean] :omit_session_token (false)
137136 # (Supported only when `aws-crt` is available) If `true`,
@@ -155,12 +154,6 @@ def initialize(options = {})
155154 @normalize_path = options . fetch ( :normalize_path , true )
156155 @omit_session_token = options . fetch ( :omit_session_token , false )
157156
158- if @signing_algorithm == :sigv4a && !Signer . use_crt?
159- raise ArgumentError , 'You are attempting to sign a' \
160- ' request with sigv4a which requires the `aws-crt` gem.' \
161- ' Please install the gem or add it to your gemfile.'
162- end
163-
164157 if @signing_algorithm == 'sigv4-s3express' . to_sym &&
165158 Signer . use_crt? && Aws ::Crt ::GEM_VERSION <= '0.1.9'
166159 raise ArgumentError ,
@@ -249,6 +242,7 @@ def sign_request(request)
249242
250243 http_method = extract_http_method ( request )
251244 url = extract_url ( request )
245+ Signer . normalize_path ( url ) if @normalize_path
252246 headers = downcase_headers ( request [ :headers ] )
253247
254248 datetime = headers [ 'x-amz-date' ]
@@ -261,7 +255,7 @@ def sign_request(request)
261255 sigv4_headers = { }
262256 sigv4_headers [ 'host' ] = headers [ 'host' ] || host ( url )
263257 sigv4_headers [ 'x-amz-date' ] = datetime
264- if creds . session_token
258+ if creds . session_token && ! @omit_session_token
265259 if @signing_algorithm == 'sigv4-s3express' . to_sym
266260 sigv4_headers [ 'x-amz-s3session-token' ] = creds . session_token
267261 else
@@ -271,26 +265,45 @@ def sign_request(request)
271265
272266 sigv4_headers [ 'x-amz-content-sha256' ] ||= content_sha256 if @apply_checksum_header
273267
268+ if @signing_algorithm == :sigv4a && @region && !@region . empty?
269+ sigv4_headers [ 'x-amz-region-set' ] = @region
270+ end
274271 headers = headers . merge ( sigv4_headers ) # merge so we do not modify given headers hash
275272
273+ algorithm = sts_algorithm
274+
276275 # compute signature parts
277276 creq = canonical_request ( http_method , url , headers , content_sha256 )
278- sts = string_to_sign ( datetime , creq )
279- sig = signature ( creds . secret_access_key , date , sts )
277+ sts = string_to_sign ( datetime , creq , algorithm )
278+
279+ sig =
280+ if @signing_algorithm == :sigv4a
281+ asymmetric_signature ( creds , sts )
282+ else
283+ signature ( creds . secret_access_key , date , sts )
284+ end
285+
286+ algorithm = sts_algorithm
280287
281288 # apply signature
282289 sigv4_headers [ 'authorization' ] = [
283- "AWS4-HMAC-SHA256 Credential=#{ credential ( creds , date ) } " ,
290+ "#{ algorithm } Credential=#{ credential ( creds , date ) } " ,
284291 "SignedHeaders=#{ signed_headers ( headers ) } " ,
285292 "Signature=#{ sig } " ,
286293 ] . join ( ', ' )
287294
295+ # skip signing the session token, but include it in the headers
296+ if creds . session_token && @omit_session_token
297+ sigv4_headers [ 'x-amz-security-token' ] = creds . session_token
298+ end
299+
288300 # Returning the signature components.
289301 Signature . new (
290302 headers : sigv4_headers ,
291303 string_to_sign : sts ,
292304 canonical_request : creq ,
293- content_sha256 : content_sha256
305+ content_sha256 : content_sha256 ,
306+ signature : sig
294307 )
295308 end
296309
@@ -424,6 +437,7 @@ def presign_url(options)
424437
425438 http_method = extract_http_method ( options )
426439 url = extract_url ( options )
440+ Signer . normalize_path ( url ) if @normalize_path
427441
428442 headers = downcase_headers ( options [ :headers ] )
429443 headers [ 'host' ] ||= host ( url )
@@ -436,8 +450,10 @@ def presign_url(options)
436450 content_sha256 ||= options [ :body_digest ]
437451 content_sha256 ||= sha256_hexdigest ( options [ :body ] || '' )
438452
453+ algorithm = sts_algorithm
454+
439455 params = { }
440- params [ 'X-Amz-Algorithm' ] = 'AWS4-HMAC-SHA256'
456+ params [ 'X-Amz-Algorithm' ] = algorithm
441457 params [ 'X-Amz-Credential' ] = credential ( creds , date )
442458 params [ 'X-Amz-Date' ] = datetime
443459 params [ 'X-Amz-Expires' ] = presigned_url_expiration ( options , expiration , Time . strptime ( datetime , "%Y%m%dT%H%M%S%Z" ) ) . to_s
@@ -450,6 +466,10 @@ def presign_url(options)
450466 end
451467 params [ 'X-Amz-SignedHeaders' ] = signed_headers ( headers )
452468
469+ if @signing_algorithm == :sigv4a && @region
470+ params [ 'X-Amz-Region-Set' ] = @region
471+ end
472+
453473 params = params . map do |key , value |
454474 "#{ uri_escape ( key ) } =#{ uri_escape ( value ) } "
455475 end . join ( '&' )
@@ -461,13 +481,23 @@ def presign_url(options)
461481 end
462482
463483 creq = canonical_request ( http_method , url , headers , content_sha256 )
464- sts = string_to_sign ( datetime , creq )
465- url . query += '&X-Amz-Signature=' + signature ( creds . secret_access_key , date , sts )
484+ sts = string_to_sign ( datetime , creq , algorithm )
485+ signature =
486+ if @signing_algorithm == :sigv4a
487+ asymmetric_signature ( creds , sts )
488+ else
489+ signature ( creds . secret_access_key , date , sts )
490+ end
491+ url . query += '&X-Amz-Signature=' + signature
466492 url
467493 end
468494
469495 private
470496
497+ def sts_algorithm
498+ @signing_algorithm == :sigv4a ? 'AWS4-ECDSA-P256-SHA256' : 'AWS4-HMAC-SHA256'
499+ end
500+
471501 def canonical_request ( http_method , url , headers , content_sha256 )
472502 [
473503 http_method ,
@@ -479,9 +509,9 @@ def canonical_request(http_method, url, headers, content_sha256)
479509 ] . join ( "\n " )
480510 end
481511
482- def string_to_sign ( datetime , canonical_request )
512+ def string_to_sign ( datetime , canonical_request , algorithm )
483513 [
484- 'AWS4-HMAC-SHA256' ,
514+ algorithm ,
485515 datetime ,
486516 credential_scope ( datetime [ 0 , 8 ] ) ,
487517 sha256_hexdigest ( canonical_request ) ,
@@ -514,10 +544,10 @@ def event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
514544 def credential_scope ( date )
515545 [
516546 date ,
517- @region ,
547+ ( @region unless @signing_algorithm == :sigv4a ) ,
518548 @service ,
519- 'aws4_request' ,
520- ] . join ( '/' )
549+ 'aws4_request'
550+ ] . compact . join ( '/' )
521551 end
522552
523553 def credential ( credentials , date )
@@ -532,6 +562,16 @@ def signature(secret_access_key, date, string_to_sign)
532562 hexhmac ( k_credentials , string_to_sign )
533563 end
534564
565+ def asymmetric_signature ( creds , string_to_sign )
566+ ec , _ = Aws ::Sigv4 ::AsymmetricCredentials . derive_asymmetric_key (
567+ creds . access_key_id , creds . secret_access_key
568+ )
569+ sts_digest = OpenSSL ::Digest ::SHA256 . digest ( string_to_sign )
570+ s = ec . dsa_sign_asn1 ( sts_digest )
571+
572+ Digest . hexencode ( s )
573+ end
574+
535575 # Comparing to original signature v4 algorithm,
536576 # returned signature is a binary string instread of
537577 # hex-encoded string. (Since ':chunk-signature' requires
@@ -899,6 +939,18 @@ def uri_escape(string)
899939 end
900940 end
901941
942+ # @api private
943+ def normalize_path ( uri )
944+ normalized_path = Pathname . new ( uri . path ) . cleanpath . to_s
945+ # Pathname is probably not correct to use. Empty paths will
946+ # resolve to "." and should be disregarded
947+ normalized_path = '' if normalized_path == '.'
948+ # Ensure trailing slashes are correctly preserved
949+ if uri . path . end_with? ( '/' ) && !normalized_path . end_with? ( '/' )
950+ normalized_path << '/'
951+ end
952+ uri . path = normalized_path
953+ end
902954 end
903955 end
904956 end
0 commit comments