aws-sigv4 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 482b4ffa8bd9e9e2d7dab0d61ab15553f0e8d05e1be2923b388157664a47a9fa
4
- data.tar.gz: c5caa84527ca213826f8c802195430caacb25750530609f1b8d7267810808574
3
+ metadata.gz: 25c3ddf60803af303d37af469c7250ceba22d6a23e62bbfb7a9a82ae3d01b8e8
4
+ data.tar.gz: 36537ef12949c9d0a632060485c3dcf340176a8241c3e65828e47991581e7c89
5
5
  SHA512:
6
- metadata.gz: c16c5df7f8c6ca10cf073c25984506ce938f26823f99d82813b5bde3fb283d23a3c480adec2945b98975946a7b32efe57a0058cd65bb383606c5ee5228711381
7
- data.tar.gz: b72ea1894eb1c419179325f8e715fb454ab02ae2d1ac243b90ed2f4032c0fd966ba157287a12009587908d0a6a2f62261c78e319a230196792b6ec4206a20718
6
+ metadata.gz: aa33926ae5a1804fee36cce9b7cadead40d9d5806154e62840be377c663d94dbac07ea537601f4fa47c1d4861dccb3bdf7801b2b1edf256a0a452a73fdf2c9de
7
+ data.tar.gz: 8e1e705a6dfef2edd5af640de60f01321f1a811f41f407e906f08881d83d197ba9011c4ed3d2a218f6f17f94fcd602e0a6759abcf7c5e5e27f5d66465c3f3f3c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 1.9.0 (2024-07-23)
5
+ ------------------
6
+
7
+ * Feature - Support `sigv4a` signing algorithm without `aws-crt`.
8
+
4
9
  1.8.0 (2023-11-28)
5
10
  ------------------
6
11
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.0
1
+ 1.9.0
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Sigv4
5
+ # To make it easier to support mixed mode, we have created an asymmetric
6
+ # key derivation mechanism. This module derives
7
+ # asymmetric keys from the current secret for use with
8
+ # Asymmetric signatures.
9
+ # @api private
10
+ module AsymmetricCredentials
11
+
12
+ N_MINUS_2 = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 - 2
13
+
14
+ # @param [String] :access_key_id
15
+ # @param [String] :secret_access_key
16
+ # @return [OpenSSL::PKey::EC, Hash]
17
+ def self.derive_asymmetric_key(access_key_id, secret_access_key)
18
+ check_openssl_support!
19
+ label = 'AWS4-ECDSA-P256-SHA256'
20
+ bit_len = 256
21
+ counter = 0x1
22
+ input_key = "AWS4A#{secret_access_key}"
23
+ d = 0 # d will end up being the private key
24
+ while true do
25
+
26
+ kdf_context = access_key_id.unpack('C*') + [counter].pack('C').unpack('C') #1 byte for counter
27
+ input = label.unpack('C*') + [0x00] + kdf_context + [bit_len].pack('L>').unpack('CCCC') # 4 bytes (change endianess)
28
+ k0 = OpenSSL::HMAC.digest("SHA256", input_key, ([0, 0, 0, 0x01] + input).pack('C*'))
29
+ c = be_bytes_to_num( k0.unpack('C*') )
30
+ if c <= N_MINUS_2
31
+ d = c + 1
32
+ break
33
+ elsif counter > 0xFF
34
+ raise 'Counter exceeded 1 byte - unable to get asym creds'
35
+ else
36
+ counter += 1
37
+ end
38
+ end
39
+
40
+ # compute the public key
41
+ group = OpenSSL::PKey::EC::Group.new('prime256v1')
42
+ public_key = group.generator.mul(d)
43
+
44
+ ec = generate_ec(public_key, d)
45
+
46
+ # pk_x and pk_y are not needed for signature, but useful in verification/testing
47
+ pk_b = public_key.to_octet_string(:uncompressed).unpack('C*') # 0x04 byte followed by 2 32-byte integers
48
+ pk_x = be_bytes_to_num(pk_b[1,32])
49
+ pk_y = be_bytes_to_num(pk_b[33,32])
50
+ [ec, {ec: ec, public_key: public_key, pk_x: pk_x, pk_y: pk_y, d: d}]
51
+ end
52
+
53
+ private
54
+
55
+ # @return [Number] The value of the bytes interpreted as a big-endian
56
+ # unsigned integer.
57
+ def self.be_bytes_to_num(bytes)
58
+ x = 0
59
+ bytes.each { |b| x = (x*256) + b }
60
+ x
61
+ end
62
+
63
+ # Prior to openssl3 we could directly set public and private key on EC
64
+ # However, openssl3 deprecated those methods and we must now construct
65
+ # a der with the keys and load the EC from it.
66
+ def self.generate_ec(public_key, d)
67
+ # format reversed from: OpenSSL::ASN1.decode_all(OpenSSL::PKey::EC.new.to_der)
68
+ asn1 = OpenSSL::ASN1::Sequence([
69
+ OpenSSL::ASN1::Integer(OpenSSL::BN.new(1)),
70
+ OpenSSL::ASN1::OctetString([d.to_s(16)].pack('H*')),
71
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::ObjectId("prime256v1")], 0, :CONTEXT_SPECIFIC),
72
+ OpenSSL::ASN1::ASN1Data.new(
73
+ [OpenSSL::ASN1::BitString(public_key.to_octet_string(:uncompressed))],
74
+ 1, :CONTEXT_SPECIFIC
75
+ )
76
+ ])
77
+ OpenSSL::PKey::EC.new(asn1.to_der)
78
+ end
79
+
80
+ def self.check_openssl_support!
81
+ return true unless defined?(JRUBY_VERSION)
82
+
83
+ # See: https://github.com/jruby/jruby-openssl/issues/306
84
+ # JRuby-openssl < 0.15 does not support OpenSSL::PKey::EC::Point#mul
85
+ return true if OpenSSL::PKey::EC::Point.instance_methods.include?(:mul)
86
+
87
+ raise 'Sigv4a Asymmetric Credential derivation requires jruby-openssl >= 0.15'
88
+ end
89
+ end
90
+ end
91
+ end
@@ -32,6 +32,9 @@ module Aws
32
32
  # @return [String] For debugging purposes.
