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 defaults to hard-fail

Revocation checking only runs when you supply evidence — and once you do, 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,
  issuerPublicKey: ca.keyPair.publicKey,
  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);

Parse and verify a CRL

ts
import { createSelfSignedCertificate } from 'micro509';
import {
  createCertificateRevocationList,
  isCertificateRevoked,
  parseCertificateRevocationListPemOrThrow,
  verifyCertificateRevocationListSignature,
} 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,
  issuerPublicKey: ca.keyPair.publicKey,
  revokedCertificates: [
    {
      serialNumber: Uint8Array.of(0x01),
      reasonCode: 'keyCompromise',
    },
  ],
});

const parsed = parseCertificateRevocationListPemOrThrow(
  crl.pem,
);

const verifyResult =
  await verifyCertificateRevocationListSignature(
    crl.pem,
    ca.certificate.pem,
  );

const entry = parsed.revokedCertificates[0];
const body = crl.pem
  .trimEnd()
  .split('\n')
  .slice(1, -1)
  .join('');
console.log(`\
verified:   ${verifyResult.ok}
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)}`);

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,
  issuerPublicKey: ca.keyPair.publicKey,
});

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,
  issuerPublicKey: ca.keyPair.publicKey,
});

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 commitment'}
producedAt: ${response.producedAt?.toISOString()}`);
} else {
  console.log(`invalid: ${result.error.code}`);
}

Orchestrated revocation check

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,
  issuerPublicKey: ca.keyPair.publicKey,
});

// 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,
  issuerPublicKey: ca.keyPair.publicKey,
  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 },
  ],
});

// Check ok, then the status discriminator
if (!result.ok) {
  throw new Error('unreachable: evidence was supplied');
}
if (result.value.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 =
    result.value.revocationReason ??
    revocationReasonFromCode(
      result.value.revocationReasonCode,
    );
  console.log(`\
status:     revoked
serial:     ${serialHex}
revoked at: ${result.value.revokedAt?.toISOString()}
reason:     ${reason}`);
} else {
  console.log('status:', result.value.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,
  issuerPublicKey: ca.keyPair.publicKey,
});

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.