aws-sigv4 1.4.0.crt → 1.4.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: a8f4bb2026a22b4dc16eb1e53bc233135225971f13d2e16e6df7874749095c9c
4
- data.tar.gz: 7a13586ac15fa24cb1c005ba27909c8687fc7416bd0bb81e8e8b9c4a89e0a161
3
+ metadata.gz: 46dbb72f9e31ff1022703f91ae7e1d2cfe78c78629e682fe99468933704aff62
4
+ data.tar.gz: 963db4a8f39031b64398dea0d2ebc7167af20e79446e23bfd270c6cb85d4738f
5
5
  SHA512:
6
- metadata.gz: f4720ab9cd966559519b0c803fc183f3ddbfa5c60bbdd0490fdef32b5d7ba0790438089d8b25130c4d40f32e81e42808e6a30cade92d3f0252d0131d53fcedc4
7
- data.tar.gz: e257d60db8a752a75a539aad1571595a8367bd3e7b4a1f1e25a81c8cc00393e787e55d8f28554e294fc6babf9048ed10e2536d40d37ba241bb931be22dd42e1e
6
+ metadata.gz: '0142942e58db9971d8ceaa2aeb7c97d1cd58bdba463366fd06831b1769a3c124f6e1f1b082c4d1b5917f794ab70556f6bd78dbb67824a35e74ccbfc5bdaafa25'
7
+ data.tar.gz: aca23ad7a8a98f24abdbbaa2afe9cde1430a7a10f627b63d7da56939665056ccc7a04226dc14e3793e3848032291aba990fc10576369f3974911593566ea9262
data/CHANGELOG.md CHANGED
@@ -1,11 +1,15 @@
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.
4
8
 
5
- 1.3.0.crt (2021-08-04)
9
+ 1.3.0 (2021-09-01)
6
10
  ------------------
7
11
 
8
- * Feature - Preview release of `aws-sigv4` version 1.3.0.crt gem - uses the Common Runtime (CRT) for signing and support for sigv4a.
12
+ * Feature - AWS SDK for Ruby no longer supports Ruby runtime versions 1.9, 2.0, 2.1, and 2.2.
9
13
 
10
14
  1.2.4 (2021-07-08)
11
15
  ------------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0.crt
1
+ 1.4.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,6 @@ module Aws
32
32
  # @return [String] For debugging purposes.
33
33
  attr_accessor :content_sha256
34
34
 
35
- attr_accessor :extra
36
-
37
35
  end
38
36
  end
39
37
  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,27 +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`,
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
65
121
  def initialize(options = {})
66
122
  @service = extract_service(options)
67
123
  @region = extract_region(options)
68
124
  @credentials_provider = extract_credentials_provider(options)
69
- @unsigned_headers = Set.new((options.fetch(:unsigned_headers, []))
70
- .map(&:downcase))
125
+ @unsigned_headers = Set.new((options.fetch(:unsigned_headers, [])).map(&:downcase))
71
126
  @unsigned_headers << 'authorization'
72
127
  @unsigned_headers << 'x-amzn-trace-id'
73
128
  @unsigned_headers << 'expect'
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)
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
79
138
  end
80
139
 
81
140
  # @return [String]
@@ -151,65 +210,102 @@ module Aws
151
210
  # a `#headers` method. The headers must be applied to your request.
152
211
  #
153
212
  def sign_request(request)
213
+
154
214
  creds = fetch_credentials
155
215
 
156
216
  http_method = extract_http_method(request)
157
217
  url = extract_url(request)
158
218
  headers = downcase_headers(request[:headers])
159
219
 
160
- datetime =
161
- if headers.include? 'x-amz-date'
162
- Time.parse(headers.delete('x-amz-date'))
163
- end
220
+ datetime = headers['x-amz-date']
221
+ datetime ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
222
+ date = datetime[0,8]
164
223
 
165
- content_sha256 = headers.delete('x-amz-content-sha256')
224
+ content_sha256 = headers['x-amz-content-sha256']
166
225
  content_sha256 ||= sha256_hexdigest(request[:body] || '')
167
226
 
168
227
  sigv4_headers = {}
169
228
  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
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
177
232
 
178
233
  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
179
234
 
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)
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)
199
239
 
200
- signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable)
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(', ')
201
246
 
247
+ # Returning the signature components.
202
248
  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}
249
+ headers: sigv4_headers,
250
+ string_to_sign: sts,
251
+ canonical_request: creq,
252
+ content_sha256: content_sha256
210
253
  )
211
254
  end
212
255
 
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
+
213
309
  # Signs a URL with query authentication. Using query parameters
214
310
  # to authenticate requests is useful when you want to express a
215
311
  # request entirely in a URL. This method is also referred as
@@ -279,120 +375,247 @@ module Aws
279
375
  # @return [HTTPS::URI, HTTP::URI]
280
376
  #
281
377
  def presign_url(options)
378
+
282
379
  creds = fetch_credentials
283
380
 
284
381
  http_method = extract_http_method(options)
285
382
  url = extract_url(options)
383
+
286
384
  headers = downcase_headers(options[:headers])
