aws-sigv4 1.4.0 → 1.4.1.crt

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: 46dbb72f9e31ff1022703f91ae7e1d2cfe78c78629e682fe99468933704aff62
4
- data.tar.gz: 963db4a8f39031b64398dea0d2ebc7167af20e79446e23bfd270c6cb85d4738f
3
+ metadata.gz: 9f4ebc6670c011eafdb6f2ca33ee5894dd723f10f1ad23ea296837bb41355a77
4
+ data.tar.gz: 6e69783f73330b8559390d56718e0fe48cb117f4fc07ae8c808a2d6ebea6263f
5
5
  SHA512:
6
- metadata.gz: '0142942e58db9971d8ceaa2aeb7c97d1cd58bdba463366fd06831b1769a3c124f6e1f1b082c4d1b5917f794ab70556f6bd78dbb67824a35e74ccbfc5bdaafa25'
7
- data.tar.gz: aca23ad7a8a98f24abdbbaa2afe9cde1430a7a10f627b63d7da56939665056ccc7a04226dc14e3793e3848032291aba990fc10576369f3974911593566ea9262
6
+ metadata.gz: 2ba3f4da7f88b55f0529e9d23a4cc6a039abb6e1af7d939a9fe2679232c28ba80f7b2791758d39f67940cfa180c631f92ec424753a38f346b290132a95263ba6
7
+ data.tar.gz: 9fdee7aeff9a4764bb7a2395628ba5a9e52b8fe1e21adf165b48ed6241e3a2b71b7cff2d7597dba2696e49009963589244410e276898a3807945cb659865a0b6
data/CHANGELOG.md CHANGED
@@ -1,15 +1,11 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
- 1.4.0 (2021-09-02)
5
- ------------------
6
-
7
- * Feature - add `signing_algorithm` option with `sigv4` default.
8
4
 
9
- 1.3.0 (2021-09-01)
5
+ 1.3.0.crt (2021-08-04)
10
6
  ------------------
11
7
 
12
- * Feature - AWS SDK for Ruby no longer supports Ruby runtime versions 1.9, 2.0, 2.1, and 2.2.
8
+ * Feature - Preview release of `aws-sigv4` version 1.3.0.crt gem - uses the Common Runtime (CRT) for signing and support for sigv4a.
13
9
 
14
10
  1.2.4 (2021-07-08)
15
11
  ------------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0
1
+ 1.4.1.crt
@@ -11,7 +11,10 @@ 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]
14
+ if options[:access_key_id] && options[:secret_access_key] &&
15
+ !options[:access_key_id].empty? &&
16
+ !options[:secret_access_key].empty?
17
+
15
18
  @access_key_id = options[:access_key_id]
16
19
  @secret_access_key = options[:secret_access_key]
17
20
  @session_token = options[:session_token]
@@ -51,8 +54,8 @@ module Aws
51
54
  # @option options [String] :session_token (nil)
52
55
  def initialize(options = {})
53
56
  @credentials = options[:credentials] ?
54
- options[:credentials] :
55
- Credentials.new(options)
57
+ options[:credentials] :
58
+ Credentials.new(options)
56
59
  end
57
60
 
58
61
  # @return [Credentials]
@@ -63,6 +66,5 @@ module Aws
63
66
  !!credentials && credentials.set?
64
67
  end
65
68
  end
66
-
67
69
  end
68
70
  end
@@ -32,6 +32,8 @@ module Aws
32
32
  # @return [String] For debugging purposes.
33
33
  attr_accessor :content_sha256
34
34
 
35
+ attr_accessor :extra
36
+
35
37
  end
36
38
  end
37
39
  end
@@ -1,79 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openssl'
4
- require 'tempfile'
5
4
  require 'time'
5
+ require 'tempfile'
6
6
  require 'uri'
7
7
  require 'set'
8
- require 'cgi'
9
8
  require 'aws-eventstream'
10
9
 
11
10
  module Aws
12
11
  module Sigv4
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
- #
12
+ # Utility class for creating AWS signature version 4 signature.
75
13
  class Signer
76
-
77
14
  # @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
78
15
  # @param [String] :service The service signing name, e.g. 's3'.
79
16
  # @param [String] :region The region name, e.g. 'us-east-1'.
@@ -118,23 +55,27 @@ module Aws
118
55
  # headers. This is required for AWS Glacier, and optional for
