Skip to main content

Verification report

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.

This report is for developers, cryptographers, and cybersecurity reviewers. It says what was checked on five libraries — not that a production function is a theorem, and not that the Glitr messenger is verified.

FieldValue
Date4 October 2026
Subjectsignal-protocol, ml-kem, pqxdh, mls, and crypto (core + cascade)
AudienceDevelopers, cryptographers, cybersecurity reviewers
StatusResearch report — subject to change
Not claimedFormal verification of the Glitr messenger, git mailbox delivery, or a stolen host

Jump to: For developers · For cryptographers · For security

Extract and CI minutiae: Signal Protocol, ML-KEM, PQXDH, MLS, crypto.

What was checked​

Five libraries:

Three activities share the label “formal verification.” They do not prove the same thing.

There is no arrow from tests to extract on purpose: passing tests does not validate the stub path. F*, Rocq, and Lean do not check the libcrux- or X25519-backed production functions.

Status words​

WordMeaning
TestedAn executable test failed or passed on the production build
ModeledA ProVerif query was discharged on an abstract process — not a statement about extracted or production Rust
Type-checkedExtracted stub code or a hand-written spec compiled. Not functional correctness. The three backends do not prove the same lemmas
AssumedAn axiom: DH commutativity, KEM correctness, AEAD round-trip, CSPRNG, …
OpenStated as a goal, or the model cannot express it yet
CitedRecorded from pinned third-party proofs (libcrux), not re-proved here

Length lemmas are proved in F* (precondition includes validation), proved in Rocq on length only, and axioms in Lean. Tested hybrid / ratchet behaviour is on the production backend. It does not prove the spec axiom hybrid_round_trip or extracted encrypt / decrypt.

Rust llvm-cov is the core crate (plus crypto-cascade). WASM is Node/wasm-pack. coverage(off) is only for proven-unreachable helpers (hkdf_derive_short, chunk_prefix_len / read_u32_be, u32_from_be4). ml-kem has none.

What this report does not say​

  • The messenger is formally verified.
  • Git mailbox delivery or a stolen host is in any model.
  • X25519, Ed25519, HKDF, AES-GCM, Argon2id, or RSA-OAEP are proved inside these libraries.
  • A stolen device, a malicious git host, or a side channel is in the model.
  • Closed source is a substitute for review of ml-kem, pqxdh, or crypto. Outsiders cannot replay those scripts from this page alone. Signal Protocol source is public.

Precise library sentences (not a blanket “unclaimed”):

  • pqxdh handshake and linked session: Modeled / type-checked under axioms.
  • crypto-core password envelope: Tested / Modeled / type-checked under Argon2+AES axioms.
  • crypto-cascade layer order and wire v3: Modeled composition; not a proof of the Glitr messenger, git delivery, or a stolen host.

FAQ sentence: signal-protocol (open source) plus ml-kem, pqxdh, and crypto (closed-source) have production tests, symbolic models under idealized primitives, and extracted type-checks of stub builds. Lattice math is cited from pinned libcrux, which is incomplete at that pin. The messenger is not verified.

A public Actions badge on a private ml-kem remote is not a source release.

Results at a glance​

signal-protocol​

ClaimStatusWhere
Core crate line coverage 100%TestedWorkspace llvm-cov; WASM src/ ignored
WASM and TypeScript wrappers behaveTestedwasm-pack, Jest
Long-term and ephemeral secrets stay off the attacker’s knowledgeModeledQuery-specific; the combined model is parallel and unlinked (see Open rows)
X3DH evaluates four DH shares in orderModeledComplete and 4-DH models
Old message keys are not recoverable from new chain keysModeledDouble Ratchet security model
After compromise, later messages can still be producedModeledSame model
Decrypt implies a prior encrypt on that plaintextModeledDouble Ratchet models
Handshake output is fed into the first ratchet stateOpenCombined model has no linking channel
Alice/Bob authentication correspondence on the full X3DH processOpenSame linking-channel limit
Extracted Double Ratchet is functionally correctOpenF* CI skips it; Rocq/Lean CI check the axiom module only
Production backend refines the stubsOpenNo refinement proof

