JOSE (JWS / JWK)

The JOSE message envelope (RFC 7515 JWS, RFC 7518 JWA, RFC 7638 JWK thumbprint), profiled for RFC 8555 ACME but usable on its own. A JWS here is the Flattened JSON Serialization only -- { protected, payload, signature } -- with the multi-signature signatures member, the unprotected header member, and the RFC 7797 unencoded-payload b64 option all structurally forbidden. Every base64url field is decoded by a STRICT codec (Node's Buffer.from(s, "base64url") accepts padding, whitespace, and non-canonical trailing bits -- all of which are rejected here), and every JSON document is read by a bounded reader that rejects a duplicate member at any nesting depth (the parser-differential smuggling class JSON.parse silently allows).

Algorithms resolve through an alg-keyed registry (ES256/384/512, RS256/384/512, PS256/384/512, EdDSA, and the RFC 9964 ML-DSA-44/65/87 PQC rows), never a switch: the registry binds alg to a key type and pins the exact signature byte length BEFORE any crypto call, so alg:"none", a MAC algorithm on the outer profile, an ES256/RSA-key confusion, and a DER-vs-raw ECDSA signature all fail closed. sign and verify are driven by ONE declarative profile table each -- the same data drives both directions.

pki.jose.base64url.encode

since 0.1.25 stable
pki.jose.base64url.encode(bytes) -> string

Encode a Buffer as unpadded base64url (RFC 4648 sec. 5): the +// characters become -/_ and the trailing = padding is omitted.

Example

pki.jose.base64url.encode(Buffer.from([1, 2, 3]));   // -> "AQID"

References

pki.jose.base64url.decode

since 0.1.25 stable
pki.jose.base64url.decode(text) -> Buffer

Decode base64url to a Buffer, STRICTLY (RFC 8555 sec. 6.1): trailing = padding, any non-alphabet character (+, /, whitespace), and a non-canonical final character (one whose discarded low bits are non-zero) each throw jose/bad-base64url. The canonical check is a re-encode round-trip.

Example

pki.jose.base64url.decode("AQID");   // -> <Buffer 01 02 03>

References

pki.jose.parseJson

since 0.1.25 stable
pki.jose.parseJson(input) -> value

Parse a JSON document (a Buffer or a string) with a bounded, strict reader: the byte size is capped (jose/too-large), nesting is capped (jose/too-deep), a duplicate member at ANY depth is rejected (jose/duplicate-member), and a Buffer is decoded as strict UTF-8 (invalid bytes throw). Unlike JSON.parse, a duplicate key never silently resolves to the last value.

Example

pki.jose.parseJson('{"a":1}');   // -> { a: 1 }

References

pki.jose.assertPublicJwk

since 0.1.25 stable
pki.jose.assertPublicJwk(jwk) -> jwk

Assert that a JWK is PUBLIC-ONLY before it is published (embedded in a JWS protected header or an ACME External Account Binding payload). A JWK carrying any private member (d, the RSA CRT parameters p/q/dp/dq/qi, the symmetric k, or the AKP priv) throws jose/private-key-material -- so an accidentally exported private JWK is never sent to a server. Returns the JWK.

Example

pki.jose.assertPublicJwk({ kty: "EC", crv: "P-256", x: "...", y: "..." });

References

pki.jose.verify

since 0.1.25 stable
pki.jose.verify(jws, opts) -> Promise<{ header, payload }>

Verify a Flattened JSON JWS against a profile (opts.profile, default "acme-outer"). Structural rules fail closed BEFORE any crypto: the signatures/header members and a detached payload are rejected, the protected header is validated against the profile (alg registry, nonce, url, exactly-one-of jwk/kid, crit), the signature byte length is pinned per alg, and the verification key is the header jwk (only where the profile permits it) or opts.key (a JWK). Returns the decoded { header, payload } (payload a raw Buffer); a failed signature throws jose/verify-failed.

Options

profile: string   // "acme-outer" | "eab-inner" | "keychange-inner"
key: object       // a public JWK, required unless the profile embeds jwk

Example

async function example() {
  var v = await pki.jose.verify(jws, { profile: "acme-outer", key: accountJwk });
  v.header.alg;   // -> "ES256"
}
example();

References

pki.jose.sign

since 0.1.25 stable
pki.jose.sign(opts) -> Promise<{ protected, payload, signature }>

Produce a Flattened JSON JWS. opts.protected is the protected-header object (validated against opts.profile), opts.payload the raw payload octets (a Buffer; the empty Buffer yields the POST-as-GET payload:""), and opts.key a private CryptoKey. The signing input is built from the encoded header and payload and signed with the alg the header names.

Options

protected: object    // the protected header (alg, nonce, url, jwk|kid)
payload: Buffer      // the raw payload octets ("" for POST-as-GET)
key: CryptoKey       // the private signing key
profile: string      // default "acme-outer"
jwk: object          // the public JWK, when the header embeds jwk

Example

async function example() {
  var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
}
example();

References

pki.jose.thumbprint

since 0.1.25 stable
pki.jose.thumbprint(jwk) -> Promise<string>

The RFC 7638 JWK SHA-256 thumbprint as base64url: the canonical JSON of ONLY the key type's required members, lexicographically ordered, no whitespace, hashed. Optional members (alg, use, kid) are excluded, so the same key always yields the same thumbprint (the ACME key-authorization anchor).

Example

async function example() {
  await pki.jose.thumbprint(accountJwk);   // -> "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
}
example();

References