aws-sigv4 1.4.1.crt → 1.5.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: 9f4ebc6670c011eafdb6f2ca33ee5894dd723f10f1ad23ea296837bb41355a77
4
- data.tar.gz: 6e69783f73330b8559390d56718e0fe48cb117f4fc07ae8c808a2d6ebea6263f
3
+ metadata.gz: 709cebb0799ad1e75e1f1e69413798d4da1d523332abc0ac59899e9686aa2e07
4
+ data.tar.gz: a4751a6a7c2356bf76b2ac550f8320e3f8b24951415eaddf1328271c1835f2d3
5
5
  SHA512:
6
- metadata.gz: 2ba3f4da7f88b55f0529e9d23a4cc6a039abb6e1af7d939a9fe2679232c28ba80f7b2791758d39f67940cfa180c631f92ec424753a38f346b290132a95263ba6
7
- data.tar.gz: 9fdee7aeff9a4764bb7a2395628ba5a9e52b8fe1e21adf165b48ed6241e3a2b71b7cff2d7597dba2696e49009963589244410e276898a3807945cb659865a0b6
6
+ metadata.gz: 07ebb364959ed8bf62c39192707a30caef1127fcf0ab078dab30ecaf695b2a9e22caa017a773987dac297615dfc824eeb8cc04a60a604837024ee850f6812dd2
7
+ data.tar.gz: f9e93932e59b1d43b92aa2c74d33d7553490ecf84ff61b491321c0b031896f30b5375c8a5f2c81d8514691917096e7c3dd5cc298c3a3fa5e6876cabb1f458c8e
data/CHANGELOG.md CHANGED
@@ -1,11 +1,20 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 1.5.0 (2022-04-20)
5
+ ------------------
6
+
7
+ * Feature - Use CRT based signers if `aws-crt` is available - provides support for `sigv4a`.
8
+
9
+ 1.4.0 (2021-09-02)
10
+ ------------------
11
+
12
+ * Feature - add `signing_algorithm` option with `sigv4` default.
4
13
 
5
- 1.3.0.crt (2021-08-04)
14
+ 1.3.0 (2021-09-01)
6
15
  ------------------
7
16
 
8
- * Feature - Preview release of `aws-sigv4` version 1.3.0.crt gem - uses the Common Runtime (CRT) for signing and support for sigv4a.
17
+ * Feature - AWS SDK for Ruby no longer supports Ruby runtime versions 1.9, 2.0, 2.1, and 2.2.
9
18
 
10
19
  1.2.4 (2021-07-08)
11
20
  ------------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.1.crt
1
+ 1.5.0
@@ -11,10 +11,7 @@ module Aws
11
11
  # @option options [required, String] :secret_access_key
12
12
  # @option options [String, nil] :session_token (nil)
13
13
  def initialize(options = {})
14
- if options[:access_key_id] && options[:secret_access_key] &&
15
- !options[:access_key_id].empty? &&
16
- !options[:secret_access_key].empty?
17
-
14
+ if options[:access_key_id] && options[:secret_access_key]
18
15
  @access_key_id = options[:access_key_id]
19
16
  @secret_access_key = options[:secret_access_key]
20
17
  @session_token = options[:session_token]
@@ -54,8 +51,8 @@ module Aws
54
51
  # @option options [String] :session_token (nil)
55
52
  def initialize(options = {})
56
53
  @credentials = options[:credentials] ?
57
- options[:credentials] :
58
- Credentials.new(options)
54
+ options[:credentials] :
55
+ Credentials.new(options)
59
56
  end
60
57
 
61
58
  # @return [Credentials]
@@ -66,5 +63,6 @@ module Aws
66
63
  !!credentials && credentials.set?
67
64
  end
68
65
  end
66
+
69
67
  end
70
68
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Aws
6
+ module Sigv4
7
+ class Request
8
+
9
+ # @option options [required, String] :http_method
10
+ # @option options [required, HTTP::URI, HTTPS::URI, String] :endpoint
11
+ # @option options [Hash<String,String>] :headers ({})
12
+ # @option options [String, IO] :body ('')
13
+ def initialize(options = {})
14
+ @http_method = nil
15
+ @endpoint = nil
16
+ @headers = {}
17
+ @body = ''
18
+ options.each_pair do |attr_name, attr_value|
19
+ send("#{attr_name}=", attr_value)
20
+ end
21
+ end
22
+
23
+ # @param [String] http_method One of 'GET', 'PUT', 'POST', 'DELETE', 'HEAD', or 'PATCH'
24
+ def http_method=(http_method)
25
+ @http_method = http_method
26
+ end
27
+
28
+ # @return [String] One of 'GET', 'PUT', 'POST', 'DELETE', 'HEAD', or 'PATCH'
29
+ def http_method
30
+ @http_method
31
+ end
32
+
33
+ # @param [String, HTTP::URI, HTTPS::URI] endpoint
34
+ def endpoint=(endpoint)
35
+ @endpoint = URI.parse(endpoint.to_s)
36
+ end
37
+
38
+ # @return [HTTP::URI, HTTPS::URI]
39
+ def endpoint
40
+ @endpoint
41
+ end
42
+
43
+ # @param [Hash] headers
44
+ def headers=(headers)
45
+ @headers = headers
46
+ end
47
+
48
+ # @return [Hash<String,String>]
49
+ def headers
50
+ @headers
51
+ end
52
+
53
+ # @param [String, IO] body
54
+ def body=(body)
55
+ @body = body
56
+ end
57
+
58
+ # @return [String, IO]
59
+ def body
60
+ @body
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -32,8 +32,8 @@ module Aws
32
32
  # @return [String] For debugging purposes.
