aws-sigv4 1.8.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: 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: []