feat: add Timestamp PDF tool with RFC 3161 support
Add document timestamping to the Secure PDF section using RFC 3161 protocol. Users can select from preset TSA servers (DigiCert, Sectigo, SSL.com, Entrust, FreeTSA) or enter a custom TSA URL. No personal certificate is required — only a cryptographic hash is sent to the server. Key changes: - Timestamp PDF page with TSA server selector, FAQ and SEO structured data - timestampPdf() function with CORS proxy URL resolution - TimestampNode for the workflow engine - Tool entry in Secure PDF category + homepage i18n - Built-in CORS proxy middleware for dev/preview - Translations for all 16 languages Tested with DigiCert, Sectigo and Entrust TSA servers. Timestamps are verifiable in Adobe Acrobat (ETSI.RFC3161 SubFilter).
This commit is contained in:
14
src/js/config/timestamp-tsa.ts
Normal file
14
src/js/config/timestamp-tsa.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface TimestampTsaPreset {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Some TSA providers only expose HTTP endpoints. RFC 3161 timestamp tokens are
|
||||
// signed at the application layer, so integrity does not depend solely on TLS.
|
||||
export const TIMESTAMP_TSA_PRESETS: TimestampTsaPreset[] = [
|
||||
{ label: 'DigiCert', url: 'http://timestamp.digicert.com' },
|
||||
{ label: 'Sectigo', url: 'http://timestamp.sectigo.com' },
|
||||
{ label: 'SSL.com', url: 'http://ts.ssl.com' },
|
||||
{ label: 'FreeTSA', url: 'https://freetsa.org/tsr' },
|
||||
{ label: 'MeSign', url: 'http://tsa.mesign.com' },
|
||||
];
|
||||
@@ -785,6 +785,13 @@ const baseCategories = [
|
||||
icon: 'ph-seal-check',
|
||||
subtitle: 'Verify digital signatures and view certificate details.',
|
||||
},
|
||||
{
|
||||
href: import.meta.env.BASE_URL + 'timestamp-pdf.html',
|
||||
name: 'Timestamp PDF',
|
||||
icon: 'ph-clock',
|
||||
subtitle:
|
||||
'Add an RFC 3161 document timestamp using a trusted TSA server.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -153,6 +153,20 @@ async function generateProxySignature(
|
||||
.join('');
|
||||
}
|
||||
|
||||
async function buildCorsProxyUrl(url: string): Promise<string> {
|
||||
let proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
|
||||
|
||||
if (!CORS_PROXY_SECRET) {
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const signature = await generateProxySignature(url, timestamp);
|
||||
proxyUrl += `&t=${timestamp}&sig=${signature}`;
|
||||
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom fetch wrapper that routes external certificate requests through a CORS proxy.
|
||||
* The zgapdfsigner library tries to fetch issuer certificates from URLs embedded in the
|
||||
@@ -192,20 +206,30 @@ function createCorsAwareFetch(): {
|
||||
url.includes('caIssuers')) &&
|
||||
!url.startsWith(window.location.origin);
|
||||
|
||||
if (isExternalCertificateUrl && CORS_PROXY_URL) {
|
||||
let proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
|
||||
const isTsaRequest =
|
||||
(init?.headers &&
|
||||
typeof init.headers === 'object' &&
|
||||
'Content-Type' in init.headers &&
|
||||
(init.headers as Record<string, string>)['Content-Type'] ===
|
||||
'application/timestamp-query') ||
|
||||
url.includes('timestamp') ||
|
||||
url.includes('/tsa') ||
|
||||
url.includes('/tsr') ||
|
||||
url.includes('/ts01') ||
|
||||
url.includes('RFC3161');
|
||||
|
||||
const shouldProxy =
|
||||
(isExternalCertificateUrl || isTsaRequest) &&
|
||||
!url.startsWith(window.location.origin);
|
||||
|
||||
if (shouldProxy && CORS_PROXY_URL) {
|
||||
const proxyUrl = await buildCorsProxyUrl(url);
|
||||
if (CORS_PROXY_SECRET) {
|
||||
const timestamp = Date.now();
|
||||
const signature = await generateProxySignature(url, timestamp);
|
||||
proxyUrl += `&t=${timestamp}&sig=${signature}`;
|
||||
console.log(
|
||||
`[CORS Proxy] Routing signed certificate request through proxy: ${url}`
|
||||
`[CORS Proxy] Routing signed request through proxy: ${url}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`[CORS Proxy] Routing certificate request through proxy: ${url}`
|
||||
);
|
||||
console.log(`[CORS Proxy] Routing request through proxy: ${url}`);
|
||||
}
|
||||
|
||||
return originalFetch(proxyUrl, init);
|
||||
@@ -302,6 +326,34 @@ export async function signPdf(
|
||||
}
|
||||
}
|
||||
|
||||
export async function timestampPdf(
|
||||
pdfBytes: Uint8Array,
|
||||
tsaUrl: string
|
||||
): Promise<Uint8Array> {
|
||||
let effectiveUrl = tsaUrl;
|
||||
|
||||
if (CORS_PROXY_URL) {
|
||||
effectiveUrl = await buildCorsProxyUrl(tsaUrl);
|
||||
|
||||
if (CORS_PROXY_SECRET) {
|
||||
console.log(
|
||||
`[Timestamp] Routing signed TSA request through proxy: ${tsaUrl}`
|
||||
);
|
||||
} else {
|
||||
console.log(`[Timestamp] Routing TSA request through proxy: ${tsaUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
const signOptions: SignOption = {
|
||||
signdate: { url: effectiveUrl },
|
||||
};
|
||||
|
||||
const signer = new PdfSigner(signOptions);
|
||||
|
||||
const timestampedPdfBytes = await signer.sign(pdfBytes);
|
||||
return new Uint8Array(timestampedPdfBytes);
|
||||
}
|
||||
|
||||
export function getCertificateInfo(certificate: forge.pki.Certificate): {
|
||||
subject: string;
|
||||
issuer: string;
|
||||
|
||||
257
src/js/logic/timestamp-pdf-page.ts
Normal file
257
src/js/logic/timestamp-pdf-page.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import {
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
downloadFile,
|
||||
getPDFDocument,
|
||||
} from '../utils/helpers.js';
|
||||
import { TIMESTAMP_TSA_PRESETS } from '../config/timestamp-tsa.js';
|
||||
import { timestampPdf } from './digital-sign-pdf.js';
|
||||
|
||||
interface TimestampState {
|
||||
pdfFile: File | null;
|
||||
pdfBytes: Uint8Array | null;
|
||||
}
|
||||
|
||||
const state: TimestampState = {
|
||||
pdfFile: null,
|
||||
pdfBytes: null,
|
||||
};
|
||||
|
||||
function getElement<T extends HTMLElement>(id: string): T | null {
|
||||
return document.getElementById(id) as T | null;
|
||||
}
|
||||
|
||||
function resetState(): void {
|
||||
state.pdfFile = null;
|
||||
state.pdfBytes = null;
|
||||
|
||||
const fileDisplayArea = getElement<HTMLDivElement>('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const fileInput = getElement<HTMLInputElement>('file-input');
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const tsaSection = getElement<HTMLDivElement>('tsa-section');
|
||||
if (tsaSection) tsaSection.classList.add('hidden');
|
||||
|
||||
updateProcessButton();
|
||||
}
|
||||
|
||||
function initializePage(): void {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = getElement<HTMLInputElement>('file-input');
|
||||
const dropZone = getElement<HTMLDivElement>('drop-zone');
|
||||
const processBtn = getElement<HTMLButtonElement>('process-btn');
|
||||
const backBtn = getElement<HTMLButtonElement>('back-to-tools');
|
||||
const tsaPreset = getElement<HTMLSelectElement>('tsa-preset');
|
||||
|
||||
populateTsaPresets(tsaPreset);
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handlePdfUpload);
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
handlePdfFile(droppedFiles[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', processTimestamp);
|
||||
}
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function populateTsaPresets(tsaPreset: HTMLSelectElement | null): void {
|
||||
if (!tsaPreset) return;
|
||||
|
||||
tsaPreset.replaceChildren();
|
||||
|
||||
for (const preset of TIMESTAMP_TSA_PRESETS) {
|
||||
const option = document.createElement('option');
|
||||
option.value = preset.url;
|
||||
option.textContent = preset.label;
|
||||
tsaPreset.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePdfUpload(e: Event): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
handlePdfFile(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePdfFile(file: File): Promise<void> {
|
||||
if (
|
||||
file.type !== 'application/pdf' &&
|
||||
!file.name.toLowerCase().endsWith('.pdf')
|
||||
) {
|
||||
showAlert('Invalid File', 'Please select a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
state.pdfFile = file;
|
||||
state.pdfBytes = new Uint8Array(
|
||||
(await readFileAsArrayBuffer(file)) as ArrayBuffer
|
||||
);
|
||||
|
||||
updatePdfDisplay();
|
||||
showTsaSection();
|
||||
updateProcessButton();
|
||||
}
|
||||
|
||||
async function updatePdfDisplay(): Promise<void> {
|
||||
const fileDisplayArea = getElement<HTMLDivElement>('file-display-area');
|
||||
|
||||
if (!fileDisplayArea || !state.pdfFile) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = state.pdfFile.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • Loading pages...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.pdfFile = null;
|
||||
state.pdfBytes = null;
|
||||
fileDisplayArea.innerHTML = '';
|
||||
hideTsaSection();
|
||||
updateProcessButton();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
if (state.pdfBytes) {
|
||||
const pdfDoc = await getPDFDocument({
|
||||
data: state.pdfBytes.slice(),
|
||||
}).promise;
|
||||
metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${pdfDoc.numPages} pages`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(state.pdfFile.size)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function showTsaSection(): void {
|
||||
const tsaSection = getElement<HTMLDivElement>('tsa-section');
|
||||
if (tsaSection) {
|
||||
tsaSection.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideTsaSection(): void {
|
||||
const tsaSection = getElement<HTMLDivElement>('tsa-section');
|
||||
if (tsaSection) {
|
||||
tsaSection.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updateProcessButton(): void {
|
||||
const processBtn = getElement<HTMLButtonElement>('process-btn');
|
||||
if (!processBtn) return;
|
||||
|
||||
if (state.pdfBytes) {
|
||||
processBtn.classList.remove('hidden');
|
||||
processBtn.disabled = false;
|
||||
} else {
|
||||
processBtn.classList.add('hidden');
|
||||
processBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getTsaUrl(): string | null {
|
||||
const tsaPreset = getElement<HTMLSelectElement>('tsa-preset');
|
||||
if (!tsaPreset) return null;
|
||||
|
||||
return tsaPreset.value;
|
||||
}
|
||||
|
||||
async function processTimestamp(): Promise<void> {
|
||||
if (!state.pdfBytes || !state.pdfFile) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const tsaUrl = getTsaUrl();
|
||||
if (!tsaUrl) return;
|
||||
|
||||
showLoader('Applying timestamp...');
|
||||
|
||||
try {
|
||||
const timestampedBytes = await timestampPdf(state.pdfBytes, tsaUrl);
|
||||
|
||||
const outputFilename = state.pdfFile.name.replace(
|
||||
/\.pdf$/i,
|
||||
'_timestamped.pdf'
|
||||
);
|
||||
const blob = new Blob([new Uint8Array(timestampedBytes)], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
downloadFile(blob, outputFilename);
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF timestamped successfully! The timestamp can be verified in Adobe Acrobat and other PDF readers.'
|
||||
);
|
||||
|
||||
resetState();
|
||||
} catch (error) {
|
||||
console.error('Timestamp error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
showAlert(
|
||||
'Timestamp Failed',
|
||||
`Failed to timestamp PDF: ${message}\n\nPlease try a different TSA server or check your internet connection.`
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
@@ -214,6 +214,7 @@ const init = async () => {
|
||||
'Deskew PDF': 'tools:deskewPdf',
|
||||
'Digital Signature': 'tools:digitalSignPdf',
|
||||
'Validate Signature': 'tools:validateSignaturePdf',
|
||||
'Timestamp PDF': 'tools:timestampPdf',
|
||||
'Scanner Effect': 'tools:scannerEffect',
|
||||
'Adjust Colors': 'tools:adjustColors',
|
||||
'Markdown to PDF': 'tools:markdownToPdf',
|
||||
|
||||
@@ -35,6 +35,7 @@ import { SanitizeNode } from './sanitize-node';
|
||||
import { EncryptNode } from './encrypt-node';
|
||||
import { DecryptNode } from './decrypt-node';
|
||||
import { DigitalSignNode } from './digital-sign-node';
|
||||
import { TimestampNode } from './timestamp-node';
|
||||
import { RedactNode } from './redact-node';
|
||||
import { RepairNode } from './repair-node';
|
||||
import { PdfToTextNode } from './pdf-to-text-node';
|
||||
@@ -509,6 +510,13 @@ export const nodeRegistry: Record<string, NodeRegistryEntry> = {
|
||||
description: 'Apply a digital signature to PDF',
|
||||
factory: () => new DigitalSignNode(),
|
||||
},
|
||||
TimestampNode: {
|
||||
label: 'Timestamp',
|
||||
category: 'Secure PDF',
|
||||
icon: 'ph-clock',
|
||||
description: 'Add an RFC 3161 document timestamp',
|
||||
factory: () => new TimestampNode(),
|
||||
},
|
||||
RedactNode: {
|
||||
label: 'Redact',
|
||||
category: 'Secure PDF',
|
||||
|
||||
60
src/js/workflow/nodes/timestamp-node.ts
Normal file
60
src/js/workflow/nodes/timestamp-node.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ClassicPreset } from 'rete';
|
||||
import { BaseWorkflowNode } from './base-node';
|
||||
import { pdfSocket } from '../sockets';
|
||||
import type { SocketData } from '../types';
|
||||
import { requirePdfInput, processBatch } from '../types';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { TIMESTAMP_TSA_PRESETS } from '../../config/timestamp-tsa.js';
|
||||
import { timestampPdf } from '../../logic/digital-sign-pdf.js';
|
||||
|
||||
export class TimestampNode extends BaseWorkflowNode {
|
||||
readonly category = 'Secure PDF' as const;
|
||||
readonly icon = 'ph-clock';
|
||||
readonly description = 'Add an RFC 3161 document timestamp';
|
||||
|
||||
constructor() {
|
||||
super('Timestamp');
|
||||
this.addInput('pdf', new ClassicPreset.Input(pdfSocket, 'PDF'));
|
||||
this.addOutput(
|
||||
'pdf',
|
||||
new ClassicPreset.Output(pdfSocket, 'Timestamped PDF')
|
||||
);
|
||||
this.addControl(
|
||||
'tsaUrl',
|
||||
new ClassicPreset.InputControl('text', {
|
||||
initial: TIMESTAMP_TSA_PRESETS[0].url,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getTsaPresets(): { label: string; url: string }[] {
|
||||
return TIMESTAMP_TSA_PRESETS;
|
||||
}
|
||||
|
||||
async data(
|
||||
inputs: Record<string, SocketData[]>
|
||||
): Promise<Record<string, SocketData>> {
|
||||
const pdfInputs = requirePdfInput(inputs, 'Timestamp');
|
||||
|
||||
const tsaUrlCtrl = this.controls['tsaUrl'] as
|
||||
| ClassicPreset.InputControl<'text'>
|
||||
| undefined;
|
||||
const tsaUrl = tsaUrlCtrl?.value || TIMESTAMP_TSA_PRESETS[0].url;
|
||||
|
||||
return {
|
||||
pdf: await processBatch(pdfInputs, async (input) => {
|
||||
const timestampedBytes = await timestampPdf(input.bytes, tsaUrl);
|
||||
|
||||
const bytes = new Uint8Array(timestampedBytes);
|
||||
const document = await PDFDocument.load(bytes);
|
||||
|
||||
return {
|
||||
type: 'pdf',
|
||||
document,
|
||||
bytes,
|
||||
filename: input.filename.replace(/\.pdf$/i, '_timestamped.pdf'),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
492
src/pages/timestamp-pdf.html
Normal file
492
src/pages/timestamp-pdf.html
Normal file
@@ -0,0 +1,492 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>
|
||||
Timestamp PDF Online Free - RFC 3161 Document Timestamp | BentoPDF
|
||||
</title>
|
||||
<meta
|
||||
name="title"
|
||||
content="Timestamp PDF Online Free - RFC 3161 Document Timestamp | BentoPDF"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="★ Add RFC 3161 document timestamp to PDF online free ★ Trusted TSA servers ★ No certificate required ★ Privacy-first ★ Works in browser"
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="timestamp pdf, rfc 3161, tsa server, document timestamp, time stamp authority, pdf timestamp"
|
||||
/>
|
||||
<meta name="author" content="BentoPDF" />
|
||||
<meta
|
||||
name="robots"
|
||||
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
|
||||
/>
|
||||
|
||||
<link rel="canonical" href="https://www.bentopdf.com/timestamp-pdf.html" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://www.bentopdf.com/timestamp-pdf" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Timestamp PDF Online Free - RFC 3161 Document Timestamp | BentoPDF"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="★ Add RFC 3161 document timestamp to PDF online free ★ Trusted TSA servers ★ No certificate required ★ Privacy-first ★ Works in browser"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://www.bentopdf.com/images/og-timestamp-pdf.png"
|
||||
/>
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:site_name" content="BentoPDF" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://www.bentopdf.com/timestamp-pdf" />
|
||||
<meta name="twitter:title" content="Timestamp PDF Free" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="★ Add RFC 3161 document timestamp to PDF online free ★ Trusted TSA servers ★ Privacy-first"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://www.bentopdf.com/images/twitter-timestamp-pdf.png"
|
||||
/>
|
||||
<meta name="twitter:site" content="@BentoPDF" />
|
||||
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="Timestamp PDF" />
|
||||
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="192x192"
|
||||
href="/images/favicon-192x192.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="512x512"
|
||||
href="/images/favicon-512x512.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/images/apple-touch-icon.png"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
{{> navbar }}
|
||||
|
||||
<div
|
||||
id="uploader"
|
||||
class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900"
|
||||
>
|
||||
<div
|
||||
id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700"
|
||||
>
|
||||
<button
|
||||
id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
|
||||
>
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools">
|
||||
Back to Tools
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<h1
|
||||
class="text-2xl font-bold text-white mb-2"
|
||||
data-i18n="tools:timestampPdf.name"
|
||||
>
|
||||
Timestamp PDF
|
||||
</h1>
|
||||
<p class="text-gray-400 mb-4" data-i18n="tools:timestampPdf.subtitle">
|
||||
Add an RFC 3161 document timestamp to your PDF using a trusted Time
|
||||
Stamp Authority (TSA) server. Proves your document existed at a
|
||||
specific point in time. No certificate required.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="bg-blue-900/30 border border-blue-700 rounded-lg p-3 mb-6 flex items-start gap-3"
|
||||
>
|
||||
<i
|
||||
data-lucide="wifi"
|
||||
class="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5"
|
||||
></i>
|
||||
<div class="text-sm">
|
||||
<p class="text-blue-300 font-medium">
|
||||
Internet connection required
|
||||
</p>
|
||||
<p class="text-blue-400/80 mt-1">
|
||||
Timestamping requires contacting the selected TSA server to obtain
|
||||
a trusted timestamp token.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="drop-zone"
|
||||
class="relative flex flex-col items-center justify-center w-full h-48 md:h-64 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<i
|
||||
data-lucide="upload-cloud"
|
||||
class="w-10 h-10 mb-3 text-gray-400"
|
||||
></i>
|
||||
<p class="mb-2 text-sm text-gray-400">
|
||||
<span class="font-semibold" data-i18n="upload.clickToSelect"
|
||||
>Click to select a file</span
|
||||
>
|
||||
<span data-i18n="upload.orDragAndDrop">or drag and drop</span>
|
||||
</p>
|
||||
<p
|
||||
class="text-xs text-gray-500"
|
||||
data-i18n="upload.hints.pdfDocuments"
|
||||
>
|
||||
PDF Documents
|
||||
</p>
|
||||
<p class="text-xs text-gray-500" data-i18n="upload.filesNeverLeave">
|
||||
Your files never leave your device.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept="application/pdf"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="tsa-section" class="hidden mt-6 space-y-4">
|
||||
<h3
|
||||
class="text-lg font-semibold text-white"
|
||||
data-i18n="tools:timestampPdf.tsaSectionTitle"
|
||||
>
|
||||
Timestamp Server (TSA)
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="tsa-preset"
|
||||
class="block text-sm font-medium text-gray-300 mb-1"
|
||||
data-i18n="tools:timestampPdf.selectTsa"
|
||||
>
|
||||
Select a TSA server
|
||||
</label>
|
||||
<select
|
||||
id="tsa-preset"
|
||||
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="process-btn"
|
||||
class="hidden w-full mt-6 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2"
|
||||
disabled
|
||||
>
|
||||
<i data-lucide="clock" class="w-5 h-5"></i>
|
||||
<span data-i18n="tools:timestampPdf.applyTimestamp"
|
||||
>Apply Timestamp</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="loader-overlay"
|
||||
class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-400 border-t-transparent mb-4"
|
||||
></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="alert-overlay"
|
||||
class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800 rounded-xl shadow-xl p-6 max-w-md w-full mx-4 border border-gray-700"
|
||||
>
|
||||
<h3
|
||||
id="alert-title"
|
||||
class="text-xl font-bold text-white mb-2"
|
||||
data-i18n="alert.title"
|
||||
>
|
||||
Alert
|
||||
</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button
|
||||
id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="max-w-4xl mx-auto px-4 py-12">
|
||||
<h2
|
||||
class="text-2xl md:text-3xl font-bold text-white mb-8 text-center"
|
||||
data-i18n="howItWorks.title"
|
||||
>
|
||||
How It Works
|
||||
</h2>
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Upload PDF</h3>
|
||||
<p class="text-gray-400">
|
||||
Select the PDF document you want to timestamp
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">
|
||||
Choose TSA Server
|
||||
</h3>
|
||||
<p class="text-gray-400">Select one of the trusted TSA servers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center text-white font-bold"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">
|
||||
Timestamp & Download
|
||||
</h3>
|
||||
<p class="text-gray-400">
|
||||
Apply the RFC 3161 timestamp and download your timestamped PDF
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-6xl mx-auto px-4 py-12">
|
||||
<h2
|
||||
class="text-2xl md:text-3xl font-bold text-white mb-6 text-center"
|
||||
data-i18n="relatedTools.title"
|
||||
>
|
||||
Related PDF Tools
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<a
|
||||
href="digital-sign-pdf.html"
|
||||
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
|
||||
>
|
||||
<h3 class="text-white font-semibold mb-1">Digital Signature</h3>
|
||||
<p class="text-gray-400 text-sm">Sign with X.509 certificate</p>
|
||||
</a>
|
||||
<a
|
||||
href="validate-signature-pdf.html"
|
||||
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
|
||||
>
|
||||
<h3 class="text-white font-semibold mb-1">Validate Signature</h3>
|
||||
<p class="text-gray-400 text-sm">Verify digital signatures</p>
|
||||
</a>
|
||||
<a
|
||||
href="encrypt-pdf.html"
|
||||
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
|
||||
>
|
||||
<h3 class="text-white font-semibold mb-1">Encrypt PDF</h3>
|
||||
<p class="text-gray-400 text-sm">Password protect your PDF</p>
|
||||
</a>
|
||||
<a
|
||||
href="flatten-pdf.html"
|
||||
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
|
||||
>
|
||||
<h3 class="text-white font-semibold mb-1">Flatten PDF</h3>
|
||||
<p class="text-gray-400 text-sm">Make annotations permanent</p>
|
||||
</a>
|
||||
<a
|
||||
href="sanitize-pdf.html"
|
||||
class="block bg-gray-800 p-4 rounded-lg hover:bg-gray-700 transition-colors border border-gray-700"
|
||||
>
|
||||
<h3 class="text-white font-semibold mb-1">Sanitize PDF</h3>
|
||||
<p class="text-gray-400 text-sm">Remove hidden data</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-4xl mx-auto px-4 py-12">
|
||||
<h2
|
||||
class="text-2xl md:text-3xl font-bold text-white mb-6 text-center"
|
||||
data-i18n="faq.sectionTitle"
|
||||
>
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
|
||||
<summary
|
||||
class="cursor-pointer font-semibold text-white flex items-center justify-between"
|
||||
>
|
||||
What is a document timestamp?
|
||||
<i data-lucide="chevron-down" class="w-5 h-5"></i>
|
||||
</summary>
|
||||
<p class="mt-3 text-gray-400">
|
||||
A document timestamp (RFC 3161) is a cryptographic proof that your
|
||||
PDF existed at a specific point in time. A trusted Time Stamp
|
||||
Authority (TSA) server signs a hash of your document along with the
|
||||
current time, creating a tamper-evident timestamp embedded in the
|
||||
PDF.
|
||||
</p>
|
||||
</details>
|
||||
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
|
||||
<summary
|
||||
class="cursor-pointer font-semibold text-white flex items-center justify-between"
|
||||
>
|
||||
Do I need a certificate?
|
||||
<i data-lucide="chevron-down" class="w-5 h-5"></i>
|
||||
</summary>
|
||||
<p class="mt-3 text-gray-400">
|
||||
No. Unlike a digital signature, a document timestamp does not
|
||||
require a personal certificate. The TSA server provides the trusted
|
||||
timestamp using its own certificate.
|
||||
</p>
|
||||
</details>
|
||||
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
|
||||
<summary
|
||||
class="cursor-pointer font-semibold text-white flex items-center justify-between"
|
||||
>
|
||||
Is my document sent to the TSA server?
|
||||
<i data-lucide="chevron-down" class="w-5 h-5"></i>
|
||||
</summary>
|
||||
<p class="mt-3 text-gray-400">
|
||||
No. Only a cryptographic hash (SHA-256) of your document is sent to
|
||||
the TSA server. Your actual PDF content never leaves your browser.
|
||||
</p>
|
||||
</details>
|
||||
<details class="bg-gray-800 p-5 rounded-lg border border-gray-700">
|
||||
<summary
|
||||
class="cursor-pointer font-semibold text-white flex items-center justify-between"
|
||||
>
|
||||
Will the timestamp be valid in PDF readers?
|
||||
<i data-lucide="chevron-down" class="w-5 h-5"></i>
|
||||
</summary>
|
||||
<p class="mt-3 text-gray-400">
|
||||
Yes. The tool creates a standard RFC 3161 document timestamp
|
||||
(ETSI.RFC3161 SubFilter) that is recognized by Adobe Acrobat and
|
||||
other major PDF viewers.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{> footer }}
|
||||
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/timestamp-pdf-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Timestamp PDF - BentoPDF",
|
||||
"applicationCategory": "PDF Tool",
|
||||
"operatingSystem": "Any - Web Browser",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.9",
|
||||
"ratingCount": "1850"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
"name": "How to add a timestamp to PDF",
|
||||
"description": "Learn how to add an RFC 3161 document timestamp to PDF using BentoPDF",
|
||||
"step": [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"position": 1,
|
||||
"name": "Upload PDF",
|
||||
"text": "Select the PDF document you want to timestamp"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"position": 2,
|
||||
"name": "Choose TSA Server",
|
||||
"text": "Select one of the trusted TSA servers"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"position": 3,
|
||||
"name": "Timestamp & Download",
|
||||
"text": "Apply the RFC 3161 timestamp and download your timestamped PDF"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://www.bentopdf.com"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Timestamp PDF",
|
||||
"item": "https://www.bentopdf.com/timestamp-pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user