libsoliton/tamarin/LO_Stream.spthy
Kamal Tufekcic 3acaa0fa3f
CryptoVerif and Tamarin models, minor doc updates
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-23 15:52:23 +03:00

251 lines
7.8 KiB
Text

theory LO_Stream
begin
/*
* LO-Stream: Streaming AEAD (Theorem 13)
* =======================================
*
* Chunked AEAD with counter-derived nonces over XChaCha20-Poly1305.
* Each stream uses a caller-provided key and a random base_nonce.
*
* State partition (§15.3):
* Linear (sequential only): next_index, finalized
* Persistent (both interfaces): key, base_nonce, caller_aad
*
* Nonce: nonce(base_nonce, index, tag_byte) — free constructor,
* injectivity by construction (§15.2).
* AAD: stream_aad(base_nonce, index, tag_byte, caller_aad) — free
* constructor, injectivity by construction (§15.4).
*
* Properties:
* P2: Integrity (INT-CTXT) — no forgery without key
* P3: Ordering — chunk accepted at position j only if encrypted at j
* P4: Truncation resistance — finalized only via genuine final chunk
* P5: Cross-stream isolation — chunk from stream A rejected by stream B
*
* Bounded to 3 sequential chunks (indices 0, 1, 2) for termination.
*
* EXPECTED RESULTS:
* Stream_Sanity: VERIFIED
* Stream_Sanity_Finalize: VERIFIED
* Theorem13_P2_Integrity: VERIFIED
* Theorem13_P3_Ordering: VERIFIED
* Theorem13_P4_No_False_Final: VERIFIED
* Theorem13_P5_Cross_Stream: VERIFIED
* Theorem13_Key_Secrecy: VERIFIED
*/
builtins: hashing
// AEAD
functions: aead_enc/4, aead_verify/4, accept/0
equations: aead_verify(k, n, aad, aead_enc(k, n, m, aad)) = accept()
// Nonce and AAD constructors (free → injective)
functions: nonce/3, stream_aad/4
// Tag bytes
functions: nonfinal/0, final/0
// Bounded indices
functions: s/1
restriction Eq:
"All x y #i. Eq(x,y) @i ==> x = y"
rule Index_Bounds:
[ ] --> [ !CB('0', s('0')), !CB(s('0'), s(s('0'))) ]
/* ================= STREAM SETUP ================= */
rule Stream_Init:
[ Fr(~key), Fr(~bn), Fr(~cid) ]
--[ StreamInit($O, ~key, ~bn, ~cid) ]->
[ !SP($O, ~key, ~bn, ~cid),
EncSt($O, ~bn, '0', 'false'),
DecSt($O, ~bn, '0', 'false'),
Out(~bn) ]
/* ================= CORRUPTION ================= */
rule Corrupt_StreamKey:
[ !SP($O, key, bn, cid) ]
--[ CorruptStream($O, key) ]->
[ Out(key) ]
/* ================= SEQUENTIAL ENCRYPTION ================= */
// Non-final chunk
rule Enc_Chunk:
let
n = nonce(bn, idx, nonfinal)
aad = stream_aad(bn, idx, nonfinal, cid)
c = aead_enc(key, n, ~m, aad)
in
[ EncSt($O, bn, idx, 'false'), !SP($O, key, bn, cid),
!CB(idx, next), Fr(~m) ]
--[ Encrypted($O, bn, idx, nonfinal, c) ]->
[ EncSt($O, bn, next, 'false'), Out(<c, idx, nonfinal>) ]
// Final chunk — consumes EncSt without replacement (finalized)
rule Enc_Final:
let
n = nonce(bn, idx, final)
aad = stream_aad(bn, idx, final, cid)
c = aead_enc(key, n, ~m, aad)
in
[ EncSt($O, bn, idx, 'false'), !SP($O, key, bn, cid), Fr(~m) ]
--[ Encrypted($O, bn, idx, final, c), EncFinalized($O, bn, idx) ]->
[ Out(<c, idx, final>) ]
/* ================= SEQUENTIAL DECRYPTION ================= */
// Non-final chunk — decryptor advances index
rule Dec_Chunk:
let
n = nonce(bn, idx, nonfinal)
aad = stream_aad(bn, idx, nonfinal, cid)
in
[ DecSt($O, bn, idx, 'false'), !SP($O, key, bn, cid),
!CB(idx, next), In(<c, idx, nonfinal>) ]
--[ Eq(aead_verify(key, n, aad, c), accept()),
SeqDec($O, bn, idx, nonfinal, c) ]->
[ DecSt($O, bn, next, 'false') ]
// Final chunk — consumes DecSt without replacement (finalized)
rule Dec_Final:
let
n = nonce(bn, idx, final)
aad = stream_aad(bn, idx, final, cid)
in
[ DecSt($O, bn, idx, 'false'), !SP($O, key, bn, cid),
In(<c, idx, final>) ]
--[ Eq(aead_verify(key, n, aad, c), accept()),
SeqDec($O, bn, idx, final, c), DecFinalized($O, bn, idx) ]->
[ ]
/* ================= RANDOM-ACCESS DECRYPTION ================= */
// Stateless — reads only persistent state
rule Dec_At:
let
n = nonce(bn, idx, tag)
aad = stream_aad(bn, idx, tag, cid)
in
[ !SP($O, key, bn, cid), In(<c, idx, tag>) ]
--[ Eq(aead_verify(key, n, aad, c), accept()),
RaDec($O, bn, idx, tag) ]->
[ ]
/* ================= SECOND STREAM (cross-stream isolation) ================= */
// Same key, different base_nonce
rule Stream_Init_B:
[ !SP($O, key, bn_a, cid_a), Fr(~bn_b), Fr(~cid_b) ]
--[ StreamInitB($O, ~bn_b) ]->
[ !SPB($O, key, ~bn_b, ~cid_b),
DecStB($O, ~bn_b, '0', 'false'),
Out(~bn_b) ]
// Stream B sequential decrypt
rule Dec_Chunk_B:
let
n = nonce(bn_b, idx, tag)
aad = stream_aad(bn_b, idx, tag, cid_b)
in
[ DecStB($O, bn_b, idx, 'false'), !SPB($O, key, bn_b, cid_b),
!CB(idx, next), In(<c, idx, tag>) ]
--[ Eq(aead_verify(key, n, aad, c), accept()),
DecB($O, bn_b, idx, tag) ]->
[ DecStB($O, bn_b, next, 'false') ]
// Stream B random-access decrypt
rule Dec_At_B:
let
n = nonce(bn_b, idx, tag)
aad = stream_aad(bn_b, idx, tag, cid_b)
in
[ !SPB($O, key, bn_b, cid_b), In(<c, idx, tag>) ]
--[ Eq(aead_verify(key, n, aad, c), accept()),
DecB($O, bn_b, idx, tag) ]->
[ ]
/* ================= LEMMAS ================= */
// Sanity: encrypt + decrypt a non-final chunk
lemma Stream_Sanity:
exists-trace
"Ex O bn idx c #i #j.
Encrypted(O, bn, idx, nonfinal, c) @i &
SeqDec(O, bn, idx, nonfinal, c) @j"
// Sanity: encrypt final + reach DecFinalized
lemma Stream_Sanity_Finalize:
exists-trace
"Ex O bn idx #i #j.
EncFinalized(O, bn, idx) @i &
DecFinalized(O, bn, idx) @j"
// P2 (Integrity): Sequential decryptor accepts only genuine ciphertexts.
// If SeqDec fires at (bn, idx, tag), the encryptor produced a chunk
// at that exact (bn, idx, tag) — or the key was corrupted.
lemma Theorem13_P2_Integrity:
"All O bn idx tag c #j.
SeqDec(O, bn, idx, tag, c) @j
==>
(Ex #i. Encrypted(O, bn, idx, tag, c) @i)
| (Ex key #k. CorruptStream(O, key) @k)
"
// P3 (Ordering): Implicit in P2 for the symbolic model — the sequential
// decryptor's index is part of the nonce/AAD, so accepting at index j
// requires a ciphertext produced at index j. Stated separately per §9.11(b).
// The adversary captures a chunk encrypted at index i and presents it at
// position j. The nonce/AAD mismatch prevents acceptance.
// P3 (Ordering): The SAME ciphertext c encrypted at index idx1 cannot
// be accepted by the sequential decryptor at a different index idx2.
// The nonce/AAD include the index, so mismatch causes AEAD failure.
lemma Theorem13_P3_Ordering:
"All O bn idx1 idx2 tag c #i #j.
Encrypted(O, bn, idx1, tag, c) @i &
SeqDec(O, bn, idx2, tag, c) @j &
not (idx1 = idx2)
==> (Ex key #k. CorruptStream(O, key) @k)"
// P4 (Truncation Resistance): DecFinalized only reachable via a genuine
// final-chunk ciphertext (tag_byte=final) from the encryptor.
lemma Theorem13_P4_No_False_Final:
"All O bn idx #j.
DecFinalized(O, bn, idx) @j
==>
(Ex c #i. Encrypted(O, bn, idx, final, c) @i)
| (Ex key #k. CorruptStream(O, key) @k)
"
// P5 (Cross-Stream Isolation): A chunk encrypted on stream A cannot be
// accepted by stream B's decryptor (different base_nonce, same key).
// Covers both sequential (DecB) and random-access (DecB) decryption.
lemma Theorem13_P5_Cross_Stream:
"All O bn_a bn_b idx tag c #i #j.
Encrypted(O, bn_a, idx, tag, c) @i &
DecB(O, bn_b, idx, tag) @j &
not (bn_a = bn_b)
==> (Ex key #k. CorruptStream(O, key) @k)"
// Key secrecy: stream key secret unless directly corrupted
lemma Theorem13_Key_Secrecy:
"All O key bn cid #i.
StreamInit(O, key, bn, cid) @i
==>
not (Ex #j. K(key) @j)
| (Ex #j. CorruptStream(O, key) @j)
"
end