1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
|
# frozen_string_literal: true
require 'openssl'
require 'tempfile'
require 'time'
require 'uri'
require 'set'
require 'cgi'
require 'pathname'
require 'aws-eventstream'
module Aws
module Sigv4
# Utility class for creating AWS signature version 4 signature. This class
# provides two methods for generating signatures:
#
# * {#sign_request} - Computes a signature of the given request, returning
# the hash of headers that should be applied to the request.
#
# * {#presign_url} - Computes a presigned request with an expiration.
# By default, the body of this request is not signed and the request
# expires in 15 minutes.
#
# ## Configuration
#
# To use the signer, you need to specify the service, region, and credentials.
# The service name is normally the endpoint prefix to an AWS service. For
# example:
#
# ec2.us-west-1.amazonaws.com => ec2
#
# The region is normally the second portion of the endpoint, following
# the service name.
#
# ec2.us-west-1.amazonaws.com => us-west-1
#
# It is important to have the correct service and region name, or the
# signature will be invalid.
#
# ## Credentials
#
# The signer requires credentials. You can configure the signer
# with static credentials:
#
# signer = Aws::Sigv4::Signer.new(
# service: 's3',
# region: 'us-east-1',
# # static credentials
# access_key_id: 'akid',
# secret_access_key: 'secret'
# )
#
# You can also provide refreshing credentials via the `:credentials_provider`.
# If you are using the AWS SDK for Ruby, you can use any of the credential
# classes:
#
# signer = Aws::Sigv4::Signer.new(
# service: 's3',
# region: 'us-east-1',
# credentials_provider: Aws::InstanceProfileCredentials.new
# )
#
# Other AWS SDK for Ruby classes that can be provided via `:credentials_provider`:
#
# * `Aws::Credentials`
# * `Aws::SharedCredentials`
# * `Aws::InstanceProfileCredentials`
# * `Aws::AssumeRoleCredentials`
# * `Aws::ECSCredentials`
#
# A credential provider is any object that responds to `#credentials`
# returning another object that responds to `#access_key_id`, `#secret_access_key`,
# and `#session_token`.
#
class Signer
# @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
# @param [String] :service The service signing name, e.g. 's3'.
# @param [String] :region The region name, e.g. 'us-east-1'. When signing
# with sigv4a, this should be a comma separated list of regions.
# @param [String] :access_key_id
# @param [String] :secret_access_key
# @param [String] :session_token (nil)
#
# @overload initialize(service:, region:, credentials:, **options)
# @param [String] :service The service signing name, e.g. 's3'.
# @param [String] :region The region name, e.g. 'us-east-1'. When signing
# with sigv4a, this should be a comma separated list of regions.
# @param [Credentials] :credentials Any object that responds to the following
# methods:
#
# * `#access_key_id` => String
# * `#secret_access_key` => String
# * `#session_token` => String, nil
# * `#set?` => Boolean
#
# @overload initialize(service:, region:, credentials_provider:, **options)
# @param [String] :service The service signing name, e.g. 's3'.
# @param [String] :region The region name, e.g. 'us-east-1'. When signing
# with sigv4a, this should be a comma separated list of regions.
# @param [#credentials] :credentials_provider An object that responds
# to `#credentials`, returning an object that responds to the following
# methods:
#
# * `#access_key_id` => String
# * `#secret_access_key` => String
# * `#session_token` => String, nil
# * `#set?` => Boolean
#
# @option options [Array<String>] :unsigned_headers ([]) A list of
# headers that should not be signed. This is useful when a proxy
# modifies headers, such as 'User-Agent', invalidating a signature.
#
# @option options [Boolean] :uri_escape_path (true) When `true`,
# the request URI path is uri-escaped as part of computing the canonical
# request string. This is required for every service, except Amazon S3,
# as of late 2016.
#
# @option options [Boolean] :apply_checksum_header (true) When `true`,
# the computed content checksum is returned in the hash of signature
# headers. This is required for AWS Glacier, and optional for
# every other AWS service as of late 2016.
#
# @option options [Symbol] :signing_algorithm (:sigv4) The
# algorithm to use for signing.
#
# @option options [Boolean] :omit_session_token (false)
# (Supported only when `aws-crt` is available) If `true`,
# then security token is added to the final signing result,
# but is treated as "unsigned" and does not contribute
# to the authorization signature.
#
# @option options [Boolean] :normalize_path (true) When `true`, the
# uri paths will be normalized when building the canonical request.
def initialize(options = {})
@service = extract_service(options)
@region = extract_region(options)
@credentials_provider = extract_credentials_provider(options)
@unsigned_headers = Set.new((options.fetch(:unsigned_headers, [])).map(&:downcase))
@unsigned_headers << 'authorization'
@unsigned_headers << 'x-amzn-trace-id'
@unsigned_headers << 'expect'
@uri_escape_path = options.fetch(:uri_escape_path, true)
@apply_checksum_header = options.fetch(:apply_checksum_header, true)
@signing_algorithm = options.fetch(:signing_algorithm, :sigv4)
@normalize_path = options.fetch(:normalize_path, true)
@omit_session_token = options.fetch(:omit_session_token, false)
end
# @return [String]
attr_reader :service
# @return [String]
attr_reader :region
# @return [#credentials] Returns an object that responds to
# `#credentials`, returning an object that responds to the following
# methods:
#
# * `#access_key_id` => String
# * `#secret_access_key` => String
# * `#session_token` => String, nil
# * `#set?` => Boolean
#
attr_reader :credentials_provider
# @return [Set<String>] Returns a set of header names that should not be signed.
# All header names have been downcased.
attr_reader :unsigned_headers
# @return [Boolean] When `true` the `x-amz-content-sha256` header will be signed and
# returned in the signature headers.
attr_reader :apply_checksum_header
# Computes a version 4 signature signature. Returns the resultant
# signature as a hash of headers to apply to your HTTP request. The given
# request is not modified.
#
# signature = signer.sign_request(
# http_method: 'PUT',
# url: 'https://domain.com',
# headers: {
# 'Abc' => 'xyz',
# },
# body: 'body' # String or IO object
# )
#
# # Apply the following hash of headers to your HTTP request
# signature.headers['host']
# signature.headers['x-amz-date']
# signature.headers['x-amz-security-token']
# signature.headers['x-amz-content-sha256']
# signature.headers['authorization']
#
# In addition to computing the signature headers, the canonicalized
# request, string to sign and content sha256 checksum are also available.
# These values are useful for debugging signature errors returned by AWS.
#
# signature.canonical_request #=> "..."
# signature.string_to_sign #=> "..."
# signature.content_sha256 #=> "..."
#
# @param [Hash] request
#
# @option request [required, String] :http_method One of
# 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'
#
# @option request [required, String, URI::HTTPS, URI::HTTP] :url
# The request URI. Must be a valid HTTP or HTTPS URI.
#
# @option request [optional, Hash] :headers ({}) A hash of headers
# to sign. If the 'X-Amz-Content-Sha256' header is set, the `:body`
# is optional and will not be read.
#
# @option request [optional, String, IO] :body ('') The HTTP request body.
# A sha256 checksum is computed of the body unless the
# 'X-Amz-Content-Sha256' header is set.
#
# @return [Signature] Return an instance of {Signature} that has
# a `#headers` method. The headers must be applied to your request.
#
def sign_request(request)
creds, _ = fetch_credentials
http_method = extract_http_method(request)
url = extract_url(request)
Signer.normalize_path(url) if @normalize_path
headers = downcase_headers(request[:headers])
datetime = headers['x-amz-date']
datetime ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
date = datetime[0,8]
content_sha256 = headers['x-amz-content-sha256']
content_sha256 ||= sha256_hexdigest(request[:body] || '')
sigv4_headers = {}
sigv4_headers['host'] = headers['host'] || host(url)
sigv4_headers['x-amz-date'] = datetime
if creds.session_token && !@omit_session_token
if @signing_algorithm == 'sigv4-s3express'.to_sym
sigv4_headers['x-amz-s3session-token'] = creds.session_token
else
sigv4_headers['x-amz-security-token'] = creds.session_token
end
end
sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
if @signing_algorithm == :sigv4a && @region && !@region.empty?
sigv4_headers['x-amz-region-set'] = @region
end
headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
algorithm = sts_algorithm
# compute signature parts
creq = canonical_request(http_method, url, headers, content_sha256)
sts = string_to_sign(datetime, creq, algorithm)
sig =
if @signing_algorithm == :sigv4a
asymmetric_signature(creds, sts)
else
signature(creds.secret_access_key, date, sts)
end
algorithm = sts_algorithm
# apply signature
sigv4_headers['authorization'] = [
"#{algorithm} Credential=#{credential(creds, date)}",
"SignedHeaders=#{signed_headers(headers)}",
"Signature=#{sig}",
].join(', ')
# skip signing the session token, but include it in the headers
if creds.session_token && @omit_session_token
sigv4_headers['x-amz-security-token'] = creds.session_token
end
# Returning the signature components.
Signature.new(
headers: sigv4_headers,
string_to_sign: sts,
canonical_request: creq,
content_sha256: content_sha256,
signature: sig
)
end
# Signs a event and returns signature headers and prior signature
# used for next event signing.
#
# Headers of a sigv4 signed event message only contains 2 headers
# * ':chunk-signature'
# * computed signature of the event, binary string, 'bytes' type
# * ':date'
# * millisecond since epoch, 'timestamp' type
#
# Payload of the sigv4 signed event message contains eventstream encoded message
# which is serialized based on input and protocol
#
# To sign events
#
# headers_0, signature_0 = signer.sign_event(
# prior_signature, # hex-encoded string
# payload_0, # binary string (eventstream encoded event 0)
# encoder, # Aws::EventStreamEncoder
# )
#
# headers_1, signature_1 = signer.sign_event(
# signature_0,
# payload_1, # binary string (eventstream encoded event 1)
# encoder
# )
#
# The initial prior_signature should be using the signature computed at initial request
#
# Note:
#
# Since ':chunk-signature' header value has bytes type, the signature value provided
# needs to be a binary string instead of a hex-encoded string (like original signature
# V4 algorithm). Thus, when returning signature value used for next event siging, the
# signature value (a binary string) used at ':chunk-signature' needs to converted to
# hex-encoded string using #unpack
def sign_event(prior_signature, payload, encoder)
creds, _ = fetch_credentials
time = Time.now
headers = {}
datetime = time.utc.strftime("%Y%m%dT%H%M%SZ")
date = datetime[0,8]
headers[':date'] = Aws::EventStream::HeaderValue.new(value: time.to_i * 1000, type: 'timestamp')
sts = event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
sig = event_signature(creds.secret_access_key, date, sts)
headers[':chunk-signature'] = Aws::EventStream::HeaderValue.new(value: sig, type: 'bytes')
# Returning signed headers and signature value in hex-encoded string
[headers, sig.unpack('H*').first]
end
# Signs a URL with query authentication. Using query parameters
# to authenticate requests is useful when you want to express a
# request entirely in a URL. This method is also referred as
# presigning a URL.
#
# See [Authenticating Requests: Using Query Parameters (AWS Signature Version 4)](http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) for more information.
#
# To generate a presigned URL, you must provide a HTTP URI and
# the http method.
#
# url = signer.presign_url(
# http_method: 'GET',
# url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
# expires_in: 60
# )
#
# By default, signatures are valid for 15 minutes. You can specify
# the number of seconds for the URL to expire in.
#
# url = signer.presign_url(
# http_method: 'GET',
# url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
# expires_in: 3600 # one hour
# )
#
# You can provide a hash of headers that you plan to send with the
# request. Every 'X-Amz-*' header you plan to send with the request
# **must** be provided, or the signature is invalid. Other headers
# are optional, but should be provided for security reasons.
#
# url = signer.presign_url(
# http_method: 'PUT',
# url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
# headers: {
# 'X-Amz-Meta-Custom' => 'metadata'
# }
# )
#
# @option options [required, String] :http_method The HTTP request method,
# e.g. 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'.
#
# @option options [required, String, HTTPS::URI, HTTP::URI] :url
# The URI to sign.
#
# @option options [Hash] :headers ({}) Headers that should
# be signed and sent along with the request. All x-amz-*
# headers must be present during signing. Other
# headers are optional.
#
# @option options [Integer<Seconds>] :expires_in (900)
# How long the presigned URL should be valid for. Defaults
# to 15 minutes (900 seconds).
#
# @option options [optional, String, IO] :body
# If the `:body` is set, then a SHA256 hexdigest will be computed of the body.
# If `:body_digest` is set, this option is ignored. If neither are set, then
# the `:body_digest` will be computed of the empty string.
#
# @option options [optional, String] :body_digest
# The SHA256 hexdigest of the request body. If you wish to send the presigned
# request without signing the body, you can pass 'UNSIGNED-PAYLOAD' as the
# `:body_digest` in place of passing `:body`.
#
# @option options [Time] :time (Time.now) Time of the signature.
# You should only set this value for testing.
#
# @return [HTTPS::URI, HTTP::URI]
#
def presign_url(options)
creds, expiration = fetch_credentials
http_method = extract_http_method(options)
url = extract_url(options)
Signer.normalize_path(url) if @normalize_path
headers = downcase_headers(options[:headers])
headers['host'] ||= host(url)
datetime = headers['x-amz-date']
datetime ||= (options[:time] || Time.now).utc.strftime("%Y%m%dT%H%M%SZ")
date = datetime[0,8]
content_sha256 = headers['x-amz-content-sha256']
content_sha256 ||= options[:body_digest]
content_sha256 ||= sha256_hexdigest(options[:body] || '')
algorithm = sts_algorithm
params = {}
params['X-Amz-Algorithm'] = algorithm
params['X-Amz-Credential'] = credential(creds, date)
params['X-Amz-Date'] = datetime
params['X-Amz-Expires'] = presigned_url_expiration(options, expiration, Time.strptime(datetime, "%Y%m%dT%H%M%S%Z")).to_s
if creds.session_token
if @signing_algorithm == 'sigv4-s3express'.to_sym
params['X-Amz-S3session-Token'] = creds.session_token
else
params['X-Amz-Security-Token'] = creds.session_token
end
end
params['X-Amz-SignedHeaders'] = signed_headers(headers)
if @signing_algorithm == :sigv4a && @region
params['X-Amz-Region-Set'] = @region
end
params = params.map do |key, value|
"#{uri_escape(key)}=#{uri_escape(value)}"
end.join('&')
if url.query
url.query += '&' + params
else
url.query = params
end
creq = canonical_request(http_method, url, headers, content_sha256)
sts = string_to_sign(datetime, creq, algorithm)
signature =
if @signing_algorithm == :sigv4a
asymmetric_signature(creds, sts)
else
signature(creds.secret_access_key, date, sts)
end
url.query += '&X-Amz-Signature=' + signature
url
end
private
def sts_algorithm
@signing_algorithm == :sigv4a ? 'AWS4-ECDSA-P256-SHA256' : 'AWS4-HMAC-SHA256'
end
def canonical_request(http_method, url, headers, content_sha256)
[
http_method,
path(url),
normalized_querystring(url.query || ''),
canonical_headers(headers) + "\n",
signed_headers(headers),
content_sha256,
].join("\n")
end
def string_to_sign(datetime, canonical_request, algorithm)
[
algorithm,
datetime,
credential_scope(datetime[0,8]),
sha256_hexdigest(canonical_request),
].join("\n")
end
# Compared to original #string_to_sign at signature v4 algorithm
# there is no canonical_request concept for an eventstream event,
# instead, an event contains headers and payload two parts, and
# they will be used for computing digest in #event_string_to_sign
#
# Note:
# While headers need to be encoded under eventstream format,
# payload used is already eventstream encoded (event without signature),
# thus no extra encoding is needed.
def event_string_to_sign(datetime, headers, payload, prior_signature, encoder)
encoded_headers = encoder.encode_headers(
Aws::EventStream::Message.new(headers: headers, payload: payload)
)
[
"AWS4-HMAC-SHA256-PAYLOAD",
datetime,
credential_scope(datetime[0,8]),
prior_signature,
sha256_hexdigest(encoded_headers),
sha256_hexdigest(payload)
].join("\n")
end
def credential_scope(date)
[
date,
(@region unless @signing_algorithm == :sigv4a),
@service,
'aws4_request'
].compact.join('/')
end
def credential(credentials, date)
"#{credentials.access_key_id}/#{credential_scope(date)}"
end
def signature(secret_access_key, date, string_to_sign)
k_date = hmac("AWS4" + secret_access_key, date)
k_region = hmac(k_date, @region)
k_service = hmac(k_region, @service)
k_credentials = hmac(k_service, 'aws4_request')
hexhmac(k_credentials, string_to_sign)
end
def asymmetric_signature(creds, string_to_sign)
ec, _ = Aws::Sigv4::AsymmetricCredentials.derive_asymmetric_key(
creds.access_key_id, creds.secret_access_key
)
sts_digest = OpenSSL::Digest::SHA256.digest(string_to_sign)
s = ec.dsa_sign_asn1(sts_digest)
Digest.hexencode(s)
end
# Comparing to original signature v4 algorithm,
# returned signature is a binary string instread of
# hex-encoded string. (Since ':chunk-signature' requires
# 'bytes' type)
#
# Note:
# converting signature from binary string to hex-encoded
# string is handled at #sign_event instead. (Will be used
# as next prior signature for event signing)
def event_signature(secret_access_key, date, string_to_sign)
k_date = hmac("AWS4" + secret_access_key, date)
k_region = hmac(k_date, @region)
k_service = hmac(k_region, @service)
k_credentials = hmac(k_service, 'aws4_request')
hmac(k_credentials, string_to_sign)
end
def path(url)
path = url.path
path = '/' if path == ''
if @uri_escape_path
uri_escape_path(path)
else
path
end
end
def normalized_querystring(querystring)
params = querystring.split('&')
params = params.map { |p| p.match(/=/) ? p : p + '=' }
# From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
# Sort the parameter names by character code point in ascending order.
# Parameters with duplicate names should be sorted by value.
#
# Default sort <=> in JRuby will swap members
# occasionally when <=> is 0 (considered still sorted), but this
# causes our normalized query string to not match the sent querystring.
# When names match, we then sort by their values. When values also
# match then we sort by their original order
params.each.with_index.sort do |a, b|
a, a_offset = a
b, b_offset = b
a_name, a_value = a.split('=')
b_name, b_value = b.split('=')
if a_name == b_name
if a_value == b_value
a_offset <=> b_offset
else
a_value <=> b_value
end
else
a_name <=> b_name
end
end.map(&:first).join('&')
end
def signed_headers(headers)
headers.inject([]) do |signed_headers, (header, _)|
if @unsigned_headers.include?(header)
signed_headers
else
signed_headers << header
end
end.sort.join(';')
end
def canonical_headers(headers)
headers = headers.inject([]) do |hdrs, (k,v)|
if @unsigned_headers.include?(k)
hdrs
else
hdrs << [k,v]
end
end
headers = headers.sort_by(&:first)
headers.map{|k,v| "#{k}:#{canonical_header_value(v.to_s)}" }.join("\n")
end
def canonical_header_value(value)
value.gsub(/\s+/, ' ').strip
end
def host(uri)
# Handles known and unknown URI schemes; default_port nil when unknown.
if uri.default_port == uri.port
uri.host
else
"#{uri.host}:#{uri.port}"
end
end
# @param [File, Tempfile, IO#read, String] value
# @return [String<SHA256 Hexdigest>]
def sha256_hexdigest(value)
if (File === value || Tempfile === value) && !value.path.nil? && File.exist?(value.path)
OpenSSL::Digest::SHA256.file(value).hexdigest
elsif value.respond_to?(:read)
sha256 = OpenSSL::Digest::SHA256.new
loop do
chunk = value.read(1024 * 1024) # 1MB
break unless chunk
sha256.update(chunk)
end
value.rewind
sha256.hexdigest
else
OpenSSL::Digest::SHA256.hexdigest(value)
end
end
def hmac(key, value)
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
end
def hexhmac(key, value)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
end
def extract_service(options)
if options[:service]
options[:service]
else
msg = "missing required option :service"
raise ArgumentError, msg
end
end
def extract_region(options)
if options[:region]
options[:region]
else
raise Errors::MissingRegionError
end
end
def extract_credentials_provider(options)
if options[:credentials_provider]
options[:credentials_provider]
elsif options.key?(:credentials) || options.key?(:access_key_id)
StaticCredentialsProvider.new(options)
else
raise Errors::MissingCredentialsError
end
end
def extract_http_method(request)
if request[:http_method]
request[:http_method].upcase
else
msg = "missing required option :http_method"
raise ArgumentError, msg
end
end
def extract_url(request)
if request[:url]
URI.parse(request[:url].to_s)
else
msg = "missing required option :url"
raise ArgumentError, msg
end
end
def downcase_headers(headers)
(headers || {}).to_hash.inject({}) do |hash, (key, value)|
hash[key.downcase] = value
hash
end
end
def extract_expires_in(options)
case options[:expires_in]
when nil then 900
when Integer then options[:expires_in]
else
msg = "expected :expires_in to be a number of seconds"
raise ArgumentError, msg
end
end
def uri_escape(string)
self.class.uri_escape(string)
end
def uri_escape_path(string)
self.class.uri_escape_path(string)
end
def fetch_credentials
credentials = @credentials_provider.credentials
if credentials_set?(credentials)
expiration = nil
if @credentials_provider.respond_to?(:expiration)
expiration = @credentials_provider.expiration
end
[credentials, expiration]
else
raise Errors::MissingCredentialsError,
'unable to sign request without credentials set'
end
end
# Returns true if credentials are set (not nil or empty)
# Credentials may not implement the Credentials interface
# and may just be credential like Client response objects
# (eg those returned by sts#assume_role)
def credentials_set?(credentials)
!credentials.access_key_id.nil? &&
!credentials.access_key_id.empty? &&
!credentials.secret_access_key.nil? &&
!credentials.secret_access_key.empty?
end
def presigned_url_expiration(options, expiration, datetime)
expires_in = extract_expires_in(options)
return expires_in unless expiration
expiration_seconds = (expiration - datetime).to_i
# In the static stability case, credentials may expire in the past
# but still be valid. For those cases, use the user configured
# expires_in and ingore expiration.
if expiration_seconds <= 0
expires_in
else
[expires_in, expiration_seconds].min
end
end
class << self
# Kept for backwards compatability
# Always return false since we are not using crt signing functionality
def use_crt?
false
end
# @api private
def uri_escape_path(path)
path.gsub(/[^\/]+/) { |part| uri_escape(part) }
end
# @api private
def uri_escape(string)
if string.nil?
nil
else
CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
end
end
# @api private
def normalize_path(uri)
normalized_path = Pathname.new(uri.path).cleanpath.to_s
# Pathname is probably not correct to use. Empty paths will
# resolve to "." and should be disregarded
normalized_path = '' if normalized_path == '.'
# Ensure trailing slashes are correctly preserved
if uri.path.end_with?('/') && !normalized_path.end_with?('/')
normalized_path << '/'
end
uri.path = normalized_path
end
end
end
end
end
|