Skip to content

arthursvpb/sign

Repository files navigation

av/sign

av/sign

Sign and verify PDFs with your own certificate, entirely in your browser. Nothing is uploaded.

React 19 + Vite Playwright e2e Client-side License: MIT

What it is · Architecture · Stack · Run · Honest limits

What it is

Pick a PDF, load your .pfx (PKCS#12) certificate, type its passphrase, optionally place a visible signature on the page, and download a signed PDF. Separately, drop in a signed PDF to see whether it was changed after signing and who the certificate claims the signer is.

The certificate and passphrase are read and used only in the browser tab. There is no server, so privacy is structural, not a promise: no endpoint exists that could receive a secret. Part of AV LABS.

Architecture

One static SPA, no backend. How the bytes are handled, end to end:

%%{init: {'theme':'base','themeVariables':{'edgeLabelBackground':'#f6f6f4','lineColor':'#9a9da3','fontFamily':'-apple-system, system-ui, Segoe UI, Roboto, sans-serif','fontSize':'13px'}}}%%
flowchart TB
  app(["av/sign · static web app, runs entirely in the browser"])
  subgraph browser["No network · the private key never leaves the page · pdf-lib + node-forge"]
    direction LR
    subgraph s["Sign"]
      direction TB
      si[("PDF + certificate (PKCS#12) + passphrase")]:::data
      s1["Reserve a signature placeholder:<br/>/ByteRange + empty /Contents"]
      s2["Hash the signed byte range (SHA-256)"]
      s3["Sign the hash with the private key:<br/>detached PKCS#7 / CMS"]
      s4["Embed the CMS into /Contents"]
      so[("Signed PDF")]:::data
      si --> s1 --> s2 --> s3 --> s4 --> so
    end
    subgraph v["Verify"]
      direction TB
      vi[("A signed PDF")]:::data
      v1["Read /ByteRange + the embedded CMS"]
      v2["Re-hash the byte range,<br/>compare to the signed digest"]
      v3["Verify the CMS against its certificate,<br/>read the signer fields"]
      v4["Verdict: intact / modified / appended<br/>(integrity, not trust)"]
      vi --> v1 --> v2 --> v3 --> v4
    end
  end
  app --> s
  app --> v
  classDef default fill:#ffffff,stroke:#cfd0d4,color:#0a0a0b;
  classDef data fill:#efeeec,stroke:#c4c5c9,color:#0a0a0b;
  style browser fill:#f6f6f4,stroke:#dcdcda,color:#6b6e76;
  style s fill:#fbfbfa,stroke:#e6e6e4,color:#0a0a0b;
  style v fill:#fbfbfa,stroke:#e6e6e4,color:#0a0a0b;
Loading

Decision record (goal, stack debate, spec, plan) in docs/.

Stack

  • React 19 + Vite 6 - static SPA, no server
  • Tailwind v4 + shadcn primitives, on the AV LABS token cascade
  • pdf-lib, @signpdf/*, node-forge - in-browser signing
  • pdf-signature-reader - in-browser verification
  • pdfjs-dist - page rendering for placement
  • TypeScript 5.7, Biome (format + lint)
  • Vitest (crypto round-trip) + Playwright (the e2e gate, against the production build)

Run

Needs Node 22+.

npm install
npm run dev        # http://localhost:5174
npm run typecheck
npm run lint        # Biome
npm test            # Vitest: sign/verify round-trip
npm run e2e         # Playwright against the production build
npm run build
npm run fixtures    # regenerate the non-confidential test PDFs

The signing path is proven by the Vitest round-trip (a real signature: digest match plus a valid RSA signature over the signed attributes) and by Playwright, which signs the bundled sample in a real browser and verifies the result reads as intact.

Honest limits

Local verification proves two things: the document has not changed since it was signed, and the signature is cryptographically valid for the certificate embedded in the file. It does not prove trust. It does not check the certificate against a trusted authority, does not check revocation (CRL/OCSP), and does not validate a trusted timestamp. A file can pass this check and still be signed with an untrusted, self-issued, expired, or revoked certificate. The app states this under every verification result and never renders a bare "valid" badge.

In Brazil, a signed PDF can also be confirmed on the official government validator (ICP-Brasil) at validar.iti.gov.br.

V1 scope is one user, one signature, sign and verify. Trusted timestamps, trust-chain and revocation validation, multi-signer flows, and any server are out of scope by design.

License

MIT (c) 2026 Arthur Vasconcellos.

Releases

No releases published

Packages

 
 
 

Contributors