119
56
  # every other AWS service as of late 2016.
120
57
  #
58
+ # @option options [Boolean] :omit_session_token (false) If `true`,
59
+ # then security token is added to the final signing result,
60
+ # but is treated as "unsigned" and does not contribute
61
+ # to the authorization signature.
62
+ #
63
+ # @option options [Boolean] :normalize_path (true) When `true`,
64
+ # the uri paths will be normalized when building the canonical request
121
65
  def initialize(options = {})
122
66
  @service = extract_service(options)
123
67
  @region = extract_region(options)
124
68
  @credentials_provider = extract_credentials_provider(options)
125
- @unsigned_headers = Set.new((options.fetch(:unsigned_headers, [])).map(&:downcase))
69
+ @unsigned_headers = Set.new((options.fetch(:unsigned_headers, []))
70
+ .map(&:downcase))
126
71
  @unsigned_headers << 'authorization'
127
72
  @unsigned_headers << 'x-amzn-trace-id'
128
73
  @unsigned_headers << 'expect'
129
- [:uri_escape_path, :apply_checksum_header].each do |opt|
130
- instance_variable_set("@#{opt}", options.key?(opt) ? !!options[opt] : true)
131
- end
132
-
133
- if options[:signing_algorithm] == :sigv4a
134
- raise ArgumentError, 'You are attempting to sign a' \
135
- ' request with sigv4a which requires aws-crt and version 1.4.0.crt or later of the aws-sigv4 gem.'\
136
- ' Please install the gem or add it to your gemfile.'
137
- end
74
+ @uri_escape_path = options.fetch(:uri_escape_path, true)
75
+ @apply_checksum_header = options.fetch(:apply_checksum_header, true)
76
+ @signing_algorithm = options.fetch(:signing_algorithm, :sigv4)
77
+ @normalize_path = options.fetch(:normalize_path, true)
78
+ @omit_session_token = options.fetch(:omit_session_token, false)
138
79
  end
139
80
 
140
81
  # @return [String]
@@ -210,102 +151,65 @@ module Aws
210
151
  # a `#headers` method. The headers must be applied to your request.
211
152
  #
212
153
  def sign_request(request)
213
-
214
154
  creds = fetch_credentials
215
155
 
216
156
  http_method = extract_http_method(request)
217
157
  url = extract_url(request)
218
158
  headers = downcase_headers(request[:headers])
219
159
 
220
- datetime = headers['x-amz-date']
221
- datetime ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
222
- date = datetime[0,8]
160
+ datetime =
161
+ if headers.include? 'x-amz-date'
162
+ Time.parse(headers.delete('x-amz-date'))
163
+ end
223
164
 
224
- content_sha256 = headers['x-amz-content-sha256']
165
+ content_sha256 = headers.delete('x-amz-content-sha256')
225
166
  content_sha256 ||= sha256_hexdigest(request[:body] || '')
226
167
 
227
168
  sigv4_headers = {}
228
169
  sigv4_headers['host'] = headers['host'] || host(url)
229
- sigv4_headers['x-amz-date'] = datetime
230
- sigv4_headers['x-amz-security-token'] = creds.session_token if creds.session_token
231
- sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
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
232
177
 
233
178
  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
234
179
 
235
- # compute signature parts
236
- creq = canonical_request(http_method, url, headers, content_sha256)
237
- sts = string_to_sign(datetime, creq)
238
- sig = signature(creds.secret_access_key, date, sts)
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)
239
199
 
240
- # apply signature
241
- sigv4_headers['authorization'] = [
242
- "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
243
- "SignedHeaders=#{signed_headers(headers)}",
244
- "Signature=#{sig}",
245
- ].join(', ')
200
+ signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable)
246
201
 
247
- # Returning the signature components.
248
202
  Signature.new(
249
- headers: sigv4_headers,
250
- string_to_sign: sts,
251
- canonical_request: creq,
252
- content_sha256: content_sha256
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}
253
210
  )
254
211
  end
255
212
 
