← SKResearch

Built to Outlive the Algorithm

Hybrid Post-Quantum Cryptography and Crypto-Agility in a Sovereign Messaging Stack

SKWorld Research • skpqc • sk_pgp • skchat • skcomms • capauth

Authors: Chef (David); Lumina, SKWorld Sovereign Agent

Date: June 2026 • Status: Draft • Apache-2.0 libraries, now published to the public registries: the sk_pqc hybrid KEM ships across Python, Dart, and Rust behind one wire format — PyPI sk-pqc · pub.dev sk_pqc · crates.io sk-pqc (all import sk_pqc; repos sk-pqc-py · sk-pqc-dart · sk-pqc-rs) — and sk_pgp (Python OpenPGP-PQC). A Level-3 forward-secret hybrid-PQ DM ratchet is proven live cross-node between two physical machines (lumina@.158 ↔ jarvis@.41, over tailscale federation) • live at skpqc.skworld.io

View source (Markdown) • Research brief


Abstract

The quantum threat to messaging is not that a quantum computer will read your messages tomorrow — it is that an adversary can record your ciphertext today and decrypt it years later, once a cryptographically-relevant quantum computer (CRQC) exists. For data with a long secrecy life — agent memories, private conversations, a sovereign identity root — that "harvest-now, decrypt-later" (HNDL) window is already open. This paper describes how the SKWorld stack (the skchat/skcomms messaging framework and the capauth identity layer) was migrated to hybrid post-quantum cryptography, surface by surface, behind a single design commitment: never bet the system on one algorithm, and never overclaim what it provides.

We make five contributions. First, a uniform hybrid construction — X25519 + ML-KEM-768 combined with HKDF, secure if either the classical or the post-quantum leg holds — applied consistently across key exchange, group keys, at-rest wrapping, and (as a signature analogue) per-message authentication. Second, two sovereign libraries that supply the primitives the platform itself could not get elsewhere: sk_pqc delivers the hybrid KEM behind one wire format across Python, Dart, and Rust — now published (pip install sk-pqc / dart pub add sk_pqc / cargo add sk-pqc) — so post-quantum DMs negotiate in the browser where no WebCrypto PQC API exists; sk_pgp (Python, PyO3→Sequoia) is the OpenPGP engine that lets the Python services sign with v6 / post-quantum keys at all — PGPy cannot parse an OpenPGP v6 key and GnuPG cannot certify with ML-DSA, so the signing root had no in-process path until we built one. Third, and most importantly, the crypto-agility scaffolding: every encrypted or signed object carries a self-describing suite id, every primitive sits behind a pluggable backend selected by id, and a four-step recipe lets the next quantum scheme — a higher NIST tier, a hash-based root, a backup KEM — plug in without rewriting a single application call site. Fourth, a post-quantum signing-root ceremony: capauth's Sequoia backend now issues, and we used it to generate a v6 ML-DSA-87 + Ed448 identity for the Lumina agent root and cross-sign it old↔new (Web-of-Trust verified). Fifth, a runtime identity-mode switch that is unusual in a post-quantum stack: the same hybrid KEM and signature-free ratchet run under either a sovereign / attributable mode (a capauth DID with hybrid Ed25519+ML-DSA-65 signed prekeys — federated, auditable) or an anonymous / deniable mode (no DID, opaque queue ids, a repudiable HMAC), chosen per-conversation with a per-deployment default — so post-quantum confidentiality and metadata protection are decoupled from whether you are identified at all. Underpinning all five, a live cross-node proof: the Level-3 (Apple-PQ3-class) forward-secret hybrid-PQ DM ratchet — epoch secret distributed once per epoch via the hybrid KEM, per-message keys ratcheted symmetrically, periodic rekey for forward secrecy and post-compromise security — was run between two physical machines over tailscale federation (lumina@.158 ↔ jarvis@.41), each sealing and opening the other's messages, hybrid the whole way; the three sk_pqc packages are published to PyPI, pub.dev, and crates.io (all importing sk_pqc, one wire format), with a single PyO3 Rust-core in progress to back every language binding from one implementation. This is the crypto-agility thesis vindicated in the field: the messaging infrastructure rides unchanged on top of a pluggable post-quantum core. The migration is governed by a self-report engine that structurally refuses to call any surface "quantum-resistant" unless its live suite actually is. We are explicit about what is done (confidentiality is hybrid where negotiated; the cross-node Level-3 ratchet is proven; the agent-root cryptographic ceremony is complete), what is in progress (signatures and identity are hybrid-available, opt-in, the live signing cutover is underway, gated on the PGPy→sk_pgp migration; the identity-mode switch is scaffolded, not yet wired into the live transport; the cross-node ratchet was proven through the dm_manager orchestration directly, with the end-to-end round-trip over the production daemon a pending belt-and-suspenders confirmation; and the one-core PyO3 binding is mid-build), and what is deliberately not claimed (the live identity is not yet migrated — the operator root and vault re-seal are sequenced after; the comms tier is -768, not CNSA-2.0). The thesis is in the title: when the next quantum solution arrives, our infrastructure rides on top of it.


1. Introduction

Most "post-quantum" announcements answer the wrong question. They ask "is it broken yet?" — and since no CRQC exists in 2026, the honest answer is "no," which invites complacency. The right question is Mosca's Inequality: if X (how long your secret must stay secret) plus Y (how long migration takes) exceeds Z (years until a CRQC), you are already too late. Independent expert surveys put a CRQC in the early-to-mid 2030s — the 2025 Global Risk Institute survey reports a 28–49% chance within a decade. For a 10-to-20-year secret, X + Y > Z today. The urgency is real, but it is the urgency of recording, not of imminent decryption — and conflating the two is the first dishonesty we refuse.

