A pure JavaScript/TypeScript library for adding RFC 3161 trusted timestamps to PDF documents. Works in Node.js, Cloudflare Workers, Deno, and modern browsers without native dependencies.
RFC 3161 defines the Time-Stamp Protocol (TSP). It allows proving that data existed at a specific time by having a trusted third party (Time Stamping Authority) cryptographically sign the hash of the data along with a timestamp.
When embedded in a PDF as a Document Timestamp (DocTimeStamp):
- It proves the document existed at the timestamp time
- It can be verified by PDF readers like Adobe Acrobat
- It does not require a signing certificate from the user
- With LTV, it remains valid even after the TSA certificate expires
- RFC 3161 compliant implementation of the Time-Stamp Protocol
- Document timestamps using the DocTimeStamp (ETSI.RFC3161) format
- LTV (Long-Term Validation) support with certificate chain embedding
- 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
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-rfc3161import { 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,
},
reason: "Document archival",
location: "Server",
});Enable LTV to embed certificate chains. This allows timestamp validation even after the TSA certificates expire:
import { timestampPdf } from "pdf-rfc3161";
const result = await timestampPdf({
pdf: pdfBytes,
tsa: { url: "https://freetsa.org/tsr" },
enableLTV: true,
});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`);For long-term preservation of signed documents, use timestampPdfLTA. This fetches fresh revocation data and adds a final document timestamp:
import { timestampPdfLTA, KNOWN_TSA_URLS } from "pdf-rfc3161";
const result = await timestampPdfLTA({
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);
console.log(`Verified: ${verified.verified}`);
}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 | Enable Long-Term Validation (default: false) |
maxSize |
number |
No | Maximum PDF size in bytes (default: 250MB) |
signatureSize |
number |
No | Size reserved for token (default: 8192). Set to 0 for automatic. |
signatureFieldName |
string |
No | Custom field name (default: "Timestamp") |
reason |
string |
No | Reason for timestamping |
location |
string |
No | Location metadata |
contactInfo |
string |
No | Contact information |
omitModificationTime |
boolean |
No | Omit /M from signature dictionary |
optimizePlaceholder |
boolean |
No | Optimize signature size (default: false) |
Returns a TimestampResult with the timestamped PDF, timestamp info, and optional ltvData.
Note: When using LTV, signatureSize: 0 uses a 16KB default. Specify larger value 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 |
No | Trust store for certificate chain validation |
strictESSValidation |
boolean |
No | Enforce PAdES compliance |
The library includes KNOWN_TSA_URLS - a list of known TSA URLs for convenience.
Note: Usage is governed by providers' Terms and Conditions. FreeTSA uses a self-signed CA requiring manual root certificate installation.
A client-side demo is included in the demo/ folder. Run it with:
npm install
npm run 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 full LTV support.
Primary use cases:
- Adding timestamps to fresh documents
- Archiving documents with PAdES-LTA for indefinite validity
- Extracting and verifying timestamp structures
Verification scope:
The verifyTimestamp() function performs cryptographic integrity verification:
- The timestamp token is properly signed by the TSA
- The document hash matches what was timestamped
- The timestamp structure is valid
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:
// Use custom fetcher for testing
const mockFetcher = new MockFetcher();
mockFetcher.setOCSPResponse("http://ocsp.example.com", mockResponse);
// Use custom curl-based fetcher
import { CurlFetcher } from "pdf-rfc3161/pki/fetchers";
// 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],
},
});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: Embed response with full LTV
const finalPdf = await session.embedTimestampToken(response);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 (Planned) |
| ETSI 319 142-1 | PAdES baseline signatures (Planned) |
| RFC 6211 | CMS Algorithm Protect (Low Priority) |
Revocation & Chain Handling:
- OCSP/CRL: The library handles Online Certificate Status Protocol (OCSP) and Certificate Revocation Lists (CRL) for Long-Term Validation (LTV).
- AIA: Authority Information Access (AIA) extensions are actively used to discover and fetch missing intermediate certificates to construct the full trust chain.
TrustStore validation:
For production chain validation, pass a TrustStore to verifyTimestamp():
import { verifyTimestamp, SimpleTrustStore } from "pdf-rfc3161";
const trustStore = new SimpleTrustStore();
trustStore.addCertificate(rootCaCert);
const verified = await verifyTimestamp(ts, {
trustStore,
strictESSValidation: true,
});- Encrypted/password-protected PDFs are not supported (pdf-lib limitation)
- The library creates document timestamps, not signature timestamps on existing signatures
- Node.js 18.0.0 or later
- Modern browsers with Web Crypto API support
- Edge runtimes: Cloudflare Workers, Vercel Edge, Deno Deploy
MIT