33
33
  attr_accessor :content_sha256
34
34
 
35
+ # @return [Hash] Internal data for debugging purposes.
35
36
  attr_accessor :extra
36
-
37
37
  end
38
38
  end
39
39
  end
@@ -1,16 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openssl'
4
- require 'time'
5
4
  require 'tempfile'
5
+ require 'time'
6
6
  require 'uri'
7
7
  require 'set'
8
+ require 'cgi'
8
9
  require 'aws-eventstream'
9
10
 
10
11
  module Aws
11
12
  module Sigv4
12
- # Utility class for creating AWS signature version 4 signature.
13
+
14
+ # Utility class for creating AWS signature version 4 signature. This class
15
+ # provides two methods for generating signatures:
16
+ #
17
+ # * {#sign_request} - Computes a signature of the given request, returning
18
+ # the hash of headers that should be applied to the request.
19
+ #
20
+ # * {#presign_url} - Computes a presigned request with an expiration.
21
+ # By default, the body of this request is not signed and the request
22
+ # expires in 15 minutes.
23
+ #
24
+ # ## Configuration
25
+ #
26
+ # To use the signer, you need to specify the service, region, and credentials.
27
+ # The service name is normally the endpoint prefix to an AWS service. For
28
+ # example:
29
+ #
30
+ # ec2.us-west-1.amazonaws.com => ec2
31
+ #
32
+ # The region is normally the second portion of the endpoint, following
33
+ # the service name.
34
+ #
35
+ # ec2.us-west-1.amazonaws.com => us-west-1
36
+ #
37
+ # It is important to have the correct service and region name, or the
38
+ # signature will be invalid.
39
+ #
40
+ # ## Credentials
41
+ #
42
+ # The signer requires credentials. You can configure the signer
43
+ # with static credentials:
44
+ #
45
+ # signer = Aws::Sigv4::Signer.new(
46
+ # service: 's3',
47
+ # region: 'us-east-1',
48
+ # # static credentials
49
+ # access_key_id: 'akid',
50
+ # secret_access_key: 'secret'
51
+ # )
52
+ #
53
+ # You can also provide refreshing credentials via the `:credentials_provider`.
54
+ # If you are using the AWS SDK for Ruby, you can use any of the credential
55
+ # classes:
56
+ #
57
+ # signer = Aws::Sigv4::Signer.new(
58
+ # service: 's3',
59
+ # region: 'us-east-1',
60
+ # credentials_provider: Aws::InstanceProfileCredentials.new
61
+ # )
62
+ #
63
+ # Other AWS SDK for Ruby classes that can be provided via `:credentials_provider`:
64
+ #
65
+ # * `Aws::Credentials`
66
+ # * `Aws::SharedCredentials`
67
+ # * `Aws::InstanceProfileCredentials`
68
+ # * `Aws::AssumeRoleCredentials`
69
+ # * `Aws::ECSCredentials`
70
+ #
71
+ # A credential provider is any object that responds to `#credentials`
72
+ # returning another object that responds to `#access_key_id`, `#secret_access_key`,
73
+ # and `#session_token`.
74
+ #
13
75
  class Signer
76
+
14
77
  # @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
15
78
  # @param [String] :service The service signing name, e.g. 's3'.
16
79
  # @param [String] :region The region name, e.g. 'us-east-1'.
@@ -55,19 +118,23 @@ module Aws
55
118
  # headers. This is required for AWS Glacier, and optional for
56
119
  # every other AWS service as of late 2016.
57
120
  #
58
- # @option options [Boolean] :omit_session_token (false) If `true`,
121
+ # @option options [Symbol] :signing_algorithm (:sigv4) The
122
+ # algorithm to use for signing. :sigv4a is only supported when
123
+ # `aws-crt` is available.
124
+ #
125
+ # @option options [Boolean] :omit_session_token (false)
126
+ # (Supported only when `aws-crt` is available) If `true`,
59
127
  # then security token is added to the final signing result,
60
128
  # but is treated as "unsigned" and does not contribute
61
129
  # to the authorization signature.
62
130
  #
63
- # @option options [Boolean] :normalize_path (true) When `true`,
64
- # the uri paths will be normalized when building the canonical request
131
+ # @option options [Boolean] :normalize_path (true) (Supported only when `aws-crt` is available)
132
+ # When `true`, the uri paths will be normalized when building the canonical request
65
133
  def initialize(options = {})
66
134
  @service = extract_service(options)
67
135
  @region = extract_region(options)
68
136
  @credentials_provider = extract_credentials_provider(options)
69
- @unsigned_headers = Set.new((options.fetch(:unsigned_headers, []))
70
- .map(&:downcase))
137
+ @unsigned_headers = Set.new((options.fetch(:unsigned_headers, [])).map(&:downcase))
71
138
  @unsigned_headers << 'authorization'
72
139
  @unsigned_headers << 'x-amzn-trace-id'
73
140
  @unsigned_headers << 'expect'
@@ -76,6 +143,12 @@ module Aws
76
143
  @signing_algorithm = options.fetch(:signing_algorithm, :sigv4)
77
144
  @normalize_path = options.fetch(:normalize_path, true)
78
145
  @omit_session_token = options.fetch(:omit_session_token, false)
