Hybrid Signing · GA

Quantum-safe code signing, in a single API call.

PostQ Hybrid Signing is a managed signer that returns a single composite signature combining ML-DSA (FIPS 204) with classical Ed25519. No PQ libraries to compile, no key material to babysit, and verification requires bothhalves so an attacker can’t strip the quantum-safe part.

FIPS 204
ML-DSA standard
AND combiner
Strip-resistant
Per-org KEK
Encrypted at rest
CI exit codes
postq verify → 2
Why hybrid?

Two signatures. One envelope. Both must hold.

Hybrid signatures combine a battle-tested classical algorithm (Ed25519) with a NIST-standardized post-quantum algorithm (ML-DSA). PostQ uses an AND combiner— a future break in either algorithm alone does not let an attacker forge a valid signature.

🔗

AND combiner

Verification requires BOTH the Ed25519 and ML-DSA halves. No silent downgrade. No protocol stripping.

🛡️

Harvest-now-decrypt-later

An attacker capturing signatures today gains nothing when scalable quantum computers arrive — the PQ half holds.

NIST FIPS 204

Backed by the standardized ML-DSA scheme (formerly CRYSTALS-Dilithium), available in three security categories.

How it works

Anatomy of a PostQ signature

When you call pq.sign(), PostQ produces two independent signatures over the same payload bytes and packs them into a single base64-encoded envelope. pq.verify() unpacks the envelope and validates both halves.

1. You upload bytes

Send the raw payload (or a hash of a large artifact) to POST /v1/sign along with the keyId. Payload is base64-encoded on the wire.

2. PostQ signs twice

The managed signer produces an Ed25519 signature and an ML-DSA-65 signature over the same bytes. Private keys never leave PostQ; they’re sealed at rest with a per-organization KEK derived from POSTQ_SIGNING_KEK.

3. You get one opaque string

Both halves are packed into a JSON envelope, base64-encoded, and returned as signature. Treat it as a single opaque token — PostQ owns the format so it can evolve (e.g. add SLH-DSA) without breaking your callers.

4. Verify rejects partial validity

pq.verify() returns ok: false if either half fails. The response also surfaces the per-algorithm booleans (classicalOk, pqOk) so you can log which half broke during incident response. The CLI’s postq verify exits with code 2on any rejection — CI-friendly.

Scenarios

Where teams actually use this

Hybrid signing fits anywhere you already have an Ed25519, RSA, or ECDSA signature today. Below are the four shapes most PostQ customers are wiring up first.

Release engineering

Sign release artifacts in CI

Mint a key once, then sign every tarball, wheel, JAR, or container digest from your build pipeline. Verify before deploy — postq verify exits with code 2 if either half is invalid, so the deploy job fails closed.

# .github/workflows/release.yml
- name: Sign release artifact
  env:
    POSTQ_API_KEY: ${{ secrets.POSTQ_SIGN_KEY }}
  run: |
    postq sign --key ${{ vars.POSTQ_KEY_ID }} \
      --in dist/app-${{ github.ref_name }}.tar.gz \
      --out dist/app-${{ github.ref_name }}.sig

- name: Upload artifact + signature
  uses: actions/upload-artifact@v4
  with:
    name: release
    path: |
      dist/app-*.tar.gz
      dist/app-*.sig
Software supply chain

Verify dependencies before install

Mirror an internal registry (npm, PyPI, container, Helm). For each tarball, store a PostQ hybrid signature. Your install scripts call /v1/verify before extracting — quantum-safe attestation without rewriting your registry.

# install.sh
set -euo pipefail
curl -fsSLO https://mirror.internal/pkg-1.4.2.tgz
curl -fsSLO https://mirror.internal/pkg-1.4.2.tgz.sig

postq verify \
  --key $PROD_KEY_ID \
  --in pkg-1.4.2.tgz \
  --sig pkg-1.4.2.tgz.sig
# exit 2 → script halts, deploy aborted

tar xf pkg-1.4.2.tgz
Document & contract signing

Sign PDFs, JWTs, audit logs

Use the SDK from your application layer to sign anything where today you'd reach for RSA-PSS or Ed25519. The output is a single base64 string — drop it in a database column, JSON field, or HTTP header.

// app/api/contracts/[id]/sign/route.ts
import { PostQ } from "@postq/sdk";
const pq = new PostQ({ apiKey: process.env.POSTQ_API_KEY! });

export async function POST(req: Request) {
  const pdf = await req.arrayBuffer();
  const { signature, algorithm } = await pq.sign({
    keyId: process.env.CONTRACT_KEY_ID!,
    payload: new Uint8Array(pdf),
  });
  // store signature alongside the document
  return Response.json({ signature, algorithm });
}
Webhook & API integrity

Quantum-safe outbound webhooks

Sign every webhook body so your customers can verify it's really from you — and stay verifiable when their auditors start asking about PQ. They verify with a public key (offline) or by calling /v1/verify.

from postq import PostQ
import json, requests

pq = PostQ(api_key=os.environ["POSTQ_API_KEY"])

def send_webhook(url: str, event: dict):
    body = json.dumps(event, sort_keys=True).encode()
    sig = pq.sign(key_id=WEBHOOK_KEY_ID, payload=body)
    requests.post(url, data=body, headers={
        "X-PostQ-Signature": sig.signature,
        "X-PostQ-Key-Id":    WEBHOOK_KEY_ID,
        "Content-Type":      "application/json",
    })
