PAdES PDF Signer
On this page
PAdES PDF Signer is a Python service that puts a visible, legally meaningful signature on a PDF and makes sure it can still be verified long after every certificate behind it has expired. It signs to PAdES B-LTA — the long-term archival level of the European standard for PDF signatures — so the document carries its own timestamp, its own proof that no certificate in the chain was revoked, and an archive timestamp over all of it. It also verifies PDFs, reporting each signature in them.
I built it because a signature that only a live network can confirm is a signature with an expiry date. A reader opening a contract in ten years may find the certificate authority's responder gone and the signer's certificate long expired; the signature must still prove who signed, when, and that it was valid at that moment. Putting every piece of that proof inside the PDF is the whole job of this service.
At a glance
| Language & framework | Python, with FastAPI, pyHanko for PDF signatures, and aiohttp for transfers |
|---|---|
| Architecture | Transaction Script Service |
| Version | 2.11.3 |
| Status | Stable, in production |
| Form | A stateless HTTP service, shipped as a single executable, called by the signing application |
| Source code | Private |
What it does
A. Signs a PDF with a visible stamp
The service takes a PDF and a stamp image — often a QR code — places the stamp in a box on a chosen page, and signs the document with the signer's own certificate. Each signature becomes a new field with a unique id, so a document can be signed by several people, one after another, without disturbing earlier signatures.
B. Embeds the proof that the signature was valid
At signing time the service collects every certificate in the chain and the revocation answers that show none of them was revoked, and stores them inside the PDF. A validator years later needs no network to know that the signature was good when it was made.
C. Seals the result with timestamps
The signature itself is timestamped by a timestamp authority, which proves when it was made; then a document timestamp is added over the whole file, including the validation data. That archive timestamp is what lets the signature outlive the algorithms and certificates it relies on.
D. Verifies signed PDFs
Given any PDF, the service reports every signature and document timestamp inside it: whether it is valid, who signed, when, why and where, how much the document changed after it, and whether long-term validation data is present.
E. Keeps the signer's secret out of every queue and log
Nothing upstream — queues, jobs, logs — ever holds the signer's passphrase in plain text, and it serves only the single signature it was given for.
Interface
| Capability | Purpose |
|---|---|
| Sign | sign the PDF behind one presigned link to PAdES B-LTA with a visible stamp, and upload the result to another; returns the signature id and the SHA-256 of the signed file |
| Verify | report every signature and document timestamp in an uploaded PDF |
| Readiness | checks the certificate authorities, the timestamp authority, and storage in parallel, and names any that fail |
A signing request names the document, the stamp, the page and box where the stamp goes, the reason and location, and where the result should be written.
Standards
| Standard | What it guarantees here |
|---|---|
| PAdES B-LTA (ETSI EN 319 142) | signatures that remain verifiable after their certificates expire, checked with an ETSI conformance checker |
| CMS signatures in PDF (ISO 32000) | signatures added as incremental updates, so earlier signatures stay intact |
| RFC 3161, Time-Stamp Protocol | a signature timestamp and an archive timestamp from a trusted authority |
| RFC 5280 and RFC 6960 | revocation lists and OCSP answers collected at signing time and stored in the document |
| PKCS#12 | the signer's key and chain received as one protected bundle |
| W3C Trace Context and OpenTelemetry | signing, timestamping, and certificate retrieval appear as steps in the caller's trace |
Inside
The service is a Transaction Script Service: each endpoint is one procedure that reads, in order, every step of a request — check the caller, unlock the signer's key, fetch the document and the stamp, build the validation context, sign, upload, record — and the work for each step sits in a module named after it: preparation, signing, trust, fetchers, verification, cryptography, telemetry. It is bundled into a single executable, so the container carries no source code.
Design decisions
The archival level was the goal, and I changed languages twice to reach it. The first version, in Python, reached the point where a widely used PDF reader accepted its long-term validation, and then stalled on the way to the full standard. Convinced the dead end was in the Python tooling, I moved the signer to Node.js, where a conformance checker's error count fell from three to one. Then the limit showed: the Node.js library could embed long-term validation only in the older Adobe style, not in the PAdES structure the standard requires. I moved back to Python with what the detour had taught me about how validation data has to be gathered, and the next version signed to full PAdES B-LTA with no errors in the ETSI conformance checker report.
Every piece of proof is collected before signing, not hoped for later. Twice, validation failed at signing time because a revocation answer was "too recent" — newer than the moment the validator was checking against. The first fix was to fetch the revocation lists of the root, the signing CA, and the timestamp authority in parallel before signing and hand them to the validation context directly; the second was a small tolerance for clock differences between services. Revocation checking is set to require an answer, so a signature is never embedded with a gap in its proof. The root certificate is left out of the document, because a validator must already trust it, not receive it from the file it is judging.
Signatures add to a document, never rewrite it. Each signature is written as an incremental update, so earlier signatures and their protection stay byte for byte as they were, and a document can collect signatures from several people. The writer also accepts PDFs with mixed cross-reference tables, so documents produced by a range of tools can be signed. Every signature gets a new field named with its own UUIDv7, and that name doubles as the signature id returned to the caller, so the record and the PDF always point at the same signature. The stamp is drawn without a border, so a QR code does not end up framed twice, and its box may start at zero, so it can sit flush against the edge of the page.
The signer's secrets are never stored anywhere on the way. The passphrase stays encrypted from the moment the application queues the job, so it never rests in job payloads or activity logs. The service holds no standing access of its own to the certificate authority: the authority decides, for each request and each caller, whether this key may be used, and nothing the service handles for a signature outlives it.
Files move by link, and only to storage the service knows. The document, the stamp, and the destination are all presigned links, so the application never streams a PDF through itself. Each link must point exactly to the expected storage origin and bucket, an allowlist kept in the source rather than in configuration, and anything else is rejected before a single byte is fetched. The response carries the SHA-256 of the signed file, so the application can find the document by its hash.
Verification is strict first, and judges each signature at its own moment. The verifier first checks that the upload is actually a PDF — its header and its end marker — and answers a fake file with a clear client error rather than a server failure. Each signature is then validated online and strictly; when that fails because the world has moved on since signing, it is evaluated again offline at its own signing time, from the validation data embedded in the document — exactly the proof the archival level exists to preserve. Trust roots are read from a mounted directory, so trusting a new root is a deployment change, not a code change.
Telemetry had to be made trustworthy before it could be trusted. Logging moved from writing straight to Elasticsearch to OpenTelemetry through a collector, continuing the trace of the application that called the service and passing it on to the timestamp authority and the certificate authority, so one signature appears as one trace across four services. Redaction inspects values, not only field names, so a presigned link's signature never reaches a log whatever the field is called. Three incidents shaped the rest. Failures were nearly invisible — 492 spans but only 14 log records in half an hour — so every rejected or failed request now goes through one handler that logs it, with a reason field kept as a string because the search index had already fixed that field's type. The readiness probe produced 99.8 per cent of all spans, so it is excluded from tracing. And inside the bundled executable two instrumentors silently switched themselves off, because the bundle lacked the package metadata they check; the build now copies that metadata, and revocation fetches appear in traces again. Metrics use delta temporality, which the collector's Elasticsearch exporter requires.
Readiness checks what signing actually needs. The service is ready only when the root CA, the signing CA, the timestamp authority, and storage all answer, checked in parallel, and an unready answer names which one failed. The probes go to each service directly rather than through the public proxy, so each dependency is measured where it actually runs.
Specifications
| Inputs | Presigned links to the PDF, the stamp image, and the destination; the signer's certificate and protected passphrase; page, box, reason, and location |
|---|---|
| Outputs | A PAdES B-LTA signed PDF uploaded to the destination; the signature id and SHA-256 to the caller; for verification, a report per signature |
| Signature | CMS in PDF, SHA-256, incremental update, visible stamp, signature and archive timestamps, embedded certificates and revocation data |
| Depends on | The signing CA for the signer's key and revocation data, the root CA, a timestamp authority, and S3-compatible storage |
| Verified with | An ETSI conformance checker and a widely used PDF reader |
| Observability | OpenTelemetry traces, logs, and metrics over OTLP |
| Runs as | A single executable built in a multi-stage container image, with configuration and trust roots mounted |
Related
The certificate authority that holds each signer's key and answers the revocation questions embedded in every signature.
The timestamp authority that dates each signature and seals the archive.
The storage service that issues the links the document, the stamp, and the signed result travel through.
Where one signature's trace across four services ends up.
Terms
- PAdES
- PDF Advanced Electronic Signatures: the European standard for digital signatures in PDF, defined in levels from basic to long-term archival.
- B-LTA
- The long-term archival level of PAdES: a signature with a timestamp, embedded validation data, and an archive timestamp over the whole document.
- Long-term validation
- Embedding the certificates and revocation answers a validator will need, so a signature can be checked without contacting anyone.
- Document timestamp
- A timestamp applied to the entire PDF rather than to one signature, protecting everything added before it.
- Incremental update
- Appending changes to the end of a PDF instead of rewriting it, so earlier content and signatures remain exactly as they were.
- PKCS#12
- A file format that bundles a private key with its certificate and chain, protected by a password.
Technologies covered
- Python
- FastAPI
- pyHanko
- OpenTelemetry
- OCSP
- PAdES