Digital Signatures
Four endpoints give a PDF a cryptographic life story: pdf-sign binds an identity to the
bytes, pdf-verify-signatures judges every signature already there, pdf-timestamp proves
the whole file existed at a point in time, and pdf-ltv archives the validation material so
the verdicts survive the certificates that produced them. All four follow the
toolbox contract: base64 or file id in, a new file id out, password
where encrypted input is plausible, and the jobs contract when
work runs long. Everything the writers produce is appended incrementally: the original
revision stays byte-identical underneath, which is why a signed document can be timestamped
and extended without breaking the signatures it already carries.
1The verbs#
| Endpoint | The concern |
|---|---|
POST /lmkit/v1/pdf-sign |
Apply a PAdES (ETSI.CAdES.detached) signature: invisible, or a visible mark with localized text, a drawn image, or both |
POST /lmkit/v1/pdf-verify-signatures |
The verdict report: every signature judged on four independent axes |
POST /lmkit/v1/pdf-timestamp |
An RFC 3161 document timestamp (ETSI.RFC3161): an authority attests the file's existence, naming no signer |
POST /lmkit/v1/pdf-ltv |
Embed certificates, CRLs, and OCSP responses into the document security store (/DSS, PAdES B-LT) |
2Signing#
pdf-sign needs an identity: a PKCS#12 (.p12/.pfx) bundle carrying the certificate and its
private key. It resolves from two places, in order:
- The request:
certificate(base64) pluscertificate_password. Nothing is stored; the bundle lives for the duration of the call. - The server:
DocumentSigning:CertificateFileplus its password, set once from the admin Signing panel. Requests that carry no certificate sign as the server. A deployment that must never persist the password leaves it empty in configuration and suppliescertificate_passwordper request instead.
When neither exists the request fails immediately with a message that says so; nothing half-signs. Signing is fully managed and in-process: the key never enters an OS key store, which is also what makes the same engine work in the browser and in containers.
The signature is invisible by default: cryptographically complete, no mark on any page.
Passing field (a page index and a rectangle in PDF points, origin bottom-left) renders a
visible mark instead, and three knobs shape it:
- Automatic text, the default: who signed, when, and why, composed in the language of
appearance_language(fourteen built in; the server's culture otherwise). Scripts beyond the built-in Helvetica compose in English unlessappearance_fontembeds a font that carries them. appearance_linesreplaces the composition with exact custom lines.appearance_imagerenders a drawn mark (a handwritten stroke, a stamp; PNG keeps its transparency) beside the text, or filling the field when the lines resolve to no text.
certification turns the signature into the document's author signature (DocMDP),
declaring which later changes stay legal: no_changes, form_filling, or
form_filling_and_annotations. A document carries at most one, and it must be the first
signature applied.
When a timestamp authority is available (from the request or from configuration), the
signing time is attested by that authority instead of claimed by the signer (PAdES B-T).
An explicit empty timestamp_authority opts a single request out.
The response returns the new file id plus a full verification of the stored output, run
through the same validator anyone else would use, so the integrity: Valid in it is a
checked fact rather than signer bookkeeping.
3Verification#
pdf-verify-signatures reports one verdict per signature, each judged on four axes that
deliberately do not blur into each other:
| Axis | Question | Values |
|---|---|---|
integrity |
Are the signed bytes untouched? | Valid, DocumentModified, MalformedSignature, ... |
identity |
Does the signer chain to a trust anchor? | Trusted, Untrusted, Unknown |
revocation |
Is the certificate revoked? | Good, Revoked, NotChecked |
timestamp |
Is the time proven or claimed? | Valid, Untrusted, Invalid, None |
The combined status is Valid only when every axis that must hold does; the document-level
overall_status is Valid only when every signature is. Untrusted means exactly what it
says: no configured anchor reaches the signer. It does not mean the document was
tampered with; a modified document reports integrity: DocumentModified. This is the same
honesty discipline the rest of the toolbox applies: the report states what was checked and
what the check found, never a softer synonym.
Trust is explicit and comes from three places that compose:
DocumentSigning:TrustAnchorsDirectory: a folder of certificate files (.cer,.crt,.pem,.der) on the server, typically the organization's issuing CA. Read live, so dropping a file in counts on the next verification.trust_rootson the request: base64 certificates for anchors that matter to one caller only.- The OS root store, on by default (
DocumentSigning:TrustSystemRoots), so publicly-issued signing certificates verify asTrustedout of the box.
Each result also carries the signer's certificate facts and the embedded chain, the digest
algorithm, the recorded reason, the DocMDP level, and covers_entire_document: whether the
signature's byte range reaches the end of the file, or later revisions were appended after
it (which is normal for a document that was signed and then timestamped or LTV-extended).
4Document timestamps#
pdf-timestamp appends an RFC 3161 document timestamp: a cryptographic attestation that the
entire file, signatures included, existed at a point in time. No signer is named; the
authority is the witness. The authority comes from timestamp_authority or from
DocumentSigning:TimestampAuthority; with neither the request fails immediately. Verification
reports these entries as ETSI.RFC3161 with the authority-attested time in timestamp_time.
Timestamps compose with signing rather than replacing it: sign first (the signature may itself carry a B-T timestamp), then timestamp the document to seal the whole revision. Every later timestamp covers everything before it, which is how long-term archives refresh their proof as algorithms age.
5Long-term validation#
A signature is judged against certificates that expire and infrastructure that disappears.
pdf-ltv embeds the validation material (certificates, CRLs, OCSP responses) into the
document security store (/DSS), so a validator years later finds everything inside the file
itself (PAdES B-LT). By default the material already carried by the document's signatures is
gathered automatically; certificates, crls, and ocsp_responses add whatever else the
archive should hold. A document with no signatures is refused: there is nothing to extend.
The archival chain, stated as calls: pdf-sign, then pdf-ltv, then pdf-timestamp, and
the PDF/A conversion runs before signing, never after: PDF/A conversion
rewrites the document, and rewriting signed bytes is precisely what integrity: DocumentModified exists to catch.
6Configuration#
The DocumentSigning section, edited from the admin Signing panel, applies live:
| Setting | Meaning |
|---|---|
CertificateFile |
The server's PKCS#12 signing identity; empty means requests must bring their own |
CertificatePassword |
Its password; empty defers to per-request certificate_password |
TimestampAuthority |
RFC 3161 authority applied when requests name none |
TrustAnchorsDirectory |
Folder of trust anchor certificates for verification |
TrustSystemRoots |
Whether the OS root store also anchors identities (default: on) |
The certificate password is a secret: configuration exports mask it, and the panel never echoes it back.
7Agents#
Two MCP tools expose the surface to agents. pdf_verify_signatures ships in
the recommended set: judging signatures needs no configuration. pdf_sign signs with the
server identity only (certificates never transit a model's context window) and stays held
back until an operator who configured an identity deliberately enables it.
8Stated plainly#
- Sign with the request's identity or the server's; the response re-verifies the output so its verdict is checked, not claimed.
- Four verdict axes that never blur: untouched bytes, trusted signer, live certificate, proven time.
- Timestamps and LTV append incrementally; nothing already signed ever breaks.
- Convert to PDF/A first, sign last: any rewrite after signing is, by design, a broken signature.