# `SignCore.PDF`
[🔗](https://github.com/utaladriz/pkcs11ex/blob/v0.1.0/lib/sign_core/pdf.ex#L1)

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:

  1. `SignCore.PDF.Writer.prepare/2` allocates the incremental
     update with a fixed-width `/Contents` placeholder. Returns
     `signed_input` — the bytes the CMS will hash.
  2. SHA-256 over `signed_input` becomes the
     `messageDigest` PKCS#9 attribute. `signed_attrs` (content-type,
     message-digest, signing-time) are DER-encoded as a SET-OF.
  3. The DER-encoded `signed_attrs` is the to-be-signed input. It
     routes through `Pkcs11ex.sign_bytes/2` → Layer 2 → NIF →
     cryptoki → HSM. **Software signing is never used.**
  4. `SignCore.CMS.SignedData.build/3` assembles the ContentInfo with
     the HSM-produced raw signature, the supplied `:x5c` chain, and
     the appropriate signature-algorithm OID for the requested
     `:alg`.
  5. `SignCore.PDF.Writer.inject_signature/2` splices the CMS DER
     into the placeholder.

## v1 limitations

  * `:PS256` and `:RS256` only. (`:PS256` emits 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/2` is part of step 9 in the Phase 4a punch-list and
    currently still returns `:not_implemented_in_v1`.

# `sign_result`

```elixir
@type sign_result() :: {:ok, binary()} | {:error, term()}
```

Result of `sign/2`. Failure carries the responsible class as the wrapper.

# `verify_result`

```elixir
@type verify_result() :: {:ok, subject_id :: term()} | {:error, term()}
```

Result of `verify/2`. Currently always `:not_implemented_in_v1`.

# `sign`

```elixir
@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 `:signer` form) — these are
    forwarded verbatim to `Pkcs11ex.sign_bytes/2`.

Optional:

  * `:alg` — `:PS256` (default) or `:RS256`.
  * `:signing_time` — `DateTime.t()` used both for the CMS
    `signing-time` attribute and the PDF `/M` entry. Defaults to
    `DateTime.utc_now/0`.
  * `:placeholder_size`, `:reason`, `:location`, `:contact_info` —
    forwarded to `SignCore.PDF.Writer.prepare/2`.

Errors propagate from each pipeline stage; see `docs/specs/api.md`
§4.1 for the full taxonomy.

# `verify`

```elixir
@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:

  1. Locate the (single) `/Sig` dict 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).
  2. **Append-attack detection.** Refuse if `c + d` does 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`.
  3. Strip the trailing zero-padding from the hex-decoded
     `/Contents` blob using the CMS SEQUENCE length prefix; parse
     the result via `SignCore.CMS.SignedData.parse/1`.
  4. **Allowlist gate (architectural invariant).** Synthesise a
     JOSE-style header from the embedded `x5c` chain and run it
     through the configured `SignCore.Policy` —
     `policy.resolve/2` then `policy.validate/3`. The candidate
     chain is **untrusted input** until both succeed. No
     cryptographic check has happened yet.
  5. Reconstruct `signed_input` = `pdf[a..a+b) ++ pdf[c..c+d)`,
     hash with SHA-256, compare against the CMS `messageDigest`
     PKCS#9 attribute. A mismatch surfaces as
     `:message_digest_mismatch` and is the canonical
     tampered-byte signal — any modification inside the signed
     byte range invalidates the digest before the math runs.
  6. Mathematically verify the embedded raw signature over the
     DER-encoded `signedAttrs` against 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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