33
33
  attr_accessor :content_sha256
34
34
 
35
+ # @return [String] For debugging purposes.
36
+ attr_accessor :signature
37
+
35
38
  # @return [Hash] Internal data for debugging purposes.
36
39
  attr_accessor :extra
37
40
  end
@@ -84,14 +84,16 @@ module Aws
84
84
 
85
85
  # @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
86
86
  # @param [String] :service The service signing name, e.g. 's3'.
87
- # @param [String] :region The region name, e.g. 'us-east-1'.
87
+ # @param [String] :region The region name, e.g. 'us-east-1'. When signing
88
+ # with sigv4a, this should be a comma separated list of regions.
88
89
  # @param [String] :access_key_id
89
90
  # @param [String] :secret_access_key
90
91
  # @param [String] :session_token (nil)
91
92
  #
92
93
  # @overload initialize(service:, region:, credentials:, **options)
93
94
  # @param [String] :service The service signing name, e.g. 's3'.
94
- # @param [String] :region The region name, e.g. 'us-east-1'.
95
+ # @param [String] :region The region name, e.g. 'us-east-1'. When signing
96
+ # with sigv4a, this should be a comma separated list of regions.
95
97
  # @param [Credentials] :credentials Any object that responds to the following
96
98
  # methods:
97
99
  #
@@ -102,7 +104,8 @@ module Aws
102
104
  #
103
105
  # @overload initialize(service:, region:, credentials_provider:, **options)
104
106
  # @param [String] :service The service signing name, e.g. 's3'.
105
- # @param [String] :region The region name, e.g. 'us-east-1'.
107
+ # @param [String] :region The region name, e.g. 'us-east-1'. When signing
108
+ # with sigv4a, this should be a comma separated list of regions.
106
109
  # @param [#credentials] :credentials_provider An object that responds
107
110
  # to `#credentials`, returning an object that responds to the following
108
111
  # methods:
@@ -127,8 +130,7 @@ module Aws
127
130
  # every other AWS service as of late 2016.
128
131
  #
129
132
  # @option options [Symbol] :signing_algorithm (:sigv4) The
130
- # algorithm to use for signing. :sigv4a is only supported when
131
- # `aws-crt` is available.
133
+ # algorithm to use for signing.
132
134
  #
133
135
  # @option options [Boolean] :omit_session_token (false)
134
136
  # (Supported only when `aws-crt` is available) If `true`,
