Class: Aws::Sigv4::Signer

Inherits:
Object
  • Object
show all
Defined in:
lib/aws-sigv4/signer.rb

Overview

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`.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(service: , region: , access_key_id: , secret_access_key: , session_token: nil, **options) ⇒ Signer #initialize(service: , region: , credentials: , **options) ⇒ Signer #initialize(service: , region: , credentials_provider: , **options) ⇒ Signer

Returns a new instance of Signer

Overloads:

  • #initialize(service: , region: , access_key_id: , secret_access_key: , session_token: nil, **options) ⇒ Signer

    Parameters:

    • :service (String)

      The service signing name, e.g. 's3'.

    • :region (String)

      The region name, e.g. 'us-east-1'.

    • :access_key_id (String)
    • :secret_access_key (String)
    • :session_token (String)

      (nil)

  • #initialize(service: , region: , credentials: , **options) ⇒ Signer

    Parameters:

    • :service (String)

      The service signing name, e.g. 's3'.

    • :region (String)

      The region name, e.g. 'us-east-1'.

    • :credentials (Credentials)

      Any object that responds to the following methods:

      • `#access_key_id` => String

      • `#secret_access_key` => String

      • `#session_token` => String, nil

      • `#set?` => Boolean

  • #initialize(service: , region: , credentials_provider: , **options) ⇒ Signer

    Parameters:

    • :service (String)

      The service signing name, e.g. 's3'.

    • :region (String)

      The region name, e.g. 'us-east-1'.

    • :credentials_provider (#credentials)

      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

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :unsigned_headers (Array<String>) — default: []

    A list of headers that should not be signed. This is useful when a proxy modifies headers, such as 'User-Agent', invalidating a signature.

  • :uri_escape_path (Boolean) — default: 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.

  • :apply_checksum_header (Boolean) — default: 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.



118
119
120
121
122
123
124
125
126
127
# File 'lib/aws-sigv4/signer.rb', line 118

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'
  [:uri_escape_path, :apply_checksum_header].each do |opt|
    instance_variable_set("@#{opt}", options.key?(opt) ? !!options[:opt] : true)
  end
end

Instance Attribute Details

#apply_checksum_headerBoolean (readonly)

When `true` the `x-amz-content-sha256` header will be signed and returned in the signature headers.

Returns:

  • (Boolean)

    When `true` the `x-amz-content-sha256` header will be signed and returned in the signature headers.



152
153
154
# File 'lib/aws-sigv4/signer.rb', line 152

def apply_checksum_header
  @apply_checksum_header
end

#credentials_provider#credentials (readonly)

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

Returns:

  • (#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



144
145
146
# File 'lib/aws-sigv4/signer.rb', line 144

def credentials_provider
  @credentials_provider
end

#regionString (readonly)

Returns:

  • (String)


133
134
135
# File 'lib/aws-sigv4/signer.rb', line 133

def region
  @region
end

#serviceString (readonly)

Returns:

  • (String)


130
131
132
# File 'lib/aws-sigv4/signer.rb', line 130

def service
  @service
end

#unsigned_headersSet<String> (readonly)

Returns a set of header names that should not be signed. All header names have been downcased.

Returns:

  • (Set<String>)

    Returns a set of header names that should not be signed. All header names have been downcased.



148
149
150
# File 'lib/aws-sigv4/signer.rb', line 148

def unsigned_headers
  @unsigned_headers
end

Class Method Details

.uri_escape(string) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



585
586
587
588
589
590
591
# File 'lib/aws-sigv4/signer.rb', line 585

def uri_escape(string)
  if string.nil?
    nil
  else
    CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
  end
end

.uri_escape_path(path) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



580
581
582
# File 'lib/aws-sigv4/signer.rb', line 580

def uri_escape_path(path)
  path.gsub(/[^\/]+/) { |part| uri_escape(part) }
end

Instance Method Details

#presign_url(options) ⇒ HTTPS::URI, HTTP::URI

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)](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.presigned_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.presigned_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.presigned_url(
  http_method: 'PUT',
  url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
  headers: {
    'X-Amz-Meta-Custom' => 'metadata'
  }
)

Parameters:

  • options (Hash)

    a customizable set of options

Options Hash (options):

  • :http_method (required, String)

    The HTTP request method, e.g. 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'.

  • :url (required, String, HTTPS::URI, HTTP::URI)

    The URI to sign.

  • :headers (Hash) — default: {}

    Headers that should be signed and sent along with the request. All x-amz-* headers must be present during signing. Other headers are optional.

  • :expires_in (Integer<Seconds>) — default: 900

    How long the presigned URL should be valid for. Defaults to 15 minutes (900 seconds).

  • :body (optional, String, IO)

    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.

  • :body_digest (optional, String)

    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`.

  • :time (Time) — default: Time.now

    Time of the signature. You should only set this value for testing.

Returns:

  • (HTTPS::URI, HTTP::URI)


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
# File 'lib/aws-sigv4/signer.rb', line 313

def presign_url(options)

  creds = get_credentials

  http_method = extract_http_method(options)
  url = extract_url(options)

  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] || '')

  params = {}
  params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
  params['X-Amz-Credential'] = credential(creds, date)
  params['X-Amz-Date'] = datetime
  params['X-Amz-Expires'] = extract_expires_in(options)
  params['X-Amz-SignedHeaders'] = signed_headers(headers)
  params['X-Amz-Security-Token'] = creds.session_token if creds.session_token

  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)
  url.query += '&X-Amz-Signature=' + signature(creds.secret_access_key, date, sts)
  url
end

#sign_request(request) ⇒ Signature

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 #=> "..."

Parameters:

  • request (Hash)

Options Hash (request):

  • :http_method (required, String)

    One of 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'

  • :url (required, String, URI::HTTPS, URI::HTTP)

    The request URI. Must be a valid HTTP or HTTPS URI.

  • :headers (optional, Hash) — default: {}

    A hash of headers to sign. If the 'X-Amz-Content-Sha256' header is set, the `:body` is optional and will not be read.

  • :body (otpional, String, IO) — default: 'X-Amz-Content-Sha256'ody. A sha256 checksum is computed of the body unless the 'X-Amz-Content-Sha256' header is set.

    '') The HTTP request body. A sha256 checksum is computed of the body unless the 'X-Amz-Content-Sha256' header is set.

Returns:



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
# File 'lib/aws-sigv4/signer.rb', line 201

def sign_request(request)

  creds = get_credentials

  http_method = extract_http_method(request)
  url = extract_url(request)
  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'] = host(url)
  sigv4_headers['x-amz-date'] = datetime
  sigv4_headers['x-amz-security-token'] = creds.session_token if creds.session_token
  sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header

  headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash

  # compute signature parts
  creq = canonical_request(http_method, url, headers, content_sha256)
  sts = string_to_sign(datetime, creq)
  sig = signature(creds.secret_access_key, date, sts)

  # apply signature
  sigv4_headers['authorization'] = [
    "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
    "SignedHeaders=#{signed_headers(headers)}",
    "Signature=#{sig}",
  ].join(', ')

  # Returning the signature components.
  Signature.new(
    headers: sigv4_headers,
    string_to_sign: sts,
    canonical_request: creq,
    content_sha256: content_sha256
  )
end