Handoff - 2026-09-03 - verify-inbrowser

Handoff - 2026-09-03 - verify-inbrowser

Goal: Make the /verify/ page do the verification itself (drop a PDF, get a result), and reframe its copy from “check whether it’s really me” to a provenance / “I stand behind my work” tone. Status: shipped (committed 9c8ffd2 + this handoff; push pending)

Context

Follows 2026-09-03-pdf-signing-verify-page.md. That page only told people how to verify (Acrobat / pyHanko / pdfsig — all need a tool install). This adds an in-browser checker so a non-technical recipient gets a one-click answer.

Decisions

  • pkijs + asn1js from jsdelivr at runtime, not vendored. The +esm bundles don’t inline their transitive deps (pvtsutils, asn1js), so vendoring turns into a fetch-the-whole-tree exercise with version skew. jsdelivr resolves it all in the browser; matches the existing MathJax/Mermaid pattern. verify-sig.js pins asn1js@3.0.5 — the exact version pkijs@3.2.4 bundles — so both share one module instance (pkijs does internal instanceof against asn1js classes).
  • Anshid-specific check (user chose this): hard-compares the signer cert’s SHA-256 to a constant. Also shows signer CN + signing date regardless.
  • Link moved off the site footer onto an About-page button (user request).
  • Node test keeps using the CDN via a custom module hook (tests/cdn-hook.mjs) that fetches each jsdelivr URL and rewrites its /npm/... sub-imports to absolute — Node dropped native https imports. No npm, no node_modules. Needs network to run.

Done

  • assets/js/verify-sig.jsverifyPdf(arrayBuffer){ok, checks[], signer, signedAt, reason}. ByteRange regex → reconstruct signed bytes → detached CMS SignedData.verify → whole-file-coverage check → signer-cert SHA-256 match. Constant ANSHID_CERT_SHA256 = 7d21c7f4…bf1e (same as the page + pyhanko output).
  • verify.md — copy rewritten (canvas-signature framing, dropped “confirm through a second channel” / “identity unknown” warnings, self-signed note is now a neutral one-time step). New “Check a document here” section with a drag/drop + file-input widget (module <script> inline in the page). File input is visually-hidden (not hidden) so it stays keyboard-focusable; <label for> drives click/keyboard, JS handles drag. <noscript> fallback. Cert block / fingerprints / manual steps unchanged (manual = power-user fallback).
  • tests/verify-sig.test.mjs + cdn-hook.mjs + register-hook.mjs. Covers: genuine PDF passes all 3, appended byte fails coverage only, flipped byte fails signature, different self-signed cert → intact but not Anshid, unsigned → no-sig reason. Run: node --import ./tests/register-hook.mjs tests/verify-sig.test.mjs <signed.pdf> <signed-other.pdf>
  • _includes/footer.html — removed “Verify Signatures”.
  • about.md — added “Verify My Documents” button (shield-check icon) after certifications.
  • _config.ymlexclude: tests, node_modules.
  • CLAUDE.md + AGENTS.md — updated the cert bullet (link now from About; refresh ANSHID_CERT_SHA256 on cert renewal too) + new verify-sig.js bullet.

State / deployed

  • Committed on main (9c8ffd2), not pushed. /verify/ widget is live only after push + Pages build.
  • Test passed locally against a real pyhanko-signed fixture (Anshid’s key) and a throwaway different-cert fixture. Fixtures were in scratchpad, not committed.
  • No local Jekyll (no Ruby) — verify.md Liquid/kramdown eyeballed against the prior version and certifications.md.

Open threads / next steps

  • Push, then verify at the live URL: drop a real signed course PDF → expect “✓ Signed by Anshid Aboobacker” + 3 green checks; check the <noscript> path, the About button, and that the footer no longer lists Verify.
  • No signed PDFs are committed in this repo yet (the assets/**/CH*.pdf slides predate signing). If Anshid re-signs and commits those, they become live test material for the widget.

Blockers / watch-outs

  • Cert expiry is deliberately not checked. After 2036 the widget will still show ✓ for a document signed while the cert was valid — correct behaviour for provenance (the file was genuinely released by Anshid). Don’t “fix” this.
  • On cert renewal: update assets/anshid-cert.pem / .cer, the fingerprints + dates in verify.md, and ANSHID_CERT_SHA256 in verify-sig.js.
  • verify-sig.js runtime-depends on jsdelivr being reachable. If jsdelivr is down the widget errors (caught → “Could not read this file”); the manual methods on the same page still work.
  • Widget assumes one signature (takes the last /ByteRange). Fine for Anshid’s single invisible sig; a multi-signed PDF would only report the last signer.