aws-sigv4 1.8.0 → 1.9.1

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: d6c968ea3d1cff2c3e6ff056a38658ce9af6f2f9b3d5fce948003a063c1f785e
4
+ data.tar.gz: '008ac56a37950824779768b8e3e942a711a0cae225231e9f9897e0426a18d121'
5
5
  SHA512:
6
- metadata.gz: c16c5df7f8c6ca10cf073c25984506ce938f26823f99d82813b5bde3fb283d23a3c480adec2945b98975946a7b32efe57a0058cd65bb383606c5ee5228711381
7
- data.tar.gz: b72ea1894eb1c419179325f8e715fb454ab02ae2d1ac243b90ed2f4032c0fd966ba157287a12009587908d0a6a2f62261c78e319a230196792b6ec4206a20718
6
+ metadata.gz: 49dfbb860585de3ca7f1f84e3ff2fa059025098fe3a0baf4ab4c4fd1ff6bdb4dab85b17b040ebc7fa5db743d1f5da9a4b0975187d2bf60aabfc29e9674b22ee3
7
+ data.tar.gz: 315d89e1c67bfc3938f267dcf50e47842f7cac514b07df07cc642be2bc99fbc55dd7256f564868c6f3e61ee125aa71d26a98af01c734a924aaa0ae5ac8e084e4
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 1.9.1 (2024-07-29)
5
+ ------------------
6
+
7
+ * Issue - Add missing require of `pathname` to `Signer`.
8
+
9
+ 1.9.0 (2024-07-23)
10
+ ------------------
11
+
12
+ * Feature - Support `sigv4a` signing algorithm without `aws-crt`.
13
+
4
14
  1.8.0 (2023-11-28)
5
15
  ------------------
6
16
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.0
1
+ 1.9.1
@@ -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
@@ -6,6 +6,7 @@ require 'time'
6
6
  require 'uri'
7
7
  require 'set'
8
8
  require 'cgi'
9
+ require 'pathname'
9
10
  require 'aws-eventstream'
10
11
 
11
12
  module Aws
@@ -84,14 +85,16 @@ module Aws
84
85
 
85
86
  # @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
86
87
  # @param [String] :service The service signing name, e.g. 's3'.
87
- # @param [String] :region The region name, e.g. 'us-east-1'.
88
+ # @param [String] :region The region name, e.g. 'us-east-1'. When signing
89
+ # with sigv4a, this should be a comma separated list of regions.
88
90
  # @param [String] :access_key_id
89
91
  # @param [String] :secret_access_key
90
92
  # @param [String] :session_token (nil)
91
93
  #
92
94
  # @overload initialize(service:, region:, credentials:, **options)
93
95
  # @param [String] :service The service signing name, e.g. 's3'.
94
- # @param [String] :region The region name, e.g. 'us-east-1'.
96
+ # @param [String] :region The region name, e.g. 'us-east-1'. When signing
97
+ # with sigv4a, this should be a comma separated list of regions.
95
98
  # @param [Credentials] :credentials Any object that responds to the following
96
99
  # methods:
97
100
  #
@@ -102,7 +105,8 @@ module Aws
102
105
  #
103
106
  # @overload initialize(service:, region:, credentials_provider:, **options)
104
107
  # @param [String] :service The service signing name, e.g. 's3'.
105
- # @param [String] :region The region name, e.g. 'us-east-1'.
108
+ # @param [String] :region The region name, e.g. 'us-east-1'. When signing
109
+ # with sigv4a, this should be a comma separated list of regions.
106
110
  # @param [#credentials] :credentials_provider An object that responds
107
111
  # to `#credentials`, returning an object that responds to the following
108
112
  # methods:
@@ -127,8 +131,7 @@ module Aws
127
131
  # every other AWS service as of late 2016.
128
132
  #
129
133
  # @option options [Symbol] :signing_algorithm (:sigv4) The
130
- # algorithm to use for signing. :sigv4a is only supported when
131
- # `aws-crt` is available.
134
+ # algorithm to use for signing.
132
135
  #
133
136
  # @option options [Boolean] :omit_session_token (false)
