aws-sigv4 1.7.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: a3056c1d15e50cbc5b076e0ef145163bfbe6381d0fc0e6ed23b46bed3ca75a42
4
- data.tar.gz: 9e1c9f247ee6c8d4e4e7e19308c9ea2ab27d392f5f6a5d27ba5d282e7a15c62c
3
+ metadata.gz: 25c3ddf60803af303d37af469c7250ceba22d6a23e62bbfb7a9a82ae3d01b8e8
4
+ data.tar.gz: 36537ef12949c9d0a632060485c3dcf340176a8241c3e65828e47991581e7c89
5
5
  SHA512:
6
- metadata.gz: 93f24fe1853d03a090c198521fed073d39384f59b5d5fcb3942d6d791a066d4c8cfd9bde674fff728b19d2aaba14c9769638fb5f5106dbd956d9a5764ceb4b39
7
- data.tar.gz: 1043a6e7c1334e586af23f724baef8d28f44cadb19589e04f98237ec0a81c9e571d28402b71c1c07da28525b7159fd1356831ae6d091bbaca77f48989a5c1f5a
6
+ metadata.gz: aa33926ae5a1804fee36cce9b7cadead40d9d5806154e62840be377c663d94dbac07ea537601f4fa47c1d4861dccb3bdf7801b2b1edf256a0a452a73fdf2c9de
7
+ data.tar.gz: 8e1e705a6dfef2edd5af640de60f01321f1a811f41f407e906f08881d83d197ba9011c4ed3d2a218f6f17f94fcd602e0a6759abcf7c5e5e27f5d66465c3f3f3c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
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
+
9
+ 1.8.0 (2023-11-28)
10
+ ------------------
11
+
12
+ * Feature - Support `sigv4-s3express` signing algorithm.
13
+
4
14
  1.7.0 (2023-11-22)
5
15
  ------------------
6
16
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.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,10 +154,11 @@ 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.'
157
+ if @signing_algorithm == 'sigv4-s3express'.to_sym &&
158
+ Signer.use_crt? && Aws::Crt::GEM_VERSION <= '0.1.9'
159
+ raise ArgumentError,
160
+ 'This version of aws-crt does not support S3 Express. Please
161
+ update this gem to at least version 0.2.0.'
159
162
  end
160
163
  end
161
164
 
@@ -239,6 +242,7 @@ module Aws
239
242
 
240
243
  http_method = extract_http_method(request)
241
244
  url = extract_url(request)
245
+ Signer.normalize_path(url) if @normalize_path
242
246
  headers = downcase_headers(request[:headers])
243
247
 
244
248
  datetime = headers['x-amz-date']
@@ -251,29 +255,55 @@ module Aws
251
255
  sigv4_headers = {}
252
256
  sigv4_headers['host'] = headers['host'] || host(url)
253
257
  sigv4_headers['x-amz-date'] = datetime
254
- sigv4_headers['x-amz-security-token'] = creds.session_token if creds.session_token
258
+ if creds.session_token && !@omit_session_token
259
+ if @signing_algorithm == 'sigv4-s3express'.to_sym
260
+ sigv4_headers['x-amz-s3session-token'] = creds.session_token
261
+ else
262
+ sigv4_headers['x-amz-security-token'] = creds.session_token
263
+ end
264
+ end
265
+
255
266
  sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
256
267
 
268
+ if @signing_algorithm == :sigv4a && @region && !@region.empty?
269
+ sigv4_headers['x-amz-region-set'] = @region
270
+ end
257
271
  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
258
272
 
273
+ algorithm = sts_algorithm
274
+
259
275
  # compute signature parts
260
276
  creq = canonical_request(http_method, url, headers, content_sha256)
261
- sts = string_to_sign(datetime, creq)
262
- 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
263
287
 
264
288
  # apply signature
265
289
  sigv4_headers['authorization'] = [
266
- "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
290
+ "#{algorithm} Credential=#{credential(creds, date)}",
267
291
  "SignedHeaders=#{signed_headers(headers)}",
268
292
  "Signature=#{sig}",
269
293
  ].join(', ')
