Document Signing CA
On this page
Document Signing CA is an issuing certificate authority, written in Rust, that gives each person who signs PDF documents a certificate of their own, and then keeps answering the question every PDF reader asks when it opens a signed file: is this certificate still valid? It issues X.509 certificates under a private root, publishes a revocation list, runs an OCSP responder, and hands the signing service each signer's key and chain as one PKCS#12 bundle.
I built it because a signature is only worth as much as the chain behind it. A PDF reader outside the organisation, years after signing, has to be able to walk from the signer's certificate to the root, find out whether the certificate was revoked, and do both without an account or a secret. Getting every field, extension, and response exactly right for those readers is most of the work.
At a glance
| Language & framework | Rust, with actix-web, sqlx on PostgreSQL, and OpenSSL — through its Rust bindings and, where they stop, its C interface |
|---|---|
| Architecture | Flat Layered |
| Version | 2.11.2 |
| Status | Stable, in production |
| Form | A standalone HTTP service: public revocation endpoints for any validator, and an authenticated API for the signing application |
| Source code | Private |
What it does
A. Issues signing certificates
The signing application asks for a certificate on behalf of a person, with the subject fields the organisation requires. The CA prepares the request, protects the signer's key so that only the signer can unlock it, and keeps the request pending until it is approved. Approval signs the request with the issuing CA's key and returns the certificate at once.
B. Answers "is this certificate still good?"
An OCSP responder answers revocation questions about any certificate the CA issued, for any client, without authentication: good, revoked with its reason and time, or unknown. Each answer is signed by a dedicated responder certificate and carries the issuing CA, so a validator can check it on its own.
C. Publishes a revocation list
For validators that prefer a list to a question, the CA publishes a certificate revocation list at the address written into every certificate it issues, with a sequence number, a reason per entry, and cache headers that tell clients when to fetch it again.
D. Delivers the key and chain together
When it is time to sign, the signing service receives the signer's certificate, the issuing CA's certificate, and the signer's private key as one PKCS#12 bundle, unlocked only with the signer's passphrase. A PDF signed with it carries the full chain a reader needs.
E. Controls who may ask
Everything except the public revocation endpoints requires a signed access token. Users have roles, tokens can be revoked before they expire, and every issuance, revocation, and revocation check is recorded with who asked, from where, and what happened.
Interface
| Capability | Access |
|---|---|
| The issuing CA's certificate, in DER, at the address certificates point to | public |
| The certificate revocation list | public |
| OCSP, by GET and by POST | public |
| Readiness, covering the database and the root CA | public |
| Request a certificate; approve a pending request | token |
| Revoke a certificate with an RFC 5280 reason | token |
| Export a certificate, its chain, and its key as PKCS#12 | token |
| Log in; manage users, roles, passwords, and access tokens | token, by role |
Any standard client can query the responder. Here is the kind of check a validator runs, with a placeholder host:
Standards
| Standard | What it guarantees here |
|---|---|
| X.509 v3 and RFC 5280 | certificates and revocation lists that any validator can parse and walk to the root |
| RFC 6960, OCSP | revocation answers by GET or POST, with nonces, from a delegated responder |
| RFC 7292, PKCS#12 | one encrypted bundle with the key, the certificate, and the chain without its trust anchor |
| PKCS#10 | certificate requests verified before signing |
| Authority Information Access and CRL Distribution Points | certificates that tell a validator where to find the issuer and the revocation status |
| JSON Web Tokens with RS256 | access tokens signed with a key the CA can rotate |
| W3C Trace Context and OpenTelemetry | traces, logs, and metrics that continue the caller's trace |
Inside
The CA follows the Flat Layered shape I use for Rust services that must work quickly: routes, controllers, services, repositories, and helpers sit side by side, one level deep, and each operation — request, approve, revoke, export, answer an OCSP query, build a revocation list — is one file in the services folder, so the path from a route to the database is short and easy to follow. PostgreSQL sits in a folder of its own inside the repositories, ready for another store. The helpers that build keys, requests, certificates, and responses were made stateless in stages: a second version of each ran beside the first until it matched, and then replaced it, ending with a build free of errors and warnings.
Design decisions
Every certificate is shaped for the reader that will judge it. A signing certificate is version 3, because it carries extensions, and an early batch issued as version 1 with version 3 extensions taught me to set the version explicitly. Its key usage is critical and limited to digital signature and non-repudiation, its extended key usage names document signing, and its authority key identifier carries only the key identifier, because the longer form pointed at the root by name and confused validators. The subject fields, their order, and their rules — exact lengths, e-mail format, minimum and maximum — live in configuration, as does whether the e-mail address also goes into the subject alternative name, so a new organisation's profile needs no new code. Certificates are valid for three years under an issuing CA valid for ten and a root valid for twenty, with path lengths that stop the chain from growing.
Trust starts from one root, and the issuing CA proves itself against it. Before the service accepts a request, the issuing CA's certificate is verified against that root, and any doubt fails closed.
Signing should not require the signer to install anything. At the moment of signing, the signing service receives a ready PKCS#12 bundle that only the signer's passphrase can unlock. The request is verified before it is signed, approval happens in one transaction with one timestamp for the certificate and the record, and a request can be issued only once.
The bundle carries the chain, and nothing more. The PKCS#12 export contains the signer's certificate and the issuing CA, but not the root: the root is a trust anchor the reader must already hold, not something a file should hand over. Before this change the signing service saw a chain of length one and tried, and failed, to fetch the missing issuer itself.
Revocation checks are public, because validators are strangers. A PDF reader checking a signature belongs to whoever received the document, not to the organisation that signed it, so the OCSP responder and the revocation list answer anyone, by GET and by POST. They sit outside the protected scope on purpose: once, the authentication layer blocked the responder, and the fix that separated them briefly made every other route return 404 until both public endpoints were registered at the top level of the application.
An OCSP answer must be checkable on its own and survive real clocks. Answers are signed by a responder certificate of their own, marked critical for OCSP signing, and the CA keeps it renewed automatically. Each answer carries the issuing CA, echoes the client's nonce, and matches the issuer using the same algorithm the client used. The start of an answer's validity is set slightly in the past, so a client whose clock runs slightly behind does not discard it as "not yet valid". A malformed or failed request receives a signed OCSP error status rather than an HTTP 500, which validators understand, and answers without a nonce carry cache headers that expire at ninety per cent of their validity.
Where the Rust library stops, the C interface continues. The Rust OpenSSL bindings cannot build a revocation list or an OCSP response, so the CA calls OpenSSL's C interface directly, through bindings generated for a narrow list of functions. The revocation list is version 2, the version Adobe's PDF reader works with; it carries a sequence number, the authority key identifier, and a reason for each entry. It is served only at the address the certificates name: that endpoint once broke without anyone noticing, and when I repaired it I removed the separate path that used to generate the list, so there is one address to check and it is the one validators use.
Revocation addresses use plain HTTP. The addresses written into every certificate — where to find the issuer, the revocation list, and the responder — are HTTP, not HTTPS: checking revocation over TLS would require checking the TLS certificate's revocation first, a loop validators avoid. The issuing CA's own certificate is served in DER at its address, and the responder's certificate points only to its issuer, never to itself. When an old certificate turned out to carry HTTPS addresses, the cause was an outdated configuration, not code; certificates cannot be edited, so the answer was to reissue.
The CA authenticates its callers itself. The signing application calls the CA directly, so access uses signed tokens. Tokens carry a key identifier, so rotating the signing key does not invalidate tokens already issued, and any token can be revoked before it expires; users can hold several roles at different levels, and passwords are stored only as hashes.
Every decision about a certificate leaves evidence. Telemetry moved from writing straight to Elasticsearch to OpenTelemetry through a collector, continuing the caller's trace. Issuance, revocation, OCSP checks, and revocation lists produce audit events that say who acted, from where, on what, and with which result. Sensitive values are redacted from logs, while the identity of the person who acted is explicitly preserved — the one field a signature audit cannot lose. Metrics use delta temporality, which the collector's Elasticsearch exporter requires; the client address comes from forwarded headers only when they arrive from trusted proxies, with the raw chain kept as evidence.
The schema travels with the binary that needs it. Migrations are part of the image and applied by a separate deployment step. They used to be applied only when an empty database was first created, so later changes never reached staging or production; keeping them in another repository was rejected, because forgetting to copy them would fail silently. Readiness reports healthy only when both the database and the root CA answer, and the service runs as a non-root user.
Specifications
| Inputs | Certificate requests, approvals, revocations, and exports from the signing application; OCSP queries and list downloads from any validator |
|---|---|
| Outputs | X.509 v3 certificates, PKCS#12 bundles, signed OCSP answers, and version 2 revocation lists |
| Keys | RSA 4096 for signers, the responder, and the CAs; SHA-256 by default |
| Validity | Signers three years; responder renewed automatically; OCSP answers and revocation lists one day |
| Storage | PostgreSQL, with migrations shipped inside the image |
| Verified with | OpenSSL's OCSP and CRL tools and the PDF readers that validate the signatures |
| Observability | OpenTelemetry traces, logs, metrics, and audit events over OTLP |
| Runs as | A container image, as a non-root user, with configuration mounted from a volume |
Related
The toolkit that applies the CA's migrations when a release reaches production.
Where the CA's audit events, traces, and metrics go once they leave the service.
Terms
- Certificate authority
- A service that vouches for a public key by signing a certificate that binds the key to a name.
- Issuing CA
- A certificate authority whose own certificate is signed by a root, and which signs the certificates people actually use.
- OCSP
- Online Certificate Status Protocol: a client asks whether one certificate is still valid and receives a signed answer.
- Certificate revocation list
- A signed list of every certificate the CA has revoked, published at an address written into its certificates.
- PKCS#12
- A file format that bundles a private key with its certificate and chain, protected by a password.
- Trust anchor
- A root certificate a validator already trusts, at which every chain it checks must end.
Technologies covered
- Rust
- PostgreSQL
- OpenTelemetry
- OCSP
- X.509