X3DH, as modeled, concatenates four Diffie–Hellman shares and a KDF:

DH1=DH(IKA,SPKB),DH2=DH(EKA,IKB),DH3=DH(EKA,SPKB),DH4=DH(EKA,OPKB),SK=HKDF(salt,  DH1 ∥ DH2 ∥ DH3 ∥ DH4,  info).\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), \\ \mathit{SK} &= \mathrm{HKDF}(\mathrm{salt},\; \mathrm{DH}_1 \,\|\, \mathrm{DH}_2 \,\|\, \mathrm{DH}_3 \,\|\, \mathrm{DH}_4,\; \mathrm{info}). \end{aligned}

DH commutativity is assumed in the extracted spec, not proved for X25519:

DH(a,pub(b))=DH(b,pub(a)).\mathrm{DH}(a, \mathrm{pub}(b)) = \mathrm{DH}(b, \mathrm{pub}(a)).

ml-kem​

ClaimStatusWhere
Core crate line coverage 100%TestedProduction Rust / CI
Hybrid encrypt/decrypt round-trip; tamper and wrong-key failTestedProduction tests
Wrong-length keys fail before the KEM is calledTested (production); type-checked (spec)Production try_from / guards; F* lemmas. No refinement to the backend.
Plaintext secrecy against the network attackerModeledHybrid security model
Shared-secret / AES-key material secrecyModeledSame model
Decrypt event implies a prior encrypt of that plaintextModeledSame model
Honest parties recover the same plaintextModeledHybrid round-trip model
Full ml-kem-core extract type-checks in F*Type-checkedHost / F* job
Extracted Rocq modules type-checkType-checkedRocq job; ? bodies axiomatized
Wrapper length lemmas in LeanAssumedAbstractKem.lean states them as axioms; CI does not prove them
Decaps(sk,Encaps(pk))=ss\mathrm{Decaps}(\mathit{sk}, \mathrm{Encaps}(\mathit{pk})) = \mathit{ss}AssumedCorrectness axiom, not IND-CPA. Lattice math cited from libcrux, not linked.
Portable libcrux arithmetic / NTT / serialize / compressCitedPin libcrux-ml-kem 0.0.10
Generic ind_cpa at that pinOpenUpstream 0/21 at the pin
Production backend refines AbstractKemOpenNo refinement proof

ML-KEM-1024 sizes used by the wrapper (FIPS 203):

∣pk∣=1568,∣sk∣=3168,∣ct∣=1568,∣ss∣=32.\lvert \mathit{pk} \rvert = 1568,\quad \lvert \mathit{sk} \rvert = 3168,\quad \lvert \mathit{ct} \rvert = 1568,\quad \lvert \mathit{ss} \rvert = 32.

Hybrid envelope:

(ct,ss)=Encaps(pk),k=HKDF-SHA256(ss,salt,ML-KEM-1024-AES-GCM),c=AES-256-GCMk, iv(m),\begin{aligned} (\mathit{ct}, \mathit{ss}) &= \mathrm{Encaps}(\mathit{pk}), \\ k &= \mathrm{HKDF\text{-}SHA256}(\mathit{ss}, \mathrm{salt}, \texttt{ML-KEM-1024-AES-GCM}), \\ c &= \mathrm{AES\text{-}256\text{-}GCM}_{k,\,\mathrm{iv}}(m), \end{aligned}

with ∣salt∣=16\lvert \mathrm{salt} \rvert = 16 and ∣iv∣=12\lvert \mathrm{iv} \rvert = 12. The statement

Deck,iv(Enck,iv(m))=m\mathrm{Dec}_{k,\mathit{iv}}(\mathrm{Enc}_{k,\mathit{iv}}(m)) = m

is assumed for AES-GCM and modeled in ProVerif as an equation.

Length rejection is a proved lemma on the F* spec (Rocq proves a length-only variant; Lean axiomatizes it):

∣pk∣≠1568  ⟹  preencap(pk)=false.\lvert \mathit{pk} \rvert \neq 1568 \implies \mathrm{pre}_{\mathrm{encap}}(\mathit{pk}) = \mathsf{false}.