270
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
+
271
300
  # Returning the signature components.
272
301
  Signature.new(
273
302
  headers: sigv4_headers,
274
303
  string_to_sign: sts,
275
304
  canonical_request: creq,
276
- content_sha256: content_sha256
305
+ content_sha256: content_sha256,
306
+ signature: sig
277
307
  )
278
308
  end
279
309
 
@@ -407,6 +437,7 @@ module Aws
407
437
 
408
438
  http_method = extract_http_method(options)
409
439
  url = extract_url(options)
440
+ Signer.normalize_path(url) if @normalize_path
410
441
 
411
442
  headers = downcase_headers(options[:headers])
412
443
  headers['host'] ||= host(url)
@@ -419,14 +450,26 @@ module Aws
419
450
  content_sha256 ||= options[:body_digest]
420
451
  content_sha256 ||= sha256_hexdigest(options[:body] || '')
421
452
 
453
+ algorithm = sts_algorithm
454
+
422
455
  params = {}
423
- params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
456
+ params['X-Amz-Algorithm'] = algorithm
424
457
  params['X-Amz-Credential'] = credential(creds, date)
425
458
  params['X-Amz-Date'] = datetime
426
459
  params['X-Amz-Expires'] = presigned_url_expiration(options, expiration, Time.strptime(datetime, "%Y%m%dT%H%M%S%Z")).to_s
427
- params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
460
+ if creds.session_token
461
+ if @signing_algorithm == 'sigv4-s3express'.to_sym
462
+ params['X-Amz-S3session-Token'] = creds.session_token
463
+ else
464
+ params['X-Amz-Security-Token'] = creds.session_token
465
+ end
466
+ end
428
467
  params['X-Amz-SignedHeaders'] = signed_headers(headers)
429
468
 
469
+ if @signing_algorithm == :sigv4a && @region
470
+ params['X-Amz-Region-Set'] = @region
471
+ end
472
+
430
473
  params = params.map do |key, value|
431
474
  "#{uri_escape(key)}=#{uri_escape(value)}"
432
475
  end.join('&')
@@ -438,13 +481,23 @@ module Aws
438
481
  end
439
482
 
440
483
  creq = canonical_request(http_method, url, headers, content_sha256)
441
- sts = string_to_sign(datetime, creq)
442
- 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
443
492
  url
444
493
  end
445
494
 
446
495
  private
447
496
 
497
+ def sts_algorithm
498
+ @signing_algorithm == :sigv4a ? 'AWS4-ECDSA-P256-SHA256' : 'AWS4-HMAC-SHA256'
499
+ end
500
+
448
501
  def canonical_request(http_method, url, headers, content_sha256)
449
502
  [
450
503
  http_method,
@@ -456,9 +509,9 @@ module Aws
456
509
  ].join("\n")
457
510
  end
458
511
 
459
- def string_to_sign(datetime, canonical_request)
512
+ def string_to_sign(datetime, canonical_request, algorithm)
460
513
  [
461
- 'AWS4-HMAC-SHA256',
514
+ algorithm,
462
515
  datetime,
463
516
  credential_scope(datetime[0,8]),
464
517
  sha256_hexdigest(canonical_request),
@@ -491,10 +544,10 @@ module Aws
491
544
  def credential_scope(date)
492
545
  [
493
546
  date,
494
- @region,
547
+ (@region unless @signing_algorithm == :sigv4a),
495
548
  @service,
496
- 'aws4_request',
497
- ].join('/')
549
+ 'aws4_request'
550
+ ].compact.join('/')
498
551
  end
499
552
 
500
553
  def credential(credentials, date)
@@ -509,6 +562,16 @@ module Aws
509
562
  hexhmac(k_credentials, string_to_sign)
510
563
  end
511
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
+
512
575
  # Comparing to original signature v4 algorithm,
513
576
  # returned signature is a binary string instread of
514
577
  # hex-encoded string. (Since ':chunk-signature' requires
@@ -876,6 +939,18 @@ module Aws
876
939
  end
877
940
  end
878
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
879
954
  end
880
955
  end
881
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.7.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-22 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: []