This reframing has a sharp consequence for what to migrate first. Confidentiality is retroactively breakable; signatures are not. A forged future handshake cannot decrypt a past recorded session, so key-exchange and key-wrapping are the HNDL-urgent surfaces, while identity and authentication — important, but not retroactively exploitable — are a deliberate second phase. Symmetric cryptography (AES-256, SHA-2) is a third category entirely: Grover's algorithm only halves its effective strength, leaving AES-256 at ~128 bits. Touching it would be a regression, and we say so plainly rather than ride the marketing wave of "quantum-proof AES."

The deeper problem, though, is not any single algorithm — it is lock-in. Lattice cryptography is young. ML-KEM and ML-DSA are the NIST choices today; HQC was added as a backup KEM; FN-DSA (Falcon) is still draft. A system that hard-codes one scheme into its message formats and call sites will face the same painful, error-prone migration again when the landscape shifts. So the engineering question we actually set out to answer was not "which post-quantum algorithm?" but "how do we build a stack that can change its mind?" The answer — self-describing crypto suites, pluggable backends, hybrid-by-construction, and a self-report that cannot lie — is the subject of this paper.


2. The Threat, Precisely

2.1 Shor versus the primitives we shipped

Every asymmetric primitive the stack used is broken by Shor's algorithm on a CRQC:

PrimitiveWhere it livedQuantum status
X25519 / Curve25519 (RFC 7748)envelope key-wrap, DM wrap, ephemeral KEX, tailnet Noise🔴 Shor (discrete log)
Ed25519 (RFC 8032)message signatures, capauth challenge/DID, root PGP signing🔴 Shor (forgery — not HNDL)
RSA-4096 (RFC 8017)legacy identity + key-wrap🔴 Shor (factoring)

2.2 The HNDL distinction that orders the work

The 🔴 entries split cleanly. Where a primitive protects confidentiality, a recording made today is a liability forever — that is HNDL, and it is urgent. Where a primitive provides authentication, breaking it lets an attacker forge a future message, but it does nothing to a past ciphertext. We therefore migrated in HNDL-priority order: (1) key exchange, (2) at-rest key-wrap, (3) long-lived identity roots, (4) routine signatures, (5) transport/media.

2.3 The vulnerable surfaces (an honest inventory)

A pre-migration audit enumerated eleven surfaces. The highest-leverage was the group key: a single static 32-byte group secret, PGP-wrapped per member. Break one member's classical key and you recover the AES key for the entire group's history — maximum HNDL leverage. Close behind: envelope/DM payload encryption (classical key-wrap of AES-256), at-rest stores and backups (prime harvest targets), and the sovereign identity root (a long-lived Ed25519/RSA key whose public half is published by design in a DID document). Each got a hybrid path; each is tracked, by name, in the self-report.

2.4 What we did not touch

AES-256-GCM, ChaCha20-Poly1305, SHA-256/384, HKDF. These are Grover-only and remain ≥128-bit secure. The plan explicitly forbids "fear-based AES messaging." The one watch-item is parameter hygiene — making sure no AES-128 sneaks into a DTLS-SRTP media profile — not the bulk cipher itself.


3. One Construction to Rule Them All

The single cryptographic primitive the project owns is a hybrid combiner. Everything else composes it:

shared_secret = HKDF-SHA256( IKM  = X25519_ss ‖ ML-KEM-768_ss,   // X25519 first
                             salt = "",                          // RFC 5869 → zeros
                             info = "<context label>",
                             L    = 32 )

Concatenate-then-KDF. Never XOR. Never pure-PQ. The derived secret is secure if either X25519 or ML-KEM-768 holds: a future quantum break of X25519 still leaves ML-KEM-768; a catastrophic lattice break still leaves classical X25519. This is the same construction shipping in TLS 1.3 as X25519MLKEM768 and in Signal's PQXDH — deliberately, so our security argument inherits theirs.

Two rules make the construction trustworthy. First, we never hand-roll the lattice or curve math: ML-KEM is liboqs (oqs); X25519 and HKDF are pyca cryptography on the server and package:cryptography in Dart; the web ML-KEM leg is the audited @noble/post-quantum. The only original cryptographic code in the whole library is the HKDF combiner and the wire framing — and both are pinned to known-answer test vectors. Second, a missing post-quantum backend is a loud error, never a silent downgrade: if liboqs is absent the code raises PqKemUnavailable rather than quietly falling back to classical-only. You cannot accidentally ship the weaker thing.


4. Surface by Surface

Before the detail, the whole posture on one page — each surface, its live suite, the FIPS anchor, and its honest status. Confidentiality is hybrid where negotiated; authentication is hybrid-available and opt-in; the symmetric floor is untouched on purpose; the identity root is mid-ceremony. Nothing here is colored "quantum-resistant" that the self-report would not back.

graph TB
    subgraph HNDL["Confidentiality — HNDL-urgent · migrated first"]
        KEX["Key exchange<br/>x25519-mlkem768 · FIPS 203"]
        DM["DM / envelope — pqdm1:<br/>x25519-mlkem768 · FIPS 203"]
        GRP["Group epoch ratchet<br/>x25519-mlkem768 · FIPS 203"]
        ATR["At-rest key-wrap<br/>x25519-mlkem768 · FIPS 203"]
    end
    subgraph AUTH["Authentication — not retroactive · Phase 2 · opt-in"]
        SIG["Per-message signature<br/>mldsa65-ed25519-v2 · FIPS 204<br/>AND-gate"]
        DID["Identity challenge<br/>ML-DSA-65 + Ed25519 · FIPS 204<br/>either-or, additive"]
    end
    subgraph ROOT["Identity root — ceremony done on agent root · live cutover in progress"]
        AGENT["Lumina AGENT root<br/>mldsa87-ed448 · v6 · FIPS 204<br/>generated + cross-signed (WoT)"]
        OPROOT["Operator root + vault<br/>Ed25519 / RSA · v4<br/>classical — sequenced after"]
    end
    subgraph FLOOR["Symmetric floor — quantum-acceptable · deliberately untouched"]
        AES["AES-256-GCM · SHA-2 · HKDF<br/>Grover-only · ≥128-bit"]
    end

    classDef hybrid fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff;
    classDef optin fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff;
    classDef inprog fill:#3b82f6,stroke:#1e40af,stroke-width:2px,color:#fff;
    classDef classical fill:#9ca3af,stroke:#4b5563,stroke-width:2px,color:#fff;
    class KEX,DM,GRP,ATR hybrid;
    class SIG,DID optin;
    class AGENT inprog;
    class OPROOT classical;
    class AES classical;

