PAdES (PDF Advanced Electronic Signature) format adapter — Phase 4a.
Sign
SignCore.PDF.sign(pdf_bytes,
module: pkcs11_module,
slot_id: slot_id,
pin: "1234",
key_label: "platform-signing-key",
alg: :PS256,
x5c: [leaf_der, intermediate_der, root_der]
)Returns a binary containing the original PDF plus an incremental
update with a /Sig field whose /Contents is the CMS SignedData
produced by the HSM. The output validates as PAdES B-B against
Poppler pdfsig and BouncyCastle verifypdf.
Pipeline:
SignCore.PDF.Writer.prepare/2allocates the incremental update with a fixed-width/Contentsplaceholder. Returnssigned_input— the bytes the CMS will hash.- SHA-256 over
signed_inputbecomes themessageDigestPKCS#9 attribute.signed_attrs(content-type, message-digest, signing-time) are DER-encoded as a SET-OF. - The DER-encoded
signed_attrsis the to-be-signed input. It routes throughPkcs11ex.sign_bytes/2→ Layer 2 → NIF → cryptoki → HSM. Software signing is never used. SignCore.CMS.SignedData.build/3assembles the ContentInfo with the HSM-produced raw signature, the supplied:x5cchain, and the appropriate signature-algorithm OID for the requested:alg.SignCore.PDF.Writer.inject_signature/2splices the CMS DER into the placeholder.
v1 limitations
:PS256and:RS256only. (:PS256emits the canonical RSASSA-PSS-params for SHA-256 / MGF1-SHA-256 / sLen=32, which OpenSSL and BouncyCastle accept.)- The base PDF must not already carry an
/AcroForm; re-signing a PDF with form fields is a Phase 4b enhancement. - Visible signature appearance streams are out of scope.
verify/2is part of step 9 in the Phase 4a punch-list and currently still returns:not_implemented_in_v1.
Summary
Types
Result of sign/2. Failure carries the responsible class as the wrapper.
Result of verify/2. Currently always :not_implemented_in_v1.
Types
Functions
@spec sign( binary(), keyword() ) :: sign_result()
Sign a PDF with PAdES B-B.
Required options:
:x5c— the signing chain. Either a single leaf DER or a list[leaf_der, intermediate_der, ..., root_der]. The first element MUST correspond to the HSM key being used.- Plus the PKCS#11 keying opts (
:module,:slot_id,:pin,:key_label, or the canonical:signerform) — these are forwarded verbatim toPkcs11ex.sign_bytes/2.
Optional:
:alg—:PS256(default) or:RS256.:signing_time—DateTime.t()used both for the CMSsigning-timeattribute and the PDF/Mentry. Defaults toDateTime.utc_now/0.:placeholder_size,:reason,:location,:contact_info— forwarded toSignCore.PDF.Writer.prepare/2.
Errors propagate from each pipeline stage; see docs/specs/api.md
§4.1 for the full taxonomy.
@spec verify( binary(), keyword() ) :: verify_result()
Verify a PAdES-signed PDF.
Returns {:ok, subject_id} where subject_id is whatever the
configured SignCore.Policy.validate/3 returned. The verify
pipeline runs in this order — every step is a checkpoint that can
refuse the signature with the documented error class:
- Locate the (single)
/Sigdict in the file: extract/ByteRange [a b c d]and/Contents <hex>. v1 refuses PDFs carrying more than one/Sig(multi-signature is post-v1). - Append-attack detection. Refuse if
c + ddoes not equal the file's total byte length — bytes beyond the signed range can carry an attacker-crafted incremental update that the original signature cannot cover by definition. Surfaces as:incremental_update_after_signature. - Strip the trailing zero-padding from the hex-decoded
/Contentsblob using the CMS SEQUENCE length prefix; parse the result viaSignCore.CMS.SignedData.parse/1. - Allowlist gate (architectural invariant). Synthesise a
JOSE-style header from the embedded
x5cchain and run it through the configuredSignCore.Policy—policy.resolve/2thenpolicy.validate/3. The candidate chain is untrusted input until both succeed. No cryptographic check has happened yet. - Reconstruct
signed_input=pdf[a..a+b) ++ pdf[c..c+d), hash with SHA-256, compare against the CMSmessageDigestPKCS#9 attribute. A mismatch surfaces as:message_digest_mismatchand is the canonical tampered-byte signal — any modification inside the signed byte range invalidates the digest before the math runs. - Mathematically verify the embedded raw signature over the
DER-encoded
signedAttrsagainst the leaf's SPKI. Failure is:signature_invalid.
Failures from step 4 short-circuit before any signature math, so
callers cannot use verify/2 as a CPU-bound oracle on attacker-
supplied certificates. Failures from step 2 short-circuit even
earlier, so an append-attack PDF is refused before any CMS work.