PdfElixide.Signature (pdf_elixide v0.14.0)

Copy Markdown View Source

Digital signatures present in a PDF document.

list/1 reports what each signature in a document claims — who signed, when, why, and which bytes the signature covers. It reads from either source (source/0), a read-only PdfElixide.Document or a PdfElixide.Editor.

{:ok, doc} = PdfElixide.Document.open("signed.pdf")
{:ok, [signature]} = PdfElixide.Signature.list(doc)
signature.signer_name

What this does not do

Nothing here verifies anything, and nothing here reads the certificate. Every field comes from the signature dictionary alone, so a value is only as trustworthy as the document it came from: a forged :signer_name reads exactly like a genuine one, and a document altered after signing still lists its signature. Treat these as claims, not findings.

The signer certificate, its issuer and its validity window are not reported: they live inside the signature blob rather than the dictionary. :contents carries that blob for callers who want to decode it themselves; t/0 says what it includes.

Whether a signature covers the whole file is likewise not a field. Use covers_whole_document?/2.

Signature fields are a different thing from the signatures reported here: an unsigned field is a placeholder with no dictionary behind it, and is not listed. PdfElixide.Form omits signature fields entirely and refuses to write to one; the Forms guide explains why.

Signature reads reject some damaged documents that form reads tolerate. The Forms guide describes those cases.

Both sources take a shared read. See the Concurrency guide.

Summary

Types

The signature format, from the dictionary's /SubFilter.

t()

One signature dictionary.

Functions

Counts the signatures in the given PDF document or editor.

Same as count/1, but raises PdfElixide.Error on failure.

Whether the signature covers the whole of a file size bytes long.

Lists the signatures in the given PDF document or editor.

Same as list/1, but raises PdfElixide.Error on failure.

Types

source()

@type source() :: PdfElixide.Document.t() | PdfElixide.Editor.t()

sub_filter()

@type sub_filter() :: :pkcs7_detached | :pkcs7_sha1 | :cades_detached | :rfc3161 | nil

The signature format, from the dictionary's /SubFilter.

nil covers two cases that cannot be told apart here: no /SubFilter at all, and one naming a format this library does not recognize.

t()

@type t() :: %PdfElixide.Signature{
  byte_range: [integer()],
  contact_info: String.t() | nil,
  contents: binary() | nil,
  location: String.t() | nil,
  reason: String.t() | nil,
  signer_name: String.t() | nil,
  signing_time: String.t() | nil,
  sub_filter: sub_filter()
}

One signature dictionary.

Every field is optional, because a signature dictionary need only carry /ByteRange and /Contents.

  • :signer_name — the name the signer claimed (/Name). It is the signer's own claim rather than the subject of the signing certificate, which is not reported here.
  • :signing_time — the claimed signing time (/M), as a raw PDF date string such as "D:20230101120000+00'00'". Not parsed into a DateTime, matching PdfElixide.Document.Metadata.
  • :reason, :location, :contact_info — free text supplied by the signer.
  • :byte_range — the byte offsets and lengths the signature covers, as [start, length, start, length]: everything except the hole holding :contents itself. Normally four integers, but a malformed document can produce any number, so match on it rather than assuming. Pass it to covers_whole_document?/2 to find out whether content was appended after signing.
  • :contents — the raw signature blob (/Contents), typically a few kilobytes per signature and held in memory for as long as the struct is. This is the whole of what the document reserved for it: a signer normally asks for more room than the CMS value needs and pads the remainder with zero bytes, so the DER value is followed by trailing padding a strict decoder will reject. Bound it by that value's own encoded length before decoding it.

Functions

count(source)

@spec count(source()) :: {:ok, non_neg_integer()} | {:error, PdfElixide.Error.t()}

Counts the signatures in the given PDF document or editor.

Answers 0 for a document with no signatures. This reads the signatures to count them, so prefer list/1 when the details are wanted too.

count!(source)

@spec count!(source()) :: non_neg_integer()

Same as count/1, but raises PdfElixide.Error on failure.

covers_whole_document?(signature, size)

@spec covers_whole_document?(t(), non_neg_integer()) :: boolean()

Whether the signature covers the whole of a file size bytes long.

true means a structurally valid byte range starts at byte zero and ends at size. This checks coverage only; it does not verify the signature.

The size has to come from you: a document handle does not carry the length of the bytes behind it.

size = File.stat!("signed.pdf").size
PdfElixide.Signature.covers_whole_document?(signature, size)
#=> true

false can mean content was appended after signing, the file was truncated, size is wrong, or :byte_range is malformed. Malformed ranges answer false rather than raising.

list(arg1)

@spec list(source()) :: {:ok, [t()]} | {:error, PdfElixide.Error.t()}

Lists the signatures in the given PDF document or editor.

Signatures come back in document order. A document with no AcroForm, no signature fields, or only unsigned ones answers {:ok, []}.

Reading from an editor reads the document as it was opened.

None of the returned values are verified; see the module documentation.

list!(source)

@spec list!(source()) :: [t()]

Same as list/1, but raises PdfElixide.Error on failure.