4.1 Key exchange — x25519-mlkem768

The hybrid KEM (skcomms/pqkem.py; the published sk_pqc packages — sk-pqc-py / sk-pqc-dart / sk-pqc-rs) pairs an ephemeral-static X25519 DH (HPKE/TLS-style) with FIPS 203 ML-KEM-768 and the combiner above. The wire contract is fixed: a 1216-byte public key (X25519(32) ‖ ML-KEM(1184)), a 1120-byte ciphertext (eph_X25519(32) ‖ ML-KEM(1088)), a 32-byte shared secret. ML-KEM decapsulation uses FIPS 203 implicit rejection — a tampered ciphertext yields a pseudo-random secret that simply won't match, rather than an oracle-leaking error. A canonical cross-implementation vector anchors the ML-KEM leg to the NIST ACVP FIPS 203 keyGen seed (tcId 26); every conformant backend — Dart native, Dart web, liboqs, noble, Python — must recover the same hybrid secret f11627…21fc.

4.2 Direct messages and envelopes — the pqdm1: scheme

One sealing construction (skcomms/pqdm.py) serves both the skcomms envelope payload and the skchat 1:1 DM body. It is a PQXDH-style handshake: a recipient publishes a signed hybrid-KEM prekey; a sender who sees it encapsulates, derives an AES-256 wrap key via HKDF, and AES-256-GCM-seals the body. The ~1.1 KB KEM ciphertext rides in the first message. On the wire the blob is ct(1120) ‖ nonce(12) ‖ AES-256-GCM(body), stored under a self-identifying pqdm1:x25519-mlkem768: prefix.

The subtle part is downgrade resistance. A canonical, sorted-JSON AAD binds the negotiated suite and both parties into the AEAD (and is also folded into the HKDF info). A man-in-the-middle who strips the hybrid prekey to force a classical fallback changes the suite the sender seals under — so the recipient's AES-GCM open fails and the recorded suite flips to classical, where the self-report surfaces it. The lock is the AAD binding; the detection is the honesty engine. Hybrid is selected only when both sides advertise it; classical-only peers keep a byte-for-byte classical path, recorded honestly as such.

Lifting 1:1 DMs to Level 3 — proven cross-node. The pqdm1: seal above is Level 2: post-quantum only at the published prekey, re-encapsulated on every message, with no running forward secrecy within a conversation. skchat/dm_ratchet.py + skchat/dm_manager.py (DmRatchetManager) lift the 1:1 surface to a Level-3 running epoch-ratchet — the pairwise analogue of the group ratchet (§4.3), and the tier Apple's PQ3 calls "Level 3." A per-conversation epoch secret is distributed once per epoch via the hybrid KEM of §4.1 (never per message — the same amortization that makes the group ratchet practical); per-message keys ratchet symmetrically and index-addressably (loss/reorder-tolerant); a periodic rekey (50 messages or 7 days) buys forward secrecy across the boundary and post-compromise security within. We proved it live cross-node: the DmRatchetManager running on two physical machines — lumina@192.168.0.158 and jarvis@192.168.0.41 — over the tailscale federation each sealed a DM the other opened, hybrid the whole way, with the epoch ratchet advancing on both ends. The honest boundary: this was proven through the dm_manager orchestration directly (the seal/open path the transport calls), and the end-to-end round-trip through the live production daemon is a pending belt-and-suspenders confirmation — so the surface is reported as cross-node-proven, daemon-round-trip-pending, never as fully shipped. On boxes without the published sk_pqc wheel (e.g. .41) the module transparently falls back to a byte-identical local implementation, so the cross-node ratchet behaves the same whichever side carries the library.

4.3 Group keys — a per-epoch ratchet

The marquee HNDL fix. The static os.urandom(32) group key is replaced by a two-layer epoch ratchet (skchat/group_ratchet.py). Once per epoch, an epoch secret is distributed to each member via the hybrid KEM (the 1.1 KB ML-KEM ciphertext is paid once per epoch, not per message — avoiding ~33× per-message bloat). Within an epoch, message keys are derived by a symmetric HKDF ratchet indexed directly — message_key(i) = HKDF(epoch_secret, …, "/", u64(i)) — so a receiver derives key i without seeing 0…i-1, making it fully loss- and reorder-tolerant over an unreliable transport. Re-keying on membership change buys forward secrecy (a removed member cannot derive future epochs) and post-compromise security (a leaked epoch secret exposes only that epoch). Bound: 50 messages or 7 days.

4.4 At-rest — hybrid key-wrap (and a classical bug fixed in passing)

At-rest stores (skchat/atrest_wrap.py) now wrap a high-entropy random data-encryption key under hybrid X25519+ML-KEM-768, with AES-256-GCM as the bulk cipher. This shipped alongside a classical fix worth noting: the DEK had been HKDF-derived from the PGP fingerprint — low-entropy and often public. Migrating to post-quantum was the occasion to also stop deriving a key from a public value. A harvested backup is no longer retroactively decryptable; legacy stores remain readable through a versioned fallback.