256
- # Signs a event and returns signature headers and prior signature
257
- # used for next event signing.
258
- #
259
- # Headers of a sigv4 signed event message only contains 2 headers
260
- # * ':chunk-signature'
261
- # * computed signature of the event, binary string, 'bytes' type
262
- # * ':date'
263
- # * millisecond since epoch, 'timestamp' type
264
- #
265
- # Payload of the sigv4 signed event message contains eventstream encoded message
266
- # which is serialized based on input and protocol
267
- #
268
- # To sign events
269
- #
270
- # headers_0, signature_0 = signer.sign_event(
271
- # prior_signature, # hex-encoded string
272
- # payload_0, # binary string (eventstream encoded event 0)
273
- # encoder, # Aws::EventStreamEncoder
274
- # )
275
- #
276
- # headers_1, signature_1 = signer.sign_event(
277
- # signature_0,
278
- # payload_1, # binary string (eventstream encoded event 1)
279
- # encoder
280
- # )
281
- #
282
- # The initial prior_signature should be using the signature computed at initial request
283
- #
284
- # Note:
285
- #
286
- # Since ':chunk-signature' header value has bytes type, the signature value provided
287
- # needs to be a binary string instead of a hex-encoded string (like original signature
288
- # V4 algorithm). Thus, when returning signature value used for next event siging, the
289
- # signature value (a binary string) used at ':chunk-signature' needs to converted to
290
- # hex-encoded string using #unpack
291
- def sign_event(prior_signature, payload, encoder)
292
- creds = fetch_credentials
293
- time = Time.now
294
- headers = {}
295
-
296
- datetime = time.utc.strftime("%Y%m%dT%H%M%SZ")
297
- date = datetime[0,8]
298
- headers[':date'] = Aws::EventStream::HeaderValue.new(value: time.to_i * 1000, type: 'timestamp')
299
-
300
- sts = event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
301
- sig = event_signature(creds.secret_access_key, date, sts)
302
-
303
- headers[':chunk-signature'] = Aws::EventStream::HeaderValue.new(value: sig, type: 'bytes')
304
-
305
- # Returning signed headers and signature value in hex-encoded string
306
- [headers, sig.unpack('H*').first]
307
- end
308
-
309
213
  # Signs a URL with query authentication. Using query parameters
310
214
  # to authenticate requests is useful when you want to express a
311
215
  # request entirely in a URL. This method is also referred as
@@ -375,247 +279,120 @@ module Aws
375
279
  # @return [HTTPS::URI, HTTP::URI]
376
280
  #
377
281
  def presign_url(options)
378
-
379
282
  creds = fetch_credentials
380
283
 
381
284
  http_method = extract_http_method(options)
382
285
  url = extract_url(options)
383
-
384
286
  headers = downcase_headers(options[:headers])
385
287
  headers['host'] ||= host(url)
386
288
 
387
- datetime = headers['x-amz-date']
388
- datetime ||= (options[:time] || Time.now).utc.strftime("%Y%m%dT%H%M%SZ")
389
- date = datetime[0,8]
289
+ datetime = headers.delete('x-amz-date')
290
+ datetime ||= (options[:time] || Time.now)
390
291
 
391
- content_sha256 = headers['x-amz-content-sha256']
292
+ content_sha256 = headers.delete('x-amz-content-sha256')
392
293
  content_sha256 ||= options[:body_digest]
393
294
  content_sha256 ||= sha256_hexdigest(options[:body] || '')
394
295
 
395
- params = {}
396
- params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
397
- params['X-Amz-Credential'] = credential(creds, date)
398
- params['X-Amz-Date'] = datetime
399
- params['X-Amz-Expires'] = extract_expires_in(options)
400
- params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
401
- params['X-Amz-SignedHeaders'] = signed_headers(headers)
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)
402
316
 
403
- params = params.map do |key, value|
404
- "#{uri_escape(key)}=#{uri_escape(value)}"
405
- end.join('&')
317
+ signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable, http_method, url.to_s)
318
+ url = URI.parse(signing_result[:path])
406
319
 
407
- if url.query
408
- url.query += '&' + params
409
- else
410
- url.query = params
320
+ if options[:extra] && options[:extra].is_a?(Hash)
321
+ options[:extra][:config] = config
322
+ options[:extra][:signable] = signable
411
323
  end
412
-
413
- creq = canonical_request(http_method, url, headers, content_sha256)
414
- sts = string_to_sign(datetime, creq)
415
- url.query += '&X-Amz-Signature=' + signature(creds.secret_access_key, date, sts)
416
324
  url
417
325
  end
418
326
 
