VerifyKitv0.5.1

Core Concepts

This guide covers the fundamental concepts behind VerifyKit's PDF signature verification engine.

The 8-Point Verification Model

Every PDF signature is evaluated against eight independent checks. Each check produces its own status (valid, invalid, warning, unknown, or pending), and the overall signature status is derived from the combination of all eight results.

1. Integrity

Verifies that the document bytes match the signed byte range. If anyone has modified the PDF after signing -- even a single byte -- the integrity check fails. This is the primary tamper-detection mechanism.

2. Signature

Validates that the cryptographic signature (CMS/PKCS#7) is mathematically correct against the signer's public key. This confirms the signature was actually produced by the holder of the corresponding private key.

3. Certificate Chain

Builds a chain from the signer's certificate up to a trusted root CA. VerifyKit ships with 119 Adobe Approved Trust List (AATL) root certificates embedded in the WASM binary. If the signer's root CA is not in the trust store, this check returns unknown.

4. Expiry

Confirms that the signer's certificate (and all intermediate certificates in the chain) were within their validity period at the time of signing. An expired certificate does not necessarily invalidate a signature if a trusted timestamp proves the signature was created before expiry.

5. Timestamp

Checks for an embedded RFC 3161 timestamp and validates its signature. A timestamp provides cryptographic proof of when the document was signed, which is critical for long-term validation -- it proves the signature existed before the certificate expired or was revoked.

6. Revocation

Determines whether the signer's certificate has been revoked by its issuing CA. The core engine performs offline revocation checking by default (examining embedded CRL/OCSP data). For live online checks against CRL distribution points and OCSP responders, install the @trexolab/verifykit-plugin-revocation plugin.

7. Algorithm

Assesses whether all cryptographic algorithms used (hashing and signing) meet minimum security requirements. Flags weak algorithms such as SHA-1 and MD5. Requires RSA keys of at least 2048 bits. Supported strong algorithms include SHA-256/384/512 with RSA, ECDSA, and Ed25519.

8. Extended Key Usage (EKU)

Verifies that the signer's certificate includes the appropriate Extended Key Usage extension that permits document signing. A certificate issued only for TLS server authentication, for example, should not be used for document signing.

Check Result Fields

Each check is represented as a SignatureCheckResult:

ts
interface SignatureCheckResult {
  label: string              // Human-readable check name
  status: VerificationStatus // Outcome
  detail: string             // Explanation text
  attempted?: boolean        // Whether the check was actively attempted (vs. skipped)
}

The attempted field distinguishes between checks that were actively tried but could not produce a result (e.g., an OCSP/CRL request that timed out) and checks that were never attempted (e.g., revocation checking without the plugin installed). This distinction enables Adobe Reader parity -- see Revocation: Attempted vs Not Attempted below.

The eight checks are accessible on every PdfSignature object:

ts
sig.integrityCheck        // Check 1
sig.signatureCheck        // Check 2
sig.certificateChainCheck // Check 3
sig.expiryCheck           // Check 4
sig.timestampCheck        // Check 5
sig.revocationCheck       // Check 6
sig.algorithmCheck        // Check 7
sig.ekuCheck              // Check 8

Verification Statuses

Every check and every signature has a status drawn from five possible values:

StatusMeaning
validThe check passed successfully. The signature or certificate is trustworthy for this criterion.
invalidThe check failed. The signature is not trustworthy -- for example, the document was tampered with, the signature is mathematically incorrect, or a certificate was revoked.
warningThe check passed with caveats. For example, a weak algorithm (SHA-1) was used but the signature is otherwise correct.
unknownThe check could not be performed. Common causes: revocation data is unavailable (offline mode), or the root CA is not in the trust store.
pendingThe check has not been executed yet. This is a transient state during async verification.

Overall Status Logic

The overall status of a signature is derived from its eight individual checks:

  • If any check is invalid, the overall status is invalid.
  • If all checks are valid, the overall status is valid.
  • If some checks are warning or unknown (but none invalid), the overall status is warning.

Display Status (Adobe Reader Parity)

The core engine's overallStatus may differ from what Adobe Reader DC shows. The React UI layer provides a getDisplayStatus(sig) function that re-derives the display status for Adobe parity:

Engine StatusConditionDisplay StatusReason
validRevocation unknown + chain trustedunknownAdobe shows "unknown" when revocation can't be checked (browser CORS blocks OCSP/CRL)
invalidOnly expiry failed, integrity + crypto validunknownAdobe treats expired cert as identity issue, not integrity failure
warningAll security checks pass (integrity, crypto, chain, expiry, algorithm)validWarning is from non-critical checks (EKU, timestamp) — Adobe shows these as valid
ts
import { getDisplayStatus } from '@trexolab/verifykit-react'
 
const displayStatus = getDisplayStatus(sig)
// Use displayStatus for UI rendering instead of sig.overallStatus

When to use: Always use getDisplayStatus() for user-facing status display (icons, colors, text). Use sig.overallStatus only for programmatic logic that needs the raw engine result.

Revocation: Attempted vs Not Attempted

The attempted field on SignatureCheckResult enables a key Adobe Reader parity distinction for revocation:

  • Not attempted (attempted is undefined or false): Revocation was never checked -- for example, the revocation plugin is not installed. In this case, the display status treats revocation as unknown but does not penalize the signature.
  • Attempted (attempted: true): Revocation checking was actively tried (e.g., OCSP/CRL requests were made) but failed or returned unknown. Adobe Reader treats this scenario differently -- an online check that fails yields "unknown" status, whereas offline (never attempted) yields "valid" for the revocation dimension.

This mirrors Adobe Reader DC behavior: offline = valid (revocation not checked, signature still shown as valid), online + failed = unknown (revocation attempted but inconclusive, signature shown as unknown).

Document Timestamp vs Signature Timestamp

A signature timestamp is an RFC 3161 token embedded within a signature's CMS structure. It proves when that specific signature was created. It is associated with a single signature.

A document timestamp is a special signature whose subFilter is ETSI.RFC3161. It covers the entire document (not just one signature) and serves as an archival timestamp for PAdES B-LTA compliance. Document timestamps are displayed with a distinct visual indicator in the React viewer and are identified by having subFilter === 'ETSI.RFC3161'.

Certification vs. Approval Signatures

A certification signature (DocMDP) locks the document with specific permission levels:

PermissionAllowed Changes
1 (No changes)No modifications allowed after certification
2 (Form fill + sign)Only form filling and additional signatures
3 (Annotate + form fill + sign)Annotations, form filling, and signatures

An approval signature has no DocMDP restriction — it simply attests that the signer approved the document at signing time.

ts
if (sig.mdpPermission != null) {
  console.log(`Certified (P=${sig.mdpPermission})`)
} else {
  console.log('Approval signature')
}

Note: Use != null (loose equality) to check mdpPermission, as the WASM engine serializes None as undefined.

AIA Certificate Chain Resolution

VerifyKit automatically resolves missing intermediate certificates using Authority Information Access (AIA) extensions. When a certificate's chain can't be built from embedded data alone, the engine fetches intermediates from AIA URLs.

  • Enabled by default (enableAIA: true)
  • Supports both individual certificate downloads and PKCS#7 containers
  • Uses AKI/SKI (Authority/Subject Key Identifier) matching for accurate chain building
  • Results are cached to avoid redundant network requests

This significantly improves verification success for PDFs that don't embed the full certificate chain.

Trust Store

VerifyKit embeds 119 Adobe Approved Trust List (AATL) root CA certificates directly in the WASM binary. These are the same root CAs that Adobe Acrobat trusts for signature validation.

Extending the Trust Store

You can add your own root CA certificates (e.g., enterprise CAs) using two modes:

Merge mode -- adds your certificates alongside the built-in 119 AATL roots:

ts
const verifier = await createVerifier({
  trustStore: {
    certificates: [myCorpRootPem],
    mode: 'merge',
  },
})

Replace mode -- discards all built-in roots and uses only your certificates:

ts
const verifier = await createVerifier({
  trustStore: {
    certificates: [rootA, rootB],
    mode: 'replace',
  },
})

Trust store configuration is accepted by createVerifier(), VerifyKitProvider (React), and VerifyKit.create() (vanilla JS).

PAdES Conformance

VerifyKit detects the PAdES (PDF Advanced Electronic Signatures) conformance level of each signature according to ETSI EN 319 142. The detected level is available via sig.padesLevel (type PAdESLevel | null).

PAdES levels are only set for ETSI signature types (ETSI.CAdES.detached, ETSI.RFC3161). For standard PKCS#7 signatures (adbe.pkcs7.detached, adbe.pkcs7.sha1), padesLevel is null.

LevelNameRequirements
B-BBasicValid CMS signature with signer certificate and signed attributes (signing time, message digest, content type). The minimum baseline for a legally valid electronic signature.
B-TTimestampB-B requirements plus an embedded RFC 3161 timestamp proving when the signature was created. Provides non-repudiation of time.
B-LTLong-TermB-T requirements plus embedded validation data (certificates and revocation information). The signature can be validated without network access.
B-LTALong-Term ArchivalB-LT requirements plus a document timestamp that protects the entire validation data. Supports validation even after the signer's certificate expires or the signing algorithm becomes weak.
ts
const result = await verifyPdf(pdfBuffer)
for (const sig of result.signatures) {
  console.log(`${sig.name}: PAdES ${sig.padesLevel}`)
  // "Alice: PAdES B-T"
}

The Verification Pipeline

When you call verifyPdf() or verifier.verify(), the engine executes the following pipeline:

  1. Parse PDF -- The WASM engine parses the PDF file structure, locating all signature dictionaries and their byte ranges.

  2. Extract signatures -- Each signature field is extracted with its CMS/PKCS#7 data, byte range, and metadata (signer name, reason, location, signing time).

  3. For each signature, run the 8 checks:

    • Integrity -- Compute the document digest over the signed byte range and compare it to the digest embedded in the CMS structure.
    • Signature -- Validate the CMS signature against the signer's public key (RSA, ECDSA, or Ed25519).
    • Certificate chain -- Build a chain from the signer certificate through intermediates to a trusted root in the trust store.
    • Expiry -- Verify that all certificates in the chain were valid at signing time.
    • Timestamp -- Parse and validate any embedded RFC 3161 timestamp token.
    • Revocation -- Check revocation status using embedded CRL/OCSP data, or delegate to a revocation plugin for online checks.
    • Algorithm -- Assess the strength of all hash and signing algorithms used.
    • EKU -- Verify the signer certificate's Extended Key Usage permits document signing.
  4. Compute overall status -- Combine the eight check results into a single overallStatus for each signature.

  5. Return results -- Package all signatures, their check results, certificate chains, timestamps, and metadata into a VerificationResult.

WASM Architecture

The core verification engine is written in Rust and compiled to WebAssembly. This architecture provides several key properties:

  • All cryptographic operations run in WASM -- RSA, ECDSA, Ed25519, and the SHA family of hash algorithms are implemented in Rust. There is no dependency on the Web Crypto API or any external JavaScript crypto libraries.

  • Cross-platform -- The same WASM binary runs in all modern browsers (Chrome 109+, Firefox 115+, Safari 16.4+, Edge 109+) and Node.js 20+.

  • 3.7x faster -- Compared to equivalent JavaScript-based PDF signature verification engines, the Rust/WASM implementation delivers roughly 3.7x faster verification.

  • No native addons -- Pure WASM with no node-gyp, no prebuild, and no platform-specific binaries. Install with npm install and it works everywhere.

  • Automatic initialization -- The WASM module is loaded and compiled automatically on first use. No manual initialization is required (though you can pre-warm it with initWasm() for lower latency on the first verification).

  • Single-pass parsing -- The PDF is parsed once; byte ranges and CMS structures are extracted in a single traversal, minimizing memory overhead.

ts
import { initWasm, isWasmReady } from '@trexolab/verifykit-core'
 
// Optional: pre-warm during app startup (20-50ms)
await initWasm()
 
// Check initialization status
console.log(isWasmReady()) // true