Shipping the PostQ Hybrid Signer
ML-DSA + Ed25519 in three SDK calls. The first PostQ remediation product is live.
Up to now PostQ has been a discovery story. The CLI, the scanner, and the in-cluster agent all answer the same question in different shapes: where is quantum-vulnerable cryptography hiding in my stack?They’re good at it, but they leave you holding the bag. Once the dashboard says “you’ve got 47 RSA-2048 signing keys in production,” the next question is the hard one: what do I sign with instead?
Today we’re shipping the answer. The PostQ Hybrid Signer is a managed signing service that lives on the PostQ API. Three endpoints, three SDK calls, one signature that is post-quantum-safe today and survives a break in either Ed25519 OR ML-DSA tomorrow.
What it is
A composite digital signature. Every payload is signed twice over the same bytes:
- Once with Ed25519. Battle-tested, fast, deterministic. Verifies in microseconds on every TLS-capable runtime in existence.
- Once with ML-DSA.NIST’s standardized post-quantum signature scheme (FIPS 204, August 2024). Three parameter sets — ML-DSA-44, -65, -87 — covering NIST security categories 2, 3, and 5. The default is
mldsa65+ed25519(~192-bit classical security).
Both halves get wrapped in a single composite envelope and emitted as one base64 blob. Verification is an AND combiner: both halves must validate or the whole signature is rejected. That’s the part that matters — if ML-DSA gets broken in 2030, every Ed25519 signature in your archive still proves authenticity. If Ed25519 gets broken first (less likely but plausible under an unrelated number-theoretic attack), the ML-DSA half holds. You don’t have to pick which algorithm to trust. You trust the AND.
What it looks like to use
Three calls. Same @postq/sdk you already have for scans — nothing new to install on top of v0.4.
import { PostQ } from "@postq/sdk";
const pq = new PostQ({ apiKey: process.env.POSTQ_API_KEY! });
// 1. Mint a managed signing key
const key = await pq.hybridKeys.create({
name: "release-signing",
algorithm: "mldsa65+ed25519",
});
// 2. Sign — one composite signature, both halves at once
const { signature } = await pq.sign({
keyId: key.id,
payload: new TextEncoder().encode("ship it"),
});
// 3. Verify — AND combiner, both halves must validate
const result = await pq.verify({
keyId: key.id,
payload: "ship it",
signature,
});
console.log(result.ok); // trueSame surface in Python and .NET:
from postq import PostQ
pq = PostQ(api_key="pq_live_…")
key = pq.hybrid_keys.create(name="release-signing")
sig = pq.sign(key_id=key.id, payload=b"ship it")
ok = pq.verify(key_id=key.id, payload=b"ship it",
signature=sig.signature).okusing var pq = new PostQClient(new() { ApiKey = "pq_live_…" });
var key = await pq.HybridKeys.CreateAsync(new() { Name = "release-signing" });
var sig = await pq.SignAsync(new() {
KeyId = key.Id,
Payload = Encoding.UTF8.GetBytes("ship it"),
});
var result = await pq.VerifyAsync(new() {
KeyId = key.Id,
Payload = Encoding.UTF8.GetBytes("ship it"),
Signature = sig.Signature,
});What we built behind the API
Three things had to be right or this product wasn’t shippable:
1. The wire format
Composite signatures are the subject of an active IETF draft (draft-ietf-lamps-pq-composite-sigs). We chose the simplest interoperable shape we could justify: a base64-encoded JSON object holding the algorithm name, a version tag, and both halves as base64. It looks like this:
{
"v": 1,
"alg": "mldsa65+ed25519",
"classical": "<base64 64-byte Ed25519 signature>",
"pq": "<base64 ~3309-byte ML-DSA-65 signature>"
}Public keys use the same envelope. Anyone who has the JSON public key can verify offline — no API call required — using standard Ed25519 plus an ML-DSA library. We picked @noble/post-quantumfor the server-side because it’s pure JavaScript, has no native dependencies, and ships in <30 KB. That meant we could deploy to Render with no extra build steps, and the same library powers any Node-side offline verifier.
2. At-rest secrecy
Private keys never leave PostQ. They live in Postgres as opaque ciphertext: AES-256-GCM with a 96-bit nonce, sealed under a 32-byte key-encryption key (KEK) that lives only in the API process’s environment. The DB row stores [12-byte nonce][ciphertext][16-byte tag] and a kek_version column so we can rotate the KEK without touching every key. A dump of the database is useless without the KEK.
3. Audit, not eavesdrop
Every sign and verify writes a row to a dedicated hybrid_signatures audit table. The row stores the operation type, the API key that called it, the key that signed, and only the SHA-256 of the payload. We never see your plaintext, we never store your plaintext, and your auditor gets a tamper-evident log without us holding leverage on what you signed.
Why an AND combiner (and not OR)
The other option was easier: emit two signatures, accept either. Most early hybrid deployments did it that way because it gives you graceful backwards compatibility. We rejected it. With an OR combiner an attacker who breaks just oneof the two algorithms can forge signatures that verify cleanly — and they get to pick which algorithm. You’ve traded one risk for two.
AND combiners give the opposite property: an attacker has to break both algorithms simultaneously, on the same payload, against the same key. That’s the whole point of hybrid — defence in depth, not a polite handoff. The cost is that PQ-naive verifiers can’t validate at all, but in 2026 that’s not a real cost: the ML-DSA libraries are everywhere, and any code base that needs to verify can install @noble/post-quantum in five seconds.
Where it fits in the PostQ story
Until today, the answer to “PostQ found my RSA-2048 key, now what?” was a remediation guide and a polite shrug. The Hybrid Signer is the first PostQ actionproduct — the first thing you can do with PostQ instead of just see.
It composes naturally with the rest of the platform:
- The CLI tells you a TLS endpoint is signing with RSA-2048; you replace its signing path with
pq.sign()and the next CLI scan reportsmldsa65+ed25519atpqSafe: true. - The K8s agent finds an Ingress with an RSA-signed certificate; you issue the next cert from a CA whose CA chain you sign with the Hybrid Signer.
- Your release pipeline used to sign artifacts with cosign — now it calls
pq.sign(), attaches the composite signature as a sigstore-style envelope, and any verifier in the world can validate it offline against the published composite public key.
What’s next
The follow-ups are already on the board:
- Key rotation primitives. The
rotated_fromcolumn is already in the schema. Soon you’ll be able to rotate a key while keeping the old one live for verifying old signatures. - SLH-DSA (FIPS 205). Stateless hash-based signatures for the most paranoid deployments. Same envelope, new algorithm name.
- JWS / COSE adapters. Sign-and-verify with composite algorithms inside JWT and CBOR Web Token envelopes, so existing JWT verifiers can opt in.
- Self-hosted KEK. Bring-your-own AWS KMS / Azure Key Vault wrap so the PostQ control plane never sees the KEK either.
Try it
npm install @postq/sdk@latest # JS / TS
pip install -U postq-sdk # Python
dotnet add package PostQ.Sdk # .NETMint a key from the dashboard or call POST /v1/hybrid-keys with any API key that has the sign:write scope. The full reference lives in the Hybrid Signing section of the docs.
Three SDKs · postq-sdk-all on GitHub · OpenAPI spec lives at api.postq.dev/openapi.yaml.