COSE signing (RFC 9052)

COSE_Sign1 signing and verification (RFC 9052 / 9053), composing the in-tree b.cbor codec for the deterministic Sig_structure encoding. COSE is the signed-statement substrate under SCITT, CWT, and C2PA — a CBOR-native counterpart to JWS.

Signing supports the classical COSE signature algorithms that are interoperable today — ES256 / ES384 / ES512 (ECDSA) and EdDSA (Ed25519), all with final IANA algorithm ids (RFC 9053) — alongside ML-DSA-87 (FIPS 204) for PQC-forward deployments. There is no classical default: the caller names the algorithm and supplies the key. Verification accepts the same set, so the framework both produces COSE other implementations can read today and consumes third-party COSE.

Standards-maturity caveat on the PQC algorithm: the COSE algorithm identifier for ML-DSA-87 is -50, a requested (non-final) IANA assignment from draft-ietf-cose-dilithium; it may change before that draft is published, so an ML-DSA-87 COSE_Sign1 is not yet broadly interoperable — pin the identifier deliberately, re-open on IANA finalization. SLH-DSA-SHAKE-256f (the framework's default PQC signature elsewhere) has no COSE algorithm identifier registered at all (the COSE SPHINCS+ draft registers only the Category-1 'small' sets), so it cannot be represented in COSE and is not offered here. The COSE_Sign1 mechanism itself, and the classical algorithms, are stable; ML-DSA-87 is the forward- looking opt-in.

Verify is bounded. The COSE_Sign1 bytes and the protected-header bstr are decoded through b.cbor.decode (depth + size caps, indefinite-length / tag / duplicate-key refusal). The protected header is the integrity-protected one; alg (label 1) lives there. A crit (label 2) listing a header label the verifier does not understand is refused (RFC 9052 §3.1) — a crit-bypass defense.

Ships COSE_Sign1 (single-signer, attached payload), COSE_Mac0, and COSE_Encrypt0 (single-recipient AEAD). Detached payload, COSE_Sign (multi-signer), and COSE_Encrypt (multi-recipient) are deferred-with-condition (operator demand).

b.cose.sign(payload, opts) #

stable0.12.33
{
  {
    alg:                 string,    // "ES256" | "ES384" | "ES512" | "EdDSA" | "ML-DSA-87"
    privateKey:          object,    // matching KeyObject or PEM
    kid?:                string,    // → unprotected header label 4
    contentType?:        number|string, // → protected header label 3 (CoAP Content-Format uint or media-type string)
    externalAad?:        Buffer,    // default empty — bound into the signature
    unprotectedHeaders?: object,    // extra unprotected map entries (numeric keys)
    protectedHeaders?:   object,    // extra INTEGRITY-PROTECTED map entries (numeric keys); label 1 (alg) is reserved
    detached?:           boolean,   // emit a nil payload (RFC 9052 §4.1) — signature still covers it; caller transmits the payload separately
  }
}

Produce a tagged COSE_Sign1 (RFC 9052) over payload (bytes). alg is one of the classical ECDSA / EdDSA algorithms (final COSE ids, interoperable today) or "ML-DSA-87" (draft id -50, PQC-forward). alg is placed in the integrity-protected header.

var coseSign1 = await b.cose.sign(Buffer.from("statement"), {
  alg: "ES256", privateKey: ecKey, kid: "key-1",
});

b.cose.verify(coseSign1, opts) #

stable0.12.33
{
  {
    algorithms:   string[],  // required — accepted alg names (allowlist)
    publicKey?:   object,    // the verification key (KeyObject / PEM)
    keyResolver?: function,  // (protectedHeaders, unprotectedHeaders) → key
    externalAad?: Buffer,    // must match what was signed
    externalPayload?: Buffer, // required when the COSE_Sign1 payload is detached (nil); bound into the Sig_structure
    maxBytes?:    number,    // forwarded to b.cbor.decode
    maxDepth?:    number,
  }
}

Verify a COSE_Sign1 (RFC 9052) and return its payload + headers. The bytes are decoded through the bounded b.cbor codec; alg is read from the integrity-protected header and must be in opts.algorithms; a crit header naming a label the verifier does not understand is refused. Accepts ML-DSA-87 plus the classical ECDSA / EdDSA COSE algorithms.

var out = await b.cose.verify(coseSign1, { algorithms: ["ML-DSA-87"], publicKey: pub });
// → { payload: , alg: "ML-DSA-87", protectedHeaders: Map, unprotectedHeaders: Map }

b.cose.encrypt0(plaintext, opts) #

stable0.12.36
{
  {
    alg:        string,   // "ChaCha20-Poly1305" (default) | "A256GCM" | "A128GCM"
    key:        Buffer,   // symmetric key (32 bytes for ChaCha/A256GCM, 16 for A128GCM)
    iv?:        Buffer,   // 12-byte IV (random if omitted)
    externalAad?: Buffer, // bound into the AEAD tag
    unprotectedHeaders?: object,
  }
}

Encrypt bytes into a tagged COSE_Encrypt0 (RFC 9052 §5.2), a single-recipient AEAD container where the recipient already holds the symmetric key (direct mode). Default algorithm is ChaCha20-Poly1305; A256GCM / A128GCM are opt-in. The Enc_structure is bound as the AEAD associated data, and the authentication tag is appended to the ciphertext per COSE.

var enc = b.cose.encrypt0(Buffer.from("secret"), { alg: "ChaCha20-Poly1305", key: k });

b.cose.decrypt0(coseEncrypt0, opts) #

stable0.12.36
{
  {
    key:        Buffer,    // symmetric key
    algorithms: string[],  // required — accepted AEAD algs (allowlist)
    externalAad?: Buffer,  // must match what was encrypted
    maxBytes?:  number,
    maxDepth?:  number,
  }
}

Decrypt a COSE_Encrypt0 and return the plaintext. The algorithm is read from the protected header and must be in opts.algorithms; the Enc_structure is reconstructed as the AEAD associated data and authentication failure (wrong key / tampered ciphertext or AAD) is refused.

var pt = b.cose.decrypt0(enc, { key: k, algorithms: ["ChaCha20-Poly1305"] }).plaintext;

b.cose.mac0(payload, opts) #

stable0.12.47
{
  {
    alg:        string,   // "HMAC-256/256" | "HMAC-384/384" | "HMAC-512/512"
    key:        Buffer,   // shared symmetric key
    externalAad?: Buffer, // bound into the MAC
    detached?:  boolean,  // emit a nil payload (caller re-supplies it on verify)
    unprotectedHeaders?: object,
  }
}

Produce a tagged COSE_Mac0 (RFC 9052 §6.2) — a single shared-key MAC over payload. The MAC is HMAC-SHA-256 / 384 / 512 (the COSE-standard MAC algorithms; HMAC is symmetric, so post-quantum strength is preserved). Use when both parties hold a shared key (e.g. an ECDH-derived key) and a non-repudiable signature is not wanted. detached: true emits a nil payload, verified later with opts.externalPayload.

var mac = b.cose.mac0(Buffer.from("data"), { alg: "HMAC-256/256", key: sharedKey });

b.cose.macVerify0(coseMac0, opts) #

stable0.12.47
{
  {
    algorithms:  string[],  // required — accepted HMAC alg names (allowlist)
    key:         Buffer,    // the shared symmetric key
    externalAad?: Buffer,
    externalPayload?: Buffer, // required for a detached payload
    maxBytes?:   number,
    maxDepth?:   number,
  }
}

Verify a COSE_Mac0 (RFC 9052 §6.2) and return its payload. The HMAC tag is recomputed over the MAC_structure and compared in constant time; the alg from the protected header must be in opts.algorithms. A detached (nil) payload is supplied via opts.externalPayload.

var out = b.cose.macVerify0(mac, { algorithms: ["HMAC-256/256"], key: sharedKey });
// → { payload: , alg: "HMAC-256/256", protectedHeaders: Map, unprotectedHeaders: Map }

b.cose.importKey(coseKey) #

stable0.12.45

Import a COSE_Key (RFC 9052 §7) — a CBOR map keyed by integer labels — as a node:crypto public KeyObject for b.cose.verify. Accepts the EC2 (kty 2: P-256 / P-384 / P-521) and OKP (kty 1: Ed25519) key types — the curves b.cose.verify has an algorithm for; the curve is allowlisted, so an unexpected key type (including secp256k1, which has no ES256K path here) is refused rather than imported. The verification key embedded in an mdoc MSO or a COSE_Key header is consumed this way.

var key = b.cose.importKey(coseKeyMap);            // → public KeyObject
var out = await b.cose.verify(sign1, { algorithms: ["ES256"], publicKey: key });

b.cose.exportKey(keyObject, opts?) #

stable0.13.20
{
  alg:   string,          // optional COSE alg label (e.g. "ES256") → COSE_Key label 3
  kid:   Buffer | string, // optional key id → COSE_Key label 2 (bstr; string encoded UTF-8)
}

Serialize a node:crypto public key as a COSE_Key (RFC 9052 §7) — the CBOR-map, integer-labelled form embedded in an mdoc MSO, a COSE_Key header, or a SCITT / C2PA verification-key field. The inverse of b.cose.importKey: a key signed with b.cose.sign can be shipped to a verifier as bytes and re-imported. Returns the CBOR-encoded bytes (like the other COSE producers); pass the decoded map to importKey to round-trip.

Accepts the same key types importKey / verify understand: EC2 (P-256 / P-384 / P-521) and OKP (Ed25519). A private key has its public half exported. Other curves / key types are refused rather than emitting a COSE_Key no verifier here would accept.

var bytes = b.cose.exportKey(pubKey, { alg: "ES256", kid: "key-1" });
var key   = b.cose.importKey(b.cbor.decode(bytes));   // round-trips

Last updated 2026-08-08T16:39:49.652Z by seeder.