Hand-rolled PAdES B-B incremental-update emitter.
The writer takes a base PDF and produces an incremental update with a
PKCS#7-detached /Sig dictionary whose /Contents is a fixed-size hex
placeholder. The caller then:
- signs the bytes the writer reports as
:signed_inputviaPkcs11ex.sign_bytes/2(or any equivalent CMS pipeline); - calls
inject_signature/2to splice the resulting CMS DER into the placeholder.
The two-phase shape is mandatory: PAdES /ByteRange covers everything
except the hex digits, so the bytes-to-be-signed cannot be known until
the placeholder's exact byte offset is fixed.
What the writer emits
Three new objects are appended:
- the existing catalog re-emitted with
/AcroFormextended (or added) so the new/Sigfield is reachable from/Root; - a signature field annotation (
/FT /Sig,/Subtype /Widget, invisible 0×0 rect) referencing the/Sigdict via/V; - the
/Sigdict itself:/Filter /Adobe.PPKLite,/SubFilter /adbe.pkcs7.detached, fixed-width/ByteRangeand/Contentsplaceholders, plus optional/M,/Reason,/Location.
Followed by a fresh xref subsection (one entry per new object number,
plus one for the re-emitted catalog), trailer with /Prev pointing
at the original startxref, and the new startxref + %%EOF.
v1 limitations
- Refuses to merge with an existing
/AcroForm. Re-signing a PDF that already has form fields needs full PDF object grammar; out of scope for v1. Returns{:error, {:writer, :existing_acroform_unsupported_in_v1}}. - Refuses to operate on PDFs whose
/Rootcatalog uses a cross- reference stream (Reader returns:xref_stream_unsupported). - The
/Sigfield is invisible — no widget rectangle, no rendering hints. Visible signature appearance streams are a Phase 4b.x feature.
Summary
Types
Per-call options
Output of prepare/2. The caller hashes :signed_input, builds a
CMS over that hash, and feeds the CMS DER to inject_signature/2.
Functions
Splices cms_der into the /Contents placeholder of a prepared PDF.
Returns {:error, {:writer, :cms_der_too_large}} if the DER exceeds
the prepared placeholder size; pads with 0x00 bytes (hex 00)
otherwise.
Builds the incremental-update bytes and returns the structure describing what to sign.
Types
@type prepare_opts() :: [ placeholder_size: pos_integer(), signing_time: DateTime.t(), reason: String.t(), location: String.t(), contact_info: String.t() ]
Per-call options:
:placeholder_size— bytes the CMS DER will occupy. The hex placeholder in the PDF is exactly twice this many ASCII chars. Default8192. Real-world PAdES B-B signatures with a single end-entity cert and no timestamp run ~6 KiB; raise this when embedding LTV material.:signing_time—DateTime.t()for the/Mentry in the Sig dict. DefaultDateTime.utc_now/0. Note: PAdES verifiers generally trust the CMSsigning-timeattribute, not/M.:reason,:location,:contact_info— optional/Reason,/Location,/ContactInfoentries in the Sig dict.
@type t() :: %SignCore.PDF.Writer{ byte_range: [non_neg_integer()], contents_length: non_neg_integer(), contents_offset: non_neg_integer(), pdf: binary(), placeholder_size: pos_integer(), signed_input: binary() }
Output of prepare/2. The caller hashes :signed_input, builds a
CMS over that hash, and feeds the CMS DER to inject_signature/2.
Functions
Splices cms_der into the /Contents placeholder of a prepared PDF.
Returns {:error, {:writer, :cms_der_too_large}} if the DER exceeds
the prepared placeholder size; pads with 0x00 bytes (hex 00)
otherwise.
@spec prepare(binary(), prepare_opts()) :: {:ok, t()} | {:error, term()}
Builds the incremental-update bytes and returns the structure describing what to sign.