@@ -136,8 +138,8 @@ module Aws
136
138
  # but is treated as "unsigned" and does not contribute
137
139
  # to the authorization signature.
138
140
  #
139
- # @option options [Boolean] :normalize_path (true) (Supported only when `aws-crt` is available)
140
- # When `true`, the uri paths will be normalized when building the canonical request
141
+ # @option options [Boolean] :normalize_path (true) When `true`, the
142
+ # uri paths will be normalized when building the canonical request.
141
143
  def initialize(options = {})
142
144
  @service = extract_service(options)
143
145
  @region = extract_region(options)
@@ -152,12 +154,6 @@ module Aws
152
154
  @normalize_path = options.fetch(:normalize_path, true)
153
155
  @omit_session_token = options.fetch(:omit_session_token, false)
154
156
 
155
- if @signing_algorithm == :sigv4a && !Signer.use_crt?
156
- raise ArgumentError, 'You are attempting to sign a' \
157
- ' request with sigv4a which requires the `aws-crt` gem.'\
158
- ' Please install the gem or add it to your gemfile.'
159
- end
160
-
161
157
  if @signing_algorithm == 'sigv4-s3express'.to_sym &&
162
158
  Signer.use_crt? && Aws::Crt::GEM_VERSION <= '0.1.9'
163
159
  raise ArgumentError,
@@ -246,6 +242,7 @@ module Aws
246
242
 
247
243
  http_method = extract_http_method(request)
248
244
  url = extract_url(request)
245
+ Signer.normalize_path(url) if @normalize_path
249
246
  headers = downcase_headers(request[:headers])
250
247
 
251
248
  datetime = headers['x-amz-date']
@@ -258,7 +255,7 @@ module Aws
258
255
  sigv4_headers = {}
259
256
  sigv4_headers['host'] = headers['host'] || host(url)
260
257
  sigv4_headers['x-amz-date'] = datetime
261
- if creds.session_token
258
+ if creds.session_token && !@omit_session_token
262
259
  if @signing_algorithm == 'sigv4-s3express'.to_sym
263
260
  sigv4_headers['x-amz-s3session-token'] = creds.session_token
264
261
  else
@@ -268,26 +265,45 @@ module Aws
268
265
 
269
266
  sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
270
267
 
268
+ if @signing_algorithm == :sigv4a && @region && !@region.empty?
269
+ sigv4_headers['x-amz-region-set'] = @region
270
+ end
271
271
  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
272
272
 
273
+ algorithm = sts_algorithm
274
+
273
275
  # compute signature parts
274
276
  creq = canonical_request(http_method, url, headers, content_sha256)
275
- sts = string_to_sign(datetime, creq)
276
- 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
277
287
 
278
288
  # apply signature
279
289
  sigv4_headers['authorization'] = [
280
- "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
290
+ "#{algorithm} Credential=#{credential(creds, date)}",
281
291
  "SignedHeaders=#{signed_headers(headers)}",
282
292
  "Signature=#{sig}",
283
293
  ].join(', ')
284
294
 
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
+
285
300
  # Returning the signature components.
286
301
  Signature.new(
287
302
  headers: sigv4_headers,
288
303
  string_to_sign: sts,
289
304
  canonical_request: creq,
290
- content_sha256: content_sha256
305
+ content_sha256: content_sha256,
306
+ signature: sig
291
307
  )
292
308
  end
293
309
 
@@ -421,6 +437,7 @@ module Aws
421
437
 
422
438
  http_method = extract_http_method(options)
423
439
  url = extract_url(options)
440
+ Signer.normalize_path(url) if @normalize_path
424
441
 
425
442
  headers = downcase_headers(options[:headers])
426
443
  headers['host'] ||= host(url)
@@ -433,8 +450,10 @@ module Aws
433
450
  content_sha256 ||= options[:body_digest]
434
451
  content_sha256 ||= sha256_hexdigest(options[:body] || '')
435
452
 
453
+ algorithm = sts_algorithm
454
+
436
455
  params = {}
437
- params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
456
+ params['X-Amz-Algorithm'] = algorithm
438
457
  params['X-Amz-Credential'] = credential(creds, date)
439
458
  params['X-Amz-Date'] = datetime
440
459
  params['X-Amz-Expires'] = presigned_url_expiration(options, expiration, Time.strptime(datetime, "%Y%m%dT%H%M%S%Z")).to_s