pqxdh​

ClaimStatusWhere
Core crate line coverage 100% (both feature builds)TestedProduction and stub Rust / CI
Honest handshake agrees on SK; initial AEAD abort; handshake→sessionTestedProduction tests + handshake_session.rs (proptest is encode/KDF/handshake only)
Implementation KATs replayTestedNot official Signal vectors
SK secret if only DH or only KEM is leakedModeledHybrid-binding model
Forged SPK/PQ signatures do not yield honest SKModeledSecurity model (honest bundle only)
Handshake SK is the SK the ratchet consumesModeledLinked session / complete models (c_sk)
Extracted stub type-checksType-checkedF* full stub; Rocq/Lean axiom depth as siblings
DH / KEM / HKDF / AEAD / ratchet initAssumed / CitedSiblings + AbstractPqxdh
Production backend refines AbstractPqxdhOpenNo refinement proof

PQXDH IKM (this crate):

SK=HKDF(032,  F ∥ DH1..n ∥ ss,  PQXDH-v1_CURVE25519_SHA-256_ML-KEM-1024).\mathit{SK} = \mathrm{HKDF}(0^{32},\; F \,\|\, \mathrm{DH}_{1..n} \,\|\, \mathit{ss},\; \texttt{PQXDH-v1\_CURVE25519\_SHA-256\_ML-KEM-1024}).

crypto​

ClaimStatusWhere
crypto-core line coverage 100% (both features)TestedProduction and stub rust gates in CI (npm run test:rust:coverage:ci is production only)
crypto-cascade line coverage 100%OpenIntended --fail-under-lines 100 on the product crate; last measured run after unhiding helpers was not 100%
Official SHA / AES-GCM KATsTestedNIST vectors
Argon2id RFC 9106 vectorsOpenParameter mismatch; pinned implementation vector only
Password-envelope secrecy without the passwordModeledpassword_security.pv
Core Aes+Password cascade nests inner keysModeledcascade_core.pv
Five-layer product order and wire v3 outer-onlyModeledComposition models; not a messenger proof
Empty password / wrong AES sizes fail before primitivesType-checkedAbstractCrypto glue lemmas
Argon2id, AES-GCM, RSA-OAEPAssumedNot re-proved
Extracting encrypt_for_peer through haxOpenExplicitly out of scope
Production backend refines AbstractCryptoOpenNo refinement proof

Password envelope (this crate):

k=Argon2idm=19456, t=2, p=1(password,  salt),env=(1,1,1,salt16,nonce12,c).k = \mathrm{Argon2id}_{m=19456,\,t=2,\,p=1}(\mathit{password},\; \mathit{salt}),\quad \mathrm{env} = (1,1,1,\mathit{salt}_{16},\mathit{nonce}_{12},c).

For developers​

What a green job means, and what it does not.

Production interface​

Callers use the functions and sizes above — not WASM bindings or a gallery UI. Signal: identity keys, signed prekeys, one-time prekeys, X3DH, then a Double Ratchet session (32-byte X25519 / Ed25519 keys, 64-byte signatures). ml-kem: generate_key_pair, encapsulate, decapsulate, hybrid encrypt / decrypt at the ML-KEM-1024 sizes in the glance table.

WASM and TypeScript wrappers are tested, not extracted.

Feature split​

Production builds enable a crypto backend (X25519 / libcrux / AES-GCM). Extraction builds without that feature (--no-default-features on ml-kem). Concrete calls become stubs (AbstractCrypto, AbstractKem).

There is no refinement proof that the stub and the backend compute the same function. A passing cargo test on the production feature does not prove the extract.

What CI actually ran​

The libraries run the same kind of jobs. The depth differs.

