QertumPost-Quantum Security
ML-DSA-65 / 87 — FIPS 204 signatures

Run the whole
chain of trust yourself

Qertum is a self-hosted certificate authority. Issue, track, renew and revoke X.509 certificates from a web console, a REST API, the qca CLI or any standard ACME client — signed with RSA, ECDSA or ML-DSA. One container, one SQLite file, no cloud dependency.

Get it runningdocker compose -f deploy/single-container/docker-compose.yml up -d
MIT licensedSelf-hostedSQLite — no external database
qca — bash
# Sign in with the device authorization grant
$ qca auth login
   Open https://ca.example.internal/device and enter FJKD-QMTP
   Signed in as alice (admin)

# Request a post-quantum web server certificate
$ qca requests create -n api.example.internal -T WebServer -k ML-DSA-65

  ID        6f1c0b3e
  STATUS    Pending

$ qca requests approve 6f1c0b3e
   Approved — queued for signing

$ qca cert download-cert 6f1c0b3e -F pem
   Wrote api.example.internal.pem
Why now

The migration is
the hard part.

A quantum computer that can forge an RSA signature does not exist yet. The deadline is still real, because replacing a PKI takes years — every root, every intermediate, every leaf, and every client that has to verify the new algorithm before you can stop issuing the old one.

So the useful thing an authority can do today is refuse to make you choose. Qertum signs with RSA, ECDSA and ML-DSA from the same root, and hybrid mode issues a classical and a post-quantum certificate together — you can start moving before every client is ready.

See what it does
2020

NIST's draft timeline deprecates RSA and ECDSA for digital signatures after this year

2025

And disallows them entirely after this one — for every certificate still in the chain

0

Certificates from a single request in hybrid mode, so the cutover needs no flag day

Capabilities

A complete CA, ready for what's next

Everything a production certificate authority needs to do, on hardware you control — plus the NIST post-quantum signatures, ready when you are.

The full request lifecycle

Pending, approved, processing, issued — with rejection, renewal and asynchronous revocation. Five certificate templates ship built in (web server, code signing, S/MIME, client auth, subordinate CA), permissions are per role, and every transition lands in an immutable audit log.

ACME and EST, built in

RFC 8555 at /acme — http-01 and dns-01 proofs, wildcards and external account binding — so certbot, cert-manager and lego renew against it unchanged. RFC 7030 enrollment lives at /.well-known/est, with Basic auth for simpleenroll and client-certificate mTLS for simplereenroll.

Revocation that publishes

A CRL at /crl and an OCSP responder at /ocsp, both public and both unauthenticated by design. Delta CRLs are optional, so a large revocation list does not have to be refetched in full to stay current.

Post-quantum — and the bridge to it

ML-DSA-65 and ML-DSA-87 (FIPS 204) sit beside RSA and ECDSA on the same authority. Hybrid mode is paired issuance: one request produces two certificates, one classical and one post-quantum, sharing a validity window — so you can deploy PQC without waiting for every client to understand it.

Private keys stay private

Keys are encrypted at rest with ASP.NET Data Protection, never appear in list responses, and every download is audited. Certificates enrolled through ACME or EST are signed straight from the client's CSR, so for those the authority never holds a private key at all.

Webhooks and your own identity provider

HMAC-SHA256 signed lifecycle webhooks are delivered through a retrying transactional outbox, so a listener that is down does not lose an event. Sign-in is OIDC: use the bundled provider, or point Qertum at Keycloak, Entra or anything else that speaks the spec.

Standards-based cryptography

Five algorithms, one authority

No experimental ciphers. The finalized FIPS 204 signatures alongside the classical algorithms your clients already verify — chosen per certificate, not per deployment.

ML-DSA-65

Dilithium3 · security level 3
Post-quantum signature

The default post-quantum choice for leaf certificates. Lattice-based, fast to verify, ~1.9 KB public key.

StandardFIPS 204

ML-DSA-87

Dilithium5 · security level 5
Post-quantum signature

The higher security margin, for roots and long-lived intermediates. ~2.6 KB public key, ~4.6 KB signature.

StandardFIPS 204

Hybrid

Paired issuance
Classical + post-quantum

ECDSA P-256 + ML-DSA-65, or P-384 + ML-DSA-87. One request yields two certificates sharing a validity window — serve the classical one to clients that need it and the PQC one to those that do not.

StandardTwo certificates

ECDSA

NIST P-256 / P-384
Classical signature

Compact keys and signatures, understood by everything. The sensible default until a client can verify ML-DSA.

StandardRFC 5480

RSA

2048-bit leaf · 4096-bit CA
Classical signature

The broadest compatibility floor, for the appliance or embedded client that will never learn a newer algorithm.