Quickstart

Three lines to a quantum-safe signature

Pick your language. Same three calls everywhere: hybridKeys.create, sign, verify.

TypeScript / JavaScript

import { PostQ } from "@postq/sdk";

const pq = new PostQ({ apiKey: process.env.POSTQ_API_KEY! });

const key = await pq.hybridKeys.create({
  name: "release-signing",
  algorithm: "mldsa65+ed25519",
});

const { signature } = await pq.sign({
  keyId: key.id,
  payload: new TextEncoder().encode("ship it"),
});

const result = await pq.verify({
  keyId: key.id,
  payload: "ship it",
  signature,
});
// result.ok === true

Python

from postq import PostQ

pq = PostQ(api_key=os.environ["POSTQ_API_KEY"])

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,
).ok

.NET

using PostQ.Sdk;

using var pq = new PostQClient(new() { ApiKey = "..." });

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,
});

CLI (CI gate)

# one-time
postq keys create --name release-signing

# in CI
postq sign --key <key-id> \
  --in dist/app.tar.gz \
  --out dist/app.sig

# pre-deploy gate (exits 2 on failure)
postq verify --key <key-id> \
  --in dist/app.tar.gz \
  --sig dist/app.sig
Algorithms

Supported algorithm pairs

All pairs use Ed25519 as the classical half. Pick the ML-DSA category that matches your threat model — higher categories cost a few hundred bytes more per signature.

IdentifierPQ algorithmNIST levelSig size
mldsa44+ed25519ML-DSA-44Cat. 2~2.5 KB
mldsa65+ed25519ML-DSA-65 (default)Cat. 3~3.4 KB
mldsa87+ed25519ML-DSA-87Cat. 5~4.7 KB

Sizes include both halves plus the JSON envelope, base64-encoded. SLH-DSA pairs are on the roadmap for hash-based fallback.

Security model

What the threat model covers

An attacker who breaks Ed25519 alone cannot forge — ML-DSA still validates.

An attacker with a CRQC who breaks ML-DSA alone cannot forge — Ed25519 still validates.

Signatures captured today remain unforgeable when CRQCs arrive.

Strip-attacks on the PQ half are rejected by the AND combiner.

Private keys never leave PostQ — sealed at rest with a per-org KEK.

API keys are scoped: sign:read for verify, sign:write for sign.

!

If both Ed25519 and ML-DSA are broken simultaneously, no scheme survives. (No scheme on earth does.)

!

If your POSTQ_API_KEY with sign:write leaks, an attacker can sign on your behalf — rotate keys via the dashboard.

API surface

Six endpoints. Stable contract.

POST

/v1/hybrid-keys

Mint a new managed signing key. Returns id, name, algorithm, publicKey.

GET

/v1/hybrid-keys

List all keys for your org. Pass ?include_revoked=true to include revoked.

GET

/v1/hybrid-keys/:id

Fetch a single key, including the public key for offline verification.

DELETE

/v1/hybrid-keys/:id

Revoke a key. Past signatures stay verifiable; new sign calls are rejected.

POST

/v1/sign

Sign a base64 payload. Returns the composite signature as a base64 string.

POST

/v1/verify

Verify a signature. Returns {ok, classicalOk, pqOk, algorithm}.

Full request/response schemas live in the docs and the published OpenAPI spec.

FAQ

Common questions

Do I need to install a PQ library to use this?

No. The SDKs and CLI talk to /v1/sign and /v1/verify over HTTPS. The actual ML-DSA implementation lives server-side — you stay on standard HTTP. That's the entire point of the managed signer.

Can I verify signatures offline?

Yes — the GET /v1/hybrid-keys/:id endpoint returns the public key. Anyone with that public key can verify the Ed25519 half today; full offline ML-DSA verification needs an ML-DSA library on the verifier (we'll ship one in the postq-verifier companion binary).

How big is a hybrid signature?

Around 3.4 KB base64-encoded for the default mldsa65+ed25519 pair. Fine for release artifacts, container digests, audit log entries, and webhooks. Probably overkill for individual JWT claims — sign a batch instead.

What happens if ML-DSA gets broken next year?

Nothing immediate — Ed25519 still validates every existing signature. We rotate the algorithm identifier (e.g. to slhdsa-shake-128s+ed25519) and you mint new keys with the new pair. Old signatures remain verifiable as long as you keep the old keys around.

Can I bring my own key material?

Not yet. PostQ generates and seals keys today. BYOK / external HSM integration is on the roadmap for regulated industries.

How are keys protected at rest?

Both the ML-DSA and Ed25519 private keys are encrypted with a per-organization KEK derived from POSTQ_SIGNING_KEK using HKDF. KEK rotation is API-driven and re-wraps existing keys without re-issuing them.

Does this work with my existing PKI?

Hybrid signing is detached signing — it doesn't issue X.509 certs (yet). Pair it with your existing PKI for transport (TLS) and use PostQ for artifact and message signing. Hybrid X.509 is on the longer-term roadmap.

What's the rate limit?

10 req/sec per API key on the free tier; unlimited on Team and Enterprise. Need higher burst? Talk to us.

Sign your next release quantum-safe.

Hybrid signing is live in the JS, Python, and .NET SDKs (v0.4.0+), the postq CLI (v0.3.0+), and the REST API at https://api.postq.dev/v1. Mint a key from the dashboard or the CLI — no migration required.