Skip to content

Document Signing CA

By Romi Fadli, S.Kom. 11 min read Security & Secrets
On this page
A chain of three certificates, root to issuing CA to signer, signing a PDF, while an OCSP answer and a revocation list from the issuing CA reach a validator shield alongside the document

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 & frameworkRust, with actix-web, sqlx on PostgreSQL, and OpenSSL — through its Rust bindings and, where they stop, its C interface
ArchitectureFlat Layered
Version2.11.2
StatusStable, in production
FormA standalone HTTP service: public revocation endpoints for any validator, and an authenticated API for the signing application
Source codePrivate
What the module is and where it stands.

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

CapabilityAccess
The issuing CA's certificate, in DER, at the address certificates point topublic
The certificate revocation listpublic
OCSP, by GET and by POSTpublic
Readiness, covering the database and the root CApublic
Request a certificate; approve a pending requesttoken
Revoke a certificate with an RFC 5280 reasontoken
Export a certificate, its chain, and its key as PKCS#12token
Log in; manage users, roles, passwords, and access tokenstoken, by role
What the CA exposes, and who may use it.

Any standard client can query the responder. Here is the kind of check a validator runs, with a placeholder host:

bash
openssl ocsp -issuer issuing-ca.pem -cert signer.pem \
  -url http://ca.example.org/ocsp \
  -CAfile root-ca.pem -resp_text

Standards

StandardWhat it guarantees here
X.509 v3 and RFC 5280certificates and revocation lists that any validator can parse and walk to the root
RFC 6960, OCSPrevocation answers by GET or POST, with nonces, from a delegated responder
RFC 7292, PKCS#12one encrypted bundle with the key, the certificate, and the chain without its trust anchor
PKCS#10certificate requests verified before signing
Authority Information Access and CRL Distribution Pointscertificates that tell a validator where to find the issuer and the revocation status
JSON Web Tokens with RS256access tokens signed with a key the CA can rotate
W3C Trace Context and OpenTelemetrytraces, logs, and metrics that continue the caller's trace
The standards the CA follows, and what each one guarantees.

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

InputsCertificate requests, approvals, revocations, and exports from the signing application; OCSP queries and list downloads from any validator
OutputsX.509 v3 certificates, PKCS#12 bundles, signed OCSP answers, and version 2 revocation lists
KeysRSA 4096 for signers, the responder, and the CAs; SHA-256 by default
ValiditySigners three years; responder renewed automatically; OCSP answers and revocation lists one day
StoragePostgreSQL, with migrations shipped inside the image
Verified withOpenSSL's OCSP and CRL tools and the PDF readers that validate the signatures
ObservabilityOpenTelemetry traces, logs, metrics, and audit events over OTLP
Runs asA container image, as a non-root user, with configuration mounted from a volume
What the CA takes, returns, and needs.

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

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A central core holding people, organisations, documents, and certificates, connected to eight surrounding services for storage, addresses, certificates, timestamps, the root, signing, thumbnails, and face challenges

The Laravel core of a document-signing platform: it owns users, identities, organisations, client systems, documents and their signed versions, stamps, and certificates, and orchestrates the services that sign, timestamp, render, and store — without ever holding a file.

9 min read Read more →
A locked offline host holding the root key, a signed revocation list crossing an air gap to a small publisher server, which serves the root certificate and the list to a validator

A tiny publish-only service that keeps a root CA reachable without making it touchable: it serves the root certificate and its revocation list, while the root key and every signature stay on an offline host — reusable by any project with its own CA.

7 min read Read more →
A signed PDF with a QR stamp, wrapped in four nested frames for the signature, its timestamp, the embedded validation data, and the archive timestamp

A Python service that signs PDFs to PAdES B-LTA with a visible stamp — embedding the timestamp, certificates, and revocation proof, then an archive timestamp — so signatures stay verifiable long after their certificates expire, and verifies signed PDFs.

10 min read Read more →
A document's fingerprint passing through a clock, signed by a certificate below it, and returning as a time seal stamped on the same document

A Rust timestamp authority that stamps a document's hash with a signed, NTP-backed time per RFC 3161, so PDF signatures stay verifiable after their certificates expire — issuing and renewing its own signing certificate, with public OCSP and CRL.

9 min read Read more →
A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles

A memory chip holding a key, circled by refresh arrows, beside a browser window where a signature stamp is being dragged into place on a PDF page

The Vue browser application of a document-signing platform: sessions held in memory and renewed by one queued refresh, PKCE sign-in, stamps placed precisely on PDF pages, identity checks with a face challenge, client consent, and role-based administration.

10 min read Read more →
A central core holding people, organisations, documents, and certificates, connected to eight surrounding services for storage, addresses, certificates, timestamps, the root, signing, thumbnails, and face challenges

The Laravel core of a document-signing platform: it owns users, identities, organisations, client systems, documents and their signed versions, stamps, and certificates, and orchestrates the services that sign, timestamp, render, and store — without ever holding a file.

9 min read Read more →
A signed PDF with a QR stamp, wrapped in four nested frames for the signature, its timestamp, the embedded validation data, and the archive timestamp

A Python service that signs PDFs to PAdES B-LTA with a visible stamp — embedding the timestamp, certificates, and revocation proof, then an archive timestamp — so signatures stay verifiable long after their certificates expire, and verifies signed PDFs.

10 min read Read more →