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

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 the X-Amz-Region-Set header.

    service

    Service name as it appears in the credential scope.

    sign_body

    True ⇒ canonical-request payload-hash line carries sha256(req.body); Falsesha256("").

    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 is True for most AWS services; S3 is the notable holdout — it needs False so keys with . / .. survive intact.

    omit_session_token

    True ⇒ when creds.session_token is Some, deliver X-Amz-Security-Token on the wire but exclude it from the canonical request being signed. Used by services that add the token after signing (the post-sts-header-after pattern). 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:

  1. input_key = "AWS4A" || secret_access_key (UTF-8)
  2. Loop counter c = 1, 2, …: kdf_context = access_key_id || c fis = "AWS4-ECDSA-P256-SHA256" || 0x00 || kdf_context || 256:i32-be buf = 1:i32-be || fis tag = HMAC-SHA256(input_key, buf) (32 bytes) k0 = U256(tag) — big-endian if k0 ≤ N-2 (with N = P-256 order): return k0 + 1.
  3. Otherwise c += 1 and retry. The counter loop almost always terminates on c = 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.

Search Document