VeriPDF Architecture
Technical architecture of the VeriPDF SDK, covering the Rust/WASM core engine, package structure, verification pipeline, plugin system, and build pipeline.
Monorepo Structure
veripdf/
├── packages/
│ ├── core/ # @trexolab/verifykit-core — Rust/WASM verification engine
│ │ ├── Cargo.toml # Rust dependencies (rsa, ecdsa, x509-cert, etc.)
│ │ ├── build.sh # wasm-pack build script
│ │ ├── src/ # Rust source
│ │ │ ├── lib.rs # WASM entry point, #[wasm_bindgen] exports
│ │ │ ├── types.rs # Rust type definitions (serde-serializable)
│ │ │ ├── error.rs # Error types
│ │ │ ├── parse/
│ │ │ │ ├── pdf_parser.rs # Raw PDF signature extraction from bytes
│ │ │ │ ├── pdf_metadata.rs # Document metadata + permissions
│ │ │ │ └── pdf_utils.rs # PDF string decoding, date parsing, Flate decompress
│ │ │ ├── verify/
│ │ │ │ ├── signature_verifier.rs # Main verification orchestrator
│ │ │ │ ├── cms_verification.rs # CMS/PKCS#7 signature validation
│ │ │ │ └── revocation_checker.rs # CRL/OCSP URL extraction
│ │ │ ├── certs/
│ │ │ │ ├── trust_store.rs # Trusted cert store (119 embedded DER certs)
│ │ │ │ ├── cert_processing.rs # Certificate info extraction, OID names
│ │ │ │ └── trusted_certs.rs # Loads embedded DER certificates at init
│ │ │ ├── crypto/
│ │ │ │ ├── hash.rs # SHA-1, SHA-256, SHA-384, SHA-512
│ │ │ │ ├── signature.rs # RSA, ECDSA (P-256/P-384/P-521), Ed25519
│ │ │ │ └── asn1_helpers.rs # ASN.1/DER parsing utilities
│ │ │ └── helpers/
│ │ │ └── format_utils.rs # buf2hex, sha256hex
│ │ ├── certs/
│ │ │ └── der/ # 119 embedded AATL root CA certificates (DER format)
│ │ ├── js/ # TypeScript wrapper over WASM
│ │ │ ├── index.ts # Public API exports (createVerifier, verifyPdf, etc.)
│ │ │ ├── types.ts # TypeScript interface definitions
│ │ │ └── wasm-loader.ts # Platform-aware WASM initialization
│ │ ├── pkg/ # wasm-pack output (generated)
│ │ ├── tsdown.config.ts # Build config (ESM + CJS + DTS)
│ │ └── package.json
│ │
│ ├── react/ # @trexolab/verifykit-react — React UI + plugin system
│ │ ├── src/
│ │ │ ├── index.ts # All exports
│ │ │ ├── context.tsx # VeriPdfProvider, useVeriPdfConfig
│ │ │ ├── store.ts # ViewerStore — pub-sub reactive store
│ │ │ ├── plugin-types.ts # ViewerPlugin, PluginContext interfaces
│ │ │ ├── plugin-host.tsx # Plugin lifecycle, slot collection, ShortcutManager
│ │ │ ├── polyfills.ts # PDF.js 5.x Map polyfills (main thread)
│ │ │ ├── worker-utils.ts # Worker URL resolution + polyfill injection
│ │ │ ├── appearance-swapper.ts # Signature widget icon replacement
│ │ │ ├── hooks/
│ │ │ │ ├── useVerification.ts # Main verification state hook
│ │ │ │ ├── useContainerSize.ts # Responsive breakpoint hook
│ │ │ │ ├── useViewerStore.ts # Store selector hooks
│ │ │ │ └── usePinchZoom.ts # Touch pinch-zoom gesture
│ │ │ ├── core-viewer/
│ │ │ │ ├── Viewer.tsx # User-facing wrapper
│ │ │ │ ├── CoreViewer.tsx # Core viewer (~300 lines)
│ │ │ │ └── PageRenderer.tsx # Page rendering (canvas, text layer, annotations)
│ │ │ ├── plugins/ # 24 individual plugins
│ │ │ │ ├── default-layout/ # Meta-plugin composing all plugins
│ │ │ │ ├── toolbar/ # Toolbar with slot system
│ │ │ │ ├── zoom/ # Zoom controls, Ctrl+wheel
│ │ │ │ ├── page-navigation/ # Page nav, shortcuts
│ │ │ │ ├── rotation/ # Rotation
│ │ │ │ ├── search/ # Text search
│ │ │ │ ├── print/ # Print via iframe
│ │ │ │ ├── download/ # Download via blob URL
│ │ │ │ ├── theme/ # Light/dark toggle
│ │ │ │ ├── fullscreen/ # Fullscreen mode
│ │ │ │ ├── selection/ # Cursor tools
│ │ │ │ ├── scroll-mode/ # Scroll modes
│ │ │ │ ├── spread-mode/ # Page spreads
│ │ │ │ ├── signature/ # Signature panel
│ │ │ │ ├── sidebar/ # Left sidebar
│ │ │ │ ├── thumbnail/ # Thumbnail tab
│ │ │ │ ├── bookmark/ # Bookmark tab
│ │ │ │ ├── attachment/ # Attachment tab
│ │ │ │ ├── highlight/ # Text highlighting
│ │ │ │ ├── open-file/ # File open + drag-drop
│ │ │ │ ├── properties/ # Document properties modal
│ │ │ │ ├── shortcut-help/ # Keyboard shortcuts help
│ │ │ │ ├── context-menu/ # Right-click menu
│ │ │ │ └── more-menu/ # Overflow menu
│ │ │ ├── viewer/ # Legacy viewer components
│ │ │ ├── signature/ # Signature UI components
│ │ │ ├── certificate/ # Certificate chain viewer
│ │ │ ├── ui/ # Shared UI primitives (Radix-based)
│ │ │ ├── styles/ # CSS theme variables
│ │ │ ├── helpers/ # Status config, check builders, formatters
│ │ │ ├── i18n/ # Translation system
│ │ │ └── download-utils.ts # PEM/DER export, blob download
│ │ ├── vite.config.ts
│ │ └── package.json
│ │
│ ├── vanilla/ # @trexolab/verifykit-vanilla — UMD/ESM drop-in
│ │ ├── src/
│ │ │ └── index.tsx # VeriPdf.create() imperative API + event bus
│ │ ├── vite.config.ts
│ │ └── package.json
│ │
│ └── plugin-revocation/ # @trexolab/verifykit-plugin-revocation — Online CRL/OCSP
│ ├── src/
│ │ └── index.ts # revocationPlugin() factory
│ ├── tsdown.config.ts
│ └── package.json
│
├── docs/ # Documentation
│ ├── README.md # Documentation hub
│ ├── getting-started.md # Setup guides
│ ├── api-reference.md # Complete API reference
│ ├── examples.md # Practical examples
│ └── architecture.md # This file
│
├── package.json # Workspace root
└── tsconfig.json # TypeScript project references
Package Dependency Graph
@trexolab/verifykit-plugin-revocation ──> @trexolab/verifykit-core
@trexolab/verifykit-react ──────────────> @trexolab/verifykit-core
@trexolab/verifykit-vanilla ────────────> @trexolab/verifykit-react ──> @trexolab/verifykit-core
External Dependencies
@trexolab/verifykit-core
├── (Rust/WASM — no npm runtime dependencies)
└── Rust crates (compiled into WASM):
├── rsa, ecdsa, p256, p384, p521, ed25519-dalek (cryptographic verification)
├── sha1, sha2, digest (hash algorithms)
├── x509-cert, x509-parser, cms, der, const-oid (ASN.1 / X.509 / CMS parsing)
├── flate2 (PDF stream decompression)
├── wasm-bindgen, serde, serde_json (WASM interop)
└── time, thiserror, regex-lite, pem, hex (utilities)
@trexolab/verifykit-react
├── @trexolab/verifykit-core
├── pdfjs-dist 5.5.207 (PDF rendering — pinned, see below)
├── pdf-lib (appearance swapping for signature widgets)
├── @radix-ui/* (accessible dialog, tabs, accordion, separator)
├── lucide-react (icons)
└── react 19+, react-dom 19+ (peer dependencies)
@trexolab/verifykit-vanilla
├── @trexolab/verifykit-react
├── react 19.2.0 (bundled — not a peer dep)
└── react-dom 19.2.0 (bundled)
@trexolab/verifykit-plugin-revocation
└── @trexolab/verifykit-core
Rust/WASM Core Architecture
The @trexolab/verifykit-core package is a Rust library compiled to WebAssembly via wasm-pack. All cryptographic operations -- signature validation, hash computation, certificate chain building, ASN.1 parsing -- run entirely in WASM. The JavaScript layer is a thin wrapper that:
- Initializes the WASM module (
wasm-loader.ts) - Converts inputs (
ArrayBuffer,File, URLstring) toUint8Arrayfor WASM - Rehydrates ISO 8601 date strings from WASM output into JavaScript
Dateobjects - Runs plugin-based revocation checking as a post-processing step
Why Rust/WASM
| Aspect | Previous JS Engine | Rust/WASM Engine |
|---|---|---|
| Crypto | Web Crypto API + pkijs + asn1js + @peculiar/x509 | Built-in Rust crates (rsa, ecdsa, sha2, x509-cert) |
| Dependencies | ~8 npm runtime deps | 0 npm runtime deps |
| Performance | Baseline | 3.7x faster |
| Trust store | PEM strings parsed at runtime | 119 DER certificates compiled into WASM binary |
| Node.js | Required @peculiar/webcrypto polyfill | Works natively (WASM runs in Node.js) |
| Bundle | ~180 KB (minified JS + deps) | ~650 KB (.wasm) + ~5 KB (JS wrapper) |
WASM Loading Pipeline
1. createVerifier() or verifyPdf() called
2. initWasm() imports ../pkg/veripdf_core_wasm (dynamic import)
3. wasm-pack glue code loads .wasm via import.meta.url
4. WASM module initializes, trust store loads 119 DER certs
5. Module cached in memory — subsequent calls are instant
The WASM module is a singleton. Multiple createVerifier() calls share the same underlying WASM instance. Each verifier can have different trust store configurations, which are applied via wasm.set_trust_store_config().
Supported Algorithms
| Category | Algorithms |
|---|---|
| Signature | RSA (PKCS#1 v1.5, PSS), ECDSA (P-256, P-384, P-521), Ed25519 |
| Hash | SHA-1 (legacy), SHA-256, SHA-384, SHA-512, MD5 (detection only) |
| Certificate | X.509 v3, CMS/PKCS#7 SignedData |
Config Hierarchy (Vanilla -> React -> Core)
Options set in @trexolab/verifykit-vanilla flow through to the React provider and core engine:
VeriPdfOptions (vanilla) VeriPdfConfig (react) VeriPdfCoreConfig (core)
───────────────────── ────────────────── ─────────────────────
.trustStore -> .trustStore -> .trustStore
.plugins -> .plugins -> .plugins
.theme -> .theme
.workerSrc -> .worker.src
.locale -> .locale
.translations -> .translations
.features -> defaultLayoutPlugin({ disable: ... })
(inverted: features.X = false
-> disable.X = true)
The vanilla wrapper creates a defaultLayoutPlugin() internally and maps features flags to disable flags (inverted logic). Core verification config (trustStore, plugins) passes through unchanged.
Verification Pipeline
Each PDF signature is validated with 8 independent checks:
| Check | What It Verifies |
|---|---|
integrityCheck | Document bytes match the signed range (tampering detection) |
signatureCheck | Cryptographic signature is mathematically valid |
certificateChainCheck | Certificate chains to a trusted root CA |
expiryCheck | Certificates were valid at signing time |
timestampCheck | RFC 3161 timestamp is present and valid |
revocationCheck | Certificate not revoked (CRL/OCSP) |
algorithmCheck | Algorithms meet minimum strength (no SHA-1, RSA >= 2048) |
ekuCheck | Extended Key Usage permits document signing |
Verification Flow
1. PDF Parsing (pdf_parser.rs)
├── Scan PDF bytes for signature dictionaries
├── Extract ByteRange (bytes covered by signature)
├── Extract Contents (DER-encoded CMS blob)
└── Detect visibility, field names, permissions, deleted signatures
2. CMS Verification (cms_verification.rs)
├── Parse CMS/PKCS#7 SignedData structure
├── Validate cryptographic signature (RSA/ECDSA/Ed25519)
├── Check certificate chain against trust store (119 AATL root CAs)
├── Validate certificate expiry, key usage, EKU
└── Check algorithm strength
3. Timestamp Verification
├── Extract RFC 3161 timestamp token
├── Verify timestamp signature
└── Extract TSA certificate info
4. Revocation Checking (revocation_checker.rs)
├── Extract embedded CRL/OCSP data from signature
└── Optional: online checking via @trexolab/verifykit-plugin-revocation (JS post-processing)
5. Document Integrity
├── Check if ByteRange covers whole document
├── Detect DSS (Document Security Store) for LTV data
└── Identify deleted/tampered signatures
6. Results Assembly
├── WASM returns JSON with ISO 8601 date strings
├── JS wrapper rehydrates dates to Date objects
├── Plugin-based revocation updates applied (if plugins configured)
└── Final VerificationResult returned
Status Aggregation
The 8 individual checks aggregate into overallStatus:
| Status | Meaning |
|---|---|
valid | All checks pass |
warning | Minor issues (no timestamp, algorithm concerns) |
invalid | Signature broken, certificate untrusted, or document modified |
unknown | Could not determine (missing data) |
PAdES Conformance
The engine detects PAdES conformance levels:
| Level | Description |
|---|---|
B-B | Basic signature (CMS signed data) |
B-T | With trusted timestamp |
B-LT | With long-term validation data (CRL/OCSP embedded) |
B-LTA | With archive timestamp for long-term archival |
Plugin System
VeriPDF has two separate plugin systems:
Core Plugins (Verification)
Extend the verification engine. Implemented as VeriPdfPlugin:
interface VeriPdfPlugin {
name: string
setup?: (ctx: PluginContext) => void | Promise<void>
trustStore?: TrustStoreConfig
revocation?: {
checkCRL?: (cert, urls) => Promise<RevocationCheckResult>
checkOCSP?: (cert, issuer, urls) => Promise<RevocationCheckResult>
}
inputResolver?: (input: PdfInput) => Promise<ArrayBuffer | null>
}Core plugins can:
- Add trusted root certificates (merge or replace the built-in AATL store)
- Provide online revocation checking (CRL/OCSP via
@trexolab/verifykit-plugin-revocation) - Resolve custom input types (fetch from cloud storage, decrypt buffers)
The revocation plugin runs as a JavaScript post-processing step after WASM verification completes. For each signature whose revocationCheck.status is "unknown", the plugin performs live OCSP/CRL lookups and updates the result.
Viewer Plugins (React UI)
Extend the React viewer. Implemented as ViewerPlugin:
interface ViewerPlugin {
name: string
dependencies?: string[]
composedPlugins?: ViewerPlugin[]
install?(ctx: PluginContext): void
onDocumentLoad?(e: DocumentLoadEvent): void
onDocumentUnload?(): void
onPageChange?(e: PageChangeEvent): void
onZoomChange?(e: ZoomChangeEvent): void
onRotationChange?(e: RotationChangeEvent): void
destroy?(): void
renderToolbarSlot?: Partial<ToolbarSlots>
sidebarTabs?: SidebarTabDefinition[]
renderOverlay?: (props) => React.ReactNode
renderRightPanel?: (props) => React.ReactNode
renderPageOverlay?: (props) => React.ReactNode
}Plugin Lifecycle (Viewer)
1. Plugin factory called: const zoom = zoomPlugin()
2. Passed to Viewer: <Viewer plugins={[zoom]} />
3. CoreViewer creates store: const store = createViewerStore()
4. PluginHost resolves: resolvePlugins(plugins) -> flat, deduped list
5. install() called: zoom.install({ store, registerShortcut, ... })
6. Document loads: zoom.onDocumentLoad({ document, numPages })
7. User interacts: zoom.api.zoomIn() -> store.update({ scale: 1.35 })
8. Component unmounts: zoom.destroy() -> cleanup
PluginHost
Manages the full viewer plugin lifecycle:
- Resolve -- Flatten composed plugins (meta-plugins like
defaultLayoutPlugin), deduplicate byname - Install -- Call
install(ctx)on each plugin in order - Collect UI -- Gather toolbar slots, sidebar tabs, overlays, and page overlays from all plugins
- Dispatch events -- Route lifecycle events to all plugins
- Destroy -- Clean up all plugins on unmount
defaultLayoutPlugin
A meta-plugin that composes all 22+ viewer plugins into a batteries-included experience:
const layout = defaultLayoutPlugin({
disable: { print: true, download: true },
toolbar: { transform: (slots) => { delete slots.Print; return slots } },
})Disabled plugins are never instantiated, so they do not contribute to bundle size with proper tree-shaking.
The result exposes APIs for all sub-plugins:
layout.zoom.zoomIn()
layout.navigation.goToPage(5)
layout.sidebar.toggle('thumbnails')
layout.search.open()
layout.theme.toggleTheme()
layout.signature.togglePanel()ViewerStore
A lightweight pub-sub reactive store that replaces prop drilling across the component tree. Plugins read and write state through this store, and React components subscribe via useViewerStoreKey() for selective re-rendering.
State Shape
| Category | Keys |
|---|---|
| Document | document, fileBuffer, fileName, loadState, errorMessage |
| Navigation | currentPage, totalPages |
| Zoom | scale, fitMode |
| Rotation | rotation |
| Layout | scrollMode, spreadMode, cursorTool |
| Theme | themeMode |
| Fullscreen | isFullscreen |
| Verification | signatures, unsignedFields, verificationStatus |
| UI panels | sidebarOpen, sidebarTab, sigPanelOpen, findOpen |
| Password | passwordNeeded, passwordError |
Key Features
- Key-level subscriptions -- Subscribe to individual keys. A zoom change does not re-render page navigation components.
- Batch updates -- Multiple
update()calls insidebatch()fire listeners only once. - Shallow merge -- Only changed keys notify subscribers.
- Zero dependencies -- No Zustand, Redux, or external state library.
Toolbar Slot System
Plugins contribute to named toolbar slots. The toolbarPlugin collects all contributions and renders them in a 3-section grid layout:
| LEFT | CENTER | RIGHT |
| SearchPopover | GoToPreviousPage CurrentPageInput | Download Print |
| | NumberOfPages GoToNextPage | ThemeToggle Fullscreen |
| | ZoomOut Zoom ZoomIn Rotate | MoreMenu |
The transform API lets consumers remove, reorder, or add custom slots:
toolbarPlugin({
transform: (slots) => {
delete slots.Print
delete slots.Download
slots.MyButton = ({ store }) => <button>Custom</button>
return slots
},
})Build Pipeline
1. @trexolab/verifykit-core (wasm-pack -> .wasm + glue JS, then tsdown -> ESM + CJS + DTS)
2. @trexolab/verifykit-plugin-revocation (tsdown -> ESM + CJS + DTS)
3. @trexolab/verifykit-react (Vite lib mode -> ESM + CJS + CSS + DTS)
4. @trexolab/verifykit-vanilla (Vite lib mode -> UMD + ESM + CSS)
Build all packages: npm run build
Core Build Steps
1. build.sh runs wasm-pack build --target web --release
2. wasm-pack outputs to pkg/:
├── veripdf_core_wasm_bg.wasm (~650 KB, optimized with LTO)
├── veripdf_core_wasm.js (glue code)
└── veripdf_core_wasm.d.ts (WASM type declarations)
3. tsdown compiles js/ (TypeScript wrapper) to dist/:
├── index.mjs (ESM entry)
├── index.cjs (CJS entry)
└── index.d.mts (TypeScript declarations)
The WASM binary is compiled with:
opt-level = 3(maximum optimization)lto = true(link-time optimization)codegen-units = 1(single codegen unit for smaller output)strip = true(strip debug symbols)
Browser and Node.js Support
Browser Support
| Browser | Minimum Version | Notes |
|---|---|---|
| Chrome | 109+ | Full support |
| Firefox | 115+ | Full support |
| Safari | 16.4+ | Full support |
| Edge | 109+ | Full support (Chromium-based) |
Required browser capabilities:
- WebAssembly (instantiation and streaming)
- Web Workers (ES module workers)
- ES2022+ features (private class fields,
structuredClone) - CSS custom properties
No IE11, legacy Edge (EdgeHTML), or polyfill path is provided.
Node.js Support
| Runtime | Minimum Version | Notes |
|---|---|---|
| Node.js | 20.19.0+ | WASM runs natively, no polyfills needed |
The WASM engine works identically in Node.js. Unlike the previous JS engine, there is no need for @peculiar/webcrypto or any crypto polyfill -- all crypto operations are compiled into the WASM binary.
Worker Architecture
PDF.js uses a Web Worker for CPU-intensive parsing and rendering.
Zero-Config Auto-Resolution
1. resolveWorkerUrl()
├── Read pdfjsLib.version -> "5.5.207"
├── Construct CDN URL -> "https://unpkg.com/pdfjs-dist@5.5.207/build/pdf.worker.min.mjs"
└── Fallback -> "/pdf.worker.min.mjs"
2. createPolyfillWorkerUrl(resolvedUrl)
├── Prepend Map polyfills (getOrInsertComputed, getOrInsert)
├── Use dynamic import() to load the real worker as ES module
├── Wrap everything in a Blob -> blob:http://...
└── Cache the Blob URL for reuse
3. Set pdfjsLib.GlobalWorkerOptions.workerSrc = blobUrl
Why Polyfill Injection is Needed
pdfjs-dist 5.5+ uses Map.prototype.getOrInsertComputed, a TC39 Stage 2 proposal not yet available in browsers. The worker runs in a separate JS context, so main-thread polyfills do not apply. The Blob URL wrapper injects polyfills into the worker's global scope.
Pinned pdfjs-dist Version
pdfjs-dist is pinned to 5.5.207 (exact, no semver range). Reasons:
- Unstable TC39 polyfills. New versions may add additional Stage 2/3 API calls not covered by the polyfill set.
- Worker/library version coupling. The worker and main library must match exactly.
- CMap and font asset compatibility. Version bumps can change these assets.
Before upgrading, verify that polyfills in polyfills.ts and worker-utils.ts still cover all non-standard APIs.
Theme System
CSS custom properties on :root with a data-theme attribute:
:root {
--bg: #ffffff;
--fg: #1a1a1a;
--primary: #0072c6;
/* 100+ variables */
}
:root[data-theme="dark"] {
--bg: #1d1d1d;
--fg: #e0e0e0;
--primary: #4da6ff;
}The VeriPdfProvider sets data-theme on <html>. The themePlugin toggles it at runtime. All library styles are scoped to .veripdf-root to avoid conflicts with host application styles.
Security Considerations
Worker Isolation
The PDF.js Web Worker runs in a separate JavaScript context. Polyfills and security patches applied to the main thread have no effect inside the worker. VeriPDF injects polyfills directly into the worker via a Blob URL wrapper.
Blob URL and CSP
Applications with a Content Security Policy must allow:
Content-Security-Policy: worker-src 'self' blob:; script-src 'self' blob:;
If the worker is fetched from a CDN, the CDN origin must be permitted in connect-src:
Content-Security-Policy: connect-src 'self' https://unpkg.com;
Self-host the worker to avoid CDN allowlisting.
WASM and CSP
WebAssembly compilation requires 'wasm-eval' or 'unsafe-eval' in script-src on some browsers. Alternatively, use 'wasm-unsafe-eval':
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval' blob:;
Trust Store in Memory
The 119 embedded AATL root CA certificates and any additional certificates provided via trustStore config are held in the WASM linear memory for the lifetime of the module. They are not written to disk or persisted to browser storage.
No External Data Transmission
VeriPDF does not send document data, verification results, or telemetry to external servers. The only network requests are:
- Worker CDN fetch -- A single
import()to download the PDF.js worker (avoidable by self-hosting). - Revocation checking (opt-in) -- When
@trexolab/verifykit-plugin-revocationis installed, it makes direct connections to CA infrastructure (CRL distribution points, OCSP responders).
All verification runs entirely client-side in the WASM engine.
Known Limitations
Single Theme per Page
The theme system sets data-theme on <html>, making it global. Multiple VeriPdfProvider instances with different theme modes will conflict. Use a single provider or the same theme mode for all viewers.
Worker URL is a Global Singleton
The generated Blob URL is cached globally and set on pdfjsLib.GlobalWorkerOptions.workerSrc. All PDF.js instances share the same worker URL. Revoking it breaks worker creation for all viewers.
Appearance Swapping Bundle Cost
The appearance swapper imports pdf-lib (~300 KB minified) unconditionally. Tree-shaking does not eliminate it because the import is unconditional in the React package.
No Incremental Verification
Verification processes all signatures in a single pass. There is no API to verify a single signature in isolation.
CMap and Font File Hosting
PDF.js requires CMap and standard font files for CJK text and certain form fields. These files are not bundled by VeriPDF -- consumers must copy them from pdfjs-dist and serve them.