If you're running AEGIS or ENLIL, every signed artifact they produce — forensic logs, AI council Decrees — carries an ML-DSA-87 (NIST FIPS 204) signature. That code started as a 179-line module tightly coupled to ENLIL's internal "Decree" object. This week we pulled it out, generalized it, and gave it the pieces every real deployment actually needs: key rotation and framework integration.
Why bother with post-quantum signing today
"Harvest now, decrypt later" is not a hypothetical. Traffic and signed artifacts captured today can be broken once a cryptographically relevant quantum computer exists. For anything with a shelf life measured in years — audit trails, compliance records, forensic logs — the migration window is now, not when NIST says it's urgent.
NIST finalized three post-quantum standards in 2024: ML-KEM (key encapsulation), ML-DSA (digital signatures, formerly CRYSTALS-Dilithium), and SLH-DSA. pqsign wraps ML-DSA-87 — the highest security level — via liboqs, the reference implementation used across the PQC ecosystem.
What was missing from "just call liboqs"
liboqs-python gives you the primitive. It doesn't give you:
-
Key persistence and rotation. Real services rotate signing keys. Naive implementations either don't support rotation, or rotation silently invalidates every signature made before it.
pqsignkeeps a small history of keys — rotating gives you a new active signer while every old key stays available to verify what it already signed. -
A one-line API for arbitrary payloads. Most examples in the wild sign raw bytes.
pqsign.sign(any_json_serializable_object)canonicalizes and signs it in one call. - Framework integration. If you're serving an API, you want responses signed and incoming requests verified without writing that plumbing yourself.
FastAPI in three lines
from fastapi import FastAPI, Depends
from pqsign.fastapi_ext import PQSignMiddleware, require_pq_signature
app = FastAPI()
app.add_middleware(PQSignMiddleware) # signs every outgoing response
@app.post("/webhook")
async def webhook(_ok: bool = Depends(require_pq_signature)):
... # only runs if the incoming request is signed and valid
Every response gets X-PQ-Signature / X-PQ-Key-Id headers. Unsigned or tampered requests to /webhook get a 401 automatically. Flask gets the equivalent as a decorator (@require_pq_signature) plus a sign_response() helper.
Key rotation without breaking old signatures
sig_old, kid_old = pqsign.sign(b"signed last month")
pqsign.rotate() # new active key
# still verifiable, using the key_id it was actually signed with
pqsign.verify(b"signed last month", sig_old, kid_old) # True
This is the part that's easy to get wrong and annoying to retrofit later — worth having solved before you need it.
Verified, not just written
7 unit tests (signing, tampering detection, persistence across restarts, rotation). Beyond that, we ran a real FastAPI server with the middleware attached: signed responses with real X-PQ-Signature headers, unsigned requests rejected with 401, correctly-signed requests accepted with 200. Caught and fixed a real FastAPI dependency-injection bug in the process (a type-annotation issue that broke server startup) before calling it done.
Where to get it
pqsign — MIT licensed, private repo, delivered via Polar ($39, one-time, includes repo access).
Built as a byproduct of running AEGIS (post-quantum cyber-defense IDS) and ENLIL (multi-model AI council with signed, auditable output) in production.
Top comments (0)