aws-sigv4 1.7.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
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: []