aws/internal/sigv4a
SigV4a — AWS Signature Version 4 with asymmetric ECDSA P-256 signatures, used by S3 Multi-Region Access Points (MRAP) and a few other multi-region offerings.
The canonical-request shape is identical to SigV4 except for
the algorithm string (AWS4-ECDSA-P256-SHA256) and the
X-Amz-Region-Set header that carries the comma-joined region
list. The string-to-sign uses the same five-line shape; the
credential scope drops the region because SigV4a is region-
agnostic by design.
Deterministic signatures via RFC 6979. Signing routes
through aws/internal/ecdsa_deterministic which derives the
ECDSA nonce k from (d, sha256(sts)) via HMAC-DRBG. Two
calls with the same (credentials, request) produce
byte-identical signatures, which makes the aws-c-auth v4a
corpus pinnable at the signature-byte level (see
test/ecdsa_deterministic_test.gleam for the RFC 6979 §A.2.5
reference-vector pins).
AWS-deterministic key derivation is wired via
derive_signing_key/2 — feeds an IAM (access-key-id,
secret-access-key) pair through AWS’s HMAC-SHA256 + P-256
modular-reduction KDF and returns the 32-byte EC private
scalar the SigV4a spec requires. Pinned by
test/sigv4a_key_derivation_test.gleam against the aws-c-auth
v4a fixture’s public-key.json (X / Y derived from the
canonical AKIDEXAMPLE / wJalrXUtnFEMI... pair).
Canonical-request helpers (canonical_headers, signed_headers,
canonical_query_string, build_canonical_uri,
normalize_path) live in aws/internal/sigv4_canonical and
are shared with the SigV4 module.
Types
Pieces produced when building a SigV4a canonical request. Mirrors
sigv4.CanonicalParts so test harnesses can pin individual
stages (canonical_request, signed_headers, payload_hash)
against AWS reference fixtures.
pub type CanonicalParts {
CanonicalParts(
canonical_request: String,
signed_headers: String,
payload_hash: String,
prepared_headers: List(http_request.Header),
)
}
Constructors
-
CanonicalParts( canonical_request: String, signed_headers: String, payload_hash: String, prepared_headers: List(http_request.Header), )
pub type EcdsaPrivateKey {
EcdsaPrivateKey(scalar: BitArray)
}
Constructors
-
EcdsaPrivateKey(scalar: BitArray)32-byte P-256 (secp256r1) scalar. SEC1 form. Build via
ecdsa_private_key_from_bytes; the wrapper validates the byte width so a malformed input fails at construction rather than at signing time.
IAM identity that signs a SigV4a request. Carries the same
fields as sigv4.SigningCredentials plus the EC private scalar
that SigV4a’s ECDSA step needs. session_token is Some for
STS / IRSA / SSO-issued credentials and triggers the
X-Amz-Security-Token header on the canonical request.
pub type Sigv4aCredentials {
Sigv4aCredentials(
access_key_id: String,
private_key: EcdsaPrivateKey,
session_token: option.Option(String),
)
}
Constructors
-
Sigv4aCredentials( access_key_id: String, private_key: EcdsaPrivateKey, session_token: option.Option(String), )
pub type Sigv4aOptions {
Sigv4aOptions(
timestamp: String,
region_set: List(String),
service: String,
sign_body: Bool,
normalize_path: Bool,
omit_session_token: Bool,
)
}
Constructors
-
Sigv4aOptions( timestamp: String, region_set: List(String), service: String, sign_body: Bool, normalize_path: Bool, omit_session_token: Bool, )Arguments
- timestamp
-
AWS-form compact timestamp:
YYYYMMDDTHHMMSSZ. - region_set
-
The region set the signature binds to. Single-region calls pass
["us-east-1"]; multi-region calls pass the list. Order is preserved into theX-Amz-Region-Setheader. - service
-
Service name as it appears in the credential scope.
- sign_body
-
True⇒ canonical-request payload-hash line carriessha256(req.body);False⇒sha256(""). - normalize_path
-
True⇒ apply RFC 3986 dot-segment removal to the request path before percent-encoding (/foo/./bar/../baz→/baz);False⇒ pass the path through unchanged. Default isTruefor most AWS services; S3 is the notable holdout — it needsFalseso keys with./..survive intact. - omit_session_token
-
True⇒ whencreds.session_tokenisSome, deliverX-Amz-Security-Tokenon the wire but exclude it from the canonical request being signed. Used by services that add the token after signing (thepost-sts-header-afterpattern).False⇒ include the token in the canonical request alongside the other prepared headers.
Values
pub fn canonical_request(
req: http_request.HttpRequest,
creds: Sigv4aCredentials,
opts: Sigv4aOptions,
) -> CanonicalParts
Build the SigV4a canonical request bytes from req + creds +
opts. Returns the canonical request, the semicolon-joined
signed-headers line, the payload-hash hex, and the prepared
header list (which the signing step appends Authorization to).
Pure function — no signing, no network.
pub fn derive_signing_key(
access_key_id: String,
secret_access_key: String,
) -> EcdsaPrivateKey
AWS SigV4a deterministic key derivation: turn an IAM
(access-key-id, secret-access-key) pair into the 32-byte
P-256 private scalar that sign/4 accepts. Matches the
algorithm in aws-sigv4::sign::v4a::generate_signing_key:
input_key = "AWS4A" || secret_access_key(UTF-8)- Loop counter
c = 1, 2, …:kdf_context = access_key_id || cfis = "AWS4-ECDSA-P256-SHA256" || 0x00 || kdf_context || 256:i32-bebuf = 1:i32-be || fistag = HMAC-SHA256(input_key, buf)(32 bytes)k0 = U256(tag)— big-endian ifk0 ≤ N-2(withN= P-256 order): returnk0 + 1. - Otherwise
c += 1and retry. The counter loop almost always terminates onc = 1; the probability of rejection per iteration is(2^256 - (N-2)) / 2^256 ≈ 2^-128.
pub fn ecdsa_p256_public_key(private_key: BitArray) -> BitArray
Uncompressed SEC1 public key (04 || X || Y, 65 bytes) for a
given 32-byte P-256 private scalar. Surfaced so callers can pin
derived keys against AWS test fixtures (which ship the public
counterpart) without re-implementing curve arithmetic.
pub fn ecdsa_p256_sign(
private_key: BitArray,
data: BitArray,
) -> BitArray
ECDSA P-256 signature over data, returning the DER-encoded
blob. Erlang’s crypto:sign/4 uses a random nonce per call;
signatures verify correctly server-side but won’t match
RFC-6979 deterministic-nonce reference vectors.
pub fn ecdsa_p256_verify(
public_key: BitArray,
data: BitArray,
signature: BitArray,
) -> Bool
ECDSA P-256 verification. public_key is the uncompressed
SEC1 form (04 || X || Y, 65 bytes).
pub fn ecdsa_private_key_from_bytes(
bytes: BitArray,
) -> Result(EcdsaPrivateKey, String)
Build an EcdsaPrivateKey from a 32-byte scalar. Returns
Error(_) when the input is the wrong length — SigV4a is
strictly P-256, so any other key size is a bug.
pub fn sign(
req: http_request.HttpRequest,
private_key: EcdsaPrivateKey,
access_key_id: String,
opts: Sigv4aOptions,
) -> http_request.HttpRequest
Sign req with private_key and access_key_id. Always
excludes a session token. For credentials that carry an STS
token use sign_with_credentials instead.
pub fn sign_with_credentials(
req: http_request.HttpRequest,
creds: Sigv4aCredentials,
opts: Sigv4aOptions,
) -> http_request.HttpRequest
Sign req with the bundled creds. Adds Authorization,
X-Amz-Date, X-Amz-Region-Set, and (when creds.session_token
is Some) X-Amz-Security-Token. X-Amz-Content-Sha256 is
emitted when opts.sign_body is set.
pub fn sign_with_iam_credentials(
req: http_request.HttpRequest,
access_key_id: String,
secret_access_key: String,
opts: Sigv4aOptions,
) -> http_request.HttpRequest
One-call SigV4a signing that takes the IAM
(access-key-id, secret-access-key) pair directly. Equivalent
to sign(req, derive_signing_key(akid, secret), akid, opts)
— derives the EC private scalar from the IAM secret then
delegates. Use this when you have raw IAM credentials and
don’t already need to hold onto the derived key (e.g. for
reuse across many requests with the same identity).
pub fn string_to_sign(
canonical: String,
opts: Sigv4aOptions,
) -> String
Build the SigV4a string-to-sign (AWS4-ECDSA-P256-SHA256\n<ts>\n<scope>\n<creq_hash>).
The scope drops the region — X-Amz-Region-Set carries it
instead — so only opts.timestamp (which holds the YYYYMMDD
date in its first 8 chars) and opts.service contribute.