A pure JavaScript/TypeScript library for adding RFC 3161 document timestamps to PDFs. Works in Node.js, Cloudflare Workers, Deno, and modern browsers without native dependencies.
RFC 3161 defines the Time-Stamp Protocol (TSP). A Time Stamping Authority (TSA) signs a data hash and timestamp. Whether that TSA is trusted is a separate caller policy decision: the caller must supply and apply an appropriate trust store.
When embedded in a PDF as a Document Timestamp (DocTimeStamp):
- It provides cryptographic evidence about a document hash and timestamp
- PDF readers such as Adobe Acrobat can inspect it; their validation result depends on the reader build and configured trust policy
- It does not require a signing certificate from the user
- Its long-term acceptance depends on the verifier's TSA, path, revocation, and freshness policy
The timestamp value dictionary is emitted as /Type /DocTimeStamp with
/SubFilter /ETSI.RFC3161; its AcroForm field remains /FT /Sig. This is a
structural interoperability behavior, not a claim of general PAdES conformance
or viewer-independent trust.
Existing AcroForm fields and widgets are preserved. A new timestamp field is appended using the requested name or a deterministic collision-safe suffix.
- RFC 3161 compliant implementation of the Time-Stamp Protocol
- Document timestamps using the DocTimeStamp (ETSI.RFC3161) format
- Candidate certificate and revocation-material embedding for LTV workflows
- Support for multiple timestamps from different TSAs
- Extraction and verification of timestamps from existing PDFs
- RFC 8933 CMS Algorithm Identifier Protection validation
- Edge runtime compatible (Cloudflare Workers, Vercel Edge, Deno Deploy)
- Browser support via the Web Crypto API
- Full TypeScript type definitions
- No native dependencies
The FreeTSA URLs in this README are testing and development examples, not a
production trust recommendation. FreeTSA uses a self-signed root; selecting
its URL does not trust its timestamps. For production verification, supply a
caller-selected TrustStore containing only roots your policy accepts.
import { timestampPdf, KNOWN_TSA_URLS } from "pdf-rfc3161";
import { readFile, writeFile } from "fs/promises";
const pdfBytes = await readFile("document.pdf");
const result = await timestampPdf({
pdf: new Uint8Array(pdfBytes),
tsa: {
url: KNOWN_TSA_URLS.FREETSA,
},
});
await writeFile("document-timestamped.pdf", result.pdf);
console.log("Timestamp added at:", result.timestamp.genTime);npm install pdf-rfc3161yarn add pdf-rfc3161pnpm add pdf-rfc3161In 0.2.0, LTV collection, timestamping-EKU enforcement, and certificate validity-at-generation-time enforcement are on by default. Production still requires a caller-selected TSA trust policy. SSRF protection (URL allowlist for AIA / OCSP / CRL fetches) is on by default and has no opt-out at the public-call level.
Minimal timestamp metadata is the default: /M, Reason, Location, and
ContactInfo are omitted unless explicitly requested. omitModificationTime: false restores /M for a compatibility use case; metadata opt-ins can take a
file outside baseline recommendations.
const result = await timestampPdf({
pdf,
tsa: { url: KNOWN_TSA_URLS.FREETSA }, // testing/development TSA
enableLTV: true,
});enableLTV embeds collected certificate and revocation candidates. It does not establish TSA
trust, validate revocation freshness, or guarantee validity after certificate expiry.
Every public timestamp path validates the TSA response against the prepared
ByteRange, request nonce, requested policy, CMS signature, signer selection,
ESS binding, and timestamping EKU immediately before the PDF embed operation.
The raw PDF embed primitive is deliberately not published. For an external TSA
round trip, use TimestampSession.createTimestampRequest() followed by
TimestampSession.embedTimestampToken(); the session applies the same gate.
const verified = await verifyTimestamp(ts, {
trustStore, // your SimpleTrustStore
pdf, // enables PDF-level checks
// requireTimestampingEKU and requireCertValidAtGenTime already default
// to true since 0.2.0 (RFC 3161 EKU 1.3.6.1.5.5.7.3.8; cert valid at
// the signing instant).
strictESSValidation: true, // ESS cert identifier must match
});Every failure carries both a human-readable verificationError and a
machine-readable verificationErrorCode (VERIFICATION_FAILED for token,
trust, profile, and document-hash failures; INVALID_ARGUMENT for
unsatisfiable chainValidationTime requests; PDF_ERROR when the shared
signature index cannot be built).
In 0.2.0, enableLTV, requireTimestampingEKU, and
requireCertValidAtGenTime default to true. See MIGRATION.md if you need
to verify legacy or non-conforming tokens whose certificates do not satisfy the
RFC 3161 EKU requirement.
| Flag | Default | Recommended for prod |
|---|---|---|
enableLTV |
true (0.2.0) |
true |
rejectOnRevocationWarning |
deprecated no-op | not applicable; TSA statuses 4/5 are always fatal |
requireTimestampingEKU |
true (0.2.0) |
true |
requireCertValidAtGenTime |
true (0.2.0) |
true |
strictESSValidation |
false |
true |
ignoreEncryption |
false |
leave as false |
pdf-rfc3161-cli ships the same operations as a CLI.
npx pdf-rfc3161-cli --help
# FreeTSA is for testing/development; its root must be explicitly trusted for verification.
npx pdf-rfc3161-cli timestamp https://freetsa.org/tsr input.pdf output.pdf
npx pdf-rfc3161-cli verify input.pdf -v
npx pdf-rfc3161-cli archive https://freetsa.org/tsr input.pdf output.pdfSee npx pdf-rfc3161-cli <command> --help for the full flag set on each subcommand.
import { timestampPdf } from "pdf-rfc3161";
const result = await timestampPdf({
pdf: pdfBytes,
tsa: {
url: "https://freetsa.org/tsr",
hashAlgorithm: "SHA-256", // or SHA-384, SHA-512
timeout: 30000,
},
});The default output omits /M, Reason, Location, and ContactInfo. If a
legacy workflow requires metadata, set omitModificationTime: false and the
metadata fields deliberately; do not treat that compatibility shape as the
default output profile.
Enable LTV to embed candidate certificates and revocation material for a verifier to evaluate:
import { timestampPdf } from "pdf-rfc3161";
const result = await timestampPdf({
pdf: pdfBytes,
tsa: { url: "https://freetsa.org/tsr" },
enableLTV: true,
});VRI is optional. If a caller has a field-specific interoperability reason, use
the internal subpath API after selecting the signed field. The VRI key is
derived inside the loaded PDF context from the complete decoded, padded
/Contents value; callers do not supply a certificate hash or PDF reference.
This is the project's implementation interpretation; see
MIGRATION.md for the standards and producer-compatibility
nuance.
import { addVRIForSignature } from "pdf-rfc3161/internals";
const updatedPdf = await addVRIForSignature(
timestampedPdf,
{ fieldName: "Timestamp" },
{
validationData: {
certificates,
crls,
ocspResponses,
},
}
);addVRI and addVRIEnhanced are deprecated transition wrappers. See
MIGRATION.md for the supported field-bound replacement. Archive renewal never
creates VRI entries automatically.
Add timestamps from multiple Time Stamping Authorities for redundancy:
import { timestampPdfMultiple, KNOWN_TSA_URLS } from "pdf-rfc3161";
const result = await timestampPdfMultiple({
pdf: pdfBytes,
tsaList: [{ url: KNOWN_TSA_URLS.FREETSA }, { url: "https://another-tsa-server" }],
enableLTV: true,
});
console.log(`Added ${result.timestamps.length} timestamps`);Use archiveTimestamp to renew RFC 3161 document timestamps. It verifies existing timestamp
tokens, collects certificate and revocation candidates only from verified tokens, additively
updates the global DSS once, and adds a final document timestamp. Network OCSP and CRL bytes are
structural candidates only; caller-supplied revocation data remains the caller's responsibility.
This is not a general PAdES-LTA upgrader or an indefinite-validity guarantee:
import { archiveTimestamp, KNOWN_TSA_URLS } from "pdf-rfc3161";
// `timestampPdfLTA` is a deprecated alias for `archiveTimestamp` kept for back-compat.
const result = await archiveTimestamp({
pdf: signedPdfBytes,
tsa: { url: KNOWN_TSA_URLS.FREETSA },
includeExistingRevocationData: true,
});Extract timestamps from an existing PDF:
import { extractTimestamps, verifyTimestamp } from "pdf-rfc3161";
const timestamps = await extractTimestamps(pdfBytes);
for (const ts of timestamps) {
console.log(`Timestamp: ${ts.info.genTime}`);
console.log(`Policy: ${ts.info.policy}`);
const verified = await verifyTimestamp(ts, { pdf: pdfBytes });
console.log(`Cryptographic/PDF verification: ${verified.verified}`);
}This example does not make a TSA trust decision because it supplies no
trustStore. Add a caller-selected store when TSA-chain trust is required.
import { timestampPdf, KNOWN_TSA_URLS } from "pdf-rfc3161";
export default {
async fetch(request: Request): Promise<Response> {
const formData = await request.formData();
const file = formData.get("pdf") as File;
const pdfBytes = new Uint8Array(await file.arrayBuffer());
const result = await timestampPdf({
pdf: pdfBytes,
tsa: { url: KNOWN_TSA_URLS.FREETSA },
enableLTV: true,
});
return new Response(result.pdf, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="timestamped.pdf"',
},
});
},
};Adds an RFC 3161 timestamp to a PDF document.
Options:
| Name | Type | Required | Description |
|---|---|---|---|
pdf |
Uint8Array |
Yes | PDF document bytes |
tsa.url |
string |
Yes | TSA server URL |
tsa.hashAlgorithm |
string |
No | SHA-256, SHA-384, or SHA-512 (default: SHA-256) |
tsa.timeout |
number |
No | Request timeout in ms (default: 30000) |
tsa.retry |
number |
No | Retry attempts (default: 3) |
tsa.retryDelay |
number |
No | Base retry delay in ms (default: 1000) |
enableLTV |
boolean |
No | Embed candidate DSS material (default: true in 0.2.0); not a trust or validity guarantee |
maxSize |
number |
No | Maximum PDF size in bytes (default: 250MB) |
signatureSize |
number |
No | Initial token space: 8KB for timestampPdf(); 16KB for an LTV-enabled TimestampSession. Omit or set 0 for that path's default. |
signatureFieldName |
string |
No | Requested field-name base; deterministic suffixes avoid collisions (default: "Timestamp") |
reason |
string |
No | Reason for timestamping |
location |
string |
No | Location metadata |
contactInfo |
string |
No | Contact information |
omitModificationTime |
boolean |
No | undefined and true omit /M; explicit false restores legacy metadata |
optimizePlaceholder |
boolean |
No | Optimize signature size (default: false) |
rejectOnRevocationWarning |
boolean |
No | Deprecated no-op retained for source compatibility; TSA statuses 4/5 are always fatal |
ignoreEncryption |
boolean |
No | Process encrypted PDFs (default: false; recommend leaving false) |
revocationData |
LTVData |
No | Caller-provided candidate material; caller is responsible for trust |
Returns a TimestampResult with the timestamped PDF, timestamp info, and optional candidate
ltvData. The deprecated tsaRevocationWarning field is never set: TSA statuses 4/5 are always
fatal.
Specify a larger signatureSize manually if you encounter "token larger than
placeholder" errors.
Adds timestamps from multiple TSAs. Takes a tsaList array and supports enableLTV.
Returns an array of ExtractedTimestamp objects from the PDF.
Verifies the cryptographic signature of an extracted timestamp.
Options:
| Name | Type | Required | Description |
|---|---|---|---|
pdf |
Uint8Array |
No | Original PDF bytes for hash verification |
trustStore |
TrustStore | null |
No | Caller-selected store for TSA-chain validation. Omit or use null for cryptographic/PDF checks only. |
strictESSValidation |
boolean |
No | Enforce PAdES ESS-cert-id compliance (default: false) |
requireTimestampingEKU |
boolean |
No | Require id-kp-timeStamping EKU on TSA cert (default: true in 0.2.0) |
requireCertValidAtGenTime |
boolean |
No | Require TSA cert valid at timestamp instant (default: true in 0.2.0) |
The library includes KNOWN_TSA_URLS - a list of known TSA URLs for convenience.
Note: Usage is governed by providers' Terms and Conditions. FreeTSA is for testing/development and uses a self-signed root. Do not treat selecting its URL as trust; verify it only with an explicitly caller-selected trust store that contains its root when that is appropriate for your policy.
A client-side demo lives in packages/demo/. The repo is a pnpm workspace; run it with:
pnpm install
pnpm --filter pdf-rfc3161-demo devimport { timestampPdf, TimestampError, TimestampErrorCode } from "pdf-rfc3161";
try {
const result = await timestampPdf({/* ... */});
} catch (error) {
if (error instanceof TimestampError) {
switch (error.code) {
case TimestampErrorCode.NETWORK_ERROR:
// Handle network issues
break;
case TimestampErrorCode.TSA_ERROR:
// TSA rejected the request
break;
case TimestampErrorCode.TIMEOUT:
// Request timed out
break;
}
}
}This library focuses on generating RFC 3161 timestamps for PDFs with candidate validation-material support for LTV workflows.
Primary use cases:
- Adding timestamps to fresh documents
- Renewing RFC 3161 document timestamps with aggregate global DSS candidate material
- Extracting and verifying timestamp structures
Verification scope:
The verifyTimestamp() function checks cryptographic self-consistency and configured profile
requirements:
- The timestamp token is properly signed by the TSA
- The document hash matches what was timestamped
- The timestamp structure is valid
Without a trustStore, those checks are cryptographic and, when pdf is
supplied, PDF-consistency checks only. They do not trust the TSA, validate a
certificate path, or establish revocation freshness. Supply a caller-owned
TrustStore to apply a trust policy.
Modular Network Architecture:
The library is designed with pluggable network interfaces to support various deployment scenarios:
- Edge Runtimes: Cloudflare Workers, Vercel Edge, Deno Deploy (uses Web Fetch API)
- Node.js: Can use HTTP client of choice (fetch, axios, node-fetch, curl via child_process)
- Testing: Deterministic mock responses without network calls
- Air-Gapped Environments: Supply pre-fetched revocation data directly
All network operations use the Fetcher pattern. The fetcher classes live on
the /advanced subpath, available via tree-shakable deep import:
import { completeLTVData } from "pdf-rfc3161/internals";
import { MockFetcher, DefaultFetcher } from "pdf-rfc3161/advanced";
// Use a mock fetcher for testing: wire it into LTV collection through
// the fetchers adapters (this pattern is executed by the test suite).
const mockFetcher = new MockFetcher();
mockFetcher.setOCSPResponse("http://ocsp.example.com", mockResponse);
const mocked = await completeLTVData(ltvData, {
fetchers: {
ocspFetcher: (url, request) => mockFetcher.fetchOCSP(url, request),
crlFetcher: (url) => mockFetcher.fetchCRL(url),
},
});
// Use DefaultFetcher with custom settings, wired the same way.
const customFetcher = new DefaultFetcher({ timeout: 10000 });
const collected = await completeLTVData(ltvData, {
fetchers: {
ocspFetcher: (url, request, context) =>
customFetcher.fetchOCSP(url, request, context),
crlFetcher: (url, context) => customFetcher.fetchCRL(url, context),
},
});
// Supply pre-fetched LTV data (no network needed)
const result = await timestampPdf({
pdf: pdfBytes,
tsa: { url: "https://tsa.example.com" },
enableLTV: true,
// Pre-fetched revocation data
revocationData: {
certificates: [issuerCert, rootCert],
ocspResponses: [preFetchedOCSP],
crls: [preFetchedCRL],
},
});Collection is best-effort: failures never fail signing. The one-call
result carries them in ltvErrors (present only when collection ran and
reported errors), TimestampSession.getLTVErrors() returns a copy of the
latest embed's diagnostics (reset on every new embed and on dispose), and
archiveTimestamp combines its own collection diagnostics with the final
timestamp's. Caller revocationData is silently ignored when enableLTV
is false.
Session Pattern for Complex Workflows:
For advanced use cases, use the Session API for step-by-step control:
const session = new TimestampSession(pdfBytes, {
// enableLTV defaults to true. Set to false for manual/no-network scenarios.
enableLTV: true,
});
// Step 1: Generate request (can send to external TSA)
const request = await session.createTimestampRequest();
// Step 2: Send request via your preferred method
const response = await myCustomTSAFetch(request);
// Step 3: Validate the request-bound response, then embed candidate LTV material
const finalPdf = await session.embedTimestampToken(response);The session rejects a malformed, ambiguous, unbound, or cryptographically
invalid response before writing the PDF. A certReq=false manual response
requires external signer-certificate candidates through the session validation
options; the one-call timestampPdf API rejects that shape.
RFC Compliance:
The library implements or aims to support the following standards:
| RFC | Description |
|---|---|
| RFC 3161 | Time-Stamp Protocol (Core implementation) |
| RFC 5816 | ESSCertIDv2 (Supported via dependencies) |
| RFC 6960 | OCSP (Implemented for LTV) |
| RFC 5544 | TimeStampedData envelope (Implemented; see pdf-rfc3161/rfcs/rfc5544) |
| RFC 8933 | CMS Algorithm Identifier Protection (Implemented; see pdf-rfc3161/rfcs/rfc8933) |
| ETSI EN 319 142-1 V1.2.1 | DocTimeStamp and DSS/VRI structural interoperability baseline; not a general PAdES conformance claim |
Revocation & Chain Handling:
- OCSP/CRL: The library can collect structurally parsed Online Certificate Status Protocol (OCSP) and Certificate Revocation List (CRL) candidate bytes. It does not verify responder signatures, certificate paths, CertIDs, freshness, scope, or revocation trust for those network candidates.
- AIA: Authority Information Access (AIA) extensions can discover and fetch intermediate certificate candidates. Fetching them does not construct or trust a certificate chain on its own.
TrustStore validation:
For production chain validation, pass a caller-owned TrustStore with the
roots you accept to verifyTimestamp(). In 0.2.0, getDefaultTrustStore()
throws TimestampError with code STATE_ERROR while the curated bundled-root
list is empty; it does not provide an implicit TSA trust anchor or full
chain-validation policy:
import { verifyTimestamp, SimpleTrustStore } from "pdf-rfc3161";
const trustStore = new SimpleTrustStore();
trustStore.addCertificate(rootCaCert);
const verified = await verifyTimestamp(ts, {
trustStore,
pdf: pdfBytes, // enables the document-hash check
// requireTimestampingEKU and requireCertValidAtGenTime default to
// true since 0.2.0; strictESSValidation stays opt-in.
strictESSValidation: true,
});- Encrypted/password-protected PDFs are rejected by default (
PDF_ERROR). PassignoreEncryption: true(or the CLI--ignore-encryptionflag on the timestamp and verify commands) as an explicit opt-in to load them without decrypting, e.g. for diagnostic tooling. Password-based decryption is not supported. - The library creates document timestamps, not signature timestamps on existing signatures
- The signer expects a clean, structurally valid input PDF. It preserves existing bytes and is not a PDF sanitizer, repair tool, or hostile-file validation gateway.
- The official
pdf-lib-incremental-save@1.17.4loader has separate hostile-PDF resource and parsing limitations. Read pdf-lib-incremental-save limitations before broadening a deployment to untrusted input; use a resource-limited sandbox when hostile inputs are in scope.
The offline interoperability and packed-consumer gates complement unit tests. They use a local TSA/root and do not require a public TSA, Acrobat Reader, or a default trust anchor. On a clean Ubuntu 24.04 AMD64 runner, first install the locked Python and binary tools using the clean-run recipe, then run:
pnpm build
PYTHON=/tmp/pdf-rfc3161-pades-python/bin/python \
pnpm --filter pdf-rfc3161-tests test:interoperability
pnpm --filter pdf-rfc3161-tests test:packageFor the tool-role boundaries and license cautions, see validation-tools.md. For a later, reproducible Reader observation, use manual-acrobat-validation.md.
- Node.js 22.18.0 or later
- Modern browsers with Web Crypto API support
- Edge runtimes: Cloudflare Workers, Vercel Edge, Deno Deploy
See MIGRATION.md for breaking-change guidance between major and minor releases. The CHANGELOG tracks every release.
MIT