Online Revocation Checking
A developer guide for integrating online CRL and OCSP certificate revocation checking into your VerifyKit-powered application.
@trexolab/verifykit-plugin-revocation
What is Certificate Revocation?
Digital certificates can be revoked before they expire. Common reasons include:
- The certificate's private key was compromised.
- The certificate was issued in error.
- The organization that owns the certificate has ceased operations.
- The certificate holder violated the CA's policies.
Once a certificate is revoked, any signatures made with it after the revocation date should not be trusted. There are two standard mechanisms for checking revocation:
CRL (Certificate Revocation List) — The CA publishes a signed list of all revoked certificate serial numbers. Clients download the full list and check whether the certificate's serial number appears in it. CRLs can be large (megabytes) and are typically updated on a schedule (hourly to daily).
OCSP (Online Certificate Status Protocol) — The client sends a small request containing the certificate's serial number to the CA's OCSP responder, which replies with the current status (good, revoked, or unknown). OCSP provides real-time status with minimal bandwidth.
Why you need online checking: PDF signatures often embed CRL data at signing time (for LTV/long-term validation), but this embedded data becomes stale. A certificate may have been revoked after the document was signed. Online checking contacts the CA's live endpoints to get the current revocation status.
Quick Start
React + Next.js (most common)
1. Install packages:
echo '@trexolab:registry=https://verifykit.trexolab.com/api/registry' > .npmrc
npm install @trexolab/verifykit-react @trexolab/verifykit-plugin-revocation2. Create the server handler:
// app/api/revocation/route.ts
import { handleRevocation } from '@trexolab/verifykit-plugin-revocation/handler'
export const POST = handleRevocation()3. Add the plugin to your provider:
// app/viewer/page.tsx
'use client'
import { useState } from 'react'
import {
VerifyKitProvider,
Viewer,
WelcomeScreen,
useVerification,
defaultLayoutPlugin,
} from '@trexolab/verifykit-react'
import { revocationPlugin } from '@trexolab/verifykit-plugin-revocation'
import '@trexolab/verifykit-react/styles.css'
const config = {
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
plugins: [revocationPlugin({ endpoint: '/api/revocation' })],
theme: { mode: 'system' as const },
}
export default function ViewerPage() {
return (
<VerifyKitProvider config={config}>
<PdfViewer />
</VerifyKitProvider>
)
}
function PdfViewer() {
const verification = useVerification()
const [layout] = useState(() => defaultLayoutPlugin())
if (!verification.fileBuffer) {
return <WelcomeScreen onOpenFile={(f) => verification.load(f)} />
}
return (
<Viewer
fileBuffer={verification.fileBuffer}
fileName={verification.fileName}
plugins={[layout.plugin]}
onOpenFile={(f) => verification.load(f)}
signatures={verification.signatures}
verificationStatus={verification.status ?? undefined}
/>
)
}That's it. When a user opens a signed PDF, the plugin automatically checks each signer's certificate against live CRL/OCSP endpoints. Results appear in the signature panel and properties dialog.
React + Vite + Express
1. Install packages:
echo '@trexolab:registry=https://verifykit.trexolab.com/api/registry' > .npmrc
npm install @trexolab/verifykit-react @trexolab/verifykit-plugin-revocation
npm install express @trexolab/verifykit-plugin-revocation2. Create Express server with handler:
// server.ts
import express from 'express'
import { expressHandler } from '@trexolab/verifykit-plugin-revocation/handler/express'
const app = express()
app.use(express.json())
app.post('/api/revocation', expressHandler())
app.listen(3001, () => {
console.log('Revocation API running on port 3001')
})3. Configure Vite proxy:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api/revocation': 'http://localhost:3001',
},
},
})4. Add the plugin to your React app:
import { VerifyKitProvider } from '@trexolab/verifykit-react'
import { revocationPlugin } from '@trexolab/verifykit-plugin-revocation'
const config = {
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
plugins: [revocationPlugin({ endpoint: '/api/revocation' })],
}
function App() {
return (
<VerifyKitProvider config={config}>
{/* your viewer component */}
</VerifyKitProvider>
)
}React + Hono
1. Create Hono server:
// server.ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { handleRevocation } from '@trexolab/verifykit-plugin-revocation/handler'
const app = new Hono()
const handler = handleRevocation()
app.post('/api/revocation', (c) => handler(c.req.raw))
serve({ fetch: app.fetch, port: 3001 })2. Add the plugin to your React app:
import { VerifyKitProvider } from '@trexolab/verifykit-react'
import { revocationPlugin } from '@trexolab/verifykit-plugin-revocation'
const config = {
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
plugins: [revocationPlugin({ endpoint: '/api/revocation' })],
}
function App() {
return (
<VerifyKitProvider config={config}>
{/* your viewer component */}
</VerifyKitProvider>
)
}Node.js (Headless)
No server needed. The plugin fetches CRL/OCSP endpoints directly.
import { createVerifier } from '@trexolab/verifykit-core'
import { revocationPlugin } from '@trexolab/verifykit-plugin-revocation'
import { readFile } from 'node:fs/promises'
async function verifyWithRevocation(filePath: string) {
const verifier = await createVerifier({
plugins: [revocationPlugin()], // no endpoint = direct mode
})
try {
const buffer = await readFile(filePath)
const result = await verifier.verify(buffer, filePath)
for (const sig of result.signatures) {
console.log(`Signer: ${sig.name}`)
console.log(` Overall: ${sig.overallStatus}`)
console.log(` Revocation: ${sig.revocationCheck.status} — ${sig.revocationCheck.detail}`)
}
return result
} finally {
verifier.dispose()
}
}
await verifyWithRevocation('signed-document.pdf')Vanilla JS
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://verifykit.trexolab.com/cdn/verifykit.css">
</head>
<body>
<div id="viewer" style="width: 100%; height: 100vh;"></div>
<script src="https://verifykit.trexolab.com/cdn/verifykit.umd.js"></script>
<script>
var viewer = VerifyKit.create(document.getElementById('viewer'), {
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
plugins: [/* revocationPlugin must be configured in your bundled setup */],
})
</script>
</body>
</html>For the vanilla UMD bundle, revocation plugin configuration is passed as part of the VerifyKit.create() options. A server handler must be mounted at /api/revocation for proxy-mode revocation to work (see the server setup examples above).
Configuration
Basic (defaults)
All defaults: OCSP enabled, CRL enabled, 10-second timeout, 10 MB max CRL size.
revocationPlugin({ endpoint: '/api/revocation' })Custom Timeout
revocationPlugin({ endpoint: '/api/revocation', timeout: 20000 })Disable OCSP (CRL only)
Useful if you know your CAs do not run OCSP responders, or if you want to avoid OCSP's potential privacy implications.
revocationPlugin({ endpoint: '/api/revocation', ocsp: false })Disable CRL (OCSP only)
Useful when CRL files are too large and you only want real-time OCSP checks.
revocationPlugin({ endpoint: '/api/revocation', crl: false })Custom Headers (authentication)
Static headers:
revocationPlugin({
endpoint: '/api/revocation',
headers: { 'Authorization': 'Bearer my-static-token' },
})Dynamic headers (re-evaluated on each request):
revocationPlugin({
endpoint: '/api/revocation',
headers: () => ({ 'Authorization': `Bearer ${getToken()}` }),
})Error Callback
revocationPlugin({
endpoint: '/api/revocation',
onError: (err, ctx) => {
console.warn(`Revocation ${ctx.type} failed for ${ctx.url}:`, err.message)
// ctx.type is 'crl' or 'ocsp'
// ctx.url is the endpoint URL
},
})Direct Mode with All Options
revocationPlugin({
// no endpoint = direct mode
timeout: 15000,
maxCrlSize: 5 * 1024 * 1024, // 5 MB
ocsp: true,
crl: true,
})Server Handler Configuration
Basic
export const POST = handleRevocation()Custom Timeout
export const POST = handleRevocation({ timeout: 15000 })Custom URL Filter (SSRF protection)
export const POST = handleRevocation({
urlFilter: (url) => {
try {
const u = new URL(url)
return u.hostname.startsWith('crl.') ||
u.hostname.startsWith('ocsp.') ||
u.hostname.endsWith('.digicert.com') ||
u.hostname.endsWith('.globalsign.com')
} catch {
return false
}
},
})Custom CRL Size Limit
export const POST = handleRevocation({ maxCrlSize: 5 * 1024 * 1024 }) // 5 MBAll Options Combined
export const POST = handleRevocation({
timeout: 15000,
maxCrlSize: 5 * 1024 * 1024,
urlFilter: (url) => {
try {
const u = new URL(url)
return u.protocol === 'http:' || u.protocol === 'https:'
} catch {
return false
}
},
})Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────────────┐ │
│ │ @trexolab/verifykit- │ POST /api/revocation │
│ │ plugin-revocation │ { type: 'ocsp', │
│ │ │ cert: {...}, │
│ │ revocationPlugin({ │ issuer: {...}, │
│ │ endpoint: '/api/ │ urls: ['http://ocsp.ca.com'] } │
│ │ revocation' │──────────────────────────┐ │
│ │ }) │ │ │
│ └─────────────────────┘ │ │
│ │ │
├────────────────────────────────────────────────────┼────────────────┤
│ Server ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ handleRevocation() / expressHandler() / processRevocation│ │
│ │ │ │
│ │ 1. Validate request payload │ │
│ │ 2. Apply SSRF URL filter │ │
│ │ 3. Fetch CRL or query OCSP responder ──────────────────┼───┐ │
│ │ │ │ │
│ └─────────────────────────────────────────────────────────┘ │ │
│ │ │
├────────────────────────────────────────────────────────────────┼────┤
│ External CA Infrastructure │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ CRL Endpoint │ │ OCSP │ │
│ │ (HTTP GET) │ │ Responder │ │
│ │ │ │ (HTTP POST) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ DER bytes │ DER bytes │
│ ▼ ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Parse response → { status: 'good', source: 'OCSP ...' }│ │
│ └───────────────────────────┬─────────────────────────────┘ │
│ │ │
│ JSON response │ │
│ to browser ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ revocationCheck.status = 'valid' │ │
│ │ revocationCheck.detail = 'Certificate is not revoked...'│ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
In Node.js direct mode (no endpoint), the plugin skips the server entirely and fetches CRL/OCSP endpoints directly from the same process.
How It Works Internally
Check Priority
- OCSP check attempted first — faster, real-time, small payload.
- If OCSP fails, returns
'unknown', or throws --> CRL fallback. - First non-unknown result wins. As soon as OCSP or CRL returns
'good'or'revoked', the result is applied and no further checks run for that signature. - If both fail,
revocationCheckstays'unknown'.
What the Core Engine Does
After WASM verification completes, the core engine's runRevocationPlugins() function runs:
- Iterates over all signatures in the verification result.
- Skips deleted signatures and signatures that already have a non-
'unknown'revocation status (e.g., from embedded CRL/OCSP data). - For each remaining signature with
revocationCheck.status === 'unknown':- Extracts the signer certificate (
sig.signerCertificate) and the issuer certificate (sig.certificateChain[1]). - Calls
plugin.revocation.checkOCSP(cert, issuer, cert.ocspUrls)if OCSP is enabled and the certificate has OCSP URLs. - If OCSP returns
'unknown'or throws, falls back toplugin.revocation.checkCRL(cert, cert.crlDistributionPoints). - Applies the result to the signature via
applyRevocationResult().
- Extracts the signer certificate (
Result Mapping
How plugin results map to signature fields and overall status:
| Plugin returns | sig.revocationCheck.status | sig.overallStatus | Explanation |
|---|---|---|---|
'good' | 'valid' | Unchanged | Certificate is not revoked. No impact on overall validity. |
'revoked' | 'invalid' | Set to 'invalid' | Certificate is revoked. Signature is invalid. |
'revokedAfterSigning' | 'valid' | Unchanged | Certificate was revoked after the signing timestamp. The signature was valid when created. |
'revokedNoTimestamp' | 'invalid' | Set to 'invalid' | Certificate is revoked and no timestamp proves the signature was made before revocation. |
'unknown' | Unchanged ('unknown') | Unchanged | Could not determine status. Verification result unaffected. |
When overallStatus is set to 'invalid', the overallMessage is also updated:
- For
'revoked':"Signer's identity is invalid because it has been revoked." - For
'revokedNoTimestamp':"Signer's identity is invalid because the certificate has been revoked."
The attempted Field
When the revocation plugin runs, it sets revocationCheck.attempted = true on the signature, regardless of whether the check succeeded. This distinguishes between:
- Not attempted (
attemptedisundefined): The plugin is not installed or revocation checking was skipped entirely. The core engine's offline check found no embedded revocation data. - Attempted (
attempted: true): The plugin actively tried to fetch CRL/OCSP data. The result might still be'unknown'if all endpoints failed.
Adobe Reader Parity
This distinction matters for getDisplayStatus() (Adobe Reader parity):
- Offline / not attempted: Adobe Reader shows the signature as valid when revocation was never checked. The display status does not downgrade to "unknown."
- Online / attempted but failed: Adobe Reader shows "unknown" when an active revocation check was inconclusive. The display status is downgraded to "unknown."
In summary: offline = valid, online + failed = unknown. This matches Adobe Reader DC behavior.
Without vs With Revocation
What the viewer shows in each scenario:
Without the Plugin
| Field | Value |
|---|---|
| Revocation bullet in SignatureListPanel | "Revocation status unknown" (gray question mark) |
| Revocation row in Properties dialog | "Certificate revocation status is unknown." |
| LTV status | "Signature is not LTV enabled." |
sig.revocationCheck.status | 'unknown' |
sig.revocationCheck.detail | 'Not checked (offline)' |
With the Plugin (certificate is good)
| Field | Value |
|---|---|
| Revocation bullet in SignatureListPanel | "Certificate is not revoked" (green checkmark) |
| Revocation row in Properties dialog | "Certificate is not revoked (verified via online OCSP)." or "Certificate is not revoked (verified via online CRL)." |
| LTV status | "Signature is LTV enabled." (if timestamp is also valid) |
sig.revocationCheck.status | 'valid' |
sig.revocationCheck.detail | 'Certificate is not revoked (online OCSP (http://ocsp.ca.com)).' |
With the Plugin (certificate is revoked)
| Field | Value |
|---|---|
| DocumentMessageBar | "At least one signature is INVALID." (red background) |
| Revocation bullet in SignatureListPanel | "Certificate has been revoked" (red X) |
| Identity row in Properties dialog | "Signer's identity is invalid because the certificate has been revoked." |
sig.revocationCheck.status | 'invalid' |
sig.overallStatus | 'invalid' |
Troubleshooting
CORS errors in browser
Symptom: Browser console shows CORS errors when verifying a signed PDF.
Cause: The plugin in proxy mode sends requests to your server, not directly to CRL/OCSP endpoints. If the handler is not mounted on the same origin as your app, or CORS is not configured, requests will fail.
Fix: Mount the server handler on the same origin as your frontend (e.g., /api/revocation in Next.js). If cross-origin is unavoidable, add CORS headers on the server:
// Next.js example with CORS
import { handleRevocation } from '@trexolab/verifykit-plugin-revocation/handler'
const handler = handleRevocation()
export async function POST(req: Request) {
const res = await handler(req)
res.headers.set('Access-Control-Allow-Origin', 'https://your-app.com')
return res
}Revocation check still says "Not checked (offline)"
Cause: The plugin was imported but not passed to createVerifier() or VerifyKitProvider.
Fix: Pass the plugin in the plugins array:
// Headless
const verifier = await createVerifier({
plugins: [revocationPlugin()], // must be in this array
})
// React
<VerifyKitProvider config={{
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
plugins: [revocationPlugin({ endpoint: '/api/revocation' })],
}}>OCSP returns unknown
Possible causes:
- Certificate does not have OCSP responder URLs. Check
cert.ocspUrls— if empty, the plugin skips OCSP and falls back to CRL. issuer.subjectNameHashis missing. This field is computed by the WASM core. Ensure you are using a recent version of@trexolab/verifykit-core.- OCSP responder is down or rate-limiting. The plugin falls back to CRL automatically.
- OCSP responder does not recognize the certificate. Some CAs only serve OCSP for their own certificates.
CRL returns unknown
Possible causes:
- Certificate does not have CRL distribution points. Check
cert.crlDistributionPoints— if empty, CRL checking is skipped. - CRL server is unreachable. Firewall rules may block outgoing HTTP from your server. Test with
curl <crl-url>. - CRL file exceeds
maxCrlSize. Some CAs publish very large CRLs. Increase the limit:tshandleRevocation({ maxCrlSize: 20 * 1024 * 1024 }) // 20 MB
All checks return unknown
Possible causes:
- No revocation URLs in the certificate. Some certificates (especially self-signed or internal CA certificates) do not include CRL or OCSP extensions. Online checking is not possible for these certificates.
- Both CRL and OCSP are disabled. Check that you have not set both
crl: falseandocsp: false. - Network issues between server and CRL/OCSP endpoints. If your server is behind a restrictive firewall or proxy, outbound HTTP to CA endpoints may be blocked.
Server returns 400
Cause: The request body is missing type, cert, or urls.
Fix (Express): Ensure express.json() middleware is applied before the handler route:
app.use(express.json()) // must come first
app.post('/api/revocation', expressHandler()) // then the handlerFix (other frameworks): Ensure the request body is parsed as JSON before reaching the handler.
"No allowed URLs" error
Symptom: Server handler returns { status: 'unknown', source: 'no allowed URLs' }.
Cause: The SSRF filter is blocking all CRL/OCSP URLs from the certificate. The default filter only allows hostnames starting with crl. or ocsp., and paths ending with .crl.
Fix: Customize urlFilter to allow your CA's specific endpoints:
handleRevocation({
urlFilter: (url) => {
try {
const u = new URL(url)
return u.protocol === 'http:' || u.protocol === 'https:'
} catch {
return false
}
},
})"fetch is not defined" (Node.js < 18)
Cause: The plugin uses the global fetch() API, which requires Node.js 18+.
Fix: Upgrade to Node.js 20.19.0+ (recommended). Alternatively, install a fetch polyfill like undici.
Timeout errors
Symptom: Revocation checks consistently return 'unknown' and onError fires with abort errors.
Cause: CRL/OCSP endpoints are slow to respond, or the default 10-second timeout is too short.
Fix: Increase the timeout on both client and server:
// Client
revocationPlugin({ endpoint: '/api/revocation', timeout: 30000 })
// Server
handleRevocation({ timeout: 30000 })Combining with Custom Trust Store
The revocation plugin works alongside custom trust store configuration:
const verifier = await createVerifier({
trustStore: {
certificates: [enterpriseRootPem],
mode: 'merge',
},
plugins: [revocationPlugin({ endpoint: '/api/revocation' })],
})Revocation checking runs after trust chain validation. If a certificate chains to your custom trust store root, the plugin will still check its revocation status against the CA's live endpoints.
Dependencies
@trexolab/verifykit-core— provides theVerifyKitPlugininterface,CertificateInfotype, andRevocationCheckResulttype. Required as a peer dependency.- No external runtime dependencies. The plugin uses the native
fetch()API (available in Node.js 18+ and all modern browsers).
Further Reading
- API Reference — complete API documentation for every function, type, and option
- Examples — additional integration examples
- Architecture — how the VerifyKit SDK is structured