419
- private
420
-
421
- def canonical_request(http_method, url, headers, content_sha256)
422
- [
423
- http_method,
424
- path(url),
425
- normalized_querystring(url.query || ''),
426
- canonical_headers(headers) + "\n",
427
- signed_headers(headers),
428
- content_sha256,
429
- ].join("\n")
430
- end
431
-
432
- def string_to_sign(datetime, canonical_request)
433
- [
434
- 'AWS4-HMAC-SHA256',
435
- datetime,
436
- credential_scope(datetime[0,8]),
437
- sha256_hexdigest(canonical_request),
438
- ].join("\n")
439
- end
440
327
 
441
- # Compared to original #string_to_sign at signature v4 algorithm
442
- # there is no canonical_request concept for an eventstream event,
443
- # instead, an event contains headers and payload two parts, and
444
- # they will be used for computing digest in #event_string_to_sign
328
+ # Signs a event and returns signature headers and prior signature
329
+ # used for next event signing.
445
330
  #
446
- # Note:
447
- # While headers need to be encoded under eventstream format,
448
- # payload used is already eventstream encoded (event without signature),
449
- # thus no extra encoding is needed.
450
- def event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
451
- encoded_headers = encoder.encode_headers(
452
- Aws::EventStream::Message.new(headers: headers, payload: payload)
453
- )
454
- [
455
- "AWS4-HMAC-SHA256-PAYLOAD",
456
- datetime,
457
- credential_scope(datetime[0,8]),
458
- prior_signature,
459
- sha256_hexdigest(encoded_headers),
460
- sha256_hexdigest(payload)
461
- ].join("\n")
462
- end
463
-
464
- def credential_scope(date)
465
- [
466
- date,
467
- @region,
468
- @service,
469
- 'aws4_request',
470
- ].join('/')
471
- end
472
-
473
- def credential(credentials, date)
474
- "#{credentials.access_key_id}/#{credential_scope(date)}"
475
- end
476
-
477
- def signature(secret_access_key, date, string_to_sign)
478
- k_date = hmac("AWS4" + secret_access_key, date)
479
- k_region = hmac(k_date, @region)
480
- k_service = hmac(k_region, @service)
481
- k_credentials = hmac(k_service, 'aws4_request')
482
- hexhmac(k_credentials, string_to_sign)
483
- end
484
-
485
- # Comparing to original signature v4 algorithm,
486
- # returned signature is a binary string instread of
487
- # hex-encoded string. (Since ':chunk-signature' requires
488
- # 'bytes' type)
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
489
355
  #
490
356
  # Note:
491
- # converting signature from binary string to hex-encoded
492
- # string is handled at #sign_event instead. (Will be used
493
- # as next prior signature for event signing)
494
- def event_signature(secret_access_key, date, string_to_sign)
495
- k_date = hmac("AWS4" + secret_access_key, date)
496
- k_region = hmac(k_date, @region)
497
- k_service = hmac(k_region, @service)
498
- k_credentials = hmac(k_service, 'aws4_request')
499
- hmac(k_credentials, string_to_sign)
500
- end
501
-
502
-
503
- def path(url)
504
- path = url.path
505
- path = '/' if path == ''
506
- if @uri_escape_path
507
- uri_escape_path(path)
508
- else
509
- path
510
- end
511
- end
512
-
513
- def normalized_querystring(querystring)
514
- params = querystring.split('&')
515
- params = params.map { |p| p.match(/=/) ? p : p + '=' }
516
- # From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
517
- # Sort the parameter names by character code point in ascending order.
518
- # Parameters with duplicate names should be sorted by value.
519
- #
520
- # Default sort <=> in JRuby will swap members
521
- # occasionally when <=> is 0 (considered still sorted), but this
522
- # causes our normalized query string to not match the sent querystring.
523
- # When names match, we then sort by their values. When values also
524
- # match then we sort by their original order
525
- params.each.with_index.sort do |a, b|
526
- a, a_offset = a
527
- b, b_offset = b
528
- a_name, a_value = a.split('=')
529
- b_name, b_value = b.split('=')
530
- if a_name == b_name
531
- if a_value == b_value
532
- a_offset <=> b_offset
533
- else
534
- a_value <=> b_value
535
- end
536
- else
537
- a_name <=> b_name
538
- end
539
- end.map(&:first).join('&')
540
- end
541
-
542
- def signed_headers(headers)
543
- headers.inject([]) do |signed_headers, (header, _)|
544
- if @unsigned_headers.include?(header)
545
- signed_headers
546
- else
547
- signed_headers << header
548
- end
549
- end.sort.join(';')
550
- end
551
-
552
- def canonical_headers(headers)
553
- headers = headers.inject([]) do |hdrs, (k,v)|
554
- if @unsigned_headers.include?(k)
555
- hdrs
556
- else
557
- hdrs << [k,v]
558
- end
559
- end
560
- headers = headers.sort_by(&:first)
561
- headers.map{|k,v| "#{k}:#{canonical_header_value(v.to_s)}" }.join("\n")
562
- end
357
+ #
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 = {}
563
369
 
