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.
Worked example: signing an Offer (Node)
import { signOffer } from "@oid4pay/oid4ac-merchant";
const body = {
"@context": "https://schema.org",
"@type": "Offer",
"sku": "test-pinata",
"name": "Test Piñata",
"amount_minor": 1299,
"currency": "EUR",
};
const signed = await signOffer(body, {
privateJwk,
keyid: "merchant-2026-05-14",
targetUri: "https://shop.alpacanica.com/products/test-pinata",
expiresIn: 300,
});
// signed.headers:
// 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>:Worked example: verifying (Python)
from oid4pay_oid4ac import verify_offer, OfferVerifyError
try:
v = verify_offer(
body=offer_body,
headers={
"Content-Digest": "sha-256=:abc123...:",
"Signature-Input": "offer-sig=(...)",
"Signature": "offer-sig=:...:",
},
jwks=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.