StandardRFC 8017
  • ML-DSA is signature-only: certificates issued under it carry digital signature, non-repudiation and CA key usages, never key encipherment or key agreement.
  • Hybrid is a leaf-and-intermediate mode. A root CA is generated as RSA, ECDSA, ML-DSA-65 or ML-DSA-87 — not as a hybrid pair.
Interactive

What post-quantum costs your handshake

Pick an algorithm for each link in the chain and see what actually goes over the wire. Sizes are the FIPS 204 final values, and the only options are the ones Qertum can really sign with.

Reading this:

Only the leaf and intermediate travel in the handshake — the root is already in the client's trust store. Classical certificates fit in a few hundred bytes; ML-DSA ones need several kilobytes, which is what pushes a chain past the congestion window.

Hybrid mode does not appear here on purpose. Qertum's hybrid is paired issuance — one request produces two separate certificates, and a client is served one chain or the other, never both. The handshake cost is whichever half it receives.

Transmitted Chain Size13,040 bytes
Fits the initial window
TLS handshake transmission (leaf + intermediate)
Intermediate CA certificate7,179 bytes
Pub key (1,952 bytes)
Sig (4,627 bytes)
Leaf certificate (api.example.internal)5,861 bytes
Pub key (1,952 bytes)
Sig (3,309 bytes)

Network diagnostics

TCP packet count9 packets
Estimated RTT overhead+0 RTT (no extra round trip)
QUIC/UDP drop riskMinimal

The transmitted chain is 12.7 KB, inside the ~14.6 KB TCP initial congestion window, so the server sends it without waiting for an acknowledgement. A fully post-quantum chain still fits — ML-DSA is large compared to ECDSA, but not large enough to cost a round trip at these security levels.

Handshake overhead comparison

All-classical baseline (ECDSA P-256)1,474 bytes
1.0x (ref)
Current configuration13,040 bytes
8.8x size
Heaviest supported (ML-DSA-87 throughout)15,638 bytes
10.6x size

Assumptions: 600 bytes of certificate metadata per certificate (subject, validity, SANs, extensions), a 1460-byte TCP MSS, and a 10-segment initial congestion window (~14.6 KB). Real certificates vary with the number of SANs and whether CT signed certificate timestamps are embedded.

Deploy

One container, one volume, one afternoon

Built for the homelab and the small network: no external database, no message broker, no cloud account. Back up /data and you have backed up the authority.

Run the container

One image supervises all five services — web console, identity provider, REST API, ACME and EST endpoints, and the signing worker — sharing a single /data volume.

Create your root

First launch redirects an admin to onboarding. Generate a root as RSA-4096, ECDSA, ML-DSA-65 or ML-DSA-87 — or import an existing one from PEM, PKCS#7 or PKCS#12.

Issue and automate

Approve requests from the console or the qca CLI, or let ACME and EST clients enroll themselves. Renewal, revocation and the CRL take care of themselves.

# The compose file lives in the repo — set QERTUM_HOST to the address
# browsers and ACME clients will use, then bring it up.
$ docker compose -f deploy/single-container/docker-compose.yml pull
$ docker compose -f deploy/single-container/docker-compose.yml up -d

   web console      :8090
   identity (OIDC)  :8081
   REST API         :8082
   ACME directory   :8083/acme/directory

# State — database, CA keypair, keyring, audit log — lives in /data.
# Device authorization grant — approve in a browser, token cached locally
$ qca auth login

# Hybrid: one request, a classical and a post-quantum certificate
$ qca requests create -n api.example.internal \
    -T WebServer -k HYBRID-ECDSA-P256

$ qca requests approve 6f1c0b3e
$ qca cert download-cert 6f1c0b3e -F pem
$ qca cert download-cert 6f1c0b3e -F pem --pqc
   Both halves of the pair written
# Any RFC 8555 client works unchanged — certbot, cert-manager, lego
$ certbot register \
    --server https://ca.example.internal:8083/acme/directory

$ certbot certonly --standalone \
    --server https://ca.example.internal:8083/acme/directory \
    -d api.example.internal

   http-01 challenge validated
  dns-01, wildcards and external account binding are supported too

Prefer independent containers? The stack is a .NET Aspire solution — generate a multi-container manifest instead. Deployment docs.

Interoperable

Speaks the standards your stack already trusts

Nothing proprietary on the wire. Your existing ACME and EST clients enroll against it unchanged, and the certificates, chains and revocation lists are the ordinary ones.

FIPS 204ML-DSARFC 5280X.509 & CRLRFC 8555ACMERFC 7030ESTRFC 6960OCSPRFC 8628Device grantPKCS#7p7b chainsPKCS#12pfx bundles
MIT licensed

Own your chain
of trust. Forever.

No subscription, no per-certificate pricing, no account with anyone. Run it on the box you already have, keep the root key on a volume you back up yourself, and read the code that signs with it.