Use case: PAdES B-B PDF signing (pdf-lib + WebCrypto) #11
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
EduarD3V/AutoFirme#11
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Use case: sign PDF producing PAdES B-B (basic), byte-range signature with placeholder, visible or invisible.
Reference: decompiled/sources/es/gob/afirma/signers/pades/ (24 classes, iText-based). Browser: pdf-lib for byte-range + AcroForm placeholder, WebCrypto PKCS1/RSA or ECDSA raw, wrap CMS per #6.
Scope limit: B-B only. No LTV/DSS/timestamp server this iteration.
Acceptance:
qpdf --checkDepends on #6.
UNBLOCK NOTICE: dependency policy relaxed — do not wait for #5 to merge. Work against this interface contract (web/src/lib/certs.ts, being finalized in #5):
Until #5 lands: mock LoadedCert in your branch (test RSA/EC key + self-signed cert generated at test time via openssl/node:crypto — commit no binaries). Test fixtures for golden vectors come from reference implementations (openssl for CAdES, xmlsec1 for XAdES) or generated by scripts at test setup. Resolve merge conflicts vs bullet-develop at integration; keep web/src/lib/certs.ts imports behind a thin adapter if the API shifts.
bullet: coder launched on branch
issue/11-use-case-pades-b-b-pdf-signing(modelnan/deepseek-v4-flash).Claimed — branch
issue/11-use-case-pades-b-b-pdf-signing. Doing: browser PAdES B-B engine (CMS SignedData + real PDF incremental update) inweb/src/lib/pades.tswith openssl-verified fixture tests.Issue #11 — PAdES B-B PDF signing (pdf-lib + WebCrypto)
Restated goal
Ship a browser-side PAdES baseline B-B PDF signing engine in
web/: given the original PDFbytes plus a signing certificate (DER), its private key as a WebCrypto
CryptoKey, it produces anew PDF whose original bytes are preserved verbatim (signature added as a real PDF
incremental update appended after the input, xref + trailer +
%%EOF), whose AcroForm gains a/Sigfield whose/Vis a signature dictionary carrying/ByteRange+/Contents, where/Contentsholds a CMSSignedData(detached, eContentTypeid-data, signed attributescontent-type + message-digest + signing-time + signing-certificate-v2 → genuinely PAdES B-B,
/SubFilter /ETSI.CAdES.detached). Visible (drawn appearance stream) and invisible widgets bothsupported. "Done" = a golden fixture PDF signed by this engine verifies with an independent
verifier (
openssl cms -verifyin the test; plusqpdf-class structure check + pyhanko PAdESvalidation run by hand) and the committed fixture is byte-reproducible from the committed source
fixture.
Dependency reality (documented, not hidden)
Issue says "wrap CMS per #6". #6's branch (
issue/6-use-case-cades-sign-in-browser) has nocommits — its tip is still #5's
27d86a5, andweb/src/lib/cms.tsdoes not exist anywhere inthe repo. #5's work (asn1.ts/keystore.ts/x509.ts) is also not in this worktree's base
(
6916892=main). So: no cross-branch imports; this branch carries a minimal self-containedDER writer/reader (
web/src/lib/der.ts) and the PAdES engine (web/src/lib/pades.ts). UI wiringis not possible here either (no cert loading in this base —
CertPickeris inert), so theengine + tests + fixture is the deliverable, and
pades.tsis the seam the UI uses once #5/#6land. Report flags the future de-duplication of
der.tsvs #5'sasn1.ts.Scope
Will change (all under
web/):web/src/lib/der.ts(new) — minimal DER encoder + TLV reader (cert serial/issuer, ECDSA raw→DER).web/src/lib/pades.ts(new) —signPdf(): CMSSignedDataassembly + PDF incremental update.web/src/lib/pades.test.ts(new) — golden + openssl-verified tests (RSA, ECDSA, visible, fixture).web/test/fixtures/*(new, binaries+keys, test-only):pades-source.pdf,pades-b-b-signed.pdf,pades-signer.crt/.key(RSA-2048),pades-signer-ec.crt/.key(P-256).web/package.json/web/pnpm-lock.yaml— addpdf-lib(already added, required by the issue).web/README.md— short PAdES section (module, fixture, verification command).Will not touch:
decompiled/,decompiled-configurador/,docs/,Containerfile,web/src/lib/App.svelte,web/src/lib/components/*,web/src/lib/i18n.ts,web/src/lib/sign.ts,web/src/lib/App.test.ts, other issue branches.Ordered tasks
web/src/lib/der.ts— encoder (seq/set/oid/octet/int/bitString/utf8/utcTime/ctx/setOf) +reader (
readTlv,children) +certIssuerAndSerial(certDer)+ecdsaRawToDer.Verify:
cd web && pnpm exec vitest run src/lib/pades.test.ts -t der(added in task 4).web/src/lib/pades.ts—signPdf(pdfBytes, opts); CMS builder (signed attrs, ESSCertIDv2);incremental-update writer (redefine page + AcroForm/catalog, append sig dict + widget, classic
xref subsection(s),
/Prevfrom originalstartxref); ByteRange patching; Contents hex fill.Verify: fixture run in task 3.
opensslRSA-2048 + P-256 self-signed certs;pdf-lib1-page source PDF;run the test once with
PADES_WRITE_FIXTURE=1to emitpades-b-b-signed.pdf.Verify:
cd web && ls -l test/fixtures+openssl x509 -in test/fixtures/pades-signer.crt -noout -subject.web/src/lib/pades.test.ts— (a) sign source fixture → bytes identical to committed golden,original prefix preserved,
/ContentsCMS verifies viaopenssl cms -verify -binary -inform DER -noverify -content, field reachable from/AcroForm; (b) committed golden verifies standalone;(c) ECDSA P-256 signature verifies; (d) visible signature:
/AP /Nform XObject present, Rect set.Verify:
cd web && pnpm test.web/README.mdPAdES section + independent verifier evidence run (pip install pikepdf pyhankoin a venv →
Pdf.check()+validate_pdf_signature) on the golden fixture.Verify:
cd web && pnpm test && pnpm check && pnpm build.pades: …), run bullet gates, write report, post Done comment.Open questions → assumptions taken
pdfsig/qpdfnot installed, no root. Assumption: the acceptance intent is "an independentstructure/CMS verifier accepts the output". Proceeding with
openssl cms -verify(independentimplementation, present) as the automated gate +
pikepdf(qpdf engine,Pdf.check()) andpyhanko(validate_pdf_signature) as manual evidence. If pip is blocked, fall back to opensslonly and say so. Report will state the substitution explicitly.
pdf-lib1.17 cannot create signature fields (PDFFormhasgetSignatureonly) and alwaysrewrites the whole document on
save()(would violate "preserves original bytes"). Assumption:hand-roll the placeholder + incremental update, keep pdf-lib only as parser/serializer of dicts.
though a bare RFC 5652 CMS would verify without it.
privateKey.algorithm.hashmay be absent for some RSA keys;defaulting to SHA-256 (the only one a browser PAdES B-B app needs) is acceptable.
fixed, so output is reproducible; if not, fall back to hash-of-CMS + verify assertions.
Risk / fallback
Fallback:
pdf-librewrite path would break byte preservation → instead fix offsets andre-verify with pikepdf; if still red, the engine throws and the report says the byte-preserving
path is unproven.
/ID, the golden-bytes assertion drops to"verify + structure" assertions and the fixture becomes input for the external verifier only;
noted in the report.
decompiled/present, java count > 0) — untouched by this diff,so it cannot regress; local gates are
pnpm test,pnpm check,pnpm build+git diff --check.DONE — commits
854a996,30eadce,a2b07f6onissue/11-use-case-pades-b-b-pdf-signing.Gates: repo
test -d decompiledOK ·find decompiled -name '*.java' | grep -q .OK ·pnpm test8/8 passed (2 files) ·pnpm check0 errors / 495 files ·pnpm buildOK ·git diff --checkclean.Acceptance: incremental update preserves the input byte for byte (
signed.subarray(0, source.length)equals the source, asserted against the source fixture and the golden fixture) ·openssl cms -verifypasses on the CMS + byte-range content re-extracted from the produced PDF for RSA, ECDSA P-256 and a visible signature · golden fixtureweb/test/fixtures/pades-b-b-signed.pdfis byte-reproducible frompades-source.pdfand additionally validates with pikepdf/libqpdf (/ByteRange [0 1590 4526 718], round-trip save) and pyhanko (intact/valid/trusted = True,coverage = ENTIRE_FILE), signed attributes = contentType, signingTime, messageDigest, signingCertificateV2.Two substitutions/notes:
pdfsig/qpdfCLI are not installed here (no root), so the equivalent verifiers are libqpdf via pikepdf + pyhanko, run on the committed fixture; and #6 has no commits yet (its branch tip is #5's27d86a5, nocms.tsanywhere), so the CMS builder + minimal DER layer ship insidepades.ts/der.tson this branch — de-duplicating them into #6's CMS module is the one deliberate follow-up. UI wiring needs #5 (inert CertPicker in this base);signPdftakes the DER cert + WebCrypto key that #5's keystore produces.bullet: coder session died — relaunched from committed state (branch
issue/11-use-case-pades-b-b-pdf-signing).bullet: implementation finished (DONE: — commits
854a996,30eadce,a2b07f6onissue/11-use-case-pades-b-b-pdf-signing.) — moving to review.bullet: reviewer launched (model
9router/reviewer, independent of coder).Claimed — branch
issue/11-use-case-pades-b-b-pdf-signing. Doing: verify the existing PAdES B-B engine, fixture and golden test (commits854a996,30eadce,a2b07f6) against the acceptance criteria with independent verifiers (pyhanko 0.37 + openssl), close any real gap, run the gates.Issue #11 — Use case: PAdES B-B PDF signing (pdf-lib + WebCrypto)
Restated goal
Ship a browser-only PAdES baseline B-B signer: given a PDF and a WebCrypto
CryptoKey, producethe signed PDF as the original bytes plus an appended incremental update carrying a
/Sigwidget (AcroForm
/Fieldsentry + page/Annots),/SubFilter /ETSI.CAdES.detached, a/ByteRangethat excludes only the/Contentshex string, and/Contentsholding a detached CMSSignedData(signed attributes: content-type, signing-time, message-digest,signing-certificate-v2). No timestamp, no DSS/LTV. Done = committed engine + one-page fixture +
golden test whose signature verifies under an implementation other than the signer, plus
pdfsig/qpdf --check-class structural verification of the output.State on entry: the branch already carries three commits from an earlier session
(
854a996engine,30eadcefixture+tests,a2b07f6README) — engine, fixture and golden test arein place and the suite is green. My job is therefore verification-first: re-prove every acceptance
criterion independently, close any real gap, then report. No gratuitous rewrite.
Scope
Change (if verification demands):
web/src/lib/pades.ts,web/src/lib/der.ts,web/src/lib/pades.test.ts,web/test/fixtures/*,web/README.md.Will not touch:
decompiled/**(read-only reference, GPL clean-room boundary).original/,extracted/(gitignored inputs).App.svelte, components,i18n.ts,routes/) — UI wiring is #5/#12 work; #11acceptance is about the signing engine, its fixture and its verification.
package.jsondependency list — pdf-lib and vitest are already present; no new deps.main, or the forge issue body.Ordered tasks + verification command
test -f /home/eduardez/workspace/AutoFirme/.bullet/meta/issue-11.plan.mdcd web && pnpm test/tmp/pades-verify-venv)python /tmp/verify_pades.py web/test/fixtures/pades-b-b-signed.pdf web/test/fixtures/pades-signer.crt web/test/fixtures/pades-source.pdf→INTACT:TRUSTED,UNTOUCHED, coverageENTIRE_FILE(3)qpdf/pdfsig-class tool (pdfsigabsent; fallback:pdfminer.six/pypdfparse +openssl cms -verifyon extracted CMS)openssl cms -verify -inform DER -content <byterange bytes>exits 0; Popplerpdfsigif installable/tmp, then pyhanko on bothpades.test.tsbyte-prefix assertionscd web && pnpm checkcd web && pnpm build.bullet/bullet.yaml+ whitespacetest -d decompiled && find decompiled -name '*.java' | grep -q .,git diff --checkDonecomment posted, branch left cleangit status --porcelainempty,test -f .../issue-11.report.mdOpen questions + assumptions
pdfsig/qpdfare not installed in this container. Assumption: "verifies in pdfsig orqpdf --check" means an independent, non-project verifier accepts the file. I use pyhanko(a real PAdES validator: CMS +
/ByteRange+ revision coverage) andopenssl cms -verify, andnote the substitution in the report. If a
qpdfbinary appeared I would also runqpdf --check.= pdfsig + incremental update + fixture + golden test). Assumption: UI wiring stays out of scope;
sign.tsalready lists PAdES as the #11 seam.signingTimeis the signer's own claimedtime in signed attributes — correct for baseline B-B.
Risk + fallback
SignerInfoversion detail orsignedAttrsDER ordering). Fallback: fix the CMS encoding inpades.ts; if the defect is in aB-B-not-required field, document the exact non-conformance in the report rather than claiming DONE.
PADES_WRITE_FIXTURE=1 pnpm testand re-verify with pyhanko.
pnpm build/pnpm checkregression from unrelated Svelte shell. Fallback: leave it red andreport — never weaken a check.
DONE — PAdES B-B verified end to end. Commits:
854a996engine,30eadcefixture+tests,a2b07f6README,c8393e8verification commands (mine; no engine/test change was warranted).pnpm test: 2 files / 8 tests passed (openssl-backed)pnpm check: 495 files, 0 errors, 0 warningspnpm build: ok, site written to build/test -d decompiled+find decompiled -name '*.java' | grep -q .: passgit diff --check/ clean tree: passAcceptance:
pdfsig(poppler 26.10) — "Total document signed" + "Signature is Valid.";qpdf --checkexit 0; pyhanko 0.37 — INTACT:TRUSTED,UNTOUCHED, coverage ENTIRE_FILE. Incremental update: first 1454 B byte-identical to source, AcroForm still AcroForm. Fresh RSA / ECDSA P-256 visible / ECDSA P-384 / second-signature variants all pdfsig-valid. Details in the report; open human decisions: pdfsig+qpdf not in CI gates, UI wiring left to the sign-flow issue, P-384 has no permanent test.REVIEW: FIXED 1 — commit
72ab9ba: findings: (1) accented /Name,/Reason,/Location and visible appearance were raw UTF-8 bytes in PDF text strings (repro: decodeText of "José García" → "José GarcÃ\ufffda"); fixed with UTF-16BE+BOM hex dict strings + WinAnsi octal appearance bytes + regression test —72ab9ba; UNFIXED: nonebullet: fixer launched (round 1/2).
FIXED: applied 1 — (1) accented
/Name,/Reason,/Locationdict strings + WinAnsi appearance (finding ofREVIEW: FIXED 1, comment #3147): engine fix is reviewer commit72ab9ba, verified unchanged and re-proved here —qpdf --checkexit 0,pdfsig"Total document signed"/"Signature is Valid.", dict =/Name <feff…>pure ASCII,pdftoppm -r 300crop of/Rect [60 60 260 120]reads "José García"; fixer commitcd09070closes the coverage gap the reviewer left (their test asserted/Nameonly, while the finding named/Reasonand/Locationtoo) —pades.test.ts:216now pins all three plus a pure-ASCII signature-dictionary assertion, and fails on pre-fix engine behaviour. UNFIXED: none. Gates atcd09070:pnpm test9/9,pnpm check495 files/0 errors,pnpm buildok, repo gates ok,git diff --checkclean. Known limitation recorded (not a finding): visible appearance maps chars >U+00FF to "?" (ponytail:note inpades.ts), out of #11 Spanish scope. Refs #11.bullet: PR #23 open →
bullet-develop(auto-merge follows).bullet: merge blocked (HTTP 405) — conciliation agent merges trunk work into
bullet-develop; PR auto-merges next tick.RECONCILED: trunk work merged into bullet-develop (conflicts resolved: web/README.md file table + PAdES section, web/package.json deps union fflate+pdf-lib, web/pnpm-lock.yaml importers; then main
9c2af1aancestry-only) — PR #23 conflicts gone, commits16cb116+329550apushedDONE — commits
16cb116(issue/11 replayed onto bullet-develop) +329550a(main9c2af1aancestry-only, tree-identical), pushed442624f..16cb116..329550a.Gates:
test -d decompiled0;find decompiled -name '*.java' | grep -q .0 (11,487);git diff --check0;merge-base --is-ancestor origin/main HEAD0.Extra:
pnpm test10 files / 92 tests pass (openssl-verified PAdES),pnpm check527 files 0 errors,pnpm buildOK.Report:
.bullet/meta/issue-11.report.md(fixer's earlier report preserved asissue-11.fixer-report.md).