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, signatureTimeStamps, cadesBaseline, algorithmProtection } (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. Where several candidates match the identifier and the signature carries an ESS signing-certificate attribute, the candidate that attribute names is the one selected, since RFC 5035 clause 5.4 makes its first identifier "the certificate used to verify the signature"; a named candidate must still verify the signature like any other. 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).

signatureTimeStamps is one row per id-aa-signatureTimeStampToken unsigned attribute (RFC 5126 clause 6.1.1, ETSI EN 319 122-1 clause 5.3), each { valid, trusted, genTime, accuracy, serialNumber, policy, revocationChecked, tsa, signer } or { valid: false, code, message }. A token that does not verify is a row verdict and does not take the signer's own ok down: the signature and the time it was made at are separate claims. Each row's imprint is checked against the signature octets this verb just verified, and a row is refused when its genTime precedes the signer's own signing-time attribute or falls after the signing certificate's notAfter (EN 319 122-1 clause 6.3 requirement m).

What a valid: true row establishes is that the token was issued over the signature octets THIS SignerInfo carries. It does not say that signature is sound, which is signers[i].ok, and rows are reported beside a failed signature rather than withheld, so a caller acting on a timestamp reads both. The imprint names signature octets rather than a signer, which is what EN 319 122-1 clause 5.3 specifies: two certificates holding one key produce one signature value over one content, and a token over it answers for either. pki.cms.attachTimestamp adds such an attribute to an existing message and pki.cms.timestampImprint produces the imprint a TSA signs.

algorithmProtection reports the RFC 6211 attribute, { present, oid, compared, mismatch }. The attribute is a copy of the digest and signature algorithm identifiers this signer names, placed where the signature covers them, and compared lists the identifiers checked against it. A disagreement fails the signer with cms/algorithm-protection-mismatch, which sec. 3.1 requires of any validator that supports the attribute: the identifiers the attribute protects are otherwise covered by nothing, and RFC 8933 sec. 6 names RSASSA-PSS as the case where the signature itself pins no digest. An absent attribute is not a failure unless requireAlgorithmProtection asks for one, and present: false says so rather than looking like a comparison that passed.

cadesBaseline is a third claim, { conformant, signingCertificateAttribute, findings, requirementsChecked }: a signature can be sound and still not be a CAdES signature. requirementsChecked lists the EN 319 122-1 requirements decided here, each unmet one carrying a { code, message } finding, so conformant is read against that list and not as a full clause 6.3 audit. The signing-certificate requirement is the attribute's CLAIM checked against the certificate this SignerInfo selected, the one reported as signers[i].cert, not the attribute's presence. Like the timestamp rows it is reported beside a failed signature rather than withheld, so conformant: true on a signer whose ok is false says the CAdES attributes are in order and nothing about the signature. pki.cms.sign reaches conformant: true with a signingCertificateV2 attribute built by pki.schema.smime.buildSigningCertificateV2.

Options

requireAlgorithmProtection  Fail a signer that carries no RFC 6211
                `CMSAlgorithmProtection` attribute, with `cms/missing-algorithm-protection`. Off
                by default: RFC 6211 says nothing about absence and RFC 8933 sec. 4.1 makes
                including it a SHOULD, so a message without one is not malformed and most CMS in
                the field carries none. A signer that DOES carry one is always held to it, with no
                option, because sec. 3.1's comparisons are MUSTs once a validator supports the
                attribute. The option is the boolean it documents: `false`, `null` and an absent
                option are the off forms, and any other value is refused `cms/bad-input`.

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, a PKCS#8 DER Buffer / PEM string, or a signer { algorithm, publicKey, sign } for a key held outside this process; 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

algorithmProtection  Emit the RFC 6211 `CMSAlgorithmProtection` signed attribute, a copy of
                         this signer's digest and signature algorithm identifiers placed where the
                         signature covers them. RFC 8933 sec. 4.1 makes including it a SHOULD, and
                         RFC 9882 sec. 4 and RFC 9814 sec. 5 repeat that for ML-DSA and SLH-DSA,
                         which carry no digest identifier inside the signature. Off by default,
                         because turning it on changes the bytes of every signature. `true` emits
                         the identifier RFC 6211 sec. 2 states, which OpenSSL and Bouncy Castle
                         use; `"registry"` emits the S/MIME arc value the IANA registry and
                         draft-ietf-lamps-rfc6211-update give it. A verifier here reads both.
                         `false`, `null` and an absent option emit none, and any other value is
                         refused `cms/bad-input`.

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

requireAlgorithmProtection  For an AuthenticatedData, refuse one that carries no RFC 6211
      `CMSAlgorithmProtection` authenticated attribute, with `cms/missing-algorithm-protection`.
      Off by default, since RFC 8933 sec. 4.1 makes including it a SHOULD. A message that DOES
      carry one is always held to it, with no option: its `digestAlgorithm` and `macAlgorithm`
      copies are compared against the fields of the `AuthenticatedData` itself, and a disagreement
      is `cms/algorithm-protection-mismatch`. That comparison runs before the MAC, because a
      mismatch is a statement about the message rather than about the key. The result's
      `algorithmProtection` field reports what was compared. The option is the boolean it
      documents: `false`, `null` and an absent option are the off forms, and any other value is
      refused `cms/bad-input`. Asking for it while decrypting any other content type is refused
      the same way: RFC 8933 sec. 6 gives the attribute a place in an `AuthenticatedData` alone,
      so an `EnvelopedData`, `AuthEnvelopedData` or `EncryptedData` cannot satisfy the
      requirement and returning its plaintext would answer neither question.

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

pki.schema.cms.parse

since 0.1.10 stable
pki.schema.cms.parse(input, caps?) -> content

Parse a DER Buffer or a PEM (CMS) string into the structured content the ContentInfo carries, dispatched by its content type. id-signedData returns { version, digestAlgorithms, encapContentInfo, certificates, crls, signerInfos }; id-envelopedData returns { version, originatorInfo, recipientInfos, encryptedContentInfo, unprotectedAttrs }; id-encryptedData returns { version, encryptedContentInfo, unprotectedAttrs }; id-ct-authData returns { version, originatorInfo, recipientInfos, macAlgorithm, digestAlgorithm, encapContentInfo, authAttrs, authAttrsBytes, mac, unauthAttrs }; id-ct-authEnvelopedData returns { version, originatorInfo, recipientInfos, encryptedContentInfo, aead, authAttrs, authAttrsBytes, mac, unauthAttrs } (aead holds the validated AES-GCM/CCM nonce + ICV length, or null for an unrecognized algorithm). A KEM recipient (RFC 9629) surfaces as { type: "ori", oriType, oriValue, kemri } with the parsed KEMRecipientInfo in kemri. Every result additionally carries contentType (the dotted OID) and contentTypeName (its registry name) naming which of the five shapes was dispatched. The raw byte ranges an external verifier hashes (eContent, signature, signedAttrsBytes, authAttrsBytes, mac) are surfaced exactly as on the wire. The remaining PKCS#7 types throw cms/unsupported-content-type; an unrecognized OID throws cms/unknown-content-type; a malformed structure throws a typed CmsError (cms/*) and a leaf-level codec fault surfaces as asn1/*.

Options

- `maxBytes` / `maxDepth` / `maxItems` (number) -- decode caps for this parse. Each
  defaults to the matching `pki.C.LIMITS` figure and may only be set lower; a value
  above it, or an option outside this set, is refused. A parse that exceeds one is
  refused with the `/too-large` code of this format.

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),
    serialNumber: 0x0a1bn, notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
    { key: key });
  var der = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
  var cms = pki.schema.cms.parse(der);
  cms.signerInfos[0].sid.serialNumberHex;   // -> "0a1b"
  cms.encapContentInfo.eContent;            // -> Buffer | null (detached)
}
example();

References

pki.schema.cms.parseAlgorithmProtection

since 0.8.53 stable
pki.schema.cms.parseAlgorithmProtection(der) -> { digestAlgorithm, signatureAlgorithm, macAlgorithm }

Decode a CMSAlgorithmProtection attribute value (RFC 6211 sec. 2): a copy of the digest algorithm identifier the structure names, plus exactly one of the signature algorithm identifier (the [1] arm, for a SignerInfo) or the message authentication code algorithm identifier (the [2] arm, for an AuthenticatedData). The arm that is absent reads as null. Each identifier is surfaced whole, { oid, name, parameters }, because the parameters are what the attribute exists to pin: a signature algorithm's parameters are not covered by the signature.

A value carrying both arms or neither is refused cms/bad-algorithm-protection-attr, which is the sec. 2 rule "Exactly one of signatureAlgorithm or macAlgorithm SHALL be present". pki.cms.parse decodes the attribute during its own walk, so a message carrying a malformed one is refused there; this verb is for reading a value held on its own.

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 signedDer = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { algorithmProtection: true });
  var si = pki.schema.cms.parse(signedDer).signerInfos[0];
  var attr = si.signedAttrs.filter(function (a) { return a.name === "cmsAlgorithmProtection"; })[0];
  var protection = pki.schema.cms.parseAlgorithmProtection(attr.values[0]);
  protection.digestAlgorithm.name;      // "sha512", the digest Ed25519 signs under here
  protection.macAlgorithm;              // null, this being a SignerInfo
}
example();

References

pki.schema.cms.pemDecode

since 0.1.10 stable
pki.schema.cms.pemDecode(text, label?) -> Buffer

Extract the DER bytes from a PEM CMS block (default label CMS). Throws PemError on a missing / mismatched envelope or a non-base64 body.

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 pemText = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { pem: true });
  var der = pki.schema.cms.pemDecode(pemText);
}
example();

References

pki.schema.cms.pemEncode

since 0.1.10 stable
pki.schema.cms.pemEncode(der, label?) -> string

Wrap DER bytes in a PEM CMS envelope (default label CMS).

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 der = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
  var pem = pki.schema.cms.pemEncode(der);
}
example();

References