HTTP Message Signatures
RFC 9421 HTTP Message Signatures — sign and verify an HTTP request or response at the message level, so a recipient checks integrity and origin without trusting the transport. Two headers carry it: Signature-Input names the covered components and the signature parameters, and Signature carries the bytes. The signature base is the canonicalized covered-component list plus those parameters (RFC 9421 §2.5), and the algorithm runs over it.
The derived components are @method, @target-uri, @authority, @scheme, @request-target, @path, @query and @query-param (§2.2). The signature parameters are created, expires, nonce, keyid, alg and tag (§2.3).
Two algorithms are offered: ed25519 for a peer that is not PQC-aware, and ml-dsa-65 (FIPS 204) when both peers are. The RSA, ECDSA-P256, ECDSA-P384 and HMAC variants of §3.3 are not offered.
b.crypto.httpSig.contentDigest(body) #
Compute the Content-Digest field value for a request or response body, as the RFC 8941 dictionary entry sha3-512=:. Cover content-digest in a signature and the body is bound to it, so a peer that alters the body invalidates the signature without the signer having to cover every byte directly.
Takes a Buffer, or a string read as UTF-8. Any other value throws.
var field = b.crypto.httpSig.contentDigest(Buffer.from("{\"ok\":true}"));
// → "sha3-512=::"
b.crypto.httpSig.sign(msg, opts) #
{
keyid: string, // the key identifier the verifier resolves (required)
alg: string, // "ed25519" | "ml-dsa-65" (required)
privateKey: string, // PEM private key (required)
covered: string[], // covered components; default ["@method", "@target-uri"]
created: number, // signature creation time, epoch seconds
expires: number, // signature expiry, epoch seconds
nonce: string, // replay nonce carried in the signature parameters
tag: string, // application tag per RFC 9421 2.3
label: string, // signature label; default "sig1"
}
Sign an HTTP request or response per RFC 9421 and return the headers to send. msg carries the method, url, headers and optional body; opts names the key and what the signature covers. A body produces a Content-Digest header alongside the signature, so covering content-digest binds the body.
alg is ed25519 or ml-dsa-65. The RSA, ECDSA and HMAC variants of RFC 9421 §3.3 are not accepted.
var sig = b.crypto.httpSig.sign({
method: "POST",
url: "https://api.example.com/orders",
headers: { "content-type": "application/json", host: "api.example.com" },
body: Buffer.from("{\"id\":1}"),
}, {
keyid: "service-a-2026-05",
alg: "ed25519",
privateKey: privateKeyPem,
covered: ["@method", "@target-uri", "content-digest"],
});
// → { headers: { "Signature-Input": "...", "Signature": "...", "Content-Digest": "..." } }
b.crypto.httpSig.verify(msg, opts) #
{
keyResolver: function, // (keyid, alg) → PEM public key (required)
toleranceMs: number, // clock skew allowed on created / expires
requiredComponents: string[], // covered-component floor; [] waives it
}
Verify the Signature-Input and Signature headers on a received message. Returns { valid, label, keyid, alg, covered } on success and { valid: false, reason } otherwise, rather than throwing, so a handler decides what a bad signature means for the route.
keyResolver(keyid, alg) returns the PEM public key to check against, or a falsy value to refuse the key. requiredComponents is the floor the covered set must include; omitting it applies @method and @target-uri, plus content-digest when the message has a body. Pass [] to waive the floor.
var res = b.crypto.httpSig.verify({
method: req.method, url: fullUrl, headers: req.headers, body: bodyBuffer,
}, {
keyResolver: function (keyid) { return keys[keyid] || null; },
toleranceMs: b.constants.TIME.minutes(5),
});
// → { valid: true, label: "sig1", keyid: "service-a-2026-05", alg: "ed25519", covered: [...] }