Jobsignal-protocolml-kempqxdhcrypto
Format, Clippy, production testsJS lint + Rust tests (no clippy/rustfmt job)YesYescore + cascade
100% line coverage on coreworkspace, ignore WASM src/ml-kem-coreboth featurescore (both features) + cascade gate
WASM testsYesNode wasm-packNode + session APIsNode + envelope/AES
ProVerif suite7 models2 models7 models (incl. linked session)9 models (core + composition)
F*Extracted modules except Double RatchetFull extract of ml-kem-coreFull stub extractFull stub extract of crypto-core
RocqHand-written axiom module onlyExtract + type-check; ? axiomatizedSame wrapper depthSame wrapper depth
LeanHand-written axiom module onlyExtract + AbstractKemAbstractPqxdhAbstractCrypto
Third-party primitive proofs—verify-libcruxCite ml-kem / signalNone (no libcrux job)

verify:host:full on ml-kem is ProVerif + F* + Rocq + Lean. It does not run the libcrux pin job. A green host:full is not the lattice suite.

The wrapper depends on crates.io libcrux-ml-kem 0.0.10 and hax-lib 0.3. The pin file and libcrux job use git commit c5fb80f… and hax-lib 0.3.7. Signal’s verification image documents hax-lib 0.3. CI does not check that the crates.io tarball equals that commit.

Treat a live public CI run as the truth when one exists; otherwise the intended suite is what this report describes. If ml-kem jobs are not visible, even “last green” is invisible for that library.

How to replay​

Surfacesignal-protocolml-kempqxdhcrypto
SourcePublic — cloneClosed — not releasedClosed — not releasedClosed — not released
Gallerysignal.positive-intentions.compositive-intentions.github.io/ml-kempositive-intentions.github.io/pqxdhpositive-intentions.github.io/crypto
This reportClaims and statusClaims and statusClaims and statusClaims and status

A public Actions badge on a private remote is not a source release.

Coverage gates, WASM/Jest detail: Signal Protocol. Pin checksums and libcrux job fields: ML-KEM. pqxdh and crypto stub llvm-cov (--no-default-features) are CI steps, not npm run test:rust:coverage:ci on crypto (that script is production crypto-core only).

For cryptographers​

What was actually proved, assumed, or only type-checked.

Symbolic vs computational​

ProVerif is a Dolev–Yao network attacker plus function symbols and equations, for example

DH(a,gb)=DH(b,ga),Dec(k,Enc(k,m))=m.\mathrm{DH}(a, g^{b}) = \mathrm{DH}(b, g^{a}), \qquad \mathrm{Dec}(k, \mathrm{Enc}(k, m)) = m.

Queries are secrecy (“the attacker cannot derive mm”) and correspondence (“a decrypt event implies a prior encrypt event”).

This is not a computational proof. The AEAD equation is idealized: it is not IND-CCA, nonce-misuse resistance, or key commitment. The models are not machine-linked to the Rust. Constants matching the implementation is intent. The KEM process treats the shared-secret argument as encapsulation coins (kem_enc(pk, ss)), not “encapsulate draws 32 random bytes after validation.”

KEM correctness Decaps(sk,Encaps(pk))=ss\mathrm{Decaps}(\mathit{sk}, \mathrm{Encaps}(\mathit{pk})) = \mathit{ss} is assumed. That is correctness, not secrecy, and not IND-CPA.

Method​

The same six steps are applied to each library. Later steps never cancel an earlier “open”.

Step 1 — Fix the production interface. Name the functions a caller actually uses. Record sizes and error paths. Everything below is relative to that interface.

Step 2 — Executable tests on the production build. Real primitives. 100% line coverage on the core crate. Tests do not prove secrecy.

Step 3 — Split production crypto from the extractable crate. Production enables a crypto feature. Extraction builds without it. No refinement proof.

Step 4 — Write a symbolic protocol model. ProVerif processes and queries, as above.

Step 5 — Extract Rust and type-check. hax translates the no-backend crate. CI type-checks that stub extract — not the production backend.

The three backends do not prove the same thing:

  • F*: length lemmas proved on AbstractKem; KEM/AEAD/hybrid assumed; extracted wrappers type-checked via replace_body. Signal CI is lite: extracted modules except Double Ratchet.
  • Rocq: length lemmas proved on a length-only predicate (no ValidatePK in those lemmas); hybrid/KEM functions axiomatized after ? fails to type-check. Signal CI is the hand-written AbstractCrypto module only.
  • Lean: even the length lemmas are axioms; CI type-checks AbstractKem / AbstractCrypto only. Extracted Lean needs Mathlib and is not built in the ml-kem job.