134
137
  # (Supported only when `aws-crt` is available) If `true`,
@@ -136,8 +139,8 @@ module Aws
136
139
  # but is treated as "unsigned" and does not contribute
137
140
  # to the authorization signature.
138
141
  #
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
142
+ # @option options [Boolean] :normalize_path (true) When `true`, the
143
+ # uri paths will be normalized when building the canonical request.
141
144
  def initialize(options = {})
142
145
  @service = extract_service(options)
143
146
  @region = extract_region(options)
@@ -152,12 +155,6 @@ module Aws
152
155
  @normalize_path = options.fetch(:normalize_path, true)
153
156
  @omit_session_token = options.fetch(:omit_session_token, false)
154
157
 
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
158
  if @signing_algorithm == 'sigv4-s3express'.to_sym &&
162
159
  Signer.use_crt? && Aws::Crt::GEM_VERSION <= '0.1.9'
163
160
  raise ArgumentError,
@@ -246,6 +243,7 @@ module Aws
246
243
 
247
244
  http_method = extract_http_method(request)
248
245
  url = extract_url(request)
246
+ Signer.normalize_path(url) if @normalize_path
249
247
  headers = downcase_headers(request[:headers])
250
248
 
251
249
  datetime = headers['x-amz-date']
@@ -258,7 +256,7 @@ module Aws
258
256
  sigv4_headers = {}
259
257
  sigv4_headers['host'] = headers['host'] || host(url)
260
258
  sigv4_headers['x-amz-date'] = datetime
261
- if creds.session_token
259
+ if creds.session_token && !@omit_session_token
262
260
  if @signing_algorithm == 'sigv4-s3express'.to_sym
263
261
  sigv4_headers['x-amz-s3session-token'] = creds.session_token
264
262
  else
@@ -268,26 +266,45 @@ module Aws
268
266
 
269
267
  sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
270
268
 
269
+ if @signing_algorithm == :sigv4a && @region && !@region.empty?
270
+ sigv4_headers['x-amz-region-set'] = @region
271
+ end
271
272
  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
272
273
 
274
+ algorithm = sts_algorithm
275
+
273
276
  # compute signature parts
274
277
  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)
278
+ sts = string_to_sign(datetime, creq, algorithm)
279
+
280
+ sig =
281
+ if @signing_algorithm == :sigv4a
282
+ asymmetric_signature(creds, sts)
283
+ else
284
+ signature(creds.secret_access_key, date, sts)
285
+ end
286
+
287
+ algorithm = sts_algorithm
277
288
 
278
289
  # apply signature
279
290
  sigv4_headers['authorization'] = [
280
- "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
291
+ "#{algorithm} Credential=#{credential(creds, date)}",
281
292
  "SignedHeaders=#{signed_headers(headers)}",
282
293
  "Signature=#{sig}",
283
294
  ].join(', ')
284
295
 
296
+ # skip signing the session token, but include it in the headers
297
+ if creds.session_token && @omit_session_token
298
+ sigv4_headers['x-amz-security-token'] = creds.session_token
299
+ end
300
+
285
301
  # Returning the signature components.
286
302
  Signature.new(
287
303
  headers: sigv4_headers,
288
304
  string_to_sign: sts,
289
305
  canonical_request: creq,
290
- content_sha256: content_sha256
306
+ content_sha256: content_sha256,
307
+ signature: sig
291
308
  )
292
309
  end
293
310
 
@@ -421,6 +438,7 @@ module Aws
421
438
 
422
439
  http_method = extract_http_method(options)
423
440
  url = extract_url(options)
441
+ Signer.normalize_path(url) if @normalize_path
424
442
 
425
443
  headers = downcase_headers(options[:headers])
426
444
  headers['host'] ||= host(url)
@@ -433,8 +451,10 @@ module Aws
433
451
  content_sha256 ||= options[:body_digest]
434
452
  content_sha256 ||= sha256_hexdigest(options[:body] || '')
435
453
 
454
+ algorithm = sts_algorithm
455
+
436
456
  params = {}