564
- def canonical_header_value(value)
565
- value.match(/^".*"$/) ? value : value.gsub(/\s+/, ' ').strip
566
- end
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')
567
373
 
568
- def host(uri)
569
- # Handles known and unknown URI schemes; default_port nil when unknown.
570
- if uri.default_port == uri.port
571
- uri.host
572
- else
573
- "#{uri.host}:#{uri.port}"
574
- end
575
- end
374
+ sts = event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
375
+ sig = event_signature(creds.secret_access_key, date, sts)
576
376
 
577
- # @param [File, Tempfile, IO#read, String] value
578
- # @return [String<SHA256 Hexdigest>]
579
- def sha256_hexdigest(value)
580
- if (File === value || Tempfile === value) && !value.path.nil? && File.exist?(value.path)
581
- OpenSSL::Digest::SHA256.file(value).hexdigest
582
- elsif value.respond_to?(:read)
583
- sha256 = OpenSSL::Digest::SHA256.new
584
- loop do
585
- chunk = value.read(1024 * 1024) # 1MB
586
- break unless chunk
587
- sha256.update(chunk)
588
- end
589
- value.rewind
590
- sha256.hexdigest
591
- else
592
- OpenSSL::Digest::SHA256.hexdigest(value)
593
- end
594
- end
377
+ headers[':chunk-signature'] = Aws::EventStream::HeaderValue.new(value: sig, type: 'bytes')
595
378
 
596
- def hmac(key, value)
597
- OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
379
+ # Returning signed headers and signature value in hex-encoded string
380
+ [headers, sig.unpack('H*').first]
598
381
  end
599
382
 
600
- def hexhmac(key, value)
601
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
602
- end
383
+ private
603
384
 
604
385
  def extract_service(options)
605
386
  if options[:service]
606
387
  options[:service]
607
388
  else
608
- msg = "missing required option :service"
389
+ msg = 'missing required option :service'
609
390
  raise ArgumentError, msg
610
391
  end
611
392
  end
612
393
 
613
394
  def extract_region(options)
614
- if options[:region]
615
- options[:region]
616
- else
617
- raise Errors::MissingRegionError
618
- end
395
+ options[:region] || raise(Errors::MissingRegionError)
619
396
  end
620
397
 
621
398
  def extract_credentials_provider(options)
@@ -628,11 +405,22 @@ module Aws
628
405
  end
629
406
  end
630
407
 
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
+
631
419
  def extract_http_method(request)
632
420
  if request[:http_method]
633
421
  request[:http_method].upcase
634
422
  else
635
- msg = "missing required option :http_method"
423
+ msg = 'missing required option :http_method'
636
424
  raise ArgumentError, msg
637
425
  end
638
426
  end
@@ -641,56 +429,97 @@ module Aws
641
429
  if request[:url]
642
430
  URI.parse(request[:url].to_s)
643
431
  else
644
- msg = "missing required option :url"
432
+ msg = 'missing required option :url'
645
433
  raise ArgumentError, msg
646
434
  end
647
435
  end
648
436
 
649
437
  def downcase_headers(headers)
650
- (headers || {}).to_hash.inject({}) do |hash, (key, value)|
651
- hash[key.downcase] = value
652
- hash
653
- end
438
+ (headers || {}).to_hash.transform_keys(&:downcase)
654
439
  end
655
440
 
656
- def extract_expires_in(options)
657
- case options[:expires_in]
658
- when nil then 900.to_s
659
- when Integer then options[:expires_in].to_s
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
660
456
  else
661
- msg = "expected :expires_in to be a number of seconds"
662
- raise ArgumentError, msg
457
+ OpenSSL::Digest::SHA256.hexdigest(value)
663
458
  end
