module AWS::SigV4

AWS Signature Version 4: signs an HTTP::Client::Request for any AWS service, with the Authorization header (.sign) or in the query (.presign), as the specification describes (docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html).

keys = AWS::Credentials::Keys.new(access_key_id: "AKIDEXAMPLE", secret_access_key: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
req = HTTP::Client::Request.get("https://example.amazonaws.com/")
signed = AWS::SigV4.sign(req, service: "service", region: "us-east-1", credentials: keys, time: Time.utc(2015, 8, 30, 12, 36))
signed.headers["x-amz-date"]   # => "20150830T123600Z"
signed.headers["authorization"]
# => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31"

/home/runner/work/aws/aws/src/sigv4.qz:18

Classes and modules

Constants

ALGORITHM: String

The algorithm's name, in the Authorization header and the string to sign.

/home/runner/work/aws/aws/src/sigv4.qz:21

UNSIGNED_PAYLOAD: String

The payload hash of an unsigned body (unsigned_payload: true).

/home/runner/work/aws/aws/src/sigv4.qz:23

EMPTY_SHA256: String

The SHA-256 of nothing: the payload hash of a request without a body.

/home/runner/work/aws/aws/src/sigv4.qz:25

UNSIGNED_HEADERS: Array[String]

The headers never signed: the signature's own, those a proxy or the client may change (User-Agent, Expect), and the hop-by-hop ones. (Content-Length is signed when the request carries one: .sign.)

/home/runner/work/aws/aws/src/sigv4.qz:29

Class methods

def self.sign(request: HTTP::Client::Request, *, service: String, region: String, credentials: AWS::Credentials::Keys, time: Time = …, payload_hash: String? = …, unsigned_payload: Bool = …, content_sha256: Bool = …) -> HTTP::Client::Request

request signed for service in region with credentials, at time: with Host (from the URL, when it has none), X-Amz-Date, X-Amz-Security-Token (for temporary keys), X-Amz-Content-Sha256 (for s3, with content_sha256: true, payload_hash: or unsigned_payload: true) and Authorization. Every header the request has is signed but UNSIGNED_HEADERS.

The payload hash is an X-Amz-Content-Sha256 the request carries, else the body's SHA-256 (HTTP::Content::Text, HTTP::Content::Binary; that of nothing for HTTP::Content::Empty), payload_hash when given, or UNSIGNED-PAYLOAD with unsigned_payload: true; a HTTP::Content::Stream body needs one of the last two.

Raises

ArgumentError for a HTTP::Content::Stream body without payload_hash or unsigned_payload, or a Content-Length header that is not the body's size (or on a streamed body, which is sent chunked).

/home/runner/work/aws/aws/src/sigv4.qz:54

def self.presign(request: HTTP::Client::Request, *, service: String, region: String, credentials: AWS::Credentials::Keys, expires_in: Duration, time: Time = …) -> URI

The URL of request signed in its query, valid for expires_in from time (at most 7 days): X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders (host and the request's other headers but UNSIGNED_HEADERS), X-Amz-Security-Token for temporary keys, and X-Amz-Signature last. The payload hash is UNSIGNED-PAYLOAD for s3, else the body's, as .sign takes it.

keys = AWS::Credentials::Keys.new(access_key_id: "AKIDEXAMPLE", secret_access_key: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
req = HTTP::Client::Request.get("https://example.amazonaws.com/")
url = AWS::SigV4.presign(req, service: "service", region: "us-east-1", credentials: keys, expires_in: 1.hour, time: Time.utc(2015, 8, 30, 12, 36))
url.query.as(String).end_with?("X-Amz-Signature=e93c787ed7f371d5c6b165c1b38ede9550f4dce4144713e844b25b7192d3865d")   # => true

Raises

ArgumentError for expires_in under a second or over 7 days, and as .sign does.

/home/runner/work/aws/aws/src/sigv4.qz:94

def self.parts(method: String, url: URI, headers: HTTP::Headers, payload_hash: String, *, service: String, region: String, credentials: AWS::Credentials::Keys, time: Time) -> AWS::SigV4::Parts

The stages of the signature of a request: method, url, headers (as they will be sent, Host included) and the payload's hash, for service in region at time. .sign and .presign are made of it; tests compare each stage with the specification's.

/home/runner/work/aws/aws/src/sigv4.qz:126

def self.signing_key(secret: String, date: String, region: String, service: String) -> Bytes

The key that signs for service in region on date (20150830): HMAC(HMAC(HMAC(HMAC("AWS4" + secret, date), region), service), "aws4_request").

/home/runner/work/aws/aws/src/sigv4.qz:173

def self.scope(time: Time, region: String, service: String) -> String

The credential scope: 20150830/us-east-1/service/aws4_request.

/home/runner/work/aws/aws/src/sigv4.qz:181