CryptoVerif and Tamarin models, minor doc updates

Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
This commit is contained in:
Kamal Tufekcic 2026-04-13 01:51:32 +03:00
commit e6d0a1ef1a
No known key found for this signature in database
18 changed files with 2925 additions and 8 deletions

121
cryptoverif/LO_Auth.cv Normal file
View file

@ -0,0 +1,121 @@
(* LO-Auth: Key Possession Proof (Theorem 6)
*
* Protocol (§6):
* Server → Client: c, where (c, ss) ← XWing.Encaps(pk_IK_client[XWing])
* Client → Server: proof = MAC(key=ss, data="lo-auth-v1")
* Server: accept iff proof = token, where token = MAC(key=ss, data="lo-auth-v1")
*
* Security claims:
* 1. Correspondence: ServerAccepts ==> ClientResponds (Theorem 6)
* 2. Injective correspondence: each acceptance maps to a distinct client
* response (single-use, matching Tamarin's Auth_Single_Use)
*
* Reduces to: X-Wing IND-CCA2 + HMAC-SHA3-256 SUF-CMA.
*
* Decaps oracle (§8.3 compositional note): In the Tamarin model, an explicit
* DecapsOracle rule models the adversary's ability to submit arbitrary
* ciphertexts and observe MAC(Decaps(sk_IK, c), "lo-auth-v1"). In CryptoVerif,
* this is subsumed by the IND-CCA2 KEM assumption, which already provides the
* adversary with a raw decapsulation oracle returning ss directly — strictly
* more powerful than the MAC-wrapped output. The Nc parameter counts all
* client-side decapsulation queries (both legitimate responses and adversarial
* oracle use); the IND-CCA2 advantage bound P_kem(time, 1, Ns, Nc) accounts
* for all such queries.
*)
(* Session counts *)
param Ns. (* Server challenge sessions *)
param Nc. (* Client response sessions (includes §8.3 Decaps oracle queries) *)
(* ---------- Type declarations ---------- *)
type kem_keyseed [large, fixed].
type kem_pkey [bounded].
type kem_skey [bounded].
type kem_secret [large, fixed].
type kem_ciphertext [bounded].
type kem_encapoutput [bounded].
type macres [fixed].
type label [fixed].
(* ---------- X-Wing KEM (IND-CCA2) ---------- *)
proba P_kem.
proba P_kem_keycoll.
proba P_kem_ctxtcoll.
expand IND_CCA2_KEM(
kem_keyseed, kem_pkey, kem_skey,
kem_secret, kem_ciphertext, kem_encapoutput,
kem_pkgen, kem_skgen, kem_encap, kem_pair, kem_decap,
injbot_kem, P_kem, P_kem_keycoll, P_kem_ctxtcoll
).
(* ---------- HMAC-SHA3-256 as SUF-CMA deterministic MAC ---------- *)
proba P_mac.
expand SUF_CMA_det_mac(kem_secret, label, macres, mac_auth, mac_check, P_mac).
const lo_auth_label: label.
(* ---------- Events ---------- *)
event ServerAccepts(kem_pkey, kem_ciphertext).
event ClientResponds(kem_pkey, kem_ciphertext).
(* ---------- Security queries ---------- *)
(* Theorem 6 (Key Possession): If the server accepts for a challenge
* ciphertext ct, then the client responded to that same ct. *)
query pk: kem_pkey, ct: kem_ciphertext;
event(ServerAccepts(pk, ct)) ==> event(ClientResponds(pk, ct)).
(* Single-use (Tamarin: Auth_Single_Use): Each server acceptance maps to
* a distinct client response — no replay of a single client response can
* satisfy two server sessions. *)
query pk: kem_pkey, ct: kem_ciphertext;
inj-event(ServerAccepts(pk, ct)) ==> inj-event(ClientResponds(pk, ct)).
(* ---------- Channels ---------- *)
channel c_start, c_srv_start, c_srv_out, c_srv_recv,
c_cli_in, c_cli_out, c_pub.
(* ---------- Server process ---------- *)
let Server(pk_client: kem_pkey) =
foreach i_s <= Ns do
in(c_srv_start, ());
let kem_pair(ss: kem_secret, ct: kem_ciphertext) = kem_encap(pk_client) in
let token: macres = mac_auth(lo_auth_label, ss) in
out(c_srv_out, ct);
in(c_srv_recv, p: macres);
if p = token then (
event ServerAccepts(pk_client, ct)
).
(* ---------- Client process ---------- *)
let Client(sk_client: kem_skey, pk_client: kem_pkey) =
foreach i_c <= Nc do
in(c_cli_in, ct: kem_ciphertext);
let injbot_kem(ss: kem_secret) = kem_decap(ct, sk_client) in
let tag: macres = mac_auth(lo_auth_label, ss) in
event ClientResponds(pk_client, ct);
out(c_cli_out, tag).
(* ---------- Main process ---------- *)
process
in(c_start, ());
new kem_seed: kem_keyseed;
let pk_client = kem_pkgen(kem_seed) in
let sk_client = kem_skgen(kem_seed) in
out(c_pub, pk_client);
(Server(pk_client) | Client(sk_client, pk_client))
(* EXPECTED
All queries proved.
END *)

231
cryptoverif/LO_KEX.cv Normal file
View file

@ -0,0 +1,231 @@
(* LO-KEX: Session Establishment (Theorems 1, 2b)
*
* Protocol (§4):
* Bob publishes bundle with σ_SPK = Sign(sk_IK_B, label ‖ pk_SPK_B)
* Alice verifies bundle, encapsulates to IK_B/SPK_B/OPK_B
* Alice signs SI: σ_SI = Sign(sk_IK_A, label ‖ SI)
* Bob verifies σ_SI, decapsulates, derives same (rk, ek)
*
* Simplifications:
* - HybridSig → single EUF-CMA scheme (§2.2: "may be treated as single")
* - Three X-Wing KEMs → three independent IND-CCA2 KEMs
* - HKDF → PRF keyed by ss_IK with (ss_SPK, ss_OPK, pks) as input
* - First-message AEAD omitted (Theorem 1 is about session key, not message)
* - σ_SPK signed by Bob's signing key (same as IK in the hybrid scheme)
* - Alice uses a SEPARATE signing key (models the distinct sk_IK_A[Ed25519+ML-DSA])
*)
proof {
crypto uf_cma(sigA_sign);
simplify;
success
}
param N_A.
param N_B.
(* ---------- Types ---------- *)
type kem_keyseed [large, fixed].
type kem_pkey [bounded].
type kem_skey [bounded].
type kem_secret [large, fixed].
type kem_ciphertext [bounded].
type kem_encapoutput [bounded].
type spk_keyseed [large, fixed].
type spk_pkey [bounded].
type spk_skey [bounded].
type spk_secret [large, fixed].
type spk_ciphertext [bounded].
type spk_encapoutput [bounded].
type opk_keyseed [large, fixed].
type opk_pkey [bounded].
type opk_skey [bounded].
type opk_secret [large, fixed].
type opk_ciphertext [bounded].
type opk_encapoutput [bounded].
type sig_keyseed [large, fixed].
type sig_pkey [bounded].
type sig_skey [bounded].
type sig_signature [bounded].
type sessionkey [large, fixed].
(* ---------- IK KEM (IND-CCA2) ---------- *)
proba P_kem_ik.
proba P_kem_ik_keycoll.
proba P_kem_ik_ctxtcoll.
expand IND_CCA2_KEM(
kem_keyseed, kem_pkey, kem_skey,
kem_secret, kem_ciphertext, kem_encapoutput,
ik_pkgen, ik_skgen, ik_encap, ik_pair, ik_decap,
injbot_ik, P_kem_ik, P_kem_ik_keycoll, P_kem_ik_ctxtcoll
).
(* ---------- SPK KEM (IND-CCA2) ---------- *)
proba P_kem_spk.
proba P_kem_spk_keycoll.
proba P_kem_spk_ctxtcoll.
expand IND_CCA2_KEM(
spk_keyseed, spk_pkey, spk_skey,
spk_secret, spk_ciphertext, spk_encapoutput,
spk_pkgen, spk_skgen, spk_encap, spk_pair, spk_decap,
injbot_spk, P_kem_spk, P_kem_spk_keycoll, P_kem_spk_ctxtcoll
).
(* ---------- OPK KEM (IND-CCA2) ---------- *)
proba P_kem_opk.
proba P_kem_opk_keycoll.
proba P_kem_opk_ctxtcoll.
expand IND_CCA2_KEM(
opk_keyseed, opk_pkey, opk_skey,
opk_secret, opk_ciphertext, opk_encapoutput,
opk_pkgen, opk_skgen, opk_encap, opk_pair, opk_decap,
injbot_opk, P_kem_opk, P_kem_opk_keycoll, P_kem_opk_ctxtcoll
).
(* ---------- Bob's signature (EUF-CMA) — signs SPK bundle ---------- *)
proba P_sig_B.
proba P_sig_B_keycoll.
expand UF_CMA_proba_signature(
sig_keyseed, sig_pkey, sig_skey, bitstring, sig_signature,
sigB_skgen, sigB_pkgen, sigB_sign, sigB_verify,
P_sig_B, P_sig_B_keycoll
).
(* ---------- Alice's signature (EUF-CMA) — signs SessionInit ---------- *)
type sigA_keyseed [large, fixed].
type sigA_pkey [bounded].
type sigA_skey [bounded].
type sigA_signature [bounded].
proba P_sig_A.
proba P_sig_A_keycoll.
expand UF_CMA_proba_signature(
sigA_keyseed, sigA_pkey, sigA_skey, bitstring, sigA_signature,
sigA_skgen, sigA_pkgen, sigA_sign, sigA_verify,
P_sig_A, P_sig_A_keycoll
).
(* ---------- HKDF as PRF ---------- *)
proba P_prf.
expand PRF_large(kem_secret, bitstring, sessionkey, kdf_kex, P_prf).
(* ---------- Domain separation constants ---------- *)
const lo_spk_sig_label: bitstring.
const lo_kex_init_sig_label: bitstring.
(* ---------- Events ---------- *)
(* Events bound on the signed session init content — what the signature
* directly authenticates. rk is derived from this, but the correspondence
* is over the authenticated payload. *)
event Alice_Init(sigA_pkey, sig_pkey, kem_ciphertext, spk_ciphertext, opk_ciphertext).
event Bob_Accept(sigA_pkey, sig_pkey, kem_ciphertext, spk_ciphertext, opk_ciphertext).
(* ---------- Security queries ---------- *)
(* Theorem 2b: Initiator authentication — if Bob accepts SI, Alice signed it *)
query pkA: sigA_pkey, pkB: sig_pkey,
cik: kem_ciphertext, cspk: spk_ciphertext, copk: opk_ciphertext;
event(Bob_Accept(pkA, pkB, cik, cspk, copk))
==> event(Alice_Init(pkA, pkB, cik, cspk, copk)).
(* Note: Injective variant not claimed — session-init replay is application-layer
* (§7.5 A4). Alice may reuse the same bundle, producing identical SI contents.
* Replay prevention is a caller obligation, not a cryptographic guarantee. *)
(* ---------- Channels ---------- *)
channel c_start, c_pub, c_alice_start, c_alice_out, c_bob_in.
(* ---------- Alice (Initiator) ---------- *)
let Alice(sk_sig_A: sigA_skey, pk_sig_A: sigA_pkey,
pk_sig_B: sig_pkey, pk_ik_B: kem_pkey,
pk_spk_B: spk_pkey, pk_opk_B: opk_pkey,
sig_spk: sig_signature) =
foreach i_a <= N_A do
in(c_alice_start, ());
(* §4.2: Verify Bob's SPK signature *)
if sigB_verify((lo_spk_sig_label, pk_spk_B), pk_sig_B, sig_spk) = true then (
(* §4.3 Step 2: Encapsulate to IK, SPK, OPK *)
let ik_pair(ss_ik: kem_secret, c_ik: kem_ciphertext) = ik_encap(pk_ik_B) in
let spk_pair(ss_spk: spk_secret, c_spk: spk_ciphertext) = spk_encap(pk_spk_B) in
let opk_pair(ss_opk: opk_secret, c_opk: opk_ciphertext) = opk_encap(pk_opk_B) in
(* §4.3 Step 3: Derive session key *)
let rk: sessionkey = kdf_kex(ss_ik, (ss_spk, ss_opk, pk_sig_A, pk_sig_B)) in
(* §4.3 Step 5: Sign session init *)
let si: bitstring = (pk_sig_A, pk_sig_B, c_ik, c_spk, c_opk) in
let sig_si: sigA_signature = sigA_sign((lo_kex_init_sig_label, si), sk_sig_A) in
event Alice_Init(pk_sig_A, pk_sig_B, c_ik, c_spk, c_opk);
out(c_alice_out, (si, sig_si, c_ik, c_spk, c_opk))
).
(* ---------- Bob (Responder) ---------- *)
let Bob(sk_ik_B: kem_skey, sk_spk_B: spk_skey, sk_opk_B: opk_skey,
pk_sig_A: sigA_pkey, pk_sig_B: sig_pkey) =
foreach i_b <= N_B do
in(c_bob_in, (sig_si: sigA_signature,
c_ik: kem_ciphertext, c_spk: spk_ciphertext,
c_opk: opk_ciphertext));
(* Bob reconstructs SI from the received components *)
let si: bitstring = (pk_sig_A, pk_sig_B, c_ik, c_spk, c_opk) in
(* §4.4 Step 2: Verify Alice's signature *)
if sigA_verify((lo_kex_init_sig_label, si), pk_sig_A, sig_si) = true then (
(* §4.4 Step 5: Decapsulate *)
let injbot_ik(ss_ik: kem_secret) = ik_decap(c_ik, sk_ik_B) in
let injbot_spk(ss_spk: spk_secret) = spk_decap(c_spk, sk_spk_B) in
let injbot_opk(ss_opk: opk_secret) = opk_decap(c_opk, sk_opk_B) in
(* §4.4 Step 7: Derive session key *)
let rk: sessionkey = kdf_kex(ss_ik, (ss_spk, ss_opk, pk_sig_A, pk_sig_B)) in
event Bob_Accept(pk_sig_A, pk_sig_B, c_ik, c_spk, c_opk)
).
(* ---------- Main process ---------- *)
process
in(c_start, ());
(* Bob: IK KEM key *)
new ik_seed: kem_keyseed;
let pk_ik_B = ik_pkgen(ik_seed) in
let sk_ik_B = ik_skgen(ik_seed) in
(* Bob: SPK *)
new spk_seed: spk_keyseed;
let pk_spk_B = spk_pkgen(spk_seed) in
let sk_spk_B = spk_skgen(spk_seed) in
(* Bob: OPK *)
new opk_seed: opk_keyseed;
let pk_opk_B = opk_pkgen(opk_seed) in
let sk_opk_B = opk_skgen(opk_seed) in
(* Bob: signing key (signs SPK bundle) *)
new sigB_seed: sig_keyseed;
let pk_sig_B = sigB_pkgen(sigB_seed) in
let sk_sig_B = sigB_skgen(sigB_seed) in
(* Alice: signing key (signs SessionInit) *)
new sigA_seed: sigA_keyseed;
let pk_sig_A = sigA_pkgen(sigA_seed) in
let sk_sig_A = sigA_skgen(sigA_seed) in
(* Bob signs SPK *)
let sig_spk: sig_signature = sigB_sign((lo_spk_sig_label, pk_spk_B), sk_sig_B) in
(* Publish everything *)
out(c_pub, (pk_ik_B, pk_spk_B, pk_opk_B, pk_sig_B, pk_sig_A, sig_spk));
(Alice(sk_sig_A, pk_sig_A, pk_sig_B, pk_ik_B, pk_spk_B, pk_opk_B, sig_spk)
| Bob(sk_ik_B, sk_spk_B, sk_opk_B, pk_sig_A, pk_sig_B))

View file

@ -0,0 +1,153 @@
(* LO-KEX: Session Key Secrecy (Theorem 1)
*
* The session key (rk) is computationally indistinguishable from random
* to any adversary that has not corrupted all of Bob's keys AND Alice's RNG.
*
* Model: Alice encapsulates to Bob's IK/SPK/OPK, derives rk via HKDF-as-PRF.
* Bob decapsulates and derives the same rk. The adversary sees all public
* values (pk_IK_B, pk_SPK_B, pk_OPK_B, ciphertexts, signatures) but not
* the shared secrets or the derived key.
*
* The `secret` query tests whether rk_A (Alice's derived key) is
* indistinguishable from a uniformly random sessionkey.
*
* Reduces to: 3× IND-CCA2 KEM + HKDF PRF.
*
* Simplifications:
* - Signatures omitted: Theorem 1 (key secrecy) does not depend on
* authentication. Bundle integrity (SPK not substituted) is implicit
* in the process structure (Alice receives authentic pk_spk_B).
* - KDF info field simplified: uses (ss_spk, ss_opk, pk_ik_B) rather
* than the full spec info (pk_IK_A, pk_IK_B, pk_EK, crypto_version).
* The PRF proof holds regardless of info content.
* - X-Wing as black-box IND-CCA2: the spec (§2.1) recommends opening
* the combiner for CryptoVerif. The black-box assumption is stronger;
* the bound is in terms of P_kem rather than component advantages.
* - No corruption oracles: the proof covers the no-corruption case.
* Corruption-parameterized secrecy is verified by Tamarin (LO_KEX.spthy).
*)
param N_A.
param N_B.
(* ---------- Types ---------- *)
type kem_keyseed [large, fixed].
type kem_pkey [bounded].
type kem_skey [bounded].
type kem_secret [large, fixed].
type kem_ciphertext [bounded].
type kem_encapoutput [bounded].
type spk_keyseed [large, fixed].
type spk_pkey [bounded].
type spk_skey [bounded].
type spk_secret [large, fixed].
type spk_ciphertext [bounded].
type spk_encapoutput [bounded].
type opk_keyseed [large, fixed].
type opk_pkey [bounded].
type opk_skey [bounded].
type opk_secret [large, fixed].
type opk_ciphertext [bounded].
type opk_encapoutput [bounded].
type sessionkey [large, fixed].
(* ---------- Three IND-CCA2 KEMs ---------- *)
proba P_kem_ik.
proba P_kem_ik_keycoll.
proba P_kem_ik_ctxtcoll.
expand IND_CCA2_KEM(
kem_keyseed, kem_pkey, kem_skey,
kem_secret, kem_ciphertext, kem_encapoutput,
ik_pkgen, ik_skgen, ik_encap, ik_pair, ik_decap,
injbot_ik, P_kem_ik, P_kem_ik_keycoll, P_kem_ik_ctxtcoll
).
proba P_kem_spk.
proba P_kem_spk_keycoll.
proba P_kem_spk_ctxtcoll.
expand IND_CCA2_KEM(
spk_keyseed, spk_pkey, spk_skey,
spk_secret, spk_ciphertext, spk_encapoutput,
spk_pkgen, spk_skgen, spk_encap, spk_pair, spk_decap,
injbot_spk, P_kem_spk, P_kem_spk_keycoll, P_kem_spk_ctxtcoll
).
proba P_kem_opk.
proba P_kem_opk_keycoll.
proba P_kem_opk_ctxtcoll.
expand IND_CCA2_KEM(
opk_keyseed, opk_pkey, opk_skey,
opk_secret, opk_ciphertext, opk_encapoutput,
opk_pkgen, opk_skgen, opk_encap, opk_pair, opk_decap,
injbot_opk, P_kem_opk, P_kem_opk_keycoll, P_kem_opk_ctxtcoll
).
(* ---------- HKDF as PRF keyed by ss_ik ---------- *)
proba P_prf.
expand PRF_large(kem_secret, bitstring, sessionkey, kdf_kex, P_prf).
(* ---------- Security query ---------- *)
(* Theorem 1: rk_A is indistinguishable from random.
* cv_onesession: secrecy for a single tested session (standard model). *)
query secret rk_A [cv_onesession].
(* ---------- Channels ---------- *)
channel c_start, c_pub, c_alice_start, c_alice_out, c_bob_in, c_bob_done.
(* ---------- Alice (Initiator) ---------- *)
let Alice(pk_ik_B: kem_pkey, pk_spk_B: spk_pkey, pk_opk_B: opk_pkey) =
foreach i_a <= N_A do
in(c_alice_start, ());
(* §4.3 Step 2: Encapsulate to IK, SPK, OPK *)
let ik_pair(ss_ik: kem_secret, c_ik: kem_ciphertext) = ik_encap(pk_ik_B) in
let spk_pair(ss_spk: spk_secret, c_spk: spk_ciphertext) = spk_encap(pk_spk_B) in
let opk_pair(ss_opk: opk_secret, c_opk: opk_ciphertext) = opk_encap(pk_opk_B) in
(* §4.3 Step 3: Derive session key *)
let rk_A: sessionkey = kdf_kex(ss_ik, (ss_spk, ss_opk, pk_ik_B)) in
out(c_alice_out, (c_ik, c_spk, c_opk)).
(* ---------- Bob (Responder) ---------- *)
(* Bob decapsulates — models the CCA2 decapsulation oracle *)
let Bob(sk_ik_B: kem_skey, sk_spk_B: spk_skey, sk_opk_B: opk_skey,
pk_ik_B: kem_pkey) =
foreach i_b <= N_B do
in(c_bob_in, (c_ik: kem_ciphertext, c_spk: spk_ciphertext,
c_opk: opk_ciphertext));
let injbot_ik(ss_ik: kem_secret) = ik_decap(c_ik, sk_ik_B) in
let injbot_spk(ss_spk: spk_secret) = spk_decap(c_spk, sk_spk_B) in
let injbot_opk(ss_opk: opk_secret) = opk_decap(c_opk, sk_opk_B) in
let rk_B: sessionkey = kdf_kex(ss_ik, (ss_spk, ss_opk, pk_ik_B)) in
out(c_bob_done, ()).
(* ---------- Main process ---------- *)
process
in(c_start, ());
(* Bob's KEM keys *)
new ik_seed: kem_keyseed;
let pk_ik_B = ik_pkgen(ik_seed) in
let sk_ik_B = ik_skgen(ik_seed) in
new spk_seed: spk_keyseed;
let pk_spk_B = spk_pkgen(spk_seed) in
let sk_spk_B = spk_skgen(spk_seed) in
new opk_seed: opk_keyseed;
let pk_opk_B = opk_pkgen(opk_seed) in
let sk_opk_B = opk_skgen(opk_seed) in
(* Publish public keys *)
out(c_pub, (pk_ik_B, pk_spk_B, pk_opk_B));
(Alice(pk_ik_B, pk_spk_B, pk_opk_B)
| Bob(sk_ik_B, sk_spk_B, sk_opk_B, pk_ik_B))

View file

@ -0,0 +1,44 @@
(* LO-Ratchet: Message Key Secrecy (Theorem 3)
*
* Given a fresh epoch key ek (from Theorem 1 + KDF_Root), proves that
* message keys mk = KDF_MsgKey(ek, counter) are indistinguishable from
* random. Combined with AEAD security under random keys (standard
* composition via [BN00]), this gives full message secrecy.
*
* Reduces to: HMAC-SHA3-256 PRF.
*)
param N_msg.
(* ---------- Types ---------- *)
type epoch_key [large, fixed].
type msg_key [large, fixed].
type counter [fixed].
(* ---------- KDF_MsgKey as PRF ---------- *)
proba P_prf.
expand PRF_large(epoch_key, counter, msg_key, kdf_msgkey, P_prf).
(* ---------- Security query ---------- *)
query secret test_mk [cv_onesession].
(* ---------- Channels ---------- *)
channel c_start, c_ready, c_test_in, c_test_out.
(* ---------- Process ---------- *)
(* Single derivation: ek is fresh, derive mk at one counter.
* The PRF transformation replaces kdf_msgkey(ek, ctr) with a random value.
* No oracle needed — the PRF_large game handles multi-query internally. *)
process
in(c_start, ());
new ek: epoch_key;
out(c_ready, ());
in(c_test_in, ctr: counter);
let test_mk: msg_key = kdf_msgkey(ek, ctr) in
out(c_test_out, ())

View file

@ -0,0 +1,116 @@
(* LO-Stream: Chunk Confidentiality + Integrity (Theorem 13, P1+P2)
*
* Adapted from CryptoVerif's TLS 1.3 Record Protocol example.
*
* IND-CPA: Bit-guessing game — adversary submits two equal-length plaintexts,
* receives encryption of one based on secret bit b0. Advantage = Pr[guess b0].
*
* INT-CTXT: Injective correspondence — if decryption succeeds at (count, msg),
* encryption must have produced (count, msg).
*
* Nonce uniqueness enforced via table lookup (nonce-respecting adversary, §9.11(f)).
* Nonce derivation: chunk_nonce = xor(base_nonce, count) with injectivity equation.
*
* Reduces to: XChaCha20-Poly1305 IND-CPA + INT-CTXT.
*)
type key [fixed, large].
type seqn [fixed].
type nonce_t [fixed, large].
type add_data [bounded].
param N_enc, N_dec.
(* ---------- Nonce derivation with injectivity ---------- *)
(* §15.2: chunk_nonce = base_nonce XOR mask(index, tag_byte)
* Modeled as xor(iv, count) per TLS 1.3 record protocol pattern.
* CryptoVerif needs the explicit injectivity equation. *)
fun xor(key, seqn): nonce_t.
equation forall k: key, n: seqn, n': seqn;
(xor(k, n) = xor(k, n')) = (n = n').
(* ---------- AEAD (IND-CPA + INT-CTXT under unique nonces) ---------- *)
proba P_cpa.
proba P_ctxt.
expand AEAD_nonce(key, bitstring, bitstring, add_data, nonce_t,
enc, dec, injbot, Z, P_cpa, P_ctxt).
(* Per-chunk AAD includes base_nonce + count (§15.4) *)
fun derive_aad(key, seqn): add_data [data].
letfun stream_encrypt(k: key, n: nonce_t, m: bitstring, aad: add_data) =
enc(m, aad, k, n).
letfun stream_decrypt(k: key, n: nonce_t, c: bitstring, aad: add_data) =
dec(c, aad, k, n).
(* ---------- Tables for nonce uniqueness (§9.11(f) game hypothesis) ---------- *)
table table_enc_nonce(seqn).
table table_dec_nonce(seqn).
(* ---------- Security queries ---------- *)
(* P1 (IND-CPA): secret bit indistinguishable *)
query secret b0 [cv_bit].
(* P2 (INT-CTXT): injective correspondence *)
event Sent(seqn, bitstring).
event Received(seqn, bitstring).
query count: seqn, msg: bitstring;
inj-event(Received(count, msg)) ==> inj-event(Sent(count, msg))
public_vars b0.
(* ---------- Channels ---------- *)
channel c_start, c_ready, c_enc_in, c_enc_out, c_dec_in, c_dec_out.
(* ---------- Encrypt oracle ---------- *)
(* Adversary submits (m0, m1, count). Oracle encrypts m_b where b = b0.
* Count must not have been used before (nonce-respecting). *)
let Encrypt(k: key, iv: key, b: bool) =
!N_enc
in(c_enc_in, (clear1: bitstring, clear2: bitstring, count: seqn));
(* Nonce-respecting: reject reused counter *)
get table_enc_nonce(=count) in yield else
insert table_enc_nonce(count);
(* Equal-length requirement for IND-CPA *)
if Z(clear1) = Z(clear2) then
let clear = if_fun(b, clear1, clear2) in
let aad: add_data = derive_aad(iv, count) in
let nonce = xor(iv, count) in
event Sent(count, clear);
let cipher = stream_encrypt(k, nonce, clear, aad) in
out(c_enc_out, cipher).
(* ---------- Decrypt oracle ---------- *)
(* Adversary submits (ciphertext, count). Must not reuse count. *)
let Decrypt(k: key, iv: key) =
!N_dec
in(c_dec_in, (cipher: bitstring, count: seqn));
get table_dec_nonce(=count) in yield else
insert table_dec_nonce(count);
let aad: add_data = derive_aad(iv, count) in
let nonce = xor(iv, count) in
let injbot(clear) = stream_decrypt(k, nonce, cipher, aad) in
event Received(count, clear).
(* ---------- Main process ---------- *)
process
in(c_start, ());
new b0: bool;
new k: key;
new iv: key; (* base_nonce — public *)
out(c_ready, iv);
(Encrypt(k, iv, b0) | Decrypt(k, iv))
(* EXPECTED
All queries proved.
END *)

112
cryptoverif/README.md Normal file
View file

@ -0,0 +1,112 @@
# CryptoVerif Models
Computational formal verification of the Soliton cryptographic protocol using
[CryptoVerif](https://bblanche.gitlabpages.inria.fr/CryptoVerif/).
These models were authored by the protocol designers and have not undergone
independent peer review. They are published for transparency and to facilitate
third-party verification. All results are machine-checkable and reproducible.
## Requirements
- CryptoVerif 2.12+
- The `pq.cvl` library (ships with CryptoVerif)
## Usage
```bash
# All models
CV_LIB=/path/to/pq ../verify.sh cryptoverif
# Single model
cryptoverif -lib /path/to/pq LO_Auth.cv
```
## Resource Usage
All 5 models complete in under 5 seconds total with negligible RAM usage.
No special hardware required.
## Results
Verified with CryptoVerif 2.12.
### LO_Auth.cv — Theorem 6 (Key Possession)
| Query | Result | Bound |
|-------|--------|-------|
| event(ServerAccepts) ==> event(ClientResponds) | proved | Ns × P_mac + P_kem |
| inj-event(ServerAccepts) ==> inj-event(ClientResponds) | proved | Ns × P_mac + P_kem |
Primitives: IND-CCA2 KEM (X-Wing), SUF-CMA deterministic MAC (HMAC-SHA3-256).
### LO_KEX.cv — Theorem 2b (Initiator Authentication)
| Query | Result | Bound |
|-------|--------|-------|
| event(Bob_Accept) ==> event(Alice_Init) | proved | P_sig_A |
Primitives: EUF-CMA signature (HybridSig). Proof uses only Alice's signature
unforgeability. Non-injective (replay is application-layer per §7.5 A4).
### LO_KEX_Secrecy.cv — Theorem 1 (Session Key Secrecy)
| Query | Result | Bound |
|-------|--------|-------|
| secret rk_A [cv_onesession] | proved | 2·P_prf + 2·P_kem_ik + 2·P_kem_spk + 2·P_kem_opk + collision terms |
Primitives: 3× IND-CCA2 KEM, PRF (HKDF). Signatures omitted (Theorem 1 is
secrecy, not authentication). No corruption oracles (Tamarin covers corruption
cases). See header comment for full simplifications list.
### LO_Ratchet_MsgSecrecy.cv — Theorem 3 (Message Key Secrecy)
| Query | Result | Bound |
|-------|--------|-------|
| secret test_mk [cv_onesession] | proved | 2 × P_prf |
Precondition: epoch key ek is fresh (from Theorem 1 + KDF_Root output
independence). Combined with AEAD IND-CPA+INT-CTXT under random keys
(standard [BN00] composition), gives full message secrecy.
### LO_Stream_Secrecy.cv — Theorem 13, Properties 1+2 (Streaming AEAD)
| Query | Result | Bound |
|-------|--------|-------|
| secret b0 [cv_bit] (IND-CPA) | proved | 2·P_ctxt + 2·P_cpa(time, N_enc) |
| inj-event(Received) ==> inj-event(Sent) (INT-CTXT) | proved | P_ctxt |
Adapted from CryptoVerif's TLS 1.3 Record Protocol example. Nonce uniqueness
enforced via table-based game hypothesis (§9.11(f)). base_nonce is public.
Key properties of the bounds:
- **INT-CTXT has no Q-factor** — direct forgery reduction
- **IND-CPA scales as N_enc × P_cpa** — Q-step hybrid argument
## Scope and Limitations
- **X-Wing as black box**: All models treat X-Wing as a monolithic IND-CCA2
KEM. The spec (§2.1) recommends opening the combiner for CryptoVerif. The
black-box assumption is stronger; bounds are in terms of P_kem rather than
component advantages (P_mlkem + P_x25519 + P_sha3_ro).
- **No corruption oracles**: The CryptoVerif KEX models prove security for
the no-corruption case. Corruption-parameterized secrecy (partial key
compromise, RNG corruption) is verified by the Tamarin models.
- **Simplified KDF info**: LO_KEX_Secrecy.cv binds fewer values in the PRF
input than the full HKDF info field. The PRF proof holds regardless of
info content; session-binding properties are verified by Tamarin.
- **Single-epoch message secrecy**: LO_Ratchet_MsgSecrecy.cv assumes a fresh
epoch key. The composition chain (Theorem 1 → KDF_Root → fresh ek → PRF →
fresh mk → AEAD) is sound but not mechanically verified end-to-end.
- **No Theorem 2c/d**: Key confirmation requires a combined KEX+Ratchet model.
## Theorem Coverage
| Theorem | Model | What's proved |
|---------|-------|---------------|
| 1 (KEX Key Secrecy) | LO_KEX_Secrecy | rk indistinguishable from random |
| 2b (Initiator Auth) | LO_KEX | σ_SI authentication via EUF-CMA |
| 3 (Message Secrecy) | LO_Ratchet_MsgSecrecy | mk indistinguishable from random |
| 6 (Auth Key Possession) | LO_Auth | Correspondence + injective |
| 13 P1 (IND-CPA) | LO_Stream_Secrecy | Bit secrecy of challenge bit |
| 13 P2 (INT-CTXT) | LO_Stream_Secrecy | Injective correspondence |