@@ -447,6 +466,10 @@ module Aws
447
466
  end
448
467
  params['X-Amz-SignedHeaders'] = signed_headers(headers)
449
468
 
469
+ if @signing_algorithm == :sigv4a && @region
470
+ params['X-Amz-Region-Set'] = @region
471
+ end
472
+
450
473
  params = params.map do |key, value|
451
474
  "#{uri_escape(key)}=#{uri_escape(value)}"
452
475
  end.join('&')
@@ -458,13 +481,23 @@ module Aws
458
481
  end
459
482
 
460
483
  creq = canonical_request(http_method, url, headers, content_sha256)
461
- sts = string_to_sign(datetime, creq)
462
- 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
463
492
  url
464
493
  end
465
494
 
466
495
  private
467
496
 
497
+ def sts_algorithm
498
+ @signing_algorithm == :sigv4a ? 'AWS4-ECDSA-P256-SHA256' : 'AWS4-HMAC-SHA256'
499
+ end
500
+
468
501
  def canonical_request(http_method, url, headers, content_sha256)
469
502
  [
470
503
  http_method,
@@ -476,9 +509,9 @@ module Aws
476
509
  ].join("\n")
477
510
  end
478
511
 
479
- def string_to_sign(datetime, canonical_request)
512
+ def string_to_sign(datetime, canonical_request, algorithm)
480
513
  [
481
- 'AWS4-HMAC-SHA256',
514
+ algorithm,
482
515
  datetime,
483
516
  credential_scope(datetime[0,8]),
484
517
  sha256_hexdigest(canonical_request),
@@ -511,10 +544,10 @@ module Aws
511
544
  def credential_scope(date)
512
545
  [
513
546
  date,
514
- @region,
547
+ (@region unless @signing_algorithm == :sigv4a),
515
548
  @service,
516
- 'aws4_request',
517
- ].join('/')
549
+ 'aws4_request'
550
+ ].compact.join('/')
518
551
  end
519
552
 
520
553
  def credential(credentials, date)
@@ -529,6 +562,16 @@ module Aws
529
562
  hexhmac(k_credentials, string_to_sign)
530
563
  end
531
564
 
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
+
532
575
  # Comparing to original signature v4 algorithm,
533
576
  # returned signature is a binary string instread of
534
577
  # hex-encoded string. (Since ':chunk-signature' requires
@@ -896,6 +939,18 @@ module Aws
896
939
  end
897
940
  end
898
941
 
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
899
954
  end
900
955
  end
901
956
  end
data/lib/aws-sigv4.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'aws-sigv4/asymmetric_credentials'
3
4
  require_relative 'aws-sigv4/credentials'
4
5
  require_relative 'aws-sigv4/errors'
5
6
  require_relative 'aws-sigv4/signature'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-sigv4
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-28 00:00:00.000000000 Z
11
+ date: 2024-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-eventstream
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: 1.0.2
33
33
  description: Amazon Web Services Signature Version 4 signing library. Generates sigv4
34
34
  signature for HTTP requests.
35
- email:
35
+ email:
36
36
  executables: []
37
37
  extensions: []
38
38
  extra_rdoc_files: []
@@ -41,6 +41,7 @@ files:
41
41
  - LICENSE.txt
42
42
  - VERSION
43
43
  - lib/aws-sigv4.rb
44
+ - lib/aws-sigv4/asymmetric_credentials.rb
44
45
  - lib/aws-sigv4/credentials.rb
45
46
  - lib/aws-sigv4/errors.rb
46
47
  - lib/aws-sigv4/request.rb
@@ -52,7 +53,7 @@ licenses:
52
53
  metadata:
53
54
  source_code_uri: https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/aws-sigv4
54
55
  changelog_uri: https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/aws-sigv4/CHANGELOG.md
55
- post_install_message:
56
+ post_install_message:
56
57
  rdoc_options: []
57
58
  require_paths:
58
59
  - lib
@@ -67,8 +68,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
68
  - !ruby/object:Gem::Version
68
69
  version: '0'
69
70
  requirements: []
70
- rubygems_version: 3.1.6
71
- signing_key:
71
+ rubygems_version: 3.4.10
72
+ signing_key:
72
73
  specification_version: 4
73
74
  summary: AWS Signature Version 4 library.
74
75
  test_files: []