Holes: where hax’s ? / ControlFlow encoding is not type-correct, those function bodies become axioms at extract time (generate_key_pair, encapsulate, decapsulate, derive_aes_key, encrypt, decrypt on the ml-kem Rocq path).

Step 6 — Cite upstream primitive proofs where they exist. ml-kem does not re-prove Kyber. It pins libcrux-ml-kem 0.0.10 (git c5fb80f…). Portable arithmetic / NTT / serialize / compress are cited. Generic ind_cpa is open (upstream 0/21 at that pin). Lattice math is not linked as a lemma in the wrapper.

Open goals that stay open​

  • Combined Signal model: X3DH and Double Ratchet run as parallel processes with no linking channel, so handshake-into-ratchet correspondence is open in signal-protocol. pqxdh discharges the analogous query on a private c_sk channel.
  • Full X3DH authentication correspondence: same linking-channel limit in signal-protocol.
  • Extracted Double Ratchet functional correctness: open (F* CI skips it).
  • Refinement from production backends to AbstractCrypto / AbstractKem / AbstractPqxdh: open.
  • Extracting crypto-cascade encrypt_for_peer through hax: open (out of scope).
  • Argon2id as a memory-hard function: open (public KDF equation only).

Per-model query tables: Signal Protocol, PQXDH, crypto. AbstractKem lemma statements and pin checksums: ML-KEM.

For security​

What you can say in a review without the extract notes.

Attacker and trust​

PartyWhat we assume
Network attackerSees and rewrites every bit on the wire. This is the ProVerif attacker.
Honest endpointsRun the modeled processes; they do not leak long-term keys unless a query says so.
Primitive oraclesDH, KEM, HKDF, AEAD behave as the equations or axioms say.
HostNot modeled. A compromised device is outside both suites.
Git host / mailboxNot modeled. Delivery and ciphertext-at-rest are product concerns.

Those last two rows are discussed as product analysis on Threat model. The dated product report (Glitr bar vs ideal-messenger gaps) is Threat-model report. Putting them on that folder does not put them in these models.

Forward secrecy in the Double Ratchet model means: after a ratchet step, old message keys are not derivable from the new chain. It does not mean a stolen laptop is safe.

Closed source is a limit on replaying ml-kem scripts. It is not a security property.

You may say​

  • These four libraries have production tests, including a 100% line-coverage gate on the named core crate(s). crypto-cascade uses the same --fail-under-lines 100 command; that product-crate gate is not yet green after unhiding helpers.
  • ProVerif models an idealized network attacker and discharges stated secrecy and correspondence queries on those models.
  • Extracted stub crates type-check in F*, Rocq, and Lean under named axioms. Depth differs (Signal F* skips Double Ratchet; Rocq/Lean are often axiom modules).
  • pqxdh models a linked handshake→session path; signal-protocol’s combined model still leaves that open.
  • crypto-core models the password envelope; crypto-cascade models layer order and wire v3 as composition only.
  • ml-kem cites portable libcrux proofs at pin 0.0.10; ind_cpa is still open at that pin.
  • Signal Protocol source is public. ml-kem, pqxdh, and crypto are not.

You may not say​

  • The Glitr messenger is formally verified.
  • crypto-cascade’s composition model is a proof of Signal, PQXDH, ML-KEM, git delivery, or a stolen host.
  • “The protocol is secure” or “Kyber is proved here.”
  • Production encrypt/decrypt was proved in F*, Rocq, or Lean (round-trip is an axiom on the spec; tests run the real backend).
  • A stolen device, a malicious git host, or a side channel was in the model.
  • A public CI badge means closed-source libraries are released.

FAQ sentence: signal-protocol (open source) plus ml-kem, pqxdh, and crypto (closed-source) have production tests, symbolic models under idealized primitives, and extracted type-checks of stub builds. Lattice math is cited from pinned libcrux, which is incomplete at that pin. The messenger is not verified.

Library pages for the long tables: Signal Protocol, ML-KEM, PQXDH, crypto.