287
385
  headers['host'] ||= host(url)
288
386
 
289
- datetime = headers.delete('x-amz-date')
290
- datetime ||= (options[:time] || Time.now)
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]
291
390
 
292
- content_sha256 = headers.delete('x-amz-content-sha256')
391
+ content_sha256 = headers['x-amz-content-sha256']
293
392
  content_sha256 ||= options[:body_digest]
294
393
  content_sha256 ||= sha256_hexdigest(options[:body] || '')
295
394
 
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)
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)
316
402
 
317
- signing_result = Aws::Crt::Auth::Signer.sign_request(config, signable, http_method, url.to_s)
318
- url = URI.parse(signing_result[:path])
403
+ params = params.map do |key, value|
404
+ "#{uri_escape(key)}=#{uri_escape(value)}"
405
+ end.join('&')
319
406
 
320
- if options[:extra] && options[:extra].is_a?(Hash)
321
- options[:extra][:config] = config
322
- options[:extra][:signable] = signable
407
+ if url.query
408
+ url.query += '&' + params
409
+ else
410
+ url.query = params
323
411
  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)
324
416
  url
325
417
  end
326
418
 
419
+ private
327
420
 
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
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
+
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
355
445
  #
356
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)
357
489
  #
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 = {}
490
+ # 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
369
501
 
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
502
 
374
- sts = event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
375
- sig = event_signature(creds.secret_access_key, date, sts)
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
376
512
 
377
- headers[':chunk-signature'] = Aws::EventStream::HeaderValue.new(value: sig, type: 'bytes')
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
378
541
 
379
- # Returning signed headers and signature value in hex-encoded string
380
- [headers, sig.unpack('H*').first]
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(';')
381
550
  end
382
551
 
383
- private
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
563
+
564
+ def canonical_header_value(value)
565
+ value.match(/^".*"$/) ? value : value.gsub(/\s+/, ' ').strip
566
+ end
567
+
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
576
+
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
595
+
596
+ def hmac(key, value)
597
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
598
+ end
599
+
600
+ def hexhmac(key, value)
601
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
602
+ end
384
603
 
385
604
  def extract_service(options)
386
605
  if options[:service]
387
606
  options[:service]
388
607
  else
389
- msg = 'missing required option :service'
608
+ msg = "missing required option :service"
390
609
  raise ArgumentError, msg
391
610
  end
392
611
  end
393
612
 
394
613
  def extract_region(options)
395
- options[:region] || raise(Errors::MissingRegionError)
614
+ if options[:region]
615
+ options[:region]
616
+ else
617
+ raise Errors::MissingRegionError
618
+ end
396
619
  end
397
620
 
398
621
  def extract_credentials_provider(options)
@@ -405,22 +628,11 @@ module Aws
405
628
  end
406
629
  end
407
630
 
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
631
  def extract_http_method(request)
420
632
  if request[:http_method]
421
633
  request[:http_method].upcase
422
634
  else
423
- msg = 'missing required option :http_method'
635
+ msg = "missing required option :http_method"
424
636
  raise ArgumentError, msg
425
637
  end
426
638
  end
@@ -429,97 +641,56 @@ module Aws
429
641
  if request[:url]
430
642
  URI.parse(request[:url].to_s)
431
643
  else
432
- msg = 'missing required option :url'
644
+ msg = "missing required option :url"
433
645
  raise ArgumentError, msg
434
646
  end
435
647
  end
436
648
 
437
649
  def downcase_headers(headers)
438
- (headers || {}).to_hash.transform_keys(&:downcase)
439
- end
440
-
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
456
- else
457
- OpenSSL::Digest::SHA256.hexdigest(value)
650
+ (headers || {}).to_hash.inject({}) do |hash, (key, value)|
651
+ hash[key.downcase] = value
652
+ hash
458
653
  end
459
654
  end
460
655
 
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
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
465
660
  else
466
- "#{uri.host}:#{uri.port}"
661
+ msg = "expected :expires_in to be a number of seconds"
662
+ raise ArgumentError, msg
467
663
  end
468
664
  end
469
665
 
470
- # Used only for event signing
471
- def credential_scope(date)
472
- [
473
- date,
474
- @region,
475
- @service,
476
- 'aws4_request',
477
- ].join('/')
666
+ def uri_escape(string)
667
+ self.class.uri_escape(string)
478
668
  end
479
669
 
480
- # Used only for event signing
481
- def hmac(key, value)
482
- OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
670
+ def uri_escape_path(string)
671
+ self.class.uri_escape_path(string)
483
672
  end
484
673
 
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")
674
+
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
506
683
  end
507
684
 
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)
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?
523
694
  end
524
695
 
525
696
  class << self
@@ -537,8 +708,8 @@ module Aws
537
708
  CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
538
709
  end
539
710
  end
711
+
540
712
  end
541
713
  end
542
714
  end
543
715
  end
544
-
data/lib/aws-sigv4.rb CHANGED
@@ -1,13 +1,6 @@
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'
7
6
  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,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.0.crt
4
+ version: 1.4.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
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'
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
70
  rubygems_version: 3.1.6
95
- signing_key:
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: []