146
+
147
+ if @signing_algorithm == :sigv4a && !Signer.use_crt?
148
+ raise ArgumentError, 'You are attempting to sign a' \
149
+ ' request with sigv4a which requires the `aws-crt` gem.'\
150
+ ' Please install the gem or add it to your gemfile.'
151
+ end
79
152
  end
80
153
 
81
154
  # @return [String]
@@ -151,65 +224,105 @@ module Aws
151
224
  # a `#headers` method. The headers must be applied to your request.
152
225
  #
153
226
  def sign_request(request)
227
+
228
+ return crt_sign_request(request) if Signer.use_crt?
229
+
154
230
  creds = fetch_credentials
155
231
 
156
232
  http_method = extract_http_method(request)
157
233
  url = extract_url(request)
158
234
  headers = downcase_headers(request[:headers])
159
235
 
160
- datetime =
161
- if headers.include? 'x-amz-date'
162
- Time.parse(headers.delete('x-amz-date'))
163
- end
236
+ datetime = headers['x-amz-date']
237
+ datetime ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
238
+ date = datetime[0,8]
164
239
 
165
- content_sha256 = headers.delete('x-amz-content-sha256')
240
+ content_sha256 = headers['x-amz-content-sha256']
166
241
  content_sha256 ||= sha256_hexdigest(request[:body] || '')
167
242
 
168
243
  sigv4_headers = {}
169
244
  sigv4_headers['host'] = headers['host'] || host(url)
170
-
171
- # Modify the user-agent to add usage of crt-signer
172
- # This should be temporary during developer preview only
173
- if headers.include? 'user-agent'
174
- headers['user-agent'] = "#{headers['user-agent']} crt-signer/#{@signing_algorithm}/#{Aws::Sigv4::VERSION}"
175
- sigv4_headers['user-agent'] = headers['user-agent']
176
- end
245
+ sigv4_headers['x-amz-date'] = datetime
246
+ sigv4_headers['x-amz-security-token'] = creds.session_token if creds.session_token
247
+ sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
177
248
 
178
249
  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
179
250
 
180
- config = Aws::Crt::Auth::SigningConfig.new(
181
- algorithm: @signing_algorithm,
182
- signature_type: :http_request_headers,
183
- region: @region,
184
- service: @service,
185
- date: datetime,
186
- signed_body_value: content_sha256,
187
- signed_body_header_type: @apply_checksum_header ?
188
- :sbht_content_sha256 : :sbht_none,
189
- credentials: creds,
190
- unsigned_headers: @unsigned_headers,
191
- use_double_uri_encode: @uri_escape_path,
192
- should_normalize_uri_path: @normalize_path,
193
- omit_session_token: @omit_session_token
194
- )
195
- http_request = Aws::Crt::Http::Message.new(
196
- http_method, url.to_s, headers
197
- )
198
- signable = Aws::Crt::Auth::Signable.new(http_request)
251
+ # compute signature parts
252
+ creq = canonical_request(http_method, url, headers, content_sha256)
253
+ sts = string_to_sign(datetime, creq)
254
+ sig = signature(creds.secret_access_key, date, sts)
199
255
 
200
- signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable)
256
+ # apply signature
257
+ sigv4_headers['authorization'] = [
258
+ "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
259
+ "SignedHeaders=#{signed_headers(headers)}",
260
+ "Signature=#{sig}",
261
+ ].join(', ')
201
262
 
263
+ # Returning the signature components.
202
264
  Signature.new(
203
- headers: sigv4_headers.merge(
204
- downcase_headers(signing_result[:headers])
205
- ),
206
- string_to_sign: 'CRT_INTERNAL',
207
- canonical_request: 'CRT_INTERNAL',
208
- content_sha256: content_sha256,
209
- extra: {config: config, signable: signable}
265
+ headers: sigv4_headers,
266
+ string_to_sign: sts,
267
+ canonical_request: creq,
268
+ content_sha256: content_sha256
210
269
  )
211
270
  end
212
271
 
272
+ # Signs a event and returns signature headers and prior signature
273
+ # used for next event signing.
274
+ #
275
+ # Headers of a sigv4 signed event message only contains 2 headers
276
+ # * ':chunk-signature'
277
+ # * computed signature of the event, binary string, 'bytes' type
278
+ # * ':date'
279
+ # * millisecond since epoch, 'timestamp' type
280
+ #
281
+ # Payload of the sigv4 signed event message contains eventstream encoded message
282
+ # which is serialized based on input and protocol
283
+ #
284
+ # To sign events
285
+ #
286
+ # headers_0, signature_0 = signer.sign_event(
287
+ # prior_signature, # hex-encoded string
288
+ # payload_0, # binary string (eventstream encoded event 0)
289
+ # encoder, # Aws::EventStreamEncoder
290
+ # )
291
+ #
292
+ # headers_1, signature_1 = signer.sign_event(
293
+ # signature_0,
294
+ # payload_1, # binary string (eventstream encoded event 1)
295
+ # encoder
296
+ # )
297
+ #
298
+ # The initial prior_signature should be using the signature computed at initial request
299
+ #
300
+ # Note:
301
+ #
302
+ # Since ':chunk-signature' header value has bytes type, the signature value provided
303
+ # needs to be a binary string instead of a hex-encoded string (like original signature
304
+ # V4 algorithm). Thus, when returning signature value used for next event siging, the
305
+ # signature value (a binary string) used at ':chunk-signature' needs to converted to
306
+ # hex-encoded string using #unpack
307
+ def sign_event(prior_signature, payload, encoder)
308
+ # Note: CRT does not currently provide event stream signing, so we always use the ruby implementation.
309
+ creds = fetch_credentials
310
+ time = Time.now
311
+ headers = {}
312
+
313
+ datetime = time.utc.strftime("%Y%m%dT%H%M%SZ")
314
+ date = datetime[0,8]
315
+ headers[':date'] = Aws::EventStream::HeaderValue.new(value: time.to_i * 1000, type: 'timestamp')
316
+
317
+ sts = event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
318
+ sig = event_signature(creds.secret_access_key, date, sts)
319
+
320
+ headers[':chunk-signature'] = Aws::EventStream::HeaderValue.new(value: sig, type: 'bytes')
321
+
322
+ # Returning signed headers and signature value in hex-encoded string
323
+ [headers, sig.unpack('H*').first]
324
+ end
325
+
213
326
  # Signs a URL with query authentication. Using query parameters
