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 +4 -4
- data/CHANGELOG.md +10 -0
- data/VERSION +1 -1
- data/lib/aws-sigv4/asymmetric_credentials.rb +91 -0
- data/lib/aws-sigv4/signature.rb +3 -0
- data/lib/aws-sigv4/signer.rb +100 -25
- data/lib/aws-sigv4.rb +1 -0
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25c3ddf60803af303d37af469c7250ceba22d6a23e62bbfb7a9a82ae3d01b8e8
|
4
|
+
data.tar.gz: 36537ef12949c9d0a632060485c3dcf340176a8241c3e65828e47991581e7c89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
data/lib/aws-sigv4/signature.rb
CHANGED
@@ -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
|
data/lib/aws-sigv4/signer.rb
CHANGED
@@ -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.
|
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)
|
140
|
-
#
|
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 ==
|
156
|
-
|
157
|
-
|
158
|
-
'
|
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
|
-
|
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
|
-
|
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
|
-
"
|
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'] =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
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.
|
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:
|
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.
|
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: []
|