A single-page browser tool that cryptographically signs PDFs, hosts encrypted verification vaults on your own GitHub repo, and reads back signatures anyone else’s copy of it can verify. No server, no upload, no vendor account. One HTML file, ~500 KB gzipped, runs in Firefox / Chrome / Safari.
Live app: https://nored.github.io/pdf-signer/web/
SubFilter /ETSI.CAdES.detached, hand-rolled SigningCertificateV2 with real ESSCertIDv2, populated issuerSerial, DER-sorted signedAttrs, RSA-SHA256).rfc3161.ai.moda by default; others accepted if they send CORS headers)./Fields and /Annots without touching prior object bodies, so DocMDP P=3 stays valid under strict Adobe verification).Open https://nored.github.io/pdf-signer/web/, generate an identity (name + password), drop a PDF, click Sign & Download. Signed file downloads to the same place your browser puts everything else. Nothing leaves the browser except an optional cert publish and vault upload, both to a GitHub repo you configure.
Long-press the theme toggle (◐, top-right) to see which commit your browser is on and whether it’s the latest.
Everything runs in the browser tab.
cdnjs.cloudflare.com on first page load (then browser-cached), an optional TSA countersignature request to the TSA URL you set (default rfc3161.ai.moda), an optional GitHub API call to publish your public cert and upload the encrypted vault (only if you configured a GitHub vault), and an optional Ghostscript-WASM download from cdn.jsdelivr.net (only if you turn on PDF normalization and open a source PDF that needs it).If any of that is a step too many, disable the vault, disable the trusted timestamp, and sign offline. The signature is still a valid PAdES-B-B CMS; only the QR verification round-trip goes away.
Zero install for the common case. For the recommended full experience, three optional steps:
1. Create your identity. Sidebar → New identity…, enter your name (goes into the cert CN and the visible signature), your email (goes into the cert subjectAltName), and a password of at least 6 characters. Generates a 2048-bit RSA keypair, self-signs a 10-year cert, encrypts the p12 with your password, and stores it in IndexedDB. Downloads an encrypted backup bundle (.pdfsigner.json) immediately. Keep that file somewhere safe; it’s the only way to move the identity to another browser.
Alternatively Import… an existing p12 from any CA (D-Trust, Bundesdruckerei, Certum, GlobalSign, sign-me, corporate CA). The importer walks the p12’s chain leaf → intermediates → root and self-trusts the root so it becomes visible to the verifier.
2. Configure a GitHub vault (optional). If you want the QR-based public verification, sidebar → GitHub vault → Configure…. You’ll need a personal access token with repo scope. The app creates a public repo named pdf-signer-vault (customizable), publishes your cert to vault/certs/<sha1>.pem, and uploads one encrypted .vault file per signed PDF. The signed PDF carries the raw HTTPS URL of the vault plus (in the QR) the AES key.
Without a vault, signing still works. Signed PDFs are valid; the verifier just needs the .vault file handed over locally (drag-and-drop) instead of fetched from a URL.
3. Turn on trusted timestamping (optional). Under Trusted timestamp (RFC 3161) in the signing sidebar, tick the checkbox. Default TSA is https://rfc3161.ai.moda (a Sectigo relay that accepts browser CORS). Every signed PDF then carries a countersigned timestamp that survives your certificate expiring.
4. Turn on PDF normalization (optional). Under PDF normalization in identity settings, tick Normalize source PDFs before signing if you regularly sign Office / Word / Google-Docs PDFs. Lazy-loads Ghostscript-WASM on first use (~5 MB gzipped, browser-cached thereafter, only fetched when a source PDF has the structural markers Adobe rejects post-sign).
Every setting is per-identity. Export the identity bundle and every setting travels with it.
/Sig dict carries the raw URL. Anyone verifying can fetch the cert over HTTPS and get transitive trust from the vault’s HTTPS chain, no manual trust setup..p12 import work too: full chain (leaf + intermediates + root) is walked, all intermediates go into the CMS, verifiers reach whatever root is in their store.adbe.pkcs7.detached in the legacy phrasing, ETSI.CAdES.detached in the SubFilter. Hand-rolled SignerInfo because node-forge.pkcs7 cannot emit SigningCertificateV2 correctly (silently produces an empty SET; Adobe strict-CAdES rejects that plus everything downstream).The two-supervisor thesis review case is the load-bearing use case. What actually works and what doesn’t:
pdfsig, EU-DSS verifysignature.eu, openssl cms -verify)./Version /1.4 in the catalog and ship a fragmented xref with free object slots. Adobe rejects our CAdES signature over those with “document has been modified” even though nothing was modified. Fix: enable Normalize source PDFs before signing in identity settings. Runs a proper PDF/A-2b conversion via Ghostscript-WASM (lazy-loaded from jsDelivr, ~5 MB gzipped, only fetched when a source actually needs it, browser-cached afterwards).rfc3161.ai.moda proxies to Sectigo (a real commercial TSA) and works from browsers. Adobe shows the trusted time; the verifier here shows it regardless.Three paths, all backed by the same CMS+chain code:
openssl cms -verify -inform DER -in signed.cms.der -content signed.range.bin -binary and poppler pdfsig signed.pdf both accept every signature this tool produces. verifysignature.eu (EU-DSS reference validator) also accepts and prints “Integrity: Preserved”..pdfsigner.json (encrypted bundle: p12 + signature image + settings + optional GitHub config).vault/<hash>.vault. QR carries the key; only the QR-holder can decrypt.vault/certs/<fingerprint>.pem. Cert itself is not sensitive; anyone can fetch and verify against it.Single file. Open web/index.html in a browser. Any change reloads instantly.
git clone https://github.com/nored/pdf-signer
cd pdf-signer
python3 -m http.server 8000 # or `npx serve`, whatever
open http://localhost:8000/web/
Runtime deps loaded from CDN (cdnjs and jsdelivr): pdf.js (Mozilla), pdf-lib (Hopding), node-forge (Digital Bazaar), qrcode-generator (Kazuhiko Arase), optionally @okathira/ghostpdl-wasm (Artifex GhostPDL via jsdelivr, only if the user enables PDF normalization).
Node-side tests in scratchpad/test/ cover the load-bearing crypto paths (incremental signing byte-preserving splice, redaction round-trip, hand-rolled SignerInfo, DSS/OCSP embedding, PDF/A normalization end-to-end).
pdf-signer itself: MIT (see LICENSE).
Runtime-only optional dependency Ghostscript-WASM is AGPL-3.0-or-later (upstream Artifex build, unmodified, fetched by the user’s browser directly from jsDelivr when the PDF normalization toggle is on). pdf-signer does not bundle, distribute, or modify it. See NOTICE.