214
327
  # to authenticate requests is useful when you want to express a
215
328
  # request entirely in a URL. This method is also referred as
@@ -279,120 +392,249 @@ module Aws
279
392
  # @return [HTTPS::URI, HTTP::URI]
280
393
  #
281
394
  def presign_url(options)
395
+
396
+ return crt_presign_url(options) if Signer.use_crt?
397
+
282
398
  creds = fetch_credentials
283
399
 
284
400
  http_method = extract_http_method(options)
285
401
  url = extract_url(options)
402
+
286
403
  headers = downcase_headers(options[:headers])
287
404
  headers['host'] ||= host(url)
288
405
 
289
- datetime = headers.delete('x-amz-date')
290
- datetime ||= (options[:time] || Time.now)
406
+ datetime = headers['x-amz-date']
407
+ datetime ||= (options[:time] || Time.now).utc.strftime("%Y%m%dT%H%M%SZ")
408
+ date = datetime[0,8]
291
409
 
292
- content_sha256 = headers.delete('x-amz-content-sha256')
410
+ content_sha256 = headers['x-amz-content-sha256']
293
411
  content_sha256 ||= options[:body_digest]
294
412
  content_sha256 ||= sha256_hexdigest(options[:body] || '')
295
413
 
296
- config = Aws::Crt::Auth::SigningConfig.new(
297
- algorithm: @signing_algorithm,
298
- signature_type: :http_request_query_params,
299
- region: @region,
300
- service: @service,
301
- date: datetime,
302
- signed_body_value: content_sha256,
303
- signed_body_header_type: @apply_checksum_header ?
304
- :sbht_content_sha256 : :sbht_none,
305
- credentials: creds,
306
- unsigned_headers: @unsigned_headers,
307
- use_double_uri_encode: @uri_escape_path,
308
- should_normalize_uri_path: @normalize_path,
309
- omit_session_token: @omit_session_token,
310
- expiration_in_seconds: options.fetch(:expires_in, 900)
311
- )
312
- http_request = Aws::Crt::Http::Message.new(
313
- http_method, url.to_s, headers
314
- )
315
- signable = Aws::Crt::Auth::Signable.new(http_request)
414
+ params = {}
415
+ params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
416
+ params['X-Amz-Credential'] = credential(creds, date)
417
+ params['X-Amz-Date'] = datetime
418
+ params['X-Amz-Expires'] = extract_expires_in(options)
419
+ params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
420
+ params['X-Amz-SignedHeaders'] = signed_headers(headers)
316
421
 
317
- signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable, http_method, url.to_s)
318
- url = URI.parse(signing_result[:path])
422
+ params = params.map do |key, value|
423
+ "#{uri_escape(key)}=#{uri_escape(value)}"
424
+ end.join('&')
319
425
 
320
- if options[:extra] && options[:extra].is_a?(Hash)
321
- options[:extra][:config] = config
322
- options[:extra][:signable] = signable
426
+ if url.query
427
+ url.query += '&' + params
428
+ else
429
+ url.query = params
323
430
  end
431
+
432
+ creq = canonical_request(http_method, url, headers, content_sha256)
433
+ sts = string_to_sign(datetime, creq)
434
+ url.query += '&X-Amz-Signature=' + signature(creds.secret_access_key, date, sts)
324
435
  url
325
436
  end
326
437
 
438
+ private
327
439
 
328
- # Signs a event and returns signature headers and prior signature
329
- # used for next event signing.
330
- #
331
- # Headers of a sigv4 signed event message only contains 2 headers
332
- # * ':chunk-signature'
333
- # * computed signature of the event, binary string, 'bytes' type
334
- # * ':date'
335
- # * millisecond since epoch, 'timestamp' type
336
- #
337
- # Payload of the sigv4 signed event message contains eventstream encoded message
338
- # which is serialized based on input and protocol
339
- #
340
- # To sign events
341
- #
342
- # headers_0, signature_0 = signer.sign_event(
343
- # prior_signature, # hex-encoded string
344
- # payload_0, # binary string (eventstream encoded event 0)
345
- # encoder, # Aws::EventStreamEncoder
346
- # )
347
- #
348
- # headers_1, signature_1 = signer.sign_event(
349
- # signature_0,
350
- # payload_1, # binary string (eventstream encoded event 1)
351
- # encoder
352
- # )
353
- #
354
- # The initial prior_signature should be using the signature computed at initial request
440
+ def canonical_request(http_method, url, headers, content_sha256)
441
+ [
442
+ http_method,
443
+ path(url),
444
+ normalized_querystring(url.query || ''),
445
+ canonical_headers(headers) + "\n",
446
+ signed_headers(headers),
447
+ content_sha256,
448
+ ].join("\n")
449
+ end
450
+
451
+ def string_to_sign(datetime, canonical_request)
452
+ [
453
+ 'AWS4-HMAC-SHA256',
454
+ datetime,
455
+ credential_scope(datetime[0,8]),
456
+ sha256_hexdigest(canonical_request),
457
+ ].join("\n")
458
+ end
459
+
460
+ # Compared to original #string_to_sign at signature v4 algorithm
461
+ # there is no canonical_request concept for an eventstream event,
462
+ # instead, an event contains headers and payload two parts, and
463
+ # they will be used for computing digest in #event_string_to_sign
355
464
  #
