CMS (Cryptographic Message Syntax) signing and verification

Verify a CMS SignedData signature (RFC 5652 sec. 5), the structure S/MIME signed mail, RFC 3161 timestamp tokens, and code-signing all rest on. verify(cms, opts) parses the SignedData (over the strict pki.schema.cms codec), locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact bytes RFC 5652 sec. 5.4 defines: when signed attributes are present it confirms the message-digest attribute equals the digest of the content and verifies the signature over the DER re-encoding of the SignedAttributes (the on-wire [0] tag replaced by a universal SET OF); otherwise it verifies directly over the content. Attached and detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA, accepted only when both components verify, per draft-ietf-lamps-cms-composite-sigs) are covered. It reports a per-signer verdict; it does not chain the signer certificate to a trust anchor, which is the caller's step through pki.path.validate.

pki.cms.verify

since 0.2.14 stable
pki.cms.verify(input, opts?) -> Promise<{ valid, trusted, eContentType, eContent, signers }>

Verify a CMS SignedData signature (RFC 5652 sec. 5). input is a PEM string, a DER Buffer, or a parsed pki.schema.cms object. Returns { valid, trusted, eContentType, eContent, signers } (eContent the encapsulated content when present, else null) where each signers[i] is { ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures } (cert the matched signer certificate DER) or carries a code on a structural failure; valid is true when there is at least one signer and every signer verified.

eContentType and signedAttributesPresent are there for a caller whose profile is stricter than RFC 5652's. Signing WITH attributes and signing the content directly are different claims: attributes bind a content type and a signing time alongside the digest, content-only binds nothing but the bytes. One message may carry a signer of each. A profile that requires attributes (RFC 8551 S/MIME does) or a particular content type can enforce it from the verdict, with no need to parse the message a second time.

valid and trusted are DIFFERENT claims and neither implies the other. A SignedData carries its own certificates, so valid establishes that the message is internally consistent: the signature is sound under a certificate the message or opts.certs supplied. Anyone can mint a certificate, sign with it, and embed it, so that says nothing about WHO signed. trusted says every signer chained to a root named in opts.trustAnchors, validated through the same RFC 5280 path engine pki.path.validate uses. Without anchors there is nothing to chain to and trusted is false, a definite answer, not a missing one. Anchors that cannot be read are a configuration fault and throw.

Trust is decided from the certificate the SignerInfo selected, the one reported as signers[i].cert, never from another certificate that happens to share its key. A subjectKeyIdentifier names a key, and several certificates can hold it with different validity windows, key usage and policies; deciding from a sibling would let an expired or wrong-purpose signer certificate be reported trusted because a different certificate chained. Supply the certificate you want used. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA, EdDSA, and the post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814) signatures are recognized, the post-quantum families in pure mode with an empty context, as is composite ML-DSA (draft-ietf-lamps-cms-composite-sigs), which pairs ML-DSA with a traditional RSA / ECDSA / EdDSA and verifies only when both components pass (never an AND-to-OR downgrade).

Options

checkPurpose  The purpose the ANCHOR's own trust metadata must permit. This is a separate
                question from `requiredEku`, since a root distributed with NSS trust bits can be
                marked untrusted for one purpose and good for another. Those bits and
                `distrustAfter` are consulted only when this names a purpose.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var key = await pki.key.export(pair.privateKey);
  var cert = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
  var detachedBytes = Buffer.from("hello");
  var p7sDer = await pki.cms.sign(detachedBytes, { cert: cert, key: key, detached: true });
  var res = await pki.cms.verify(p7sDer, { content: detachedBytes });
  res.valid;                         // boolean
  res.signers[0].ok;                 // per-signer verdict
}
example();

References

  • spec RFC 9814
  • defends cms-signature-forgery (CWE-347)

pki.cms.sign

since 0.2.15 stable
pki.cms.sign(content, signers, opts?) -> Promise<Buffer|string>

