Applying a digital signature to a finished PDF.
Every other part of Tincture is a pure transformation: a document goes in, bytes come out, and nothing depends on where those bytes land. A signature cannot work that way. It covers the file, including the offsets of its own objects, so it can only be computed once the file exists — and it then has to be written back into the middle of it without moving anything.
So signing is a three-step dance:
- Reserve. The document is serialised with a signature dictionary whose
/Contentsis a run of zeros of a fixed size, and whose/ByteRangeis a row of placeholders of a fixed width. - Measure. The placeholders are replaced with the real offsets, written to exactly the same width so nothing shifts.
- Sign. The file minus the
/Contentsstring is hashed and signed, and the result is written into the space reserved for it.
Step 2 has to preserve every byte position, which is why the placeholders are
fixed-width rather than formatted naturally. Writing [0 1234 5678 90] where
[0 ********** ...] stood would move every subsequent byte and invalidate the
offsets being recorded.
What is signed
/ByteRange [0 a b c] covers bytes 0..a and b..b+c — that is, the whole
file except the <...> holding the signature. A verifier repeats exactly that
and compares. Anything altered afterwards, anywhere in the file, breaks it.
What this does not do
There is no timestamp authority (RFC 3161), so signatures carry the signing machine's own clock and cannot prove when they were made to a third party — only that the document has not changed since. That is the difference between this and a long-term validation signature.
Summary
Functions
Fill in the byte range and signature of a serialised document.
The default number of bytes reserved for the signature.
The placeholder signature dictionary written before the file exists.
Functions
Fill in the byte range and signature of a serialised document.
Takes the bytes as written with placeholders, and returns them with the real
offsets and a signature covering everything outside the /Contents string.
@spec default_reserved_bytes() :: pos_integer()
The default number of bytes reserved for the signature.
The placeholder signature dictionary written before the file exists.
/Contents and /ByteRange are sized here and must not change size later.