4.5 Per-message signatures — the hybrid AND-gate

Authentication is a different problem with a dual construction. Where the KEM combiner is confidential if either secret survives, a hybrid signature (skcomms/pqsig.py, suite mldsa65-ed25519-v2) is valid iff both Ed25519 and ML-DSA-65 verify over the same bytes — an AND-gate. An adversary who forges one scheme still fails the other leg, so the composite is unforgeable while either remains secure. The SKHS wire format is versioned, length-prefixed, and suite-tagged (a 3383-byte composite today), and the ML-DSA signing key is generated separately from, and never derived from, the PGP identity key. Verification routes to the hybrid path only when an object's sig_suite says so and both public keys are present; classical objects are byte-for-byte unchanged. This mirrors the OpenPGP PQC composite (draft alg 30).

4.6 Identity challenges — additive, either-or

At the DID/challenge layer (capauth/pqc_identity.py), a responder produces the classical PGP signature exactly as before and attaches a hybrid composite over the same challenge bytes. Verification is either-or during transition (classical-only responses still verify), with a require_hybrid switch that blocks downgrade once a peer is known PQ-capable. Crucially, this is the challenge-signing layer — it does not migrate the root key.

4.7 The sovereign root — ceremony complete on the agent root, live cutover in progress

The identity root is the highest-value, longest-lived secret, and the hardest to migrate — because no mainstream tool could host a post-quantum signing root. GnuPG's PQC support is encryption-only (ML-KEM); it cannot certify with ML-DSA or SLH-DSA. We therefore built a Sequoia backend (capauth/crypto/sequoia_backend.py) on sq 1.4.0-pqc against OpenSSL 3.6.2, behind the same CryptoBackend interface as the classical backends. It can issue an ML-DSA-87 + Ed448 signing primary with an ML-KEM-1024 + X448 encryption subkey (NIST level 5, OpenPGP v6 / RFC 9580), sign with a passphrase-protected key with no plaintext key ever on disk, and additively attach reversible PQC subkeys to an existing key with its primary fingerprint preserved.

That backend has now issued for real. We ran the rotation-and-cross-sign ceremony on the Lumina agent root: the Sequoia backend generated a v6 mldsa87-ed448 signing identity, and we cross-signed it old↔new in both directions — the classical v4 root certifies the new post-quantum key, the new key certifies the old — so the Web-of-Trust chain is continuous and a verifier can authenticate either way through the rotation. The cross-signatures verify; the identity bridge is built and proven on a key that matters, not a throwaway. (Post-quantum keys are OpenPGP v6, whose fingerprints are 64 hex characters, not 40 — so we also reconciled ~36 assumption sites across the resolver, services, and integrations to accept both, additively; classical fingerprints still work.)

sequenceDiagram
    autonumber
    participant Op as Operator (owner-gated)
    participant Seq as capauth Sequoia backend
    participant Old as Classical root (v4 · Ed25519)
    participant New as PQC root (v6 · ML-DSA-87 + Ed448)
    participant WoT as Web-of-Trust verify
    participant Svc as Python services

    Op->>Seq: issue v6 ML-DSA-87 + Ed448 primary (+ ML-KEM-1024/X448 subkey)
    Seq-->>New: generated (NIST L5 · FIPS 204 · RFC 9580)
    Old->>New: cross-sign — old certifies new
    New->>Old: cross-sign — new certifies old
    New->>WoT: verify continuity, both directions
    WoT-->>Op: cross-sigs valid · identity continuous
    Note over Seq,New: Cryptographic ceremony COMPLETE on the agent root
    rect rgb(219,234,254)
        Note over Svc,New: LIVE SIGNING CUTOVER — IN PROGRESS
        Svc->>Svc: migrate PGPy to sk_pgp (PGPy cannot parse v6)
        Svc-->>New: sign live with the v6/PQC key via sk_pgp  (pending)
    end
    Note over Op: Operator root + vault re-seal — sequenced AFTER

And here is the honest boundary. The cryptographic ceremony is done on the agent root; the live signing cutover is not. Today the Python services still sign through PGPy, which cannot parse a v6 key — so the running identity continues to sign as classical v4 until those services move to the new in-process engine (§4.8). That migration is in flight; only after it lands does the agent actually sign live with the post-quantum key. The operator (human) root and the vault re-seal are sequenced after that — a deliberate, owner-driven step, not an automated commit. So we say it plainly, and the self-report says it on every line it touches: the live identity is not yet migrated. The capability is proven and the key exists; the cutover is underway and reported as in-progress, never as done.

4.8 sk_pgp — the Python OpenPGP-PQC signing engine

The ceremony above exposed a hard gap: capauth could issue a v6 post-quantum key, but the Python services that have to use it for everyday signing could not touch it. PGPy — the pure-Python OpenPGP library the stack grew up on — cannot parse an OpenPGP v6 key at all, and the available alternative, shelling out to GnuPG, can encrypt to ML-KEM but cannot certify or sign with ML-DSA or SLH-DSA. Sequoia can do all of it, but it is a Rust toolchain (sq), and routing every sign through a subprocess is brittle, slow, and leaks key material into argv and temp files. The signing root had a certificate but no in-process engine.

So we built one. sk_pgp (github.com/smilinTux/sk_pgp, Apache-2.0) is a sovereign Python OpenPGP library that binds Sequoia directly through PyO3 — Rust crypto, Python API, no subprocess. It is the deliberate PGPy replacement: same call-site shape (generate / sign / verify / parse), but able to read v6 keys and sign with post-quantum primitives in-process. We have proven, in-process, ML-DSA-87 + Ed448 generate / sign / verify end-to-end, and it ships as a self-contained wheel (the Rust + Sequoia + OpenSSL stack compiled in) so a service installs it like any other dependency rather than provisioning a Rust toolchain on every host. The same two rules hold as everywhere else: we bind a vetted library (Sequoia), we do not hand-roll the lattice math, and a v6/PQC key that sk_pgp is not present to handle is a loud, typed error — never a silent fall to classical.

