RFC 9421 HTTP Message Signature canonical shape
The shape
| Element | Pin |
|---|---|
| Algorithm | ed25519 (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-Input | created (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 digest | RFC 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 |
| Caching | Server 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
- Signed Offer responses (the merchant signs the Offer JSON-LD body).
- Signed catalog responses at
/.well-known/oid4ac-catalog.
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.