356
465
  # Note:
466
+ # While headers need to be encoded under eventstream format,
467
+ # payload used is already eventstream encoded (event without signature),
468
+ # thus no extra encoding is needed.
469
+ def event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
470
+ encoded_headers = encoder.encode_headers(
471
+ Aws::EventStream::Message.new(headers: headers, payload: payload)
472
+ )
473
+ [
474
+ "AWS4-HMAC-SHA256-PAYLOAD",
475
+ datetime,
476
+ credential_scope(datetime[0,8]),
477
+ prior_signature,
478
+ sha256_hexdigest(encoded_headers),
479
+ sha256_hexdigest(payload)
480
+ ].join("\n")
481
+ end
482
+
483
+ def credential_scope(date)
484
+ [
485
+ date,
486
+ @region,
487
+ @service,
488
+ 'aws4_request',
489
+ ].join('/')
490
+ end
491
+
492
+ def credential(credentials, date)
493
+ "#{credentials.access_key_id}/#{credential_scope(date)}"
494
+ end
495
+
496
+ def signature(secret_access_key, date, string_to_sign)
497
+ k_date = hmac("AWS4" + secret_access_key, date)
498
+ k_region = hmac(k_date, @region)
499
+ k_service = hmac(k_region, @service)
500
+ k_credentials = hmac(k_service, 'aws4_request')
501
+ hexhmac(k_credentials, string_to_sign)
502
+ end
503
+
504
+ # Comparing to original signature v4 algorithm,
505
+ # returned signature is a binary string instread of
506
+ # hex-encoded string. (Since ':chunk-signature' requires
507
+ # 'bytes' type)
357
508
  #
358
- # Since ':chunk-signature' header value has bytes type, the signature value provided
359
- # needs to be a binary string instead of a hex-encoded string (like original signature
360
- # V4 algorithm). Thus, when returning signature value used for next event siging, the
361
- # signature value (a binary string) used at ':chunk-signature' needs to converted to
362
- # hex-encoded string using #unpack
363
- def sign_event(prior_signature, payload, encoder)
364
- # CRT does not currently provide event stream signing
365
- # use the Ruby implementation
366
- creds = @credentials_provider.credentials
367
- time = Time.now
368
- headers = {}
509
+ # Note:
510
+ # converting signature from binary string to hex-encoded
511
+ # string is handled at #sign_event instead. (Will be used
512
+ # as next prior signature for event signing)
513
+ def event_signature(secret_access_key, date, string_to_sign)
514
+ k_date = hmac("AWS4" + secret_access_key, date)
515
+ k_region = hmac(k_date, @region)
516
+ k_service = hmac(k_region, @service)
517
+ k_credentials = hmac(k_service, 'aws4_request')
518
+ hmac(k_credentials, string_to_sign)
519
+ end
369
520
 
370
- datetime = time.utc.strftime("%Y%m%dT%H%M%SZ")
371
- date = datetime[0,8]
372
- headers[':date'] = Aws::EventStream::HeaderValue.new(value: time.to_i * 1000, type: 'timestamp')
373
521
 
374
- sts = event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
375
- sig = event_signature(creds.secret_access_key, date, sts)
522
+ def path(url)
523
+ path = url.path
524
+ path = '/' if path == ''
525
+ if @uri_escape_path
526
+ uri_escape_path(path)
527
+ else
528
+ path
529
+ end
530
+ end
376
531
 
377
- headers[':chunk-signature'] = Aws::EventStream::HeaderValue.new(value: sig, type: 'bytes')
532
+ def normalized_querystring(querystring)
533
+ params = querystring.split('&')
534
+ params = params.map { |p| p.match(/=/) ? p : p + '=' }
535
+ # From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
536
+ # Sort the parameter names by character code point in ascending order.
537
+ # Parameters with duplicate names should be sorted by value.
538
+ #
539
+ # Default sort <=> in JRuby will swap members
540
+ # occasionally when <=> is 0 (considered still sorted), but this
541
+ # causes our normalized query string to not match the sent querystring.
542
+ # When names match, we then sort by their values. When values also
543
+ # match then we sort by their original order
544
+ params.each.with_index.sort do |a, b|
545
+ a, a_offset = a
546
+ b, b_offset = b
547
+ a_name, a_value = a.split('=')
548
+ b_name, b_value = b.split('=')
549
+ if a_name == b_name
550
+ if a_value == b_value
551
+ a_offset <=> b_offset
552
+ else
553
+ a_value <=> b_value
554
+ end
555
+ else
556
+ a_name <=> b_name
557
+ end
558
+ end.map(&:first).join('&')
559
+ end
378
560
 