This is the missing leg of the root cutover. The reason §4.7's live migration is "in progress, gated on PGPy→sk_pgp" is precisely that this engine has to land in the services before the agent can sign live with its v6 key. sk_pgp slots into the same CryptoBackend interface (§5) as a fourth implementation alongside PGPy, GnuPG, and the Sequoia-CLI backend — so adopting it is a backend swap, not an application rewrite.

graph TB
    SVC["Python services<br/>capauth · skcomms · skchat"]
    SVC --> Q{"Need: parse OpenPGP v6<br/>AND sign with PQC?"}
    Q --> PGPY["PGPy (pure-Python)<br/>✗ cannot parse v6 keys"]
    Q --> GPG["GnuPG subprocess<br/>~ v6 + ML-KEM encrypt only<br/>✗ cannot SIGN with ML-DSA / SLH-DSA"]
    Q --> SKPGP["sk_pgp  (PyO3 → Sequoia)<br/>✓ parse v6 · ✓ ML-DSA-87 + Ed448 gen/sign/verify<br/>✓ in-process · ✓ self-contained wheel"]
    SKPGP --> SEQ["Sequoia (Rust)<br/>vetted · bound, never hand-rolled"]

    classDef bad fill:#9ca3af,stroke:#4b5563,stroke-width:2px,color:#fff;
    classDef warn fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff;
    classDef good fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff;
    class PGPY bad;
    class GPG warn;
    class SKPGP,SEQ good;

4.9 sk_pqc — one hybrid KEM across Python, Dart, and Rust (and in the browser)

A messaging stack lives where its clients run, and ours run everywhere — server, native app, and the web, where, as of 2026, no browser exposes a WebCrypto PQC API. sk_pqc is now published in three languages behind one wire format, live on the public registries rather than as mere source drops — Apache-2.0, PyPI sk-pqc (pip install sk-pqc), pub.dev sk_pqc (dart pub add sk_pqc), and crates.io sk-pqc (cargo add sk-pqc, crates.io cut 2026-06-27; repos sk-pqc-py · sk-pqc-dart · sk-pqc-rs) — each delivering the same x25519-mlkem768 hybrid KEM (the byte-exact 1216 / 1120 / 32 contract of §4.1), and each importing as sk_pqc. The Dart package carries the web story: one API with two backends, chosen by conditional import — native via dart:ffi → liboqs, web via dart:js_interop → the audited @noble/post-quantum — and a Dart PqDmCodec that is a byte-for-byte mirror of the server's pqdm.py, so a chat between the Flutter app and the in-house agent negotiates hybrid post-quantum DMs in-page. A cross-implementation interop gate confirms a message sealed in any one language opens in the others. Today each binding carries its own vetted ML-KEM implementation behind the shared wire contract (liboqs on the server, @noble/post-quantum on the web, RustCrypto ml-kem in the crate); in progress is a single PyO3 Rust-core — sk-pqc-rs --features python builds an abi3 sk_pqc_rs wheel from the pure-Rust X25519+ML-KEM-768 core (no OpenSSL, no liboqs to link), so that one implementation backs every language binding rather than three parallel ones. That is the ultimate expression of the project's two rules — bind one vetted core, never hand-roll, swap it by id — and it leaves the wire format untouched. The packages are honest in their own READMEs about exactly what they are: a hybrid KEM at the -768 tier, KEM-only (signatures are future work), experimental and unaudited, "post-quantum," never "quantum-proof."

4.10 The identity-mode switch — sovereign DID ↔ anonymous, over one ratchet

A post-quantum messenger usually forces a single identity posture: Signal and Apple PQ3 are account-bound; no-identity / mix-network designs are deniable-by-construction. Our stack treats attribution as a runtime mode over the same cryptographic core. One envelope, one hybrid KEM (§4.1), one signature-free ratchet (§4.3) — and a single auth_mode suite-id, selected at session establishment, decides whether the session is attributable or anonymous. There is no second protocol stack and no fork; the KEM, padding, and metadata seal are byte-identical in both modes, and only how (or whether) the session is attributed changes.

