chore(tlsn): add transcript auth tests (#1014)

* chore(tlsn): add transcript auth tests

* clippy
This commit is contained in:
sinu.eth
2025-10-10 14:10:17 -07:00
committed by GitHub
parent f99fce5b5a
commit 0ec2392716
4 changed files with 250 additions and 53 deletions

60
Cargo.lock generated
View File

@@ -2009,7 +2009,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "clmul"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"bytemuck",
"cfg-if",
@@ -4211,7 +4211,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "matrix-transpose"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"thiserror 1.0.69",
]
@@ -4268,7 +4268,7 @@ dependencies = [
[[package]]
name = "mpz-circuits"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"bincode 1.3.3",
"itybity 0.3.1",
@@ -4284,7 +4284,7 @@ dependencies = [
[[package]]
name = "mpz-cointoss"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"futures",
"mpz-cointoss-core",
@@ -4297,7 +4297,7 @@ dependencies = [
[[package]]
name = "mpz-cointoss-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"mpz-core",
"opaque-debug",
@@ -4308,7 +4308,7 @@ dependencies = [
[[package]]
name = "mpz-common"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"bytes",
@@ -4328,7 +4328,7 @@ dependencies = [
[[package]]
name = "mpz-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"aes 0.9.0-rc.1",
"bcs",
@@ -4354,7 +4354,7 @@ dependencies = [
[[package]]
name = "mpz-fields"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"ark-ff 0.4.2",
"ark-secp256r1",
@@ -4374,7 +4374,7 @@ dependencies = [
[[package]]
name = "mpz-garble"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"derive_builder 0.11.2",
@@ -4400,7 +4400,7 @@ dependencies = [
[[package]]
name = "mpz-garble-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"aes 0.9.0-rc.1",
"bitvec",
@@ -4431,7 +4431,7 @@ dependencies = [
[[package]]
name = "mpz-hash"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"blake3",
"itybity 0.3.1",
@@ -4441,10 +4441,27 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "mpz-ideal-vm"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"futures",
"mpz-common",
"mpz-core",
"mpz-memory-core",
"mpz-vm-core",
"rangeset",
"serde",
"serio",
"thiserror 1.0.69",
]
[[package]]
name = "mpz-memory-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"blake3",
"futures",
@@ -4459,7 +4476,7 @@ dependencies = [
[[package]]
name = "mpz-ole"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"futures",
@@ -4477,7 +4494,7 @@ dependencies = [
[[package]]
name = "mpz-ole-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"hybrid-array",
"itybity 0.3.1",
@@ -4493,7 +4510,7 @@ dependencies = [
[[package]]
name = "mpz-ot"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"cfg-if",
@@ -4516,7 +4533,7 @@ dependencies = [
[[package]]
name = "mpz-ot-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"aes 0.9.0-rc.1",
"blake3",
@@ -4547,7 +4564,7 @@ dependencies = [
[[package]]
name = "mpz-share-conversion"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"mpz-common",
@@ -4563,7 +4580,7 @@ dependencies = [
[[package]]
name = "mpz-share-conversion-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"mpz-common",
"mpz-core",
@@ -4577,7 +4594,7 @@ dependencies = [
[[package]]
name = "mpz-vm-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"futures",
@@ -4590,7 +4607,7 @@ dependencies = [
[[package]]
name = "mpz-zk"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"async-trait",
"blake3",
@@ -4608,7 +4625,7 @@ dependencies = [
[[package]]
name = "mpz-zk-core"
version = "0.1.0-alpha.3"
source = "git+https://github.com/privacy-ethereum/mpz?rev=f30e07c#f30e07c01d8f62863fe1703e92d9323db96961df"
source = "git+https://github.com/privacy-ethereum/mpz?rev=c6e8a53#c6e8a530d6888526439697b3a1393d33690c8fa8"
dependencies = [
"blake3",
"cfg-if",
@@ -7163,6 +7180,7 @@ dependencies = [
"mpz-garble",
"mpz-garble-core",
"mpz-hash",
"mpz-ideal-vm",
"mpz-memory-core",
"mpz-ole",
"mpz-ot",

View File

@@ -66,19 +66,20 @@ tlsn-harness-runner = { path = "crates/harness/runner" }
tlsn-wasm = { path = "crates/wasm" }
tlsn = { path = "crates/tlsn" }
mpz-circuits = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-memory-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-common = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-vm-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-garble = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-garble-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-ole = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-ot = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-share-conversion = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-fields = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-zk = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-hash = { git = "https://github.com/privacy-ethereum/mpz", rev = "f30e07c" }
mpz-circuits = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-memory-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-common = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-vm-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-garble = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-garble-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-ole = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-ot = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-share-conversion = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-fields = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-zk = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-hash = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
mpz-ideal-vm = { git = "https://github.com/privacy-ethereum/mpz", rev = "c6e8a53" }
rangeset = { version = "0.2" }
serio = { version = "0.2" }

View File

@@ -60,7 +60,9 @@ rangeset = { workspace = true }
webpki-roots = { workspace = true }
[dev-dependencies]
mpz-ideal-vm = { workspace = true }
rstest = { workspace = true }
tlsn-core = { workspace = true, features = ["fixtures"] }
tlsn-server-fixture = { workspace = true }
tlsn-server-fixture-certs = { workspace = true }
tokio = { workspace = true, features = ["full"] }

View File

@@ -235,11 +235,15 @@ fn alloc_keystream<'a>(
return Ok(keystream);
};
if range.start >= pos + record.len {
let record_range = pos..pos + record.len;
if range.start >= record_range.end {
current_range = Some(range);
break;
}
// Range with record offset applied.
let offset_range = range.start - pos..range.end - pos;
let explicit_nonce = if let Some(explicit_nonce) = explicit_nonce {
explicit_nonce
} else {
@@ -249,7 +253,7 @@ fn alloc_keystream<'a>(
};
const BLOCK_SIZE: usize = 16;
let block_num = (range.start - pos) / BLOCK_SIZE;
let block_num = offset_range.start / BLOCK_SIZE;
let block = if let Some((current_block_num, block)) = current_block.take()
&& current_block_num == block_num
{
@@ -260,14 +264,16 @@ fn alloc_keystream<'a>(
block
};
let start = (range.start - pos) % BLOCK_SIZE;
let end = (start + range.len()).min(BLOCK_SIZE);
let len = end - start;
// Range within the block.
let block_range_start = offset_range.start % BLOCK_SIZE;
let len =
(range.end.min(record_range.end) - range.start).min(BLOCK_SIZE - block_range_start);
let block_range = block_range_start..block_range_start + len;
keystream.push(block.get(start..end).expect("range is checked"));
keystream.push(block.get(block_range).expect("range is checked"));
// If the range is larger than a block, process the tail.
if range.len() > BLOCK_SIZE {
// If the range extends past the block, process the tail.
if range.start + len < range.end {
current_range = Some(range.start + len..range.end);
}
}
@@ -394,6 +400,19 @@ enum ProofInner<'a> {
},
}
fn aes_ctr_apply_keystream(key: &[u8; 16], iv: &[u8; 4], explicit_nonce: &[u8], input: &mut [u8]) {
let mut full_iv = [0u8; 16];
full_iv[0..4].copy_from_slice(iv);
full_iv[4..12].copy_from_slice(&explicit_nonce[..8]);
const START_CTR: u32 = 2;
let mut cipher = Ctr32BE::<Aes128>::new(key.into(), &full_iv.into());
cipher
.try_seek(START_CTR * 16)
.expect("start counter is less than keystream length");
cipher.apply_keystream(input);
}
fn verify_plaintext_with_key<'a>(
key: [u8; 16],
iv: [u8; 4],
@@ -404,20 +423,10 @@ fn verify_plaintext_with_key<'a>(
let mut pos = 0;
let mut text = Vec::new();
for record in records {
let mut full_iv = [0u8; 16];
full_iv[0..4].copy_from_slice(&iv);
full_iv[4..12].copy_from_slice(&record.explicit_nonce[..8]);
const START_CTR: u32 = 2;
let mut cipher = Ctr32BE::<Aes128>::new(&key.into(), &full_iv.into());
cipher
.try_seek(START_CTR * 16)
.expect("start counter is less than keystream length");
text.clear();
text.extend_from_slice(&plaintext[pos..pos + record.len]);
cipher.apply_keystream(&mut text);
aes_ctr_apply_keystream(&key, &iv, &record.explicit_nonce, &mut text);
if text != ciphertext[pos..pos + record.len] {
return Err(PlaintextAuthError(ErrorRepr::InvalidPlaintext));
@@ -453,3 +462,170 @@ enum ErrorRepr {
#[error("plaintext does not match ciphertext")]
InvalidPlaintext,
}
#[cfg(test)]
#[allow(clippy::all)]
mod tests {
use super::*;
use mpz_common::context::test_st_context;
use mpz_ideal_vm::IdealVm;
use mpz_vm_core::prelude::*;
use rand::{Rng, SeedableRng, rngs::StdRng};
use rstest::*;
use std::ops::Range;
fn build_vm(key: [u8; 16], iv: [u8; 4]) -> (IdealVm, Array<U8, 16>, Array<U8, 4>) {
let mut vm = IdealVm::new();
let key_ref = vm.alloc::<Array<U8, 16>>().unwrap();
let iv_ref = vm.alloc::<Array<U8, 4>>().unwrap();
vm.mark_public(key_ref).unwrap();
vm.mark_public(iv_ref).unwrap();
vm.assign(key_ref, key).unwrap();
vm.assign(iv_ref, iv).unwrap();
vm.commit(key_ref).unwrap();
vm.commit(iv_ref).unwrap();
(vm, key_ref, iv_ref)
}
fn expected_aes_ctr<'a>(
key: [u8; 16],
iv: [u8; 4],
records: impl IntoIterator<Item = &'a RecordParams>,
ranges: &RangeSet<usize>,
) -> Vec<u8> {
let mut keystream = Vec::new();
let mut pos = 0;
for record in records {
let mut record_keystream = vec![0u8; record.len];
aes_ctr_apply_keystream(&key, &iv, &record.explicit_nonce, &mut record_keystream);
for mut range in ranges.iter_ranges() {
range.start = range.start.max(pos);
range.end = range.end.min(pos + record.len);
if range.start < range.end {
keystream
.extend_from_slice(&record_keystream[range.start - pos..range.end - pos]);
}
}
pos += record.len;
}
keystream
}
#[rstest]
#[case::single_record_empty([0], [])]
#[case::multiple_empty_records_empty([0, 0], [])]
#[case::multiple_records_empty([128, 64], [])]
#[case::single_block_full([16], [0..16])]
#[case::single_block_partial([16], [2..14])]
#[case::partial_block_full([15], [0..15])]
#[case::out_of_bounds([16], [0..17])]
#[case::multiple_records_full([128, 63, 33, 15, 4], [0..243])]
#[case::multiple_records_partial([128, 63, 33, 15, 4], [1..15, 16..17, 18..19, 126..130, 224..225, 242..243])]
#[tokio::test]
async fn test_alloc_keystream(
#[case] record_lens: impl IntoIterator<Item = usize>,
#[case] ranges: impl IntoIterator<Item = Range<usize>>,
) {
let mut rng = StdRng::seed_from_u64(0);
let mut key = [0u8; 16];
let mut iv = [0u8; 4];
rng.fill(&mut key);
rng.fill(&mut iv);
let mut total_len = 0;
let records = record_lens
.into_iter()
.map(|len| {
let mut explicit_nonce = [0u8; 8];
rng.fill(&mut explicit_nonce);
total_len += len;
RecordParams {
explicit_nonce: explicit_nonce.to_vec(),
len,
}
})
.collect::<Vec<_>>();
let ranges = RangeSet::from(ranges.into_iter().collect::<Vec<_>>());
let is_out_of_bounds = ranges.end().unwrap_or(0) > total_len;
let (mut ctx, _) = test_st_context(1024);
let (mut vm, key_ref, iv_ref) = build_vm(key, iv);
let keystream = match alloc_keystream(&mut vm, key_ref, iv_ref, &ranges, &records) {
Ok(_) if is_out_of_bounds => panic!("should be out of bounds"),
Ok(keystream) => keystream,
Err(PlaintextAuthError(ErrorRepr::OutOfBounds)) if is_out_of_bounds => {
return;
}
Err(e) => panic!("unexpected error: {:?}", e),
};
vm.execute(&mut ctx).await.unwrap();
let keystream: Vec<u8> = keystream
.iter()
.flat_map(|slice| vm.get(*slice).unwrap().unwrap())
.collect();
assert_eq!(keystream.len(), ranges.len());
let expected = expected_aes_ctr(key, iv, &records, &ranges);
assert_eq!(keystream, expected);
}
#[rstest]
#[case::single_record_empty([0])]
#[case::single_record([32])]
#[case::multiple_records([128, 63, 33, 15, 4])]
#[case::multiple_records_with_empty([128, 63, 33, 0, 15, 4])]
fn test_verify_plaintext_with_key(
#[case] record_lens: impl IntoIterator<Item = usize>,
#[values(false, true)] tamper: bool,
) {
let mut rng = StdRng::seed_from_u64(0);
let mut key = [0u8; 16];
let mut iv = [0u8; 4];
rng.fill(&mut key);
rng.fill(&mut iv);
let mut total_len = 0;
let records = record_lens
.into_iter()
.map(|len| {
let mut explicit_nonce = [0u8; 8];
rng.fill(&mut explicit_nonce);
total_len += len;
RecordParams {
explicit_nonce: explicit_nonce.to_vec(),
len,
}
})
.collect::<Vec<_>>();
let mut plaintext = vec![0u8; total_len];
rng.fill(plaintext.as_mut_slice());
let mut ciphertext = plaintext.clone();
expected_aes_ctr(key, iv, &records, &(0..total_len).into())
.iter()
.zip(ciphertext.iter_mut())
.for_each(|(key, pt)| {
*pt ^= *key;
});
if tamper {
plaintext.first_mut().map(|pt| *pt ^= 1);
}
match verify_plaintext_with_key(key, iv, &records, &plaintext, &ciphertext) {
Ok(_) if tamper && !plaintext.is_empty() => panic!("should be invalid"),
Err(e) if !tamper => panic!("unexpected error: {:?}", e),
_ => {}
}
}
}