379
- # Returning signed headers and signature value in hex-encoded string
380
- [headers, sig.unpack('H*').first]
561
+ def signed_headers(headers)
562
+ headers.inject([]) do |signed_headers, (header, _)|
563
+ if @unsigned_headers.include?(header)
564
+ signed_headers
565
+ else
566
+ signed_headers << header
567
+ end
568
+ end.sort.join(';')
381
569
  end
382
570
 
383
- private
571
+ def canonical_headers(headers)
572
+ headers = headers.inject([]) do |hdrs, (k,v)|
573
+ if @unsigned_headers.include?(k)
574
+ hdrs
575
+ else
576
+ hdrs << [k,v]
577
+ end
578
+ end
579
+ headers = headers.sort_by(&:first)
580
+ headers.map{|k,v| "#{k}:#{canonical_header_value(v.to_s)}" }.join("\n")
581
+ end
582
+
583
+ def canonical_header_value(value)
584
+ value.match(/^".*"$/) ? value : value.gsub(/\s+/, ' ').strip
585
+ end
586
+
587
+ def host(uri)
588
+ # Handles known and unknown URI schemes; default_port nil when unknown.
589
+ if uri.default_port == uri.port
590
+ uri.host
591
+ else
592
+ "#{uri.host}:#{uri.port}"
593
+ end
594
+ end
595
+
596
+ # @param [File, Tempfile, IO#read, String] value
597
+ # @return [String<SHA256 Hexdigest>]
598
+ def sha256_hexdigest(value)
599
+ if (File === value || Tempfile === value) && !value.path.nil? && File.exist?(value.path)
600
+ OpenSSL::Digest::SHA256.file(value).hexdigest
601
+ elsif value.respond_to?(:read)
602
+ sha256 = OpenSSL::Digest::SHA256.new
603
+ loop do
604
+ chunk = value.read(1024 * 1024) # 1MB
605
+ break unless chunk
606
+ sha256.update(chunk)
607
+ end
608
+ value.rewind
609
+ sha256.hexdigest
610
+ else
611
+ OpenSSL::Digest::SHA256.hexdigest(value)
612
+ end
613
+ end
614
+
615
+ def hmac(key, value)
616
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
617
+ end
618
+
619
+ def hexhmac(key, value)
620
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
621
+ end
384
622
 
385
623
  def extract_service(options)
386
624
  if options[:service]
387
625
  options[:service]
388
626
  else
389
- msg = 'missing required option :service'
627
+ msg = "missing required option :service"
390
628
  raise ArgumentError, msg
391
629
  end
392
630
  end
393
631
 
394
632
  def extract_region(options)
395
- options[:region] || raise(Errors::MissingRegionError)
633
+ if options[:region]
634
+ options[:region]
635
+ else
636
+ raise Errors::MissingRegionError
637
+ end
396
638
  end
397
639
 
398
640
  def extract_credentials_provider(options)
@@ -405,22 +647,11 @@ module Aws
405
647
  end
406
648
  end
407
649
 
408
- # the credentials used by CRT must be a
409
- # CRT StaticCredentialsProvider object
410
- def fetch_credentials
411
- credentials = @credentials_provider.credentials
412
- Aws::Crt::Auth::StaticCredentialsProvider.new(
413
- credentials.access_key_id,
414
- credentials.secret_access_key,
415
- credentials.session_token
416
- )
417
- end
418
-
419
650
  def extract_http_method(request)
420
651
  if request[:http_method]
421
652
  request[:http_method].upcase
422
653
  else
423
- msg = 'missing required option :http_method'
654
+ msg = "missing required option :http_method"
424
655
  raise ArgumentError, msg
425
656
  end
426
657
  end
@@ -429,101 +660,187 @@ module Aws
429
660
  if request[:url]
430
661
  URI.parse(request[:url].to_s)
431
662
  else
432
- msg = 'missing required option :url'
663
+ msg = "missing required option :url"
433
664
  raise ArgumentError, msg
434
665
  end
435
666
  end
436
667
 
437
668
  def downcase_headers(headers)
438
- (headers || {}).to_hash.transform_keys(&:downcase)
669
+ (headers || {}).to_hash.inject({}) do |hash, (key, value)|
670
+ hash[key.downcase] = value
671
+ hash
672
+ end
439
673
  end
440
674
 
441
- # @param [File, Tempfile, IO#read, String] value
442
- # @return [String<SHA256 Hexdigest>]
443
- def sha256_hexdigest(value)
444
- if (value.is_a?(File) || value.is_a?(Tempfile)) && !value.path.nil? && File.exist?(value.path)
445
- OpenSSL::Digest::SHA256.file(value).hexdigest
446
- elsif value.respond_to?(:read)
447
- sha256 = OpenSSL::Digest.new('SHA256')
448
- loop do
449
- chunk = value.read(1024 * 1024) # 1MB
450
- break unless chunk
451
-
452
- sha256.update(chunk)
453
- end
454
- value.rewind
455
- sha256.hexdigest
675
+ def extract_expires_in(options)
676
+ case options[:expires_in]
677
+ when nil then 900.to_s
678
+ when Integer then options[:expires_in].to_s
456
679
  else
457
- OpenSSL::Digest::SHA256.hexdigest(value)
680
+ msg = "expected :expires_in to be a number of seconds"
681
+ raise ArgumentError, msg
458
682
  end
459
683
  end
460
684
 
