CMS

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 -- 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 -- that is the caller's step through pki.path.validate.

pki.cms.verify

since 0.2.14 experimental
pki.cms.verify(input, opts?) -> Promise<{ valid, 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, signers } where each signers[i] is { ok, sid, cert } (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. 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) -- pure mode, empty context -- signatures are recognized, 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

certs    Extra signer certificates (an array of DER `Buffer`s) to match against, in
                addition to the certificates embedded in the SignedData.

Example

async function example() {
  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 experimental
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 -- the message digest pinned per parameter set) are covered, as is composite ML-DSA (draft-ietf-lamps-cms-composite-sigs) -- the arm's pre-hash fixes the digestAlgorithm and the two component keys are signed together.

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 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 experimental
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 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 experimental
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. { 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. Malformed input throws a typed CmsError.

Options

pem           Return a PEM string instead of a DER Buffer.

Example

async function example() {
  var env = await pki.cms.encrypt(Buffer.from("secret"), [{ cert: recipientCertDer }]);
}
example();

References

pki.cms.authenticate

since 0.3.14 experimental
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 -> 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() {
  var r = pki.cms; // authenticate a message to an RSA recipient, then verify via decrypt
  var auth = await r.authenticate(Buffer.from("hi"), [{ cert: recipientCertDer }]);
  var out = await r.decrypt(auth, { key: recipientKeyPkcs8, cert: recipientCertDer });
  out.authenticated;   // true
}
example();

References

pki.cms.decrypt

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

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), 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 -- a bad key-wrap, a padding fault, a GCM tag mismatch, a PWRI check-byte mismatch, an AuthenticatedData MAC/message-digest mismatch -- collapses to the SINGLE uniform cms/decrypt-failed verdict (Bleichenbacher / EFAIL oracle freedom, and no unwrap-success bit for the MAC path), and 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).

Options

maxIterations  Lower the PBKDF2 iteration cap (a DoS bound; downward only).

Example

async function example() {
  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 experimental
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 experimental
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 res = await pki.cms.decompress(compressedDer);
  res.content;   // the recovered plaintext Buffer
}
example();

References