437
- params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
457
+ params['X-Amz-Algorithm'] = algorithm
438
458
  params['X-Amz-Credential'] = credential(creds, date)
439
459
  params['X-Amz-Date'] = datetime
440
460
  params['X-Amz-Expires'] = presigned_url_expiration(options, expiration, Time.strptime(datetime, "%Y%m%dT%H%M%S%Z")).to_s
@@ -447,6 +467,10 @@ module Aws
447
467
  end
448
468
  params['X-Amz-SignedHeaders'] = signed_headers(headers)
449
469
 
470
+ if @signing_algorithm == :sigv4a && @region
471
+ params['X-Amz-Region-Set'] = @region
472
+ end
473
+
450
474
  params = params.map do |key, value|
451
475
  "#{uri_escape(key)}=#{uri_escape(value)}"
452
476
  end.join('&')
@@ -458,13 +482,23 @@ module Aws
458
482
  end
459
483
 
460
484
  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)
485
+ sts = string_to_sign(datetime, creq, algorithm)
486
+ signature =
487
+ if @signing_algorithm == :sigv4a
488
+ asymmetric_signature(creds, sts)
489
+ else
490
+ signature(creds.secret_access_key, date, sts)
491
+ end
492
+ url.query += '&X-Amz-Signature=' + signature
463
493
  url
464
494
  end
465
495
 
466
496
  private
467
497
 
498
+ def sts_algorithm
499
+ @signing_algorithm == :sigv4a ? 'AWS4-ECDSA-P256-SHA256' : 'AWS4-HMAC-SHA256'
500
+ end
501
+
468
502
  def canonical_request(http_method, url, headers, content_sha256)
469
503
  [
470
504
  http_method,
@@ -476,9 +510,9 @@ module Aws
476
510
  ].join("\n")
477
511
  end
478
512
 
479
- def string_to_sign(datetime, canonical_request)
513
+ def string_to_sign(datetime, canonical_request, algorithm)
480
514
  [
481
- 'AWS4-HMAC-SHA256',
515
+ algorithm,
482
516
  datetime,
483
517
  credential_scope(datetime[0,8]),
484
518
  sha256_hexdigest(canonical_request),
@@ -511,10 +545,10 @@ module Aws
511
545
  def credential_scope(date)
512
546
  [
513
547
  date,
514
- @region,
548
+ (@region unless @signing_algorithm == :sigv4a),
515
549
  @service,
516
- 'aws4_request',
517
- ].join('/')
550
+ 'aws4_request'
551
+ ].compact.join('/')
518
552
  end
519
553
 
520
554
  def credential(credentials, date)
@@ -529,6 +563,16 @@ module Aws
529
563
  hexhmac(k_credentials, string_to_sign)
530
564
  end
531
565
 
566
+ def asymmetric_signature(creds, string_to_sign)
567
+ ec, _ = Aws::Sigv4::AsymmetricCredentials.derive_asymmetric_key(
568
+ creds.access_key_id, creds.secret_access_key
569
+ )
570
+ sts_digest = OpenSSL::Digest::SHA256.digest(string_to_sign)
571
+ s = ec.dsa_sign_asn1(sts_digest)
572
+
573
+ Digest.hexencode(s)
574
+ end
575
+
532
576
  # Comparing to original signature v4 algorithm,
533
577
  # returned signature is a binary string instread of
534
578
  # hex-encoded string. (Since ':chunk-signature' requires
@@ -896,6 +940,18 @@ module Aws
896
940
  end
897
941
  end
898
942
 
943
+ # @api private
944
+ def normalize_path(uri)
945
+ normalized_path = Pathname.new(uri.path).cleanpath.to_s
946
+ # Pathname is probably not correct to use. Empty paths will
947
+ # resolve to "." and should be disregarded
948
+ normalized_path = '' if normalized_path == '.'
949
+ # Ensure trailing slashes are correctly preserved
950
+ if uri.path.end_with?('/') && !normalized_path.end_with?('/')
951
+ normalized_path << '/'
952
+ end
953
+ uri.path = normalized_path
954
+ end
899
955
  end
900
956
  end
901
957
  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.1
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-29 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: []