pdf-signer

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/

What it does

Try it

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.

Privacy

Everything runs in the browser tab.

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.

Setup

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.

How signatures work here

Multi-party workflows

The two-supervisor thesis review case is the load-bearing use case. What actually works and what doesn’t:

Adobe compatibility notes

Verifying signatures

Three paths, all backed by the same CMS+chain code:

  1. In-app: open any PDF, verify runs automatically, per-signature badges appear on each widget.
  2. Public QR link: scan the QR on any signed page, land on the verifier URL, the vault decrypts client-side and the check runs. Non-technical recipient sees a plain green / yellow / red status; a first-visit help panel explains what each state means. Verification receipt (PDF) downloadable with a QR back to the live check.
  3. Independent tools: 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”.

Identity and vault storage

Development

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).

License

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.