461
- def host(uri)
462
- # Handles known and unknown URI schemes; default_port nil when unknown.
463
- if uri.default_port == uri.port
464
- uri.host
685
+ def uri_escape(string)
686
+ self.class.uri_escape(string)
687
+ end
688
+
689
+ def uri_escape_path(string)
690
+ self.class.uri_escape_path(string)
691
+ end
692
+
693
+
694
+ def fetch_credentials
695
+ credentials = @credentials_provider.credentials
696
+ if credentials_set?(credentials)
697
+ credentials
465
698
  else
466
- "#{uri.host}:#{uri.port}"
699
+ raise Errors::MissingCredentialsError,
700
+ 'unable to sign request without credentials set'
467
701
  end
468
702
  end
469
703
 
470
- # Used only for event signing
471
- def credential_scope(date)
472
- [
473
- date,
474
- @region,
475
- @service,
476
- 'aws4_request',
477
- ].join('/')
704
+ # Returns true if credentials are set (not nil or empty)
705
+ # Credentials may not implement the Credentials interface
706
+ # and may just be credential like Client response objects
707
+ # (eg those returned by sts#assume_role)
708
+ def credentials_set?(credentials)
709
+ !credentials.access_key_id.nil? &&
710
+ !credentials.access_key_id.empty? &&
711
+ !credentials.secret_access_key.nil? &&
712
+ !credentials.secret_access_key.empty?
478
713
  end
479
714
 
480
- # Used only for event signing
481
- def hmac(key, value)
482
- OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
715
+ ### CRT Code
716
+
717
+ # the credentials used by CRT must be a
718
+ # CRT StaticCredentialsProvider object
719
+ def crt_fetch_credentials
720
+ creds = fetch_credentials
721
+ Aws::Crt::Auth::StaticCredentialsProvider.new(
722
+ creds.access_key_id,
723
+ creds.secret_access_key,
724
+ creds.session_token
725
+ )
483
726
  end
484
727
 
485
- # Compared to original #string_to_sign at signature v4 algorithm
486
- # there is no canonical_request concept for an eventstream event,
487
- # instead, an event contains headers and payload two parts, and
488
- # they will be used for computing digest in #event_string_to_sign
489
- #
490
- # Note:
491
- # While headers need to be encoded under eventstream format,
492
- # payload used is already eventstream encoded (event without signature),
493
- # thus no extra encoding is needed.
494
- def event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
495
- encoded_headers = encoder.encode_headers(
496
- Aws::EventStream::Message.new(headers: headers, payload: payload)
728
+ def crt_sign_request(request)
729
+ creds = crt_fetch_credentials
730
+ http_method = extract_http_method(request)
731
+ url = extract_url(request)
732
+ headers = downcase_headers(request[:headers])
733
+
734
+ datetime =
735
+ if headers.include? 'x-amz-date'
736
+ Time.parse(headers.delete('x-amz-date'))
737
+ end
738
+
739
+ content_sha256 = headers.delete('x-amz-content-sha256')
740
+ content_sha256 ||= sha256_hexdigest(request[:body] || '')
741
+
742
+ sigv4_headers = {}
743
+ sigv4_headers['host'] = headers['host'] || host(url)
744
+
745
+ # Modify the user-agent to add usage of crt-signer
746
+ # This should be temporary during developer preview only
747
+ if headers.include? 'user-agent'
748
+ headers['user-agent'] = "#{headers['user-agent']} crt-signer/#{@signing_algorithm}/#{Aws::Sigv4::VERSION}"
749
+ sigv4_headers['user-agent'] = headers['user-agent']
750
+ end
751
+
752
+ headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
753
+
754
+ config = Aws::Crt::Auth::SigningConfig.new(
755
+ algorithm: @signing_algorithm,
756
+ signature_type: :http_request_headers,
757
+ region: @region,
758
+ service: @service,
759
+ date: datetime,
760
+ signed_body_value: content_sha256,
761
+ signed_body_header_type: @apply_checksum_header ?
762
+ :sbht_content_sha256 : :sbht_none,
763
+ credentials: creds,
764
+ unsigned_headers: @unsigned_headers,
765
+ use_double_uri_encode: @uri_escape_path,
766
+ should_normalize_uri_path: @normalize_path,
767
+ omit_session_token: @omit_session_token
768
+ )
769
+ http_request = Aws::Crt::Http::Message.new(
770
+ http_method, url.to_s, headers
771
+ )
772
+ signable = Aws::Crt::Auth::Signable.new(http_request)
773
+
774
+ signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable)
775
+
776
+ Signature.new(
777
+ headers: sigv4_headers.merge(
778
+ downcase_headers(signing_result[:headers])
779
+ ),
780
+ string_to_sign: 'CRT_INTERNAL',
781
+ canonical_request: 'CRT_INTERNAL',
782
+ content_sha256: content_sha256,
783
+ extra: {config: config, signable: signable}
497
784
  )
498
- [
499
- "AWS4-HMAC-SHA256-PAYLOAD",
500
- datetime,
501
- credential_scope(datetime[0,8]),
502
- prior_signature,
503
- sha256_hexdigest(encoded_headers),
504
- sha256_hexdigest(payload)
505
- ].join("\n")
506
785
  end
507
786
 