664
459
  end
665
460
 
666
- def uri_escape(string)
667
- self.class.uri_escape(string)
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
465
+ else
466
+ "#{uri.host}:#{uri.port}"
467
+ end
668
468
  end
669
469
 
670
- def uri_escape_path(string)
671
- self.class.uri_escape_path(string)
470
+ # Used only for event signing
471
+ def credential_scope(date)
472
+ [
473
+ date,
474
+ @region,
475
+ @service,
476
+ 'aws4_request',
477
+ ].join('/')
672
478
  end
673
479
 
480
+ # Used only for event signing
481
+ def hmac(key, value)
482
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
483
+ end
674
484
 
675
- def fetch_credentials
676
- credentials = @credentials_provider.credentials
677
- if credentials_set?(credentials)
678
- credentials
679
- else
680
- raise Errors::MissingCredentialsError,
681
- 'unable to sign request without credentials set'
682
- end
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)
497
+ )
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")
683
506
  end
684
507
 
685
- # Returns true if credentials are set (not nil or empty)
686
- # Credentials may not implement the Credentials interface
687
- # and may just be credential like Client response objects
688
- # (eg those returned by sts#assume_role)
689
- def credentials_set?(credentials)
690
- !credentials.access_key_id.nil? &&
691
- !credentials.access_key_id.empty? &&
692
- !credentials.secret_access_key.nil? &&
693
- !credentials.secret_access_key.empty?
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)
694
523
  end
695
524
 
696
525
  class << self
@@ -708,8 +537,8 @@ module Aws
708
537
  CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
709
538
  end
710
539
  end
711
-
712
540
  end
713
541
  end
714
542
  end
715
543
  end
544
+
data/lib/aws-sigv4.rb CHANGED
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aws-crt'
3
4
  require_relative 'aws-sigv4/credentials'
4
5
  require_relative 'aws-sigv4/errors'
5
6
  require_relative 'aws-sigv4/signature'
6
7
  require_relative 'aws-sigv4/signer'
8
+
9
+ module Aws
10
+ module Sigv4
11
+ VERSION = File.read(File.expand_path('../VERSION', __dir__)).strip
12
+ end
13
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-sigv4
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1.crt
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
11
  date: 2021-09-02 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'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: aws-eventstream
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -30,9 +44,22 @@ dependencies:
30
44
  - - ">="
31
45
  - !ruby/object:Gem::Version
32
46
  version: 1.0.2
33
- description: Amazon Web Services Signature Version 4 signing library. Generates sigv4
34
- signature for HTTP requests.
35
- email:
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:
36
63
  executables: []
37
64
  extensions: []
38
65
  extra_rdoc_files: []
@@ -43,16 +70,13 @@ files:
43
70
  - lib/aws-sigv4.rb
44
71
  - lib/aws-sigv4/credentials.rb
45
72
  - lib/aws-sigv4/errors.rb
46
- - lib/aws-sigv4/request.rb
47
73
  - lib/aws-sigv4/signature.rb
48
74
  - lib/aws-sigv4/signer.rb
49
- homepage: https://github.com/aws/aws-sdk-ruby
75
+ homepage: https://github.com/awslabs/aws-crt-ruby
50
76
  licenses:
51
77
  - Apache-2.0
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:
78
+ metadata: {}
79
+ post_install_message:
56
80
  rdoc_options: []
57
81
  require_paths:
58
82
  - lib
@@ -60,15 +84,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
84
  requirements:
61
85
  - - ">="
62
86
  - !ruby/object:Gem::Version
63
- version: '2.3'
87
+ version: '2.5'
64
88
  required_rubygems_version: !ruby/object:Gem::Requirement
65
89
  requirements:
66
- - - ">="
90
+ - - ">"
67
91
  - !ruby/object:Gem::Version
68
- version: '0'
92
+ version: 1.3.1
69
93
  requirements: []
70
- rubygems_version: 3.1.6
71
- signing_key:
94
+ rubygems_version: 3.2.7
95
+ signing_key:
72
96
  specification_version: 4
73
- summary: AWS Signature Version 4 library.
97
+ summary: AWS SDK for Ruby - Common Runtime (CRT) based Signer
74
98
  test_files: []
@@ -1,65 +0,0 @@
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