The mode is selectable per-conversation with a per-deployment default (a sovereign deployment can default every new conversation to attributable; the personal default is anonymous). Two invariants make the switch safe, and both are machine-checked by the self-report engine of §6. First, sovereign never silently downgrades: a session negotiated as attributable cannot be stripped back to anonymous mid-stream — a downgrade attempt fails the establishment check and surfaces as a refused/mismatched mode, never as the quietly-weaker thing. Second — and this is the subtle part — the ratchet steps stay signature-free in both modes: identity is asserted only at establishment, so content deniability survives even in sovereign mode (sovereign proves who set up the session, never who authored a given message; anonymous mode simply adds the deniable HMAC over the same signature-free ratchet). This is the same scoping the field already endorses for deniability (Signal's PQXDH keeps authentication off the message keys on purpose); we generalize it into a mode the operator can choose.

flowchart TD
    est["session establishment<br/>(per-conversation · per-deployment default)"] --> mode{"auth_mode suite-id"}
    mode -->|"ANONYMOUS / deniable"| AN["no DID · opaque queue ids<br/>deniable HMAC (repudiable)<br/>OOB / QR link exchange"]
    mode -->|"SOVEREIGN / attributable"| SV["capauth DID + FQID<br/>hybrid Ed25519+ML-DSA-65 signed prekeys<br/>federated · non-repudiable · auditable"]
    AN --> core["shared hybrid KEM + signature-free ratchet<br/>IDENTICAL in both modes"]
    SV --> core
    core --> tx["padded · metadata-sealed transport"]
    SV -. "never silently downgrades" .-x AN

    classDef anon fill:#6b21a8,stroke:#a855f7,stroke-width:2px,color:#fff;
    classDef sov fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff;
    classDef shared fill:#3b82f6,stroke:#1e40af,stroke-width:2px,color:#fff;
    class AN anon;
    class SV sov;
    class core,tx shared;

Honest status. This switch is the design and scaffold layer of the messaging RFC (RFC-0001), not yet a shipped wire feature: the anonymous-queue addressing primitive and the sovereign signed-prekey both exist in-tree, and the mode-flag plumbing + invariant enforcement are the active work (tracked under coord c0f105a1). We present it here as a headline capability of the architecture — the same hybrid-PQC core serving either identity posture — while reporting honestly that it is mid-implementation, default-OFF, and not yet on the live transport.


5. The Scaffolding: Built to Outlive the Algorithm

This is the thesis. Each surface above is valuable, but any of them could be rebuilt on a different algorithm in a year — and a stack that hard-codes its choices would pay the full migration cost again. We refused to. NIST's own crypto-agility guidance (CSWP 39, December 2025) names the pattern: separate policy (which algorithm) from mechanism (the call site), keep an inventory, and measure agility maturity. Our infrastructure is built so that which algorithm is configuration, not code.

Every object self-describes its suite. A single registry (skcomms/crypto_suites.py) maps a suite_id to its kind, status, primitives, and FIPS references. Its initial release added no algorithms at all — pure scaffolding — and its status vocabulary is deliberately four honest states (classical / hybrid-pq / pq / symmetric), never "quantum-safe." Planned suites (a level-5 KEM, an SLH-DSA root) sit in the registry inactive until their implementation lands, so the self-report cannot describe vaporware. On the wire, a SignedEnvelope carries its sig_suite, a group carries its kem_suite and epoch, an identity carries its Algorithm, a conversation carries its negotiated_suite. Deserialize an object from a year ago and it still parses — and is correctly described as classical.

Every primitive sits behind a pluggable backend selected by id. Identity flows through a CryptoBackend interface with four interchangeable implementations (PGPy, GnuPG, Sequoia-CLI, and the in-process sk_pgp) — adding the post-quantum signing engine was a new backend, not a rewrite, and the live root cutover (§4.7) is exactly "select the sk_pgp backend by id." The KEM flows through get_kem_backend(suite_id), which selects by id only; no caller branches on a concrete class. sk_pqc's web and native ML-KEM providers sit behind one API the same way, in each of its Python, Dart, and Rust packages.

The whole pattern fits on one diagram: a self-describing object names a suite_id; the registry resolves that id to a status and a set of primitives; backends are selected by that id — never by a concrete class; and the backends bind vetted libraries we never hand-roll. The next quantum scheme enters as a registry row and a backend, and the application call sites above never change.

graph LR
    subgraph Wire["On-the-wire object"]
        OBJ["SignedEnvelope · Group · Identity · Conversation<br/>carries a self-describing suite_id"]
    end
    subgraph Registry["Suite registry — crypto_suites.py"]
        REG["suite_id → kind · status · primitives · FIPS refs<br/>status ∈ {classical · hybrid-pq · pq · symmetric}<br/>planned suites stay inactive"]
    end
    subgraph Backends["Pluggable backends — selected BY ID, never by class"]
        KEM["get_kem_backend(suite_id)"]
        CB["CryptoBackend ABC<br/>PGPy · GnuPG · Sequoia-CLI · sk_pgp"]
    end
    subgraph Prim["Vetted primitive libraries — bound, never hand-rolled"]
        OQS["liboqs<br/>ML-KEM / ML-DSA / SLH-DSA"]
        PYCA["pyca cryptography<br/>X25519 · HKDF · AES-256-GCM"]
        SEQ["Sequoia (via sk_pgp / sq)<br/>v6 OpenPGP · PQC certs"]
        NOBLE["@noble/post-quantum<br/>web ML-KEM"]
    end

    OBJ -->|reads suite_id| REG
    REG -->|by id| KEM
    REG -->|by id| CB
    KEM --> OQS
    KEM --> PYCA
    KEM --> NOBLE
    CB --> SEQ
    CB --> OQS

    classDef obj fill:#4a90e2,stroke:#1e3a8a,stroke-width:2px,color:#fff;
    classDef reg fill:#8b5cf6,stroke:#6b21a8,stroke-width:2px,color:#fff;
    classDef back fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff;
    classDef prim fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff;
    class OBJ obj;
    class REG reg;
    class KEM,CB back;
    class OQS,PYCA,SEQ,NOBLE prim;

So how does the next quantum scheme plug in? Four steps, no application changes:

  1. Register a new CryptoSuite (id, status, primitives, FIPS refs) — active = false until proven.
  2. Add a backend behind the existing interface, registered by suite id (a new KEM backend, a new signing CryptoBackend, a new ML-KEM provider in sk_pqc).
  3. Bump a wire version — the pqdm and SKHS formats are versioned and length-prefixed precisely so a new variant carries a new tag without breaking old parsers.
  4. Negotiate the new suite through the prekey / sig_suite machinery already in place; the self-report describes it once its registry entry goes active.

The applications never change. They read the negotiated suite and route by id. When a higher NIST tier, a hash-based root, or an entirely new family becomes the right answer, that answer is a registry entry and a backend class — and the infrastructure rides on top of it. That is what "built to outlive the algorithm" means.

We did not just argue this — we watched it hold in the field. The same x25519-mlkem768 suite, selected by id, carried a Level-3 forward-secret DM ratchet between two physical machines over the tailscale federation (§4.2) with zero application changes, and the three published sk_pqc packages negotiate that exact wire contract byte-for-byte across Python, Dart, and Rust. The in-progress PyO3 Rust-core (§4.9) tightens the same screw one turn further: one vetted implementation, swapped behind the id, backing every language. The agility thesis is not a promise about a future migration — it is the property that let the cross-node ratchet exist at all.


6. The Honesty Engine

A migration this large invites overclaiming, and overclaiming in cryptography is its own vulnerability — it tells users they are safe where they are not. So the migration is governed by a structural rule: no external quantum-resistance claim may be made unless it maps to a line in the self-report (sksecurity/pqc_report.py).

The report is per-object, not aspirational. A default report describes the conservative (classical) posture; a live report reflects the operator's actual objects. A specific group, conversation, store, envelope, or challenge is described by its real suite — so a silent-downgrade attempt appears as a classical line, not a hybrid one. The group-key surface is marked hybrid only when every group is hybrid; while any remains classical it reads "hybrid-pq for X/N groups." An unknown suite id resolves as classical — the engine has no path to call something quantum-resistant unless the registry says so and the suite is active.

The forbidden-claims discipline is explicit and enforced: never "quantum-proof" / "unbreakable" / "quantum-safe" (the defensible words are quantum-resistant / post-quantum); never "end-to-end quantum-resistant" while any leg is classical; never "PQC" when only signatures migrated (it does nothing for HNDL); never "CNSA-2.0 compliant" (we deliberately run the -768 tier, not the level-5 ceiling); never imply AES-256 is "broken." Every claim must cite surface + FIPS number + hybrid-vs-classical. Progress is recorded in an append-only ledger — narrative plus machine-readable snapshots — so the trajectory is reconstructable from data, not memory. A system that refuses to overclaim is itself a feature.


7. Where This Sits

Our hybrid choice is not idiosyncratic — it is the deployed consensus, and we adopted it deliberately so our security argument inherits the field's scrutiny.

In August 2024 NIST finalized the three standards this work builds on: FIPS 203 (ML-KEM, key encapsulation, from CRYSTALS-Kyber), FIPS 204 (ML-DSA, signatures, from Dilithium), and FIPS 205 (SLH-DSA, hash-based signatures, from SPHINCS+) [1, 2]. NIST's own framing is a model of calibrated language — ML-KEM is "believed to be secure, even against adversaries who possess a quantum computer," not "proven" or "quantum-proof" [1] — and it names ML-KEM-768 as the recommended default [1], the exact parameter set in the TLS 1.3 X25519MLKEM768 hybrid group [3] and in our x25519-mlkem768 suite.

The two leading production messengers validate both our construction and our scoping. Signal's PQXDH (2023) upgrades the X3DH handshake by combining X25519 with a post-quantum KEM (CRYSTALS-Kyber-1024) inside a single KDF so that "any attacker must break both" — the canonical secure-if-either-holds combiner, independently formally verified at USENIX Security 2024 [4, 5]. Apple's iMessage PQ3 (2024) goes further to "Level 3," layering a periodic post-quantum rekeying ratchet on top of an ML-KEM-1024 + P-256 initialization, in a purely additive hybrid "where defeating PQ3 requires defeating both," with a machine-checked TAMARIN proof from ETH Zürich at USENIX Security 2025 [6, 7, 8].

The part that matters most for us: both protocols ship post-quantum confidentiality but keep authentication classical — on purpose, and they say so plainly. PQXDH states that "authentication in PQXDH is not quantum-secure" because "post-quantum secure deniable mutual authentication is an open research problem" [5]; PQ3 retains ECDSA-P256 sender authentication "because these mechanisms can't be attacked retroactively with future quantum computers" [6, 7]. That is exactly the HNDL-ordering of Section 2 — confidentiality is urgent, authentication is a scoped Phase 2 — which means our phased posture is the industry consensus, not a shortcut. Where we differ:

The open problems we share with everyone are real. Post-quantum group/federated messaging is still maturing — Signal is itself extending to a post-quantum "Triple Ratchet" (SPQR) for ongoing-message PQ security [12], and the IETF's MLS and the Matrix community are actively working group PQC [13]. Post-quantum transport below the application (WireGuard's Noise, WebRTC's DTLS) lacks crypto-agility and waits on upstreams. And the OpenPGP PQC composites we use for identity ride a pre-RFC draft [10] whose code points can still move — which is exactly why we keep them additive and reversible.


8. Evidence


9. What We Keep Reviewing — and Tomorrow

Crypto-agility is a practice, not a one-time port. The append-only ledger and the forbidden-words discipline are a standing review applied to every claim. Concretely ahead:

Every one of these is a registry entry and a backend away — which is the entire point.


10. Conclusion

The quantum migration is often framed as a race against a doomsday clock. We think that framing is both wrong and dangerous: wrong because the CRQC is years out, dangerous because it invites either complacency ("not broken yet") or overclaiming ("quantum-proof now"). The honest framing is HNDL plus Mosca: long-lived secrets must be protected today, with hybrid constructions that cannot be worse than the classical crypto they extend, and with a posture that states exactly what is and is not protected.

But the durable contribution is not the algorithm — it is the scaffolding. We built skchat/skcomms/capauth so that the choice of post-quantum scheme is a registry entry and a pluggable backend, so that every object on the wire says what protects it, and so that a self-report engine makes overclaiming structurally impossible. ML-KEM-768 and ML-DSA-65 are today's answers. When tomorrow's answer arrives — a higher tier, a hash-based root, a new family entirely — our infrastructure will ride on top of it. That is what it means to build something to outlive the algorithm.


References

  1. NIST, FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard (13 Aug 2024). csrc.nist.gov/pubs/fips/203/final
  2. NIST, "NIST Releases First 3 Finalized Post-Quantum Encryption Standards" (13 Aug 2024); U.S. Federal Register, Announcing Issuance of FIPS 203/204/205, doc. 2024-17956 (14 Aug 2024).
  3. IETF, draft-ietf-tls-ecdhe-mlkem (X25519MLKEM768 for TLS 1.3) and draft-ietf-tls-hybrid-design; analysis in IACR ePrint 2024/039.
  4. Signal, "Quantum Resistance and the Signal Protocol — PQXDH" (Sept 2023). signal.org/blog/pqxdh
  5. Signal, The PQXDH Key Agreement Protocol (specification). signal.org/docs/specifications/pqxdh — formally verified by Bhargavan et al., USENIX Security 2024.
  6. Apple Security Engineering & Architecture, "iMessage with PQ3: The New State of the Art in Quantum-Secure Messaging at Scale" (Feb 2024). security.apple.com/blog/imessage-pq3
  7. D. Stebila, Security Analysis of the iMessage PQ3 Protocol (2024). security.apple.com
  8. F. Linker, R. Sasse, D. Basin, "A Formal Analysis of Apple's iMessage PQ3 Protocol," USENIX Security 2025 (machine-checked with the TAMARIN prover). usenix.org
  9. Sequoia-PGP, "Post-Quantum Cryptography" (Nov 2025). sequoia-pgp.org
  10. IETF, draft-ietf-openpgp-pqc — Post-Quantum Cryptography in OpenPGP (Standards Track, pre-RFC). datatracker.ietf.org/doc/draft-ietf-openpgp-pqc
  11. "Quantum-Safe vs. Quantum-Resistant vs. Post-Quantum: cutting through the snake oil." postquantum.com/quantum-snake-oil
  12. Signal, "SPQR: Post-Quantum Ratcheting" / Triple Ratchet (2025); see also B. Schneier, "Signal's Post-Quantum Cryptographic Implementation" (Oct 2025). signal.org/blog/spqr
  13. IETF MLS (RFC 9420) and its emerging PQC ciphersuite work; Matrix.org engineering blog. rfc9420 · blog.matrix.org
  14. NIST, Considerations for Achieving Cryptographic Agility (CSWP 39, Dec 2025).
  15. SimpleX Chat, SMP — Simplex Messaging Protocol and Private message routing (no-identity queue addressing; v5.6 post-quantum / v5.8 private-routing design notes). simplex.chat · github.com/simplex-chat/simplexmq

Standards and drafts move fast: NIST has since added HQC as a backup KEM and FN-DSA (Falcon) standardization is pending; OpenPGP-PQC code points and MLS PQC ciphersuites are still evolving and should be re-checked at implementation time.


Appendix A — Canonical Identifiers

Suite ids — active: x25519-mlkem768 (hybrid KEM), mldsa65-ed25519-v2 (hybrid sig); backend-issuable: mldsa87-ed448-v2 (root); planned/inactive: x25519-mlkem768-v2, slh-dsa-shake-256-v2; classical: ed25519-v1, rsa4096-v1, rsa-pgp-wrap-v1, x25519-pgp-wrap-v1; symmetric: aes256-gcm-v1.

Wire markers — pqdm1: (sealed DM scheme); SKHS magic, version 0x01, suite-tag 0x01 (hybrid signature).

Standards — FIPS 203 (ML-KEM), 204 (ML-DSA), 205 (SLH-DSA), 197 / SP 800-38D / SP 800-108 (AES-GCM, HKDF); RFC 5869 (HKDF), 7748 (X25519), 8032 (Ed25519/Ed448), 9580 (OpenPGP v6); NIST CSWP 39 (crypto-agility); draft-ietf-openpgp-pqc (pre-RFC composites, code points 30/31/32–34/35/36).

Tooling — sq 1.4.0-pqc.1 (sequoia-openpgp 2.2.0-pqc) on OpenSSL 3.6.2; sk_pgp (PyO3→Sequoia, in-process); liboqs 0.14.0; @noble/post-quantum; RustCrypto ml-kem + x25519-dalek (the sk-pqc-rs core); pyca cryptography; package:cryptography (Dart); maturin/PyO3 abi3 (sk_pqc_rs one-core wheel, in progress).

Libraries — sk_pqc (hybrid KEM + Level-3 DM/group ratchet, one wire format across Python/Dart/Rust), Apache-2.0, published to public registries: PyPI sk-pqc (pip install sk-pqc) · pub.dev sk_pqc (dart pub add sk_pqc) · crates.io sk-pqc (cargo add sk-pqc, cut 2026-06-27) — all import sk_pqc; repos sk-pqc-py · sk-pqc-dart · sk-pqc-rs · live at skpqc.skworld.io; sk_pgp (Python OpenPGP-PQC engine, PyO3→Sequoia), Apache-2.0, github.com/smilinTux/sk_pgp.


Appendix B — Sizes at a Glance

ElementBytes
Hybrid KEM public key (X25519 ‖ ML-KEM-768)1216
Hybrid KEM ciphertext1120
Hybrid shared secret (HKDF-SHA256)32
Group epoch payload / member / epoch1180
pqdm sealed-blob minimum1148
ML-DSA-65 signature3309
Hybrid SKHS composite signature3383

Relative cost: ML-KEM-768 ciphertext ≈ 33× X25519; ML-DSA-65 signature ≈ 50× Ed25519 (signing is fast). Amortizing the KEM across an epoch, not a message, is what keeps group messaging practical.


SKWorld Research • part of the PQC-MIGRATION program • honesty engine: sksecurity pqc-dashboard • the live root is still classical, and we will tell you the day it isn't. ⚛️


SKResearch • GitHub • GPL v3.0