508
- # Comparing to original signature v4 algorithm,
509
- # returned signature is a binary string instread of
510
- # hex-encoded string. (Since ':chunk-signature' requires
511
- # 'bytes' type)
512
- #
513
- # Note:
514
- # converting signature from binary string to hex-encoded
515
- # string is handled at #sign_event instead. (Will be used
516
- # as next prior signature for event signing)
517
- def event_signature(secret_access_key, date, string_to_sign)
518
- k_date = hmac("AWS4" + secret_access_key, date)
519
- k_region = hmac(k_date, @region)
520
- k_service = hmac(k_region, @service)
521
- k_credentials = hmac(k_service, 'aws4_request')
522
- hmac(k_credentials, string_to_sign)
787
+ def crt_presign_url(options)
788
+ creds = crt_fetch_credentials
789
+
790
+ http_method = extract_http_method(options)
791
+ url = extract_url(options)
792
+ headers = downcase_headers(options[:headers])
793
+ headers['host'] ||= host(url)
794
+
795
+ datetime = headers.delete('x-amz-date')
796
+ datetime ||= (options[:time] || Time.now)
797
+
798
+ content_sha256 = headers.delete('x-amz-content-sha256')
799
+ content_sha256 ||= options[:body_digest]
800
+ content_sha256 ||= sha256_hexdigest(options[:body] || '')
801
+
802
+ config = Aws::Crt::Auth::SigningConfig.new(
803
+ algorithm: @signing_algorithm,
804
+ signature_type: :http_request_query_params,
805
+ region: @region,
806
+ service: @service,
807
+ date: datetime,
808
+ signed_body_value: content_sha256,
809
+ signed_body_header_type: @apply_checksum_header ?
810
+ :sbht_content_sha256 : :sbht_none,
811
+ credentials: creds,
812
+ unsigned_headers: @unsigned_headers,
813
+ use_double_uri_encode: @uri_escape_path,
814
+ should_normalize_uri_path: @normalize_path,
815
+ omit_session_token: @omit_session_token,
816
+ expiration_in_seconds: options.fetch(:expires_in, 900)
817
+ )
818
+ http_request = Aws::Crt::Http::Message.new(
819
+ http_method, url.to_s, headers
820
+ )
821
+ signable = Aws::Crt::Auth::Signable.new(http_request)
822
+
823
+ signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable, http_method, url.to_s)
824
+ url = URI.parse(signing_result[:path])
825
+
826
+ if options[:extra] && options[:extra].is_a?(Hash)
827
+ options[:extra][:config] = config
828
+ options[:extra][:signable] = signable
829
+ end
830
+ url
523
831
  end
524
832
 
525
833
  class << self
526
834
 
835
+ def use_crt?
836
+ begin
837
+ require 'aws-crt'
838
+ return true
839
+ rescue LoadError
840
+ return false
841
+ end
842
+ end
843
+
527
844
  # @api private
528
845
  def uri_escape_path(path)
529
846
  path.gsub(/[^\/]+/) { |part| uri_escape(part) }
@@ -537,8 +854,8 @@ module Aws
537
854
  CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
538
855
  end
539
856
  end
857
+
540
858
  end
541
859
  end
542
860
  end
543
861
  end
544
-
data/lib/aws-sigv4.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aws-crt'
4
3
  require_relative 'aws-sigv4/credentials'
5
4
  require_relative 'aws-sigv4/errors'
6
5
  require_relative 'aws-sigv4/signature'
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-sigv4
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1.crt
4
+ version: 1.5.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: 2021-09-02 00:00:00.000000000 Z
11
+ date: 2022-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: aws-crt
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: aws-eventstream
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -44,22 +30,9 @@ dependencies:
44
30
  - - ">="
45
31
  - !ruby/object:Gem::Version
46
32
  version: 1.0.2
47
- - !ruby/object:Gem::Dependency
48
- name: rspec
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- description: Amazon Web Services signing library. Generates signatures for HTTP requests
62
- email:
33
+ description: Amazon Web Services Signature Version 4 signing library. Generates sigv4
34
+ signature for HTTP requests.
35
+ email:
63
36
  executables: []
64
37
  extensions: []
65
38
  extra_rdoc_files: []
@@ -70,13 +43,16 @@ files:
70
43
  - lib/aws-sigv4.rb
71
44
  - lib/aws-sigv4/credentials.rb
72
45
  - lib/aws-sigv4/errors.rb
46
+ - lib/aws-sigv4/request.rb
73
47
  - lib/aws-sigv4/signature.rb
74
48
  - lib/aws-sigv4/signer.rb
75
- homepage: https://github.com/awslabs/aws-crt-ruby
49
+ homepage: https://github.com/aws/aws-sdk-ruby
76
50
  licenses:
77
51
  - Apache-2.0
78
- metadata: {}
79
- post_install_message:
52
+ metadata:
53
+ source_code_uri: https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/aws-sigv4
54
+ changelog_uri: https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/aws-sigv4/CHANGELOG.md
55
+ post_install_message:
80
56
  rdoc_options: []
81
57
  require_paths:
82
58
  - lib
@@ -84,15 +60,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
60
  requirements:
85
61
  - - ">="
86
62
  - !ruby/object:Gem::Version
87
- version: '2.5'
63
+ version: '2.3'
88
64
  required_rubygems_version: !ruby/object:Gem::Requirement
89
65
  requirements:
90
- - - ">"
66
+ - - ">="
91
67
  - !ruby/object:Gem::Version
92
- version: 1.3.1
68
+ version: '0'
93
69
  requirements: []
94
- rubygems_version: 3.2.7
95
- signing_key:
70
+ rubygems_version: 3.1.6
71
+ signing_key:
96
72
  specification_version: 4
97
- summary: AWS SDK for Ruby - Common Runtime (CRT) based Signer
73
+ summary: AWS Signature Version 4 library.
98
74
  test_files: []