Skip to main content

PQXDH formal verification

Research, not an audit
These pages are research and development and may not match the live app. Do not treat this as an audit. Shared for testing and demo only. Please use responsibly.

pqxdh composes X25519, ML-KEM-1024, HKDF, an initial AEAD, and a Double Ratchet hook toward Signal PQXDH revision 3. Gallery: positive-intentions.github.io/pqxdh. This page uses the same claim words as Signal Protocol and ML-KEM (tested / modeled / type-checked / assumed / open / cited). It is not an audit of the messenger.

Start from the verification report.

Interface under test​

Primitives stay in siblings. This crate owns the composition:

DH1=DH(IKA,SPKB),DH2=DH(EKA,IKB),DH3=DH(EKA,SPKB),DH4=DH(EKA,OPKB) (optional),(ct,ss)=Encaps(PQPKB),SK=HKDF(032,  F ∥ DH1..n ∥ ss,  PQXDH-v1_CURVE25519_SHA-256_ML-KEM-1024),AD=EncodeEC(IKA) ∥ EncodeEC(IKB).\begin{aligned} \mathrm{DH}_1 &= \mathrm{DH}(\mathit{IK}_A, \mathit{SPK}_B), \\ \mathrm{DH}_2 &= \mathrm{DH}(\mathit{EK}_A, \mathit{IK}_B), \\ \mathrm{DH}_3 &= \mathrm{DH}(\mathit{EK}_A, \mathit{SPK}_B), \\ \mathrm{DH}_4 &= \mathrm{DH}(\mathit{EK}_A, \mathit{OPK}_B)\ \text{(optional)}, \\ (\mathit{ct}, \mathit{ss}) &= \mathrm{Encaps}(\mathit{PQPK}_B), \\ \mathit{SK} &= \mathrm{HKDF}(0^{32},\; F \,\|\, \mathrm{DH}_{1..n} \,\|\, \mathit{ss},\; \texttt{PQXDH-v1\_CURVE25519\_SHA-256\_ML-KEM-1024}), \\ \mathrm{AD} &= \mathrm{EncodeEC}(\mathit{IK}_A) \,\|\, \mathrm{EncodeEC}(\mathit{IK}_B). \end{aligned}

Documented deviations stay in every status table: Ed25519 is not XEdDSA; HKDF is SHA-256 only; encodings are not libsignal protobufs.

Production enables crypto-backend (path deps on signal-protocol-core and ml-kem-core). Extraction builds --no-default-features. Stubs use AbstractPqxdh axioms.

Executable tests​

SuiteWhat it coversGate
Rust tests (backend on)Keygen, bundle verify, initiate/respond, optional OTPK, initial AEAD abort, handshake→session (handshake_session.rs and session.rs)Must pass
Rust tests (backend off)Length and field-rejection paths on extractable stubsMust pass
Line coveragepqxdh-core100% lines on both feature builds
proptestEncode/decode inverses, derive_sk sensitivity, honest handshake, tampered SPKMust pass
Implementation KATsFixed-seed replay (npm run kats:gen). Not official Signal vectorsMust pass
Fuzzdecode_ec, decode_kem, verify_prekey_bundle, respondTime-boxed CI
WASMHandshake + session APIs, including one-time vs last-resort PQ prekeysMust pass

Wrapper lemmas​

Hand-written AbstractPqxdh (F*, Rocq, Lean).

Assumed​

AssumptionFormal content
DH commutativityDH(a,pub(b))=DH(b,pub(a))\mathrm{DH}(a, \mathrm{pub}(b)) = \mathrm{DH}(b, \mathrm{pub}(a))
KEM correctnessCited from ML-KEM / libcrux
HKDF / AES-GCM / Ed25519Sibling and crate axioms
Double Ratchet initCited from Signal Protocol

Proved on the spec (glue)​

Wrong-length keys fail before DH/KEM/HKDF. KDF IKM length is 32+32⋅n+3232 + 32\cdot n + 32 for n∈{3,4}n \in \{3,4\}. AD is two EncodeEC values. session_from_handshake rejects ∣SK∣≠32\lvert \mathit{SK} \rvert \neq 32.

Symbolic models (ProVerif)​

ModelQueries
pqxdh_kdf.pvIKM = F∥∥DH1..n∥∥ssF \|\| \mathrm{DH}_{1..n} \|\| \mathit{ss}; SK secrecy
pqxdh_4dh_kem.pvFour DH ops + encaps/decaps
pqxdh_handshake.pvinitiate/respond, optional OTPK, initial AEAD abort
pqxdh_security.pvForged SPK/PQ signatures do not yield an honest SK (Modeled (honest bundle only))
pqxdh_hybrid_binding.pvSK secret if only DH or only KEM is given to the attacker
pqxdh_session.pvPrivate channel c_sk from handshake to ratchet; session_decrypt(m) \Rightarrow handshake_agree(sk)
pqxdh_complete.pvBundle → handshake → c_sk → first ratchet messages

Session and complete models are one Alice process and one Bob process that output SK on a private channel, then continue as ratchet processes that read that same SK. That is the linking gap left open in signal-protocol’s combined model.

These assume DH, KEM, HKDF, and AEAD equations. They do not prove X25519, libcrux, or AES-GCM. Constants match the Rust by intent.

Extracted Rust (hax → F*, Rocq, Lean)​

BackendAutomated jobDepth
F*Full stub extractType-checks extracted pqxdh-core plus AbstractPqxdh
RocqExtract + length lemmas? bodies may be axiomatized
LeanAbstractPqxdh onlyExtracted Lean is not built in this job

There is no verify-libcrux job here. Lattice math stays on the ml-kem pin.

Automated jobs​

JobWhat it runs
Format, Clippy, cargo testProduction and stub pqxdh-core
Coverage100% lines on both feature builds
WASMNode wasm-pack
ProVerifSeven models
F* / Rocq / LeanStub extract and axiom modules
FuzzTime-boxed cargo-fuzz

verify:host:full is ProVerif + F* + Rocq + Lean.

What this does not prove​

  • WASM bindings or the gallery
  • X25519, Ed25519, HKDF, AES-GCM, or libcrux lattice math
  • libsignal wire compatibility (not claimed)
  • Official Signal PQXDH KATs (they are not published)
  • The recipient cascade, git mailbox delivery, or a stolen device
  • The Glitr messenger
  • Refinement from the crypto-backend production build to AbstractPqxdh

Next: crypto formal verification — password sealing and the modeled cascade composition.