Class: Aws::Sigv4::Signer
- Inherits:
-
Object
- Object
- Aws::Sigv4::Signer
- 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`.
Constant Summary collapse
- @@use_crt =
begin require 'aws-crt' true rescue LoadError false end
Instance Attribute Summary collapse
-
#apply_checksum_header ⇒ Boolean
readonly
When ‘true` the `x-amz-content-sha256` header will be signed and returned in the signature headers.
-
#credentials_provider ⇒ #credentials
readonly
Returns an object that responds to ‘#credentials`, returning an object that responds to the following methods:.
- #region ⇒ String readonly
- #service ⇒ String readonly
-
#unsigned_headers ⇒ Set<String>
readonly
Returns a set of header names that should not be signed.
Class Method Summary collapse
- .normalize_path(uri) ⇒ Object private
- .uri_escape(string) ⇒ Object private
- .uri_escape_path(path) ⇒ Object private
- .use_crt? ⇒ Boolean
Instance Method Summary collapse
-
#initialize(options = {}) ⇒ Signer
constructor
A new instance of Signer.
-
#presign_url(options) ⇒ HTTPS::URI, HTTP::URI
Signs a URL with query authentication.
-
#sign_event(prior_signature, payload, encoder) ⇒ Object
Signs a event and returns signature headers and prior signature used for next event signing.
-
#sign_request(request) ⇒ Signature
Computes a version 4 signature signature.
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.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/aws-sigv4/signer.rb', line 144 def initialize( = {}) @service = extract_service() @region = extract_region() @credentials_provider = extract_credentials_provider() @unsigned_headers = Set.new((.fetch(:unsigned_headers, [])).map(&:downcase)) @unsigned_headers << 'authorization' @unsigned_headers << 'x-amzn-trace-id' @unsigned_headers << 'expect' @uri_escape_path = .fetch(:uri_escape_path, true) @apply_checksum_header = .fetch(:apply_checksum_header, true) @signing_algorithm = .fetch(:signing_algorithm, :sigv4) @normalize_path = .fetch(:normalize_path, true) @omit_session_token = .fetch(:omit_session_token, false) if @signing_algorithm == 'sigv4-s3express'.to_sym && Signer.use_crt? && Aws::Crt::GEM_VERSION <= '0.1.9' raise ArgumentError, 'This version of aws-crt does not support S3 Express. Please update this gem to at least version 0.2.0.' end end |
Instance Attribute Details
#apply_checksum_header ⇒ Boolean (readonly)
189 190 191 |
# File 'lib/aws-sigv4/signer.rb', line 189 def apply_checksum_header @apply_checksum_header end |
#credentials_provider ⇒ #credentials (readonly)
181 182 183 |
# File 'lib/aws-sigv4/signer.rb', line 181 def credentials_provider @credentials_provider end |
#region ⇒ String (readonly)
170 171 172 |
# File 'lib/aws-sigv4/signer.rb', line 170 def region @region end |
#service ⇒ String (readonly)
167 168 169 |
# File 'lib/aws-sigv4/signer.rb', line 167 def service @service end |
#unsigned_headers ⇒ Set<String> (readonly)
185 186 187 |
# File 'lib/aws-sigv4/signer.rb', line 185 def unsigned_headers @unsigned_headers end |
Class Method Details
.normalize_path(uri) ⇒ 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.
944 945 946 947 948 949 950 951 952 953 954 |
# File 'lib/aws-sigv4/signer.rb', line 944 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 |
.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.
935 936 937 938 939 940 941 |
# File 'lib/aws-sigv4/signer.rb', line 935 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.
930 931 932 |
# File 'lib/aws-sigv4/signer.rb', line 930 def uri_escape_path(path) path.gsub(/[^\/]+/) { |part| uri_escape(part) } end |
.use_crt? ⇒ Boolean
925 926 927 |
# File 'lib/aws-sigv4/signer.rb', line 925 def use_crt? @@use_crt 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.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'
}
)
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 |
# File 'lib/aws-sigv4/signer.rb', line 433 def presign_url() return crt_presign_url() if Signer.use_crt? creds, expiration = fetch_credentials http_method = extract_http_method() url = extract_url() Signer.normalize_path(url) if @normalize_path headers = downcase_headers([:headers]) headers['host'] ||= host(url) datetime = headers['x-amz-date'] datetime ||= ([:time] || Time.now).utc.strftime("%Y%m%dT%H%M%SZ") date = datetime[0,8] content_sha256 = headers['x-amz-content-sha256'] content_sha256 ||= [:body_digest] content_sha256 ||= sha256_hexdigest([: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(, 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 |
#sign_event(prior_signature, payload, encoder) ⇒ Object
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
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/aws-sigv4/signer.rb', line 346 def sign_event(prior_signature, payload, encoder) # Note: CRT does not currently provide event stream signing, so we always use the ruby implementation. 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 |
#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 #=> "..."
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 |
# File 'lib/aws-sigv4/signer.rb', line 238 def sign_request(request) return crt_sign_request(request) if Signer.use_crt? 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 |