HTTP message signatures (RFC 9421)
As of 0.37.0 DaloyJS ships first-party HTTP Message Signatures (RFC 9421) — the IETF-standard way to prove a server-to-server request came from a trusted peer. Where webhook HMAC binds a signature to a request body and mTLS authenticates the TLS peer, message signatures bind a signature to a caller-chosen set of HTTP message components(method, path, authority, selected headers…) carried in the standard Signature / Signature-Input headers.
The module is dependency-free and runtime-portable (WebCrypto only, no node: imports) and is imported from the @daloyjs/core root or the @daloyjs/core/http-signatures subpath.
Secure-by-default
- The verifier requires an explicit
algorithmsallowlist — there is no implicit “accept any algorithm” mode, and a resolved key may pin its own algorithm to defeat algorithm-confusion. createdis required by default and the signature is rejected once it is older thanDEFAULT_MAX_SIGNATURE_AGE_SECONDS(300s), or ifcreatedis in the future /expireshas passed (outside a small clock-skew tolerance).- A configurable
requiredComponentsset must be covered (default["@method", "@path"]), so a peer cannot sign an empty or irrelevant component set. - Raw HMAC keys must be at least 32 bytes (RFC 7518 §3.2). SHA-1 and
alg: "none"-style escapes do not exist. - Optional
noncereplay defense via anisReplaycallback.
Supported algorithms
The labels map 1:1 onto the RFC 9421 HTTP Signature Algorithms registry:
hmac-sha256— symmetric shared secret (simplest to deploy).ed25519,ecdsa-p256-sha256,ecdsa-p384-sha384— asymmetric (publish a public key, no shared secret).rsa-pss-sha512,rsa-v1_5-sha256— RSA.
Verify inbound requests (middleware)
httpSignatureAuth() rejects any request without a valid signature with a 401 (Cache-Control: no-store) and stamps the verified result on ctx.state.httpSignature.
Sign an outbound request
signRequest() returns a new Request with the Signature and Signature-Input headers attached (the original is not mutated).
Bind the body with Content-Digest (RFC 9530)
Message signatures cover headers and derived components, not the body. To bind the body, compute a Content-Digest header with contentDigest(), include content-digest in the covered components, and re-check it on the receiving side with verifyContentDigest().
Low-level sign / verify
signMessage() and verifyMessage() work with plain method/URL/headers when you are not inside a request/response object.
Rejection reasons
verifyMessage() / verifyRequest() never throw on a forged or malformed signature — they return { valid: false, reason } with a stable code such as invalid_signature, signature_stale, created_in_future, signature_expired, missing_created, missing_required_component, alg_not_allowed, alg_mismatch, key_not_found, replay_detected, tag_mismatch, or malformed_signature_headers. They throw only on a programming error (an empty algorithms allowlist, or WebCrypto being unavailable).