Produce a CMS SignedData (RFC 5652 sec. 5) over content (a Buffer): the structure S/MIME signed mail, RFC 3161 timestamp tokens, and code signing rest on, and exactly what pki.cms.verify consumes and OpenSSL cms -verify validates. Each signers[i] is { cert, key, digestAlgorithm?, pss? }: cert the signer certificate (PEM or DER), key its private key (a WebCrypto CryptoKey or a PKCS#8 DER Buffer / PEM string; for a composite ML-DSA signer, the two component keys { mldsa, trad }, each PKCS#8). The signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the message-digest attribute is bound to the content digest and the signature is over the canonical DER SET OF SignedAttributes; otherwise over the content directly. RSA (PKCS#1 v1.5 and, with pss, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, Ed448, and the post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814, with the message digest pinned per parameter set) are covered, as is composite ML-DSA (draft-ietf-lamps-cms-composite-sigs), where the arm's pre-hash fixes the digestAlgorithm and the two component keys are signed together.

content may instead be an async iterable of byte chunks, such as an async generator, which signs a large payload without holding it in memory. This form requires detached: true and signed attributes. The payload is hashed incrementally to build the message-digest attribute, in a single pass that serves every signer, so signers may use different digest algorithms. The signature covers the same content as the buffered form.

Options

unsignedAttributes  Unsigned attributes for every SignerInfo, each `{ type, values }`
                         (`type` an OID name or dotted string, `values` an array of DER value
                         `Buffer`s). Placed in the SignerInfo `[1]` unsignedAttrs, outside the
                         signature, so they carry no cryptographic assurance and a verifier never
                         reports them authenticated. The vehicle for an RFC 3161 timestamp token
                         (`timeStampToken`); content-type / message-digest / signing-time are
                         rejected here (RFC 5652 sec. 11). Use `pki.cms.countersign` for a
                         countersignature specifically.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
  var signerCertDer = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: signerKeyPkcs8 });
  var p7 = await pki.cms.sign(Buffer.from("hello"), { cert: signerCertDer, key: signerKeyPkcs8 });
  var res = await pki.cms.verify(p7);   // res.valid === true
}
example();

References

pki.cms.countersign

since 0.3.13 stable
pki.cms.countersign(cms, signers, opts?) -> Promise<Buffer|string>

Add one or more countersignatures to an existing CMS SignedData (RFC 5652 sec. 11.4). A countersignature is itself a SignerInfo whose signature covers the CONTENTS of the countersigned SignerInfo's signature value (not the content), attached as the id-countersignature unsigned attribute, the construction Authenticode timestamps and signature-attestation workflows rest on. cms is the SignedData (a DER Buffer or a PEM CMS string; the wire bytes are preserved, so the countersigned primary still verifies byte-for-byte). Each signers[i] is the same descriptor pki.cms.sign takes ({ cert, key, digestAlgorithm?, pss? }, any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite key), and countersigns over the target signature octets; pki.cms.verify returns each countersignature verdict under signers[i].countersignatures. A countersignature never carries a content-type attribute (sec. 11.4); multiple countersignatures on one signer are multiple values of the one id-countersignature attribute.

Options

pem                   Return a PEM string instead of a DER Buffer.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
  var signerCertDer = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: signerKeyPkcs8 });
  var p7 = await pki.cms.sign(Buffer.from("hi"), { cert: signerCertDer, key: signerKeyPkcs8 });
  var cs = await pki.cms.countersign(p7, { cert: signerCertDer, key: signerKeyPkcs8 });
  var res = await pki.cms.verify(cs);
  res.signers[0].countersignatures[0].ok;   // true
}
example();

References

pki.cms.encrypt

since 0.2.23 stable
pki.cms.encrypt(content, recipients, opts?) -> Promise<Buffer | string>

