aws-sigv4 1.4.1.crt → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []