CRLs (certificate revocation lists): read, sign and shard
pki.crl.sign builds a TBSCertList, signs it, and emits a CertificateList (RFC 5280 sec. 5) that pki.schema.crl.parse, pki.path.crlChecker, and OpenSSL all accept, over any signature algorithm the toolkit registry resolves: RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite (hybrid) arms. pki.crl.verify checks a CRL signature through the one path-validation signature engine, and pki.crl.isRevoked looks a serial up in a parsed CRL. Parsing lives at pki.schema.crl.parse.pki.crl.sign
pki.crl.sign(spec, issuer, opts?) -> Promise<Buffer|string>
Build, sign, and DER-encode an X.509 certificate revocation list. spec describes the CRL: thisUpdate / nextUpdate (Dates), an optional crlNumber, a revoked array (each entry a serialNumber + revocationDate with an optional reason or invalidityDate), and an optional extensions object (authorityKeyIdentifier, issuerAltName, issuingDistributionPoint, deltaCRLIndicator, freshestCRL, authorityInfoAccess) or an array of pre-encoded Extension DER. issuerAltName takes the same GeneralName list the certificate extension takes, since RFC 5280 sec. 5.2.2 defines its OID and syntax by reference to sec. 4.2.1.7, and is emitted non-critical as that section says a conforming CRL issuer should. issuer is the signing side: { cert, key } takes the issuer DN + SPKI from a CA certificate; { name, publicKey, key } (or spec.issuer + { publicKey, key }) supplies them explicitly. The signature algorithm is resolved from the signing key, so every algorithm the toolkit signs with (RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, composite) is available without a per-algorithm branch.
The authorityKeyIdentifier is emitted whether or not the object form names it, because sec. 5.2.1 places it on every CRL a conforming issuer signs: its keyIdentifier is the issuer certificate's subjectKeyIdentifier or, without one, the method (1) value of the issuer key. A stated value is held to the issuer certificate's subjectKeyIdentifier when the issuer is given as a certificate carrying one; authorityKeyIdentifier: false is refused. The pre-encoded array form emits exactly what it is given. The version is derived from the field set (v2 when any CRL or entry extension is present, else v1). The outer signatureAlgorithm is emitted from the same source as tbsCertList.signature (sec. 5.1.1.2); an empty revocation list omits revokedCertificates instead of emitting an empty SEQUENCE (sec. 5.1.2.6); reasonCode is an ENUMERATED and invalidityDate is always GeneralizedTime (sec. 5.3.1/5.3.2); per-extension criticality is fixed by the RFC; and the produced signature is verified under the issuer key before return. A violation throws a typed CrlError; where the spec carries raw DER (an issuer Name Buffer or a pre-encoded Extension), a malformed leaf inside those bytes throws Asn1Error.
Before the key is used, the CRL the spec describes is linted against the RFC 5280 section 5 profile and refused with crl/profile-violation if any rule grades it error, naming the rule and its clause. A crlNumber and a nextUpdate are what a spec most often lacks, and sec. 5.2.3 and sec. 5.1.2.5 require both of a conforming CRL issuer.
Options
- `pem` (boolean) -- return a PEM `X509 CRL` string instead of DER.
- `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
- `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
- `profile` (string) -- a second lint profile to hold the CRL to beside RFC 5280 section 5, named
from `pki.lint.profiles()`. `"none"` runs no rules at all and emits what the spec describes,
which is how a deliberately non-conforming CRL is produced.
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: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"], subjectKeyIdentifier: true } },
{ key: signerKeyPkcs8 });
var der = await pki.crl.sign({
thisUpdate: new Date("2026-01-01T00:00:00Z"), nextUpdate: new Date("2026-02-01T00:00:00Z"),
crlNumber: 7n,
revoked: [{ serialNumber: 0x1234n, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }],
extensions: { authorityKeyIdentifier: true },
}, { cert: signerCertDer, key: signerKeyPkcs8 });
pki.schema.crl.parse(der).revokedCertificates[0].serialNumberHex; // "1234"
}
example();
References
- spec RFC 5280 sec. 5
- spec RFC 9882
- spec RFC 9814
- defends
crl-forgery (CWE-347)
pki.crl.verify
pki.crl.verify(crl, issuer) -> Promise<{ valid, issuerMaySign, signatureValid, issuer, code?, reason? }>
Verify a CRL's signature over its exact parsed tbsCertList bytes under the issuer public key. crl is a DER Buffer, a PEM string, or a parsed CRL; issuer is { cert } (DER/PEM/parsed), { publicKey } (SPKI DER), or a raw SPKI Buffer. Verification composes the one path-validation signature engine pki.path.crlChecker uses, the same algorithm-confusion (RFC 9814 sec. 4 key-OID == sig-OID) and EdDSA low-order-point gates, so there is no second, weaker CRL verifier. The verdict's valid is false on any verification fault, and signatureValid reports the signature check on its own; malformed input throws a typed CrlError.
Given a certificate in place of a bare key, it also asks what only a certificate can answer: that the certificate is the issuer this CRL names, and that its keyUsage, when it carries one, asserts cRLSign (RFC 5280 sec. 4.2.1.3, the same rule this module's signing side already enforces). Those two are issuerMaySign, reported beside signatureValid so a caller sees which held: valid is their conjunction. A signature verifying says only that SOME key signed these bytes, so a bare valid boolean would let a CRL minted under an end-entity certificate of the same CA read as that CA's own. Handed a bare SPKI there is no certificate to carry either restriction, so issuerMaySign is true and only the signature is checked. issuer is the CRL's issuer name; on a failure code / reason name which check failed. Currency and distribution-point scope remain pki.path.crlChecker.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var signerSpki = await pki.key.export(pair.publicKey);
var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
var signerCertDer = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: signerSpki,
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: signerKeyPkcs8 });
var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
{ cert: signerCertDer, key: signerKeyPkcs8 });
var res = await pki.crl.verify(crlDer, { publicKey: signerSpki }); // { valid, signatureValid, issuerMaySign, issuer }
}
example();
References
- spec RFC 5280 sec. 5.1.1.3
- spec RFC 9814
- defends
crl-signature-bypass (CWE-347)
pki.crl.isRevoked
pki.crl.isRevoked(crl, serialNumber, opts?) -> entry | null
Look a certificate serial number up in a CRL's revokedCertificates list. crl is a DER Buffer, a PEM string, or a parsed CRL; serialNumber is a BigInt, a safe integer, a decimal / 0x-hex string, or a magnitude Buffer. Returns the matching revoked-certificate entry ({ serialNumber, serialNumberHex, revocationDate, crlEntryExtensions }) or null when the serial is not listed. It does not verify the CRL signature; call pki.crl.verify or pki.path.crlChecker for that.
With opts.time and opts.historicalMode, an entry is read against that instant the way pki.path.crlChecker reads it: by default a listed serial is revoked whatever its revocationDate says, since a date in the future is post-dating or clock skew and must not read good, and only an explicit historical reading has an entry dated after the instant not yet applying. historicalMode without time names no instant to read against and is refused.
Pass opts.time to ask the question at an instant, and a CRL that does not speak for that instant is refused rather than answered from (crl/not-current): one whose thisUpdate is later, one whose nextUpdate has passed, and one carrying no nextUpdate at all, which states no window and so cannot be told from a replayed copy. Without opts.time currency goes unasked and the verb is the structural lookup it has always been; null then means "not listed on this CRL", which is weaker than "not revoked". pki.path.crlChecker decides currency against the material it fetched and is the verb to reach for when the answer has to mean the stronger thing.
It does check scope first, because a serial number means something only within the set of certificates a CRL speaks for, and this verb is given a serial and nothing else. So a CRL that speaks for part of its issuer's certificates is refused, never answered from:
- A DELTA CRL lists changes since a base, so a serial in it may be there to say the certificate was RELEASED; read alone, the entry meaning "no longer revoked" reads as "revoked" (crl/delta-not-authoritative). Merge it with its base through pki.path.crlChecker. - An INDIRECT CRL carries entries for other issuers, whose serials are unrelated to yours (crl/indirect-not-supported), as does any CRL carrying certificateIssuer on an entry while not declaring itself indirect, a contradiction about whose certificates it lists. - Any other issuingDistributionPoint narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons (crl/scope-not-authoritative). Which part applies is decided against fields of the CERTIFICATE, which this verb never sees, so an absent serial is not an unrevoked certificate. pki.path.crlChecker is handed the certificate and performs the RFC 5280 sec. 6.3.3 correspondence.
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: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: signerKeyPkcs8 });
var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n,
revoked: [{ serialNumber: 0x1234n, revocationDate: new Date("2026-01-15T00:00:00Z") }] },
{ cert: signerCertDer, key: signerKeyPkcs8 });
pki.crl.isRevoked(crlDer, 0x1234n) ? "revoked" : "not listed";
}
example();
References
pki.schema.crl.parse
pki.schema.crl.parse(input, caps?) -> crl
Parse a DER Buffer or a PEM (X509 CRL) string into a structured CRL: { version, issuer, thisUpdate, nextUpdate, revokedCertificates, crlExtensions, tbsBytes, signatureAlgorithm, signatureValue }. Every field is validated on the way in; a malformed CertificateList / TBSCertList throws a typed CrlError (crl/*) and a leaf-level codec fault surfaces as asn1/*.
Each extension record is { oid, name, critical, value, valueBytes }. valueBytes is the raw extnValue octets and value is the decoded form for the three extensions this parse decodes in place, cRLNumber, reasonCode and invalidityDate, and the same octets for every other.
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 caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
var der = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n,
revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z") }] },
{ cert: caCert, key: key });
var crl = pki.schema.crl.parse(der);
crl.revokedCertificates[0].serialNumberHex; // -> "0a3f"
}
example();
References
- spec RFC 5280
pki.schema.crl.pemDecode
pki.schema.crl.pemDecode(text, label?) -> Buffer
Extract the DER bytes from a PEM CRL block (default label X509 CRL). 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 caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
var pemText = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
{ cert: caCert, key: key }, { pem: true });
var der = pki.schema.crl.pemDecode(pemText);
}
example();
References
pki.schema.crl.pemEncode
pki.schema.crl.pemEncode(der, label?) -> string
Wrap CRL DER bytes in a PEM envelope with 64-column base64 lines (default label X509 CRL, the RFC 7468 sec. 6 armor pemDecode expects back).
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
var der = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
{ cert: caCert, key: key });
var pem = pki.schema.crl.pemEncode(der);
}
example();
References
pki.schema.crl.decodeExtensions
pki.schema.crl.decodeExtensions(crlOrBytes, opts?) -> rows
Read a CRL's extensions as decoded records: the CRL's own extensions first, then each revoked entry's. The argument is the object parse returned or the bytes parse takes.
One array spans both scopes rather than two, because RFC 5280 sec. 5.3 makes an unreadable critical ENTRY extension a verdict about the whole CRL, and a caller handed two lists can read one and not the other. Each row names where it was read: scope is "crl" or "crl-entry", and containerIndex is the position of the revoked entry an entry row came from, or null for the CRL's own.
Each row is { oid, name, critical, value, scope, index, containerIndex, state, decoded, code, profile }. state is the discriminator: "decoded", "unrecognized" for an OID no decoder is registered for AT THAT SCOPE, and "undecodable". A cRLNumber on a revoked entry and a reasonCode on the CRL are each unrecognized rather than undecodable, because neither scope profiles the other's extension.
Options
- `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
undecodable value. It does not turn an unrecognized OID into a throw.
- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call. Each may only
tighten the built-in ceiling, and each bounds the decoding this call performs.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 7n,
revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }] },
{ cert: caCert, key: key });
var rows = pki.schema.crl.decodeExtensions(crlDer);
rows.filter(function (r) { return r.scope === "crl-entry" && r.name === "reasonCode"; })[0].decoded; // -> 1
}
example();
References
- spec RFC 5280 sec. 5.2
- spec RFC 5280 sec. 5.3
pki.schema.crl.decodeExtension
pki.schema.crl.decodeExtension(ext, opts) -> row
Read one CRL extension record as a decoded row. opts.scope is required and says which table to read it against, because the same identifier means different things at the three scopes a CRL reader meets. The argument is { oid, name, critical, value }, with value the raw extnValue octets, which a parse result carries as valueBytes.
Options
- `scope` (string, required) -- `"crl"` for a CRL's own extension, `"crl-entry"` for a revoked
entry's, `"ocsp-single"` for one carried in an OCSP `SingleResponse` (RFC 6960 sec. 4.4.5).
- `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
undecodable value.
- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"),
nextUpdate: new Date("2026-02-01T00:00:00Z"), crlNumber: 7n,
revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }] },
{ cert: caCert, key: key });
var e = pki.schema.crl.parse(crlDer).revokedCertificates[0].crlEntryExtensions[0];
pki.schema.crl.decodeExtension({ oid: e.oid, name: e.name, critical: e.critical, value: e.valueBytes },
{ scope: "crl-entry" }).decoded; // -> 1
}
example();
References
- spec RFC 5280 sec. 5.2
- spec RFC 5280 sec. 5.3
- spec RFC 6960 sec. 4.4.5