Encrypt content as a CMS EnvelopedData (CBC content), AuthEnvelopedData (AEAD content, the default), or EncryptedData. recipients is an ARRAY of recipient descriptors for the enveloped family, each wrapping the same fresh content-encryption key: { cert } auto-dispatches off the recipient certificate's public-key algorithm: RSA yields a ktri with RSAES-OAEP-SHA256 (PKCS#1 v1.5 is never emitted); EC P-256/384/521 a kari with ephemeral-static ECDH and the X9.63 KDF; X25519/X448 a kari per RFC 8418 (HKDF); ML-KEM-512/768/1024 an ori/KEMRecipientInfo per RFC 9629 + 9936; a composite ML-KEM (id-MLKEM768-* / id-MLKEM1024-*, draft-lamps-pq-composite-kem) the same ori/KEMRecipientInfo with the composite as the kem algorithm and AES-256-Wrap. { password } yields a pwri (PBKDF2 + RFC 3211 PWRI-KEK); { kek, kekId } a kekri (AES key wrap). For EncryptedData (no RecipientInfos), pass a single non-array { cek } or { password } descriptor. The default AES-256-GCM content encryption produces an authenticated AuthEnvelopedData; a CBC choice produces an unauthenticated EnvelopedData. content is a byte source (a Buffer, typed array, DataView, or ArrayBuffer) OR an async iterable of byte chunks. An async iterable is streamed through the content cipher incrementally, so a large plaintext is never buffered whole; the ciphertext is assembled before the DER is emitted, and the emitted message is byte-for-byte what the buffered form of the same plaintext would produce. Malformed input throws a typed CmsError.

Options

pem           Return a PEM string instead of a DER Buffer.

Example

async function example() {
  var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
  var pair = await pki.key.generate(rsa);
  var recipientKeyPkcs8 = await pki.key.export(pair.privateKey);
  var recipientCertDer = await pki.x509.sign({ subject: "Recipient", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: recipientKeyPkcs8 });
  var env = await pki.cms.encrypt(Buffer.from("secret"), [{ cert: recipientCertDer }]);
}
example();

References

pki.cms.authenticate

since 0.3.14 stable
pki.cms.authenticate(content, recipients, opts?) -> Promise<Buffer | string>

Produce a CMS AuthenticatedData (RFC 5652 sec. 9): CLEARTEXT content plus a MAC (HMAC-SHA-2), authenticated but not encrypted, the authenticated-only sibling of AuthEnvelopedData. A single fresh MAC key is minted and wrapped for every recipient with the same RecipientInfo model pki.cms.encrypt uses for a content-encryption key, so recipients is the identical array of descriptors: { cert } (RSA -> ktri RSAES-OAEP, EC/X25519/X448 -> kari, ML-KEM or composite ML-KEM -> ori/KEMRI), { password } (pwri), or { kek, kekId } (kekri). By default the MAC covers the authenticated attributes (a content-type and a message-digest = digest of the content) re-tagged to the EXPLICIT SET OF (RFC 5652 sec. 9.2); with authenticatedAttributes: false (id-data content only) it covers the content octets directly. pki.cms.decrypt recovers the MAC key, recomputes the MAC, and (with authenticated attributes) independently confirms the message-digest before releasing the content, returning authenticated: true. Malformed input throws a typed CmsError.

Options

pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.

Example

async function example() {
  // authenticate a message to an RSA recipient, then verify the MAC via decrypt
  var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
  var pair = await pki.key.generate(rsa);
  var recipientKeyPkcs8 = await pki.key.export(pair.privateKey);
  var recipientCertDer = await pki.x509.sign({ subject: "Recipient", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: recipientKeyPkcs8 });
  var auth = await pki.cms.authenticate(Buffer.from("hi"), [{ cert: recipientCertDer }]);
  var out = await pki.cms.decrypt(auth, { key: recipientKeyPkcs8, cert: recipientCertDer });
  out.authenticated;   // true
}
example();

References

pki.cms.decrypt

since 0.2.23 stable
pki.cms.decrypt(input, keyMaterial, opts?) -> Promise<{ content, contentType, contentTypeName, recipientType, recipientIndex, contentEncryptionAlgorithm, authenticated, authenticatedBy, originAuthenticated, originatorInfo }>

