VerifyKitv0.5.1
All posts
|7 min read|TrexoLab
node.jsserver-sideheadlessbatch processingci/cd

PDF Signature Verification in Node.js: Headless Server-Side Processing

While browser-side verification is ideal for interactive user experiences, many workflows require server-side PDF signature verification — automated document processing, CI/CD pipeline validation, compliance auditing, and batch processing of signed documents. VerifyKit's core engine runs headlessly in Node.js 20+, Deno, and Bun with zero DOM dependencies.

Installing the Core Package

bash
npm install @trexolab/verifykit-core

No native add-ons, no node-gyp, no platform-specific binaries. The package contains a pure WebAssembly module that runs on any platform supporting Node.js 20+.

Basic Verification

ts
import { createVerifier } from '@trexolab/verifykit-core'
import { readFileSync } from 'fs'
 
const verifier = await createVerifier()
const pdfBuffer = readFileSync('contract.pdf')
const result = await verifier.verify(pdfBuffer)
 
console.log(`Signatures found: ${result.signatures.length}`)
 
for (const sig of result.signatures) {
  console.log(`\n${sig.signerName}`)
  console.log(`  Overall:    ${sig.overallStatus}`)
  console.log(`  Integrity:  ${sig.integrityCheck.status}`)
  console.log(`  Signature:  ${sig.signatureCheck.status}`)
  console.log(`  Chain:      ${sig.certificateChainCheck.status}`)
  console.log(`  Expiry:     ${sig.expiryCheck.status}`)
  console.log(`  Timestamp:  ${sig.timestampCheck.status}`)
  console.log(`  Revocation: ${sig.revocationCheck.status}`)
  console.log(`  Algorithm:  ${sig.algorithmCheck.status}`)
  console.log(`  EKU:        ${sig.ekuCheck.status}`)
  console.log(`  PAdES:      ${sig.padesLevel ?? 'N/A'}`)
}

Batch Processing

Process a directory of signed PDFs:

ts
import { createVerifier } from '@trexolab/verifykit-core'
import { readdir, readFile } from 'fs/promises'
import { join } from 'path'
 
const verifier = await createVerifier()
const dir = './signed-documents'
 
const files = (await readdir(dir)).filter(f => f.endsWith('.pdf'))
 
const results = await Promise.all(
  files.map(async (file) => {
    const pdf = await readFile(join(dir, file))
    const result = await verifier.verify(pdf)
    return {
      file,
      signatures: result.signatures.length,
      valid: result.signatures.every(s => s.overallStatus === 'valid'),
      statuses: result.signatures.map(s => ({
        signer: s.signerName,
        status: s.overallStatus,
        pades: s.padesLevel,
      })),
    }
  })
)
 
// Summary
const valid = results.filter(r => r.valid).length
console.log(`\n${valid}/${results.length} documents fully valid`)

Online Revocation Checking

For server-side verification, you can perform direct CRL/OCSP requests without a proxy (since there are no CORS restrictions in Node.js):

ts
import { createVerifier } from '@trexolab/verifykit-core'
import { createRevocationPlugin } from '@trexolab/verifykit-plugin-revocation'
 
const revocation = createRevocationPlugin({ mode: 'direct' })
 
const verifier = await createVerifier({
  plugins: [revocation],
})
 
const result = await verifier.verify(pdfBuffer)
// Revocation checks now include live CRL/OCSP responses

CI/CD Pipeline Integration

Add signature verification to your CI/CD pipeline:

ts
import { createVerifier } from '@trexolab/verifykit-core'
import { readFileSync } from 'fs'
 
async function validateDocument(path: string): Promise<boolean> {
  const verifier = await createVerifier()
  const result = await verifier.verify(readFileSync(path))
 
  if (result.signatures.length === 0) {
    console.error(`FAIL: ${path} — no signatures found`)
    return false
  }
 
  for (const sig of result.signatures) {
    if (sig.overallStatus === 'invalid') {
      console.error(
        `FAIL: ${path} — ${sig.signerName}: ${sig.overallStatus}`
      )
      console.error(`  Integrity: ${sig.integrityCheck.detail}`)
      return false
    }
  }
 
  console.log(`PASS: ${path} — ${result.signatures.length} valid signature(s)`)
  return true
}
 
// Exit with error code if validation fails
const path = process.argv[2]
if (!path) {
  console.error('Usage: node validate.mjs <path-to-pdf>')
  process.exit(1)
}
 
const ok = await validateDocument(path)
process.exit(ok ? 0 : 1)

Custom Trust Store

For enterprise environments with internal CAs:

ts
const verifier = await createVerifier({
  trustStore: {
    certificates: [
      readFileSync('corp-root-ca.pem', 'utf-8'),
      readFileSync('dept-intermediate-ca.pem', 'utf-8'),
    ],
    mode: 'merge', // keeps the 119 AATL roots + adds yours
  },
})

Use mode: 'replace' if you only want to trust your own CAs and discard the built-in AATL roots.

Certification and Permissions

Server-side verification can also check document certification (DocMDP) permissions:

ts
for (const sig of result.signatures) {
  if (sig.mdpPermission != null) {
    console.log(`Certified (P=${sig.mdpPermission})`)
    // P=1: No changes allowed
    // P=2: Form fill + sign only
    // P=3: Annotate + form fill + sign
  } else {
    console.log('Approval signature')
  }
}

Runtime Support

The same core package works across JavaScript runtimes:

RuntimeMinimum Version
Node.js20.19.0+
DenoLatest
BunLatest

No configuration differences between runtimes. The WASM binary is base64-embedded, so there are no file system dependencies to manage.

Next Steps

Ready to verify PDF signatures in your application?

Get started with VerifyKit in under 5 minutes.