Skip to main content

RFC 9421 HTTP Message Signature canonical shape

The shape

ElementPin
Algorithmed25519 (default); ecdsa-p256-sha256 as fallback. Reject rsa-pss-sha512 for new keys. Reject hmac-sha256 always.
Signature base components (mandatory)@method, @target-uri, @authority, content-type, content-digest
Parameters in Signature-Inputcreated (UNIX s), expires (created + max 300s for offers, max 600s for catalogs), nonce (16 random bytes, base64url), keyid (resolves in merchant JWKS), alg matches keyid's alg
Body digestRFC 9530 SHA-256; Content-Digest: sha-256=:<base64>: header set on the response
Verifier rules(a) expires > now; (b) created < now + 60s (skew); (c) keyid ∈ jwks; (d) signature valid against keyid's public key; (e) JWKS cache TTL min(300s, response Cache-Control max-age); (f) on keyid miss, re-fetch JWKS once
CachingServer sends Cache-Control: max-age=60, must-revalidate, public plus ETag; agent honours; on stale, re-verifies signature on each fetch

Where it is used

Signing an Offer (merchant side)

The merchant signs the Offer body with its own private key, using whatever RFC 9421 tooling it prefers. Signing is not part of the OID4Pay SDK: the SDK is verify only. The merchant computes the canonical body digest, builds the signature base over the mandatory components, signs it with the key whose public half is published in its JWKS, and emits the three headers below. The counterparty then verifies them with verifyOffer.

// The merchant emits these headers alongside the Offer JSON-LD body.
// Body (canonicalised: sorted keys, compact JSON) is hashed for the digest.

Content-Digest: sha-256=:abc123...:
Signature-Input: offer-sig=("@method" "@target-uri" "@authority"
  "content-type" "content-digest");created=1747260100;
  expires=1747260400;nonce="x9...";keyid="merchant-2026-05-14";alg="ed25519"
Signature: offer-sig=:<b64 ed25519 sig>:

// The signature is computed over the signature base assembled from the
// covered components and the @signature-params line, signed with the
// merchant's ed25519 private key. The matching public key resolves in
// the merchant JWKS under keyid="merchant-2026-05-14".

Worked example: verifying (Python)

from oid4pay_oid4ac import verify_offer, OfferVerifyError, OfferSignatureHeaders

sig_headers = OfferSignatureHeaders(
    signature_input="offer-sig=(...)",
    signature="offer-sig=:...:",
    content_digest="sha-256=:abc123...:",
)

try:
    v = verify_offer(
        offer_body,
        sig_headers,
        merchant_jwks,
        expected_target_uri="https://shop.alpacanica.com/products/test-pinata",
    )
    print(v.body_digest, v.keyid, v.alg)
except OfferVerifyError as exc:
    print("rejected:", exc.code)

Canonicalisation

For /.well-known/oid4ac-catalog bodies that carry the signature inside the JSON (so caches and reverse proxies cannot strip headers), the signature MUST cover the canonical (sorted-keys, compact JSON) form of the body MINUS the signature, signature_input, and content_digest members.