Decrypt a CMS EnvelopedData / AuthEnvelopedData / EncryptedData, or verify a CMS AuthenticatedData (DER Buffer or PEM). It selects the recipient the key material targets, acquires the content-encryption or MAC key through the matching arm (ktri OAEP or PKCS#1 v1.5 decrypt-only; kari ECDH / X25519 / X448; kekri; pwri; ori/ML-KEM or composite ML-KEM), and decrypts (or MAC-verifies) the content. keyMaterial is { key, cert } (the recipient private key + its certificate, which drives recipient matching), { password }, { kek, kekId? }, or { cek } (EncryptedData raw-key mode). Fail-closed and oracle-free: every secret-dependent failure collapses to the SINGLE uniform cms/decrypt-failed verdict (Bleichenbacher / EFAIL oracle freedom, and no unwrap-success bit for the MAC path). That covers a bad key-wrap, a padding fault, a GCM tag mismatch, a PWRI check-byte mismatch, and an AuthenticatedData MAC/message-digest mismatch. The PKCS#1 v1.5 arm applies the RFC 3218 implicit-rejection countermeasure so its failure is indistinguishable. For an AuthenticatedData the MAC (HMAC-SHA-2) and, when authenticated attributes are present, the message-digest are verified before the content is released, and the result carries macAlgorithm / digestAlgorithm in place of contentEncryptionAlgorithm. authenticated is true for AuthEnvelopedData and AuthenticatedData; a CBC EnvelopedData surfaces authenticated: false (the EFAIL caveat in the verdict itself).

authenticated is a claim about the CONTENT and the key that opened it, never about who sent the message, so the origin question is answered separately. originAuthenticated is false for every recipient type: a ktri or ephemeral-static kari message is minted by anyone holding the recipient's PUBLIC key, and a pwri or kekri message by any co-recipient sharing the secret. Read authenticated: true as "these bytes were not altered after the key was chosen", and do not read it as a sender identity. To bind one, verify a signature (pki.cms.verify) over the plaintext. authenticatedBy names what the integrity rests on ("content-encryption-key" for AEAD, "message-authentication-key" for AuthenticatedData, null for an unauthenticated cipher). originatorInfo is surfaced as the sender supplied it and is UNAUTHENTICATED: it sits outside the AEAD's authenticated data, so it is a hint, never evidence, and any certificate it carries must be validated before use.

With stream: true the result's content is an async iterable of plaintext Buffer chunks rather than a single Buffer, for a payload too large to hold whole. An AuthEnvelopedData, an AuthenticatedData, and the AEAD path verify integrity before the iterable yields, so a forged message throws cms/decrypt-failed and no unverified plaintext is ever exposed. A CBC EnvelopedData or EncryptedData carries no integrity, so it yields plaintext as the cipher produces it and a corrupt message surfaces the same uniform cms/decrypt-failed on iteration, after earlier chunks reached the caller who holds the key.

Options

stream         Return `content` as an async iterable of plaintext `Buffer` chunks instead of a single `Buffer`.

Example

async function example() {
  var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
  var pair = await pki.key.generate(rsa);
  var recipientKeyPkcs8 = await pki.key.export(pair.privateKey);
  var recipientCertDer = await pki.x509.sign({ subject: "Recipient", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: recipientKeyPkcs8 });
  var envDer = await pki.cms.encrypt(Buffer.from("secret"), [{ cert: recipientCertDer }]);
  var res = await pki.cms.decrypt(envDer, { key: recipientKeyPkcs8, cert: recipientCertDer });
  res.content;   // the recovered plaintext Buffer
}
example();

References

pki.cms.compress

since 0.2.27 stable
pki.cms.compress(content, opts?) -> Promise<Buffer | string>

ZLIB-compress content and wrap it as a CMS CompressedData ContentInfo (RFC 3274): version 0, compressionAlgorithm = id-alg-zlibCompress (parameters omitted), encapContentInfo = the inner content type plus the RFC 1950 ZLIB stream (RFC 1951 DEFLATE) as the eContent. There is exactly one compression algorithm in RFC 3274, ZLIB, so there is no algorithm selector. CompressedData is a size transform with NO integrity, confidentiality, or authentication (RFC 8551 sec. 2.4.5); compress it, then sign or encrypt it if you need protection. Malformed input throws a typed CmsError.

Options

pem         Return a PEM string instead of a DER Buffer.

Example

async function example() {
  var z = await pki.cms.compress(Buffer.from("compress me"));
}
example();

References

pki.cms.decompress

since 0.2.27 stable
pki.cms.decompress(input, opts?) -> Promise<{ content, contentType, contentTypeName, compressionAlgorithm }>

Parse a CMS CompressedData (DER Buffer or PEM) and recover its content. It requires version 0, compressionAlgorithm = id-alg-zlibCompress with absent-or-NULL parameters (RFC 3274 sec. 2), and a present eContent, then inflates the RFC 1950 ZLIB stream. The inflate is BOUNDED at C.LIMITS.COMPRESS_MAX_BYTES (16 MiB) and stops before the output is materialized, a decompression-bomb / resource-exhaustion defense (CWE-409): a cap breach throws cms/decompress-too-large, and every malformed / truncated / corrupt stream collapses to the uniform cms/decompress-failed. opts.maxOutputBytes tightens the cap DOWNWARD only. The verdict carries NO authenticated / valid field. CompressedData is not a security assertion (RFC 8551 sec. 2.4.5).

Options

maxOutputBytes Lower the decompressed-output cap (a DoS bound; downward only).

Example

async function example() {
  var compressedDer = await pki.cms.compress(Buffer.from("compress me"));
  var res = await pki.cms.decompress(compressedDer);
  res.content;   // the recovered plaintext Buffer
}
example();

References

pki.cms.certsOnly

since 0.6.3 stable
pki.cms.certsOnly(certs, opts?) -> Buffer | string

Build a "certs-only" certificate-management message (RFC 8551 sec. 3.8): a degenerate CMS SignedData that conveys certificates and/or CRLs and signs nothing. The emitted structure is version 1 with an empty digestAlgorithms, an id-data encapContentInfo whose eContent is absent, the certificates in certificates [0] and any CRLs in crls [1] (both DER-sorted and deduplicated), and an empty signerInfos. It is how an AIA caIssuers bundle or an application/pkcs7-mime; smime-type=certs-only attachment distributes a set of certificates. certs is a certificate or an array of them (DER Buffer or PEM string), each parsed before embedding so a non-certificate is a typed CmsError and is never emitted. At least one certificate or CRL is required.

Options

pem  Return a PEM string instead of a DER Buffer.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var leafDer = await pki.x509.sign({ subject: "Leaf", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: await pki.key.export(pair.privateKey) });
  var bundle = pki.cms.certsOnly([leafDer]);
  var r = pki.cms.parseCertsOnly(bundle);   // { certificates: [leafDer], crls: [] }
}
example();

References

pki.cms.parseCertsOnly

since 0.6.3 stable
pki.cms.parseCertsOnly(input, opts?) -> { certificates, crls }

Read a certs-only certificate-management message (DER Buffer or PEM) and recover the raw certificate and CRL DER it carries. It requires a degenerate SignedData (id-data with no eContent, empty signerInfos) and returns { certificates: [DER], crls: [DER] }; a non-degenerate structure is a typed cms/not-certs-only. A CRL-only message is accepted (RFC 8551 sec. 3.8 conveys "certificates and/or CRLs"), unlike the stricter RFC 5272 Simple PKI Response pki.est.parseCertsOnly; a message carrying neither certificates nor CRLs is refused. Each embedded certificate and CRL is validated before it is returned.

Options

maxCerts Cap the number of certificates and CRLs parsed and returned (a DoS bound on an untrusted bundle).

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var certDer = await pki.x509.sign({ subject: "Leaf", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: await pki.key.export(pair.privateKey) });
  var bundleDer = pki.cms.certsOnly([certDer]);
  var r = pki.cms.parseCertsOnly(bundleDer);
  r.certificates;   // array of raw certificate DER Buffers
}
example();

References

pki.cms.isCertsOnly

since 0.6.3 stable
pki.cms.isCertsOnly(input) -> boolean

Recognize a certs-only certificate-management message structurally: true when input (DER Buffer or PEM) is a CMS SignedData with an id-data encapContentInfo whose eContent is absent and an empty signerInfos, false for any other well-formed CMS (a signed message, an EnvelopedData). Undecodable bytes throw a typed CmsError.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var certDer = await pki.x509.sign({ subject: "Leaf", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: await pki.key.export(pair.privateKey) });
  pki.cms.isCertsOnly(pki.cms.certsOnly([certDer]));   // true
}
example();

References