Skip to content

Revocation ​

The examples below build their own ca, leaf, CRL, and OCSP material inline so each one runs on its own.

Revocation checking is opt-in. Pass CRLs or OCSP responses in the revocation input to verifyCertificateChain. micro509 does not retrieve evidence from certificate URLs or contact responders. Your application is responsible for obtaining evidence and supplying it for evaluation. Omitting revocation skips this step. Passing revocation: {} enables it with no evidence, so certificates that need evidence receive an indeterminate status.

Revocation checking defaults to hard-fail

When chain-level revocation checking is enabled, RevocationPolicy.mode defaults to 'hard-fail': certificates whose revocation status is indeterminate (no applicable CRL/OCSP evidence, expired evidence, untrusted signer) are denied. If availability matters more than strictness — partial evidence is normal in your setup — opt out explicitly with policy: { mode: 'soft-fail' }, which allows indeterminate status and denies only on a confirmed revoked verdict.

CRL lifecycle ​

Create a CRL ​

ts
import {
  createSelfSignedCertificate,
  createCertificateRevocationList,
} from 'micro509';

const ca = await createSelfSignedCertificate({
  subject: { commonName: 'My CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

const crl = await createCertificateRevocationList({
  issuer: { commonName: 'My CA' },
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
  crlNumber: 1,
  thisUpdate: new Date(),
  nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
  revokedCertificates: [
    {
      serialNumber: Uint8Array.of(0x01),
      revocationDate: new Date(),
      reasonCode: 'keyCompromise',
    },
  ],
});

console.log(crl.pem);

CRL location and issuer identity ​

A URI in distributionPoint.fullName identifies where a CRL can be retrieved. The separate cRLIssuer field identifies its signer. Under RFC 5280 §4.2.1.13, that field may contain only the CRL issuer's directoryName, encoded exactly as the CRL's issuer Name. A semantically equivalent name with different DER encoding, or a list mixing a directory name with a URI, cannot authorize the CRL.

Parse and validate a CRL ​

ts
import { createSelfSignedCertificate } from 'micro509';
import {
  createCertificateRevocationList,
  isCertificateRevoked,
  validateCertificateRevocationList,
} from 'micro509/revocation';

const ca = await createSelfSignedCertificate({
  subject: { commonName: 'My CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

const crl = await createCertificateRevocationList({
  issuer: { commonName: 'My CA' },
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
  crlNumber: 1,
  nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
  revokedCertificates: [
    {
      serialNumber: Uint8Array.of(0x01),
      reasonCode: 'keyCompromise',
    },
  ],
});

const validationResult = await validateCertificateRevocationList({
  crl: crl.pem,
  issuerCertificate: ca.certificate.pem,
});

if (!validationResult.ok) {
  console.log(`validation failed: ${validationResult.code}`);
} else {
  const parsed = validationResult.value;
  const entry = parsed.revokedCertificates[0];
  const body = crl.pem.trimEnd().split('\n').slice(1, -1).join('');
  console.log(`\
validated:  true
sig algo:   ${parsed.signatureAlgorithmName}
signature:  …${body.slice(-44)}
thisUpdate: ${parsed.thisUpdate.toISOString()}
entry 01:   revoked ${entry?.revocationDate.toISOString().slice(0, 10)}, reason ${entry?.reasonCode}
revoked 01: ${isCertificateRevoked('01', parsed)}
revoked 02: ${isCertificateRevoked('02', parsed)}`);
}

RFC 5280 §5.1.2.5 requires conforming CRL issuers to include nextUpdate, so createCertificateRevocationList requires it. The builder also throws next_update_not_after_this_update unless nextUpdate is at least one second after thisUpdate. That ordering is a micro509 invariant, and RFC 5280 and X.509 do not specify it. RFC 5280 §5.2 requires the Authority Key Identifier and CRL Number extensions in every CRL, so issuerCertificate and crlNumber are required too.

RFC 5280 §5.1.2.5 does not specify how a client handles a received CRL without nextUpdate, and §3.3 leaves the required recency of revocation data to local policy. micro509 fails such a CRL with stale_crl unless you set a maximum age, which then governs it.

Bound the age of thisUpdate with these options. A CRL older than the bound fails with stale_crl, and the chain reports it as crl_expired.

OptionWhere
maxAgeMsvalidateCertificateRevocationList, checkCertificateRevocationAgainstCrl
crlMaxAgeMscheckCertificateRevocation
crlMaxAgeMspolicy of checkChainRevocation and verifyCertificateChain({ revocation })
responderRevocationCrlMaxAgeMsvalidateOcspResponse, for the responderRevocationCrls of delegated responders

The chain-level crlMaxAgeMs also applies to CRLs for CRL signers and delegated OCSP responders. clockSkewMs, on the same input or on the chain policy, widens the bound by the same amount and also applies to the thisUpdate and nextUpdate checks.

Unrecognized reason codes ​

RFC 5280 §5.3.1 lists CRLReason values 0 to 10, with 7 unused, and X.509 adds weakAlgorithmOrKey (11). Neither says what a relying party does with a code outside its list. Parsing keeps such a code as { type: 'unrecognized', code }, and the unrecognizedReasonCode option picks the treatment of evidence that revokes a certificate with it.

ValueEffect
'revoked' (default)The certificate is revoked, and the code is reported
'reject'The evidence cannot settle the status and fails with reason_code_unrecognized. Other evidence then cannot settle it as good either
OptionWhere
unrecognizedReasonCodecheckCertificateRevocationAgainstCrl, checkCertificateRevocation
unrecognizedReasonCodepolicy of checkChainRevocation and verifyCertificateChain({ revocation })
responderRevocationUnrecognizedReasonCodevalidateOcspResponse, for the responderRevocationCrls of delegated responders

The chain reports a rejected code as crl_reason_code_unrecognized or ocsp_reason_code_unrecognized. A CRL signer listed with a rejected code makes the CRLs it signs unusable, reported as crl_signer_indeterminate. A delegated OCSP responder listed with one fails validateOcspResponse with responder_revocation_unknown, even when another CRL reports the responder good. Codes are bigint, so a code of any length is kept exactly.

A CRL whose entry marks its cRLReason critical and holds an unrecognized code is malformed, since RFC 5280 Appendix B requires rejecting a critical extension with values the implementation does not recognize.

OCSP ​

Build a request ​

ts
import {
  createCertificate,
  createSelfSignedCertificate,
  generateKeyPair,
} from 'micro509';
import {
  createOcspRequest,
  parseOcspRequestDerOrThrow,
} from 'micro509/revocation';

const ca = await createSelfSignedCertificate({
  subject: { commonName: 'Demo CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

const leafKeys = await generateKeyPair();
const leaf = await createCertificate({
  issuer: { commonName: 'Demo CA' },
  subject: { commonName: 'app.example.com' },
  publicKey: leafKeys.publicKey,
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
});

const request = await createOcspRequest({
  requests: [
    {
      certificate: leaf.pem,
      issuerCertificate: ca.certificate.pem,
    },
  ],
});

// Parse it back to see the CertID the responder will look up
const certId = parseOcspRequestDerOrThrow(request.der).requests[0];
console.log(`\
serial:   ${certId?.serialNumberHex}
hashed:   with ${certId?.hashAlgorithmName} (RFC 9919 default)
key hash: ${certId?.issuerKeyHashHex}`);
console.log(request.pem);

Parse and validate a response ​

ts
import {
  createCertificate,
  createSelfSignedCertificate,
  generateKeyPair,
} from 'micro509';
import {
  createOcspRequest,
  createOcspResponse,
  parseOcspResponseDerOrThrow,
  validateOcspResponse,
} from 'micro509/revocation';

const ca = await createSelfSignedCertificate({
  subject: { commonName: 'Demo CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

const leafKeys = await generateKeyPair();
const leaf = await createCertificate({
  issuer: { commonName: 'Demo CA' },
  subject: { commonName: 'app.example.com' },
  publicKey: leafKeys.publicKey,
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
});

const nonce = crypto.getRandomValues(new Uint8Array(16));
const nonceHex = Array.from(nonce, (byte) =>
  byte.toString(16).padStart(2, '0'),
).join('');
const request = await createOcspRequest({
  nonce,
  requests: [
    {
      certificate: leaf.pem,
      issuerCertificate: ca.certificate.pem,
    },
  ],
});

// Responder signs an OCSP response for the leaf
const ocsp = await createOcspResponse({
  signerPrivateKey: ca.keyPair.privateKey,
  signerCertificate: ca.certificate.pem,
  nonce,
  responses: [
    {
      certificate: leaf.pem,
      issuerCertificate: ca.certificate.pem,
      certStatus: 'good',
    },
  ],
});

const response = parseOcspResponseDerOrThrow(ocsp.der);

// Verifies the signature, binds and authorizes the
// responder against the issuer, matches the nonce and
// every requested CertID, and checks freshness
const result = await validateOcspResponse({
  response,
  request: request.der,
  issuerCertificate: ca.certificate.pem,
});

if (result.ok) {
  const entry = result.value.responses?.[0];
  const responder = response.responderId;
  console.log(`\
status:     ${response.responseStatus}, ${entry?.certStatus}
serial:     ${entry?.certId.serialNumberHex}
certId:     hashed with ${entry?.certId.hashAlgorithmName} (RFC 9919 default)
responder:  ${responder?.type === 'byKeyHash' ? `key hash ${responder.keyHashHex}` : 'by name'}
signature:  ${response.signatureAlgorithmName}, verified
nonce:      ${response.nonce === nonceHex ? 'echoed' : response.nonce}
window:     ${entry?.thisUpdate.toISOString()} → ${entry?.nextUpdate?.toISOString() ?? 'no nextUpdate'}
producedAt: ${response.producedAt?.toISOString()}`);
} else {
  console.log(`invalid: ${result.error.code}`);
}

Require nextUpdate (RFC 9919) ​

RFC 6960 §4.2.2.1 lets a response omit nextUpdate, meaning newer information is always available, and validateOcspResponse accepts such a response by default. RFC 9919 is a profile for lightweight, high-volume environments, and its §5 requires a client that follows it to reject a response without nextUpdate. Opt in with profile: 'rfc9919' on validateOcspResponse, or ocspProfile: 'rfc9919' on checkCertificateRevocation and the chain-level revocation policy. validateOcspResponse then fails with next_update_missing, checkCertificateRevocation reports next_update_missing as the indeterminate reason, and the chain reports ocsp_next_update_missing.

ts
const result = await validateOcspResponse({
  response,
  issuerCertificate: ca.certificate.pem,
  profile: 'rfc9919',
});

Orchestrated revocation check ​

checkCertificateRevocation accepts a certificate, its issuer, and an array of CRL or OCSP evidence. Use unwrap(result).status to decide what to do:

StatusMeaning
goodApplicable evidence reports the certificate as not revoked.
revokedEvidence identifies the certificate as revoked. Inspect the revocation time and reason.
indeterminateThe supplied evidence cannot establish a status. Inspect the reason before deciding how to proceed.

This API represents unusable evidence as an indeterminate status inside an ok result. An ok result alone does not mean the certificate is good. Chain verification applies your revocation policy to that status. The example below creates both a CRL and an OCSP response that report the leaf as revoked.

Generate a certificate and evaluate CRL and OCSP evidence
ts
import {
  createCertificate,
  createSelfSignedCertificate,
  generateKeyPair,
  parseCertificatePem,
  unwrap,
} from 'micro509';
import {
  checkCertificateRevocation,
  createCertificateRevocationList,
  createOcspResponse,
  revocationReasonFromCode,
} from 'micro509/revocation';

const ca = await createSelfSignedCertificate({
  subject: { commonName: 'Demo CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

const leafKeys = await generateKeyPair();
const leaf = await createCertificate({
  issuer: { commonName: 'Demo CA' },
  subject: { commonName: 'app.example.com' },
  publicKey: leafKeys.publicKey,
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
});

// Leaf serial as bytes for the CRL entry
const parsedLeaf = unwrap(parseCertificatePem(leaf.pem));
const serialHex = parsedLeaf.serialNumberHex;
const leafSerial = Uint8Array.from(serialHex.match(/.{2}/g) ?? [], (byte) =>
  parseInt(byte, 16),
);

// CRL evidence that revokes the leaf
const crl = await createCertificateRevocationList({
  issuer: { commonName: 'Demo CA' },
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
  crlNumber: 1,
  nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
  revokedCertificates: [
    {
      serialNumber: leafSerial,
      revocationDate: new Date(),
      reasonCode: 'keyCompromise',
    },
  ],
});

// OCSP evidence that also reports revoked
const ocsp = await createOcspResponse({
  signerPrivateKey: ca.keyPair.privateKey,
  signerCertificate: ca.certificate.pem,
  responses: [
    {
      certificate: leaf.pem,
      issuerCertificate: ca.certificate.pem,
      certStatus: 'revoked',
      revokedAt: new Date(),
      revocationReasonCode: 1,
    },
  ],
});

const result = await checkCertificateRevocation({
  certificate: leaf.pem,
  issuerCertificate: ca.certificate.pem,
  evidence: [
    { kind: 'crl', crl: crl.pem },
    { kind: 'ocsp', response: ocsp.der },
  ],
});

// Evidence failures are represented by an indeterminate status.
const status = unwrap(result);
if (status.status === 'revoked') {
  // CRL evidence reports a RevocationReason name,
  // OCSP a raw CRLReason integer;
  // revocationReasonFromCode() maps the integer to
  // the same name so either path yields one answer
  const reason =
    status.revocationReason ??
    revocationReasonFromCode(status.revocationReasonCode);
  console.log(`\
status:     revoked
serial:     ${serialHex}
revoked at: ${status.revokedAt?.toISOString()}
reason:     ${reason}`);
} else {
  console.log('status:', status.status);
}

Chain-level revocation ​

checkChainRevocation() evaluates CRL and OCSP evidence for every certificate in a validated chain (the trust anchor is never checked). Each OCSP response is fully validated — signature, responder binding and authorization, freshness — before its verdict is trusted, and a validated revoked verdict from either evidence kind always wins, regardless of policy.prefer.

When both sources yield a validated good verdict, policy.prefer decides which one is reported: the default 'best-available' picks the source with the fresher evidence (later thisUpdate, ties favoring OCSP), while 'ocsp' and 'crl' pin the reported source unconditionally. The winning evidence's timestamp is reported as source.thisUpdate on each certificate's status, so callers can enforce their own maximum evidence age.

The same inputs are available on verifyCertificateChain() via the revocation option:

ts
import {
  createCertificate,
  createOcspResponse,
  createSelfSignedCertificate,
  generateKeyPair,
  verifyCertificateChain,
} from 'micro509';

const ca = await createSelfSignedCertificate({
  subject: { commonName: 'Demo CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

const leafKeys = await generateKeyPair();
const leaf = await createCertificate({
  issuer: { commonName: 'Demo CA' },
  subject: { commonName: 'app.example.com' },
  publicKey: leafKeys.publicKey,
  signerPrivateKey: ca.keyPair.privateKey,
  issuerCertificate: ca.certificate.pem,
});

const ocsp = await createOcspResponse({
  signerPrivateKey: ca.keyPair.privateKey,
  signerCertificate: ca.certificate.pem,
  responses: [
    {
      certificate: leaf.pem,
      issuerCertificate: ca.certificate.pem,
      certStatus: 'good',
      thisUpdate: new Date(Date.now() - 60_000),
      nextUpdate: new Date(Date.now() + 3_600_000),
    },
  ],
});

const result = await verifyCertificateChain({
  leaf: leaf.pem,
  roots: [ca.certificate.pem],
  revocation: {
    // hard-fail is the default: indeterminate status ⇒ verification fails
    ocspResponses: [ocsp.der],
  },
});

if (result.ok) {
  const parsedLeaf = result.value.leaf;
  console.log(`\
verified: true
leaf:     ${parsedLeaf.subject.values.commonName}
serial:   ${parsedLeaf.serialNumberHex}
chain:    ${result.value.chain.length} certificates`);
} else {
  console.log(`verified: false (${result.error.code})`);
}

Released under the MIT License.