From f99fce5b5a925346b6f958e3c9c5ad8e826b81ed Mon Sep 17 00:00:00 2001 From: "sinu.eth" <65924192+sinui0@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:39:24 -0700 Subject: [PATCH] fix(tlsn): do not implicitly reveal encoder secret (#1011) --- crates/attestation/src/builder.rs | 12 +- crates/attestation/src/lib.rs | 11 +- crates/attestation/src/presentation.rs | 5 + crates/attestation/src/serialize.rs | 1 + crates/attestation/tests/api.rs | 2 +- crates/core/src/lib.rs | 6 +- crates/core/src/transcript/encoding.rs | 2 - crates/core/src/transcript/encoding/proof.rs | 36 +++-- crates/core/src/transcript/encoding/tree.rs | 12 +- crates/core/src/transcript/proof.rs | 22 ++- .../transcript_internal/commit/encoding.rs | 8 +- crates/tlsn/src/verifier/verify.rs | 5 +- crates/tlsn/tests/test.rs | 151 ++++++++++-------- 13 files changed, 162 insertions(+), 111 deletions(-) diff --git a/crates/attestation/src/builder.rs b/crates/attestation/src/builder.rs index 326d06f86..731f79403 100644 --- a/crates/attestation/src/builder.rs +++ b/crates/attestation/src/builder.rs @@ -5,7 +5,7 @@ use rand::{Rng, rng}; use tlsn_core::{ connection::{ConnectionInfo, ServerEphemKey}, hash::HashAlgId, - transcript::TranscriptCommitment, + transcript::{TranscriptCommitment, encoding::EncoderSecret}, }; use crate::{ @@ -25,6 +25,7 @@ pub struct Sign { connection_info: Option, server_ephemeral_key: Option, cert_commitment: ServerCertCommitment, + encoder_secret: Option, extensions: Vec, transcript_commitments: Vec, } @@ -86,6 +87,7 @@ impl<'a> AttestationBuilder<'a, Accept> { connection_info: None, server_ephemeral_key: None, cert_commitment, + encoder_secret: None, transcript_commitments: Vec::new(), extensions, }, @@ -106,6 +108,12 @@ impl AttestationBuilder<'_, Sign> { self } + /// Sets the secret for encoding commitments. + pub fn encoder_secret(&mut self, secret: EncoderSecret) -> &mut Self { + self.state.encoder_secret = Some(secret); + self + } + /// Adds an extension to the attestation. pub fn extension(&mut self, extension: Extension) -> &mut Self { self.state.extensions.push(extension); @@ -129,6 +137,7 @@ impl AttestationBuilder<'_, Sign> { connection_info, server_ephemeral_key, cert_commitment, + encoder_secret, extensions, transcript_commitments, } = self.state; @@ -159,6 +168,7 @@ impl AttestationBuilder<'_, Sign> { AttestationBuilderError::new(ErrorKind::Field, "handshake data was not set") })?), cert_commitment: field_id.next(cert_commitment), + encoder_secret: encoder_secret.map(|secret| field_id.next(secret)), extensions: extensions .into_iter() .map(|extension| field_id.next(extension)) diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 01f3dccbc..089c8d801 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -219,7 +219,7 @@ use tlsn_core::{ connection::{ConnectionInfo, ServerEphemKey}, hash::{Hash, HashAlgorithm, TypedHash}, merkle::MerkleTree, - transcript::TranscriptCommitment, + transcript::{TranscriptCommitment, encoding::EncoderSecret}, }; use crate::{ @@ -327,6 +327,7 @@ pub struct Body { connection_info: Field, server_ephemeral_key: Field, cert_commitment: Field, + encoder_secret: Option>, extensions: Vec>, transcript_commitments: Vec>, } @@ -372,6 +373,7 @@ impl Body { connection_info: conn_info, server_ephemeral_key, cert_commitment, + encoder_secret, extensions, transcript_commitments, } = self; @@ -389,6 +391,13 @@ impl Body { ), ]; + if let Some(encoder_secret) = encoder_secret { + fields.push(( + encoder_secret.id, + hasher.hash_separated(&encoder_secret.data), + )); + } + for field in extensions.iter() { fields.push((field.id, hasher.hash_separated(&field.data))); } diff --git a/crates/attestation/src/presentation.rs b/crates/attestation/src/presentation.rs index 754b043fd..efaa79c9e 100644 --- a/crates/attestation/src/presentation.rs +++ b/crates/attestation/src/presentation.rs @@ -91,6 +91,11 @@ impl Presentation { transcript.verify_with_provider( &provider.hash, &attestation.body.connection_info().transcript_length, + attestation + .body + .encoder_secret + .as_ref() + .map(|field| &field.data), attestation.body.transcript_commitments(), ) }) diff --git a/crates/attestation/src/serialize.rs b/crates/attestation/src/serialize.rs index 54ce33e30..285c3371d 100644 --- a/crates/attestation/src/serialize.rs +++ b/crates/attestation/src/serialize.rs @@ -49,5 +49,6 @@ impl_domain_separator!(tlsn_core::connection::ConnectionInfo); impl_domain_separator!(tlsn_core::connection::CertBinding); impl_domain_separator!(tlsn_core::transcript::TranscriptCommitment); impl_domain_separator!(tlsn_core::transcript::TranscriptSecret); +impl_domain_separator!(tlsn_core::transcript::encoding::EncoderSecret); impl_domain_separator!(tlsn_core::transcript::encoding::EncodingCommitment); impl_domain_separator!(tlsn_core::transcript::hash::PlaintextHash); diff --git a/crates/attestation/tests/api.rs b/crates/attestation/tests/api.rs index edb41e210..488be81dd 100644 --- a/crates/attestation/tests/api.rs +++ b/crates/attestation/tests/api.rs @@ -64,7 +64,6 @@ fn test_api() { let encoding_commitment = EncodingCommitment { root: encoding_tree.root(), - secret: encoder_secret(), }; let request_config = RequestConfig::default(); @@ -96,6 +95,7 @@ fn test_api() { .connection_info(connection_info.clone()) // Server key Notary received during handshake .server_ephemeral_key(server_ephemeral_key) + .encoder_secret(encoder_secret()) .transcript_commitments(vec![TranscriptCommitment::Encoding(encoding_commitment)]); let attestation = attestation_builder.build(&provider).unwrap(); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1aef6eea0..849660c63 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -20,8 +20,8 @@ use serde::{Deserialize, Serialize}; use crate::{ connection::{HandshakeData, ServerName}, transcript::{ - Direction, PartialTranscript, Transcript, TranscriptCommitConfig, TranscriptCommitRequest, - TranscriptCommitment, TranscriptSecret, + encoding::EncoderSecret, Direction, PartialTranscript, Transcript, TranscriptCommitConfig, + TranscriptCommitRequest, TranscriptCommitment, TranscriptSecret, }, }; @@ -220,6 +220,8 @@ pub struct VerifierOutput { pub server_name: Option, /// Transcript data. pub transcript: Option, + /// Encoding commitment secret. + pub encoder_secret: Option, /// Transcript commitments. pub transcript_commitments: Vec, } diff --git a/crates/core/src/transcript/encoding.rs b/crates/core/src/transcript/encoding.rs index 4da243f7b..985e738fd 100644 --- a/crates/core/src/transcript/encoding.rs +++ b/crates/core/src/transcript/encoding.rs @@ -19,6 +19,4 @@ use crate::hash::TypedHash; pub struct EncodingCommitment { /// Merkle root of the encoding commitments. pub root: TypedHash, - /// Seed used to generate the encodings. - pub secret: EncoderSecret, } diff --git a/crates/core/src/transcript/encoding/proof.rs b/crates/core/src/transcript/encoding/proof.rs index 085064bcc..611a2ebb6 100644 --- a/crates/core/src/transcript/encoding/proof.rs +++ b/crates/core/src/transcript/encoding/proof.rs @@ -8,7 +8,7 @@ use crate::{ merkle::{MerkleError, MerkleProof}, transcript::{ commit::MAX_TOTAL_COMMITTED_DATA, - encoding::{new_encoder, Encoder, EncodingCommitment}, + encoding::{new_encoder, Encoder, EncoderSecret, EncodingCommitment}, Direction, }, }; @@ -48,13 +48,14 @@ impl EncodingProof { pub fn verify_with_provider( &self, provider: &HashProvider, + secret: &EncoderSecret, commitment: &EncodingCommitment, sent: &[u8], recv: &[u8], ) -> Result<(RangeSet, RangeSet), EncodingProofError> { let hasher = provider.get(&commitment.root.alg)?; - let encoder = new_encoder(&commitment.secret); + let encoder = new_encoder(secret); let Self { inclusion_proof, openings, @@ -232,10 +233,7 @@ mod test { use crate::{ fixtures::{encoder_secret, encoder_secret_tampered_seed, encoding_provider}, hash::Blake3, - transcript::{ - encoding::{EncoderSecret, EncodingTree}, - Transcript, - }, + transcript::{encoding::EncodingTree, Transcript}, }; use super::*; @@ -246,7 +244,7 @@ mod test { commitment: EncodingCommitment, } - fn new_encoding_fixture(secret: EncoderSecret) -> EncodingFixture { + fn new_encoding_fixture() -> EncodingFixture { let transcript = Transcript::new(POST_JSON, OK_JSON); let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len())); @@ -257,10 +255,7 @@ mod test { let proof = tree.proof([&idx_0, &idx_1].into_iter()).unwrap(); - let commitment = EncodingCommitment { - root: tree.root(), - secret, - }; + let commitment = EncodingCommitment { root: tree.root() }; EncodingFixture { transcript, @@ -275,11 +270,12 @@ mod test { transcript, proof, commitment, - } = new_encoding_fixture(encoder_secret_tampered_seed()); + } = new_encoding_fixture(); let err = proof .verify_with_provider( &HashProvider::default(), + &encoder_secret_tampered_seed(), &commitment, transcript.sent(), transcript.received(), @@ -295,13 +291,19 @@ mod test { transcript, proof, commitment, - } = new_encoding_fixture(encoder_secret()); + } = new_encoding_fixture(); let sent = &transcript.sent()[transcript.sent().len() - 1..]; let recv = &transcript.received()[transcript.received().len() - 2..]; let err = proof - .verify_with_provider(&HashProvider::default(), &commitment, sent, recv) + .verify_with_provider( + &HashProvider::default(), + &encoder_secret(), + &commitment, + sent, + recv, + ) .unwrap_err(); assert!(matches!(err.kind, ErrorKind::Proof)); @@ -313,7 +315,7 @@ mod test { transcript, mut proof, commitment, - } = new_encoding_fixture(encoder_secret()); + } = new_encoding_fixture(); let Opening { idx, .. } = proof.openings.values_mut().next().unwrap(); @@ -322,6 +324,7 @@ mod test { let err = proof .verify_with_provider( &HashProvider::default(), + &encoder_secret(), &commitment, transcript.sent(), transcript.received(), @@ -337,7 +340,7 @@ mod test { transcript, mut proof, commitment, - } = new_encoding_fixture(encoder_secret()); + } = new_encoding_fixture(); let Opening { blinder, .. } = proof.openings.values_mut().next().unwrap(); @@ -346,6 +349,7 @@ mod test { let err = proof .verify_with_provider( &HashProvider::default(), + &encoder_secret(), &commitment, transcript.sent(), transcript.received(), diff --git a/crates/core/src/transcript/encoding/tree.rs b/crates/core/src/transcript/encoding/tree.rs index 1b7837345..6b8ba2165 100644 --- a/crates/core/src/transcript/encoding/tree.rs +++ b/crates/core/src/transcript/encoding/tree.rs @@ -222,14 +222,12 @@ mod tests { let proof = tree.proof([&idx_0, &idx_1].into_iter()).unwrap(); - let commitment = EncodingCommitment { - root: tree.root(), - secret: encoder_secret(), - }; + let commitment = EncodingCommitment { root: tree.root() }; let (auth_sent, auth_recv) = proof .verify_with_provider( &HashProvider::default(), + &encoder_secret(), &commitment, transcript.sent(), transcript.received(), @@ -260,14 +258,12 @@ mod tests { .proof([&idx_0, &idx_1, &idx_2, &idx_3].into_iter()) .unwrap(); - let commitment = EncodingCommitment { - root: tree.root(), - secret: encoder_secret(), - }; + let commitment = EncodingCommitment { root: tree.root() }; let (auth_sent, auth_recv) = proof .verify_with_provider( &HashProvider::default(), + &encoder_secret(), &commitment, transcript.sent(), transcript.received(), diff --git a/crates/core/src/transcript/proof.rs b/crates/core/src/transcript/proof.rs index 999925f84..07804327c 100644 --- a/crates/core/src/transcript/proof.rs +++ b/crates/core/src/transcript/proof.rs @@ -10,7 +10,7 @@ use crate::{ hash::{HashAlgId, HashProvider}, transcript::{ commit::{TranscriptCommitment, TranscriptCommitmentKind}, - encoding::{EncodingProof, EncodingProofError, EncodingTree}, + encoding::{EncoderSecret, EncodingProof, EncodingProofError, EncodingTree}, hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret}, Direction, PartialTranscript, RangeSet, Transcript, TranscriptSecret, }, @@ -51,6 +51,7 @@ impl TranscriptProof { self, provider: &HashProvider, length: &TranscriptLength, + encoder_secret: Option<&EncoderSecret>, commitments: impl IntoIterator, ) -> Result { let mut encoding_commitment = None; @@ -86,6 +87,13 @@ impl TranscriptProof { // Verify encoding proof. if let Some(proof) = self.encoding_proof { + let secret = encoder_secret.ok_or_else(|| { + TranscriptProofError::new( + ErrorKind::Encoding, + "contains an encoding proof but missing encoder secret", + ) + })?; + let commitment = encoding_commitment.ok_or_else(|| { TranscriptProofError::new( ErrorKind::Encoding, @@ -95,6 +103,7 @@ impl TranscriptProof { let (auth_sent, auth_recv) = proof.verify_with_provider( provider, + secret, commitment, self.transcript.sent_unsafe(), self.transcript.received_unsafe(), @@ -575,7 +584,7 @@ mod tests { use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON}; use crate::{ - fixtures::encoding_provider, + fixtures::{encoder_secret, encoding_provider}, hash::{Blake3, Blinder, HashAlgId}, transcript::TranscriptCommitConfigBuilder, }; @@ -602,7 +611,12 @@ mod tests { let provider = HashProvider::default(); let err = transcript_proof - .verify_with_provider(&provider, &transcript.length(), &[]) + .verify_with_provider( + &provider, + &transcript.length(), + Some(&encoder_secret()), + &[], + ) .err() .unwrap(); @@ -676,6 +690,7 @@ mod tests { .verify_with_provider( &provider, &transcript.length(), + None, &[TranscriptCommitment::Hash(commitment)], ) .unwrap(); @@ -724,6 +739,7 @@ mod tests { .verify_with_provider( &provider, &transcript.length(), + None, &[TranscriptCommitment::Hash(commitment)], ) .unwrap_err(); diff --git a/crates/tlsn/src/transcript_internal/commit/encoding.rs b/crates/tlsn/src/transcript_internal/commit/encoding.rs index 04375ebeb..3bd5d26c2 100644 --- a/crates/tlsn/src/transcript_internal/commit/encoding.rs +++ b/crates/tlsn/src/transcript_internal/commit/encoding.rs @@ -43,7 +43,7 @@ pub(crate) async fn transfer( store: &K, sent: &ReferenceMap, recv: &ReferenceMap, -) -> Result { +) -> Result<(EncoderSecret, EncodingCommitment), EncodingError> { let secret = EncoderSecret::new(rand::rng().random(), store.delta().as_block().to_bytes()); let encoder = new_encoder(&secret); @@ -83,9 +83,8 @@ pub(crate) async fn transfer( ctx.io_mut().with_limit(frame_limit).send(encodings).await?; let root = ctx.io_mut().expect_next().await?; - ctx.io_mut().send(secret.clone()).await?; - Ok(EncodingCommitment { root, secret }) + Ok((secret, EncodingCommitment { root })) } /// Receives and commits to the encodings for the provided plaintext ranges. @@ -166,9 +165,8 @@ pub(crate) async fn receive( let root = tree.root(); ctx.io_mut().send(root.clone()).await?; - let secret = ctx.io_mut().expect_next().await?; - let commitment = EncodingCommitment { root, secret }; + let commitment = EncodingCommitment { root }; Ok((commitment, tree)) } diff --git a/crates/tlsn/src/verifier/verify.rs b/crates/tlsn/src/verifier/verify.rs index c8a59e378..8c49c66c8 100644 --- a/crates/tlsn/src/verifier/verify.rs +++ b/crates/tlsn/src/verifier/verify.rs @@ -135,6 +135,7 @@ pub(crate) async fn verify + KeyStore + Send + Sync>( sent_proof.verify().map_err(VerifierError::verify)?; recv_proof.verify().map_err(VerifierError::verify)?; + let mut encoder_secret = None; if let Some(commit_config) = transcript_commit && let Some((sent, recv)) = commit_config.encoding() { @@ -147,7 +148,8 @@ pub(crate) async fn verify + KeyStore + Send + Sync>( .index(recv) .expect("ranges were authenticated"); - let commitment = encoding::transfer(ctx, vm, &sent_map, &recv_map).await?; + let (secret, commitment) = encoding::transfer(ctx, vm, &sent_map, &recv_map).await?; + encoder_secret = Some(secret); transcript_commitments.push(TranscriptCommitment::Encoding(commitment)); } @@ -160,6 +162,7 @@ pub(crate) async fn verify + KeyStore + Send + Sync>( Ok(VerifierOutput { server_name, transcript: has_reveal.then_some(transcript), + encoder_secret, transcript_commitments, }) } diff --git a/crates/tlsn/tests/test.rs b/crates/tlsn/tests/test.rs index 54132b2f9..b83ddd148 100644 --- a/crates/tlsn/tests/test.rs +++ b/crates/tlsn/tests/test.rs @@ -6,11 +6,12 @@ use tlsn::{ hash::{HashAlgId, HashProvider}, prover::{ProveConfig, Prover, ProverConfig, TlsConfig}, transcript::{ - Direction, TranscriptCommitConfig, TranscriptCommitment, TranscriptCommitmentKind, - TranscriptSecret, + Direction, Transcript, TranscriptCommitConfig, TranscriptCommitment, + TranscriptCommitmentKind, TranscriptSecret, }, verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig}, }; +use tlsn_core::ProverOutput; use tlsn_server_fixture::bind; use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN}; @@ -34,11 +35,80 @@ async fn test() { let (socket_0, socket_1) = tokio::io::duplex(2 << 23); - tokio::join!(prover(socket_0), verifier(socket_1)); + let ((full_transcript, prover_output), verifier_output) = + tokio::join!(prover(socket_0), verifier(socket_1)); + + let partial_transcript = verifier_output.transcript.unwrap(); + let ServerName::Dns(server_name) = verifier_output.server_name.unwrap(); + + assert_eq!(server_name.as_str(), SERVER_DOMAIN); + assert!(!partial_transcript.is_complete()); + assert_eq!( + partial_transcript + .sent_authed() + .iter_ranges() + .next() + .unwrap(), + 0..10 + ); + assert_eq!( + partial_transcript + .received_authed() + .iter_ranges() + .next() + .unwrap(), + 0..10 + ); + + let encoding_tree = prover_output + .transcript_secrets + .iter() + .find_map(|secret| { + if let TranscriptSecret::Encoding(tree) = secret { + Some(tree) + } else { + None + } + }) + .unwrap(); + + let encoding_commitment = prover_output + .transcript_commitments + .iter() + .find_map(|commitment| { + if let TranscriptCommitment::Encoding(commitment) = commitment { + Some(commitment) + } else { + None + } + }) + .unwrap(); + + let prove_sent = RangeSet::from(1..full_transcript.sent().len() - 1); + let prove_recv = RangeSet::from(1..full_transcript.received().len() - 1); + let idxs = [ + (Direction::Sent, prove_sent.clone()), + (Direction::Received, prove_recv.clone()), + ]; + let proof = encoding_tree.proof(idxs.iter()).unwrap(); + let (auth_sent, auth_recv) = proof + .verify_with_provider( + &HashProvider::default(), + &verifier_output.encoder_secret.unwrap(), + encoding_commitment, + full_transcript.sent(), + full_transcript.received(), + ) + .unwrap(); + + assert_eq!(auth_sent, prove_sent); + assert_eq!(auth_recv, prove_recv); } #[instrument(skip(verifier_socket))] -async fn prover(verifier_socket: T) { +async fn prover( + verifier_socket: T, +) -> (Transcript, ProverOutput) { let (client_socket, server_socket) = tokio::io::duplex(2 << 16); let server_task = tokio::spawn(bind(server_socket.compat())); @@ -127,52 +197,13 @@ async fn prover(verifier_soc let output = prover.prove(&config).await.unwrap(); prover.close().await.unwrap(); - let encoding_tree = output - .transcript_secrets - .iter() - .find_map(|secret| { - if let TranscriptSecret::Encoding(tree) = secret { - Some(tree) - } else { - None - } - }) - .unwrap(); - - let encoding_commitment = output - .transcript_commitments - .iter() - .find_map(|commitment| { - if let TranscriptCommitment::Encoding(commitment) = commitment { - Some(commitment) - } else { - None - } - }) - .unwrap(); - - let prove_sent = RangeSet::from(1..sent_tx_len - 1); - let prove_recv = RangeSet::from(1..recv_tx_len - 1); - let idxs = [ - (Direction::Sent, prove_sent.clone()), - (Direction::Received, prove_recv.clone()), - ]; - let proof = encoding_tree.proof(idxs.iter()).unwrap(); - let (auth_sent, auth_recv) = proof - .verify_with_provider( - &HashProvider::default(), - encoding_commitment, - transcript.sent(), - transcript.received(), - ) - .unwrap(); - - assert_eq!(auth_sent, prove_sent); - assert_eq!(auth_recv, prove_recv); + (transcript, output) } #[instrument(skip(socket))] -async fn verifier(socket: T) { +async fn verifier( + socket: T, +) -> VerifierOutput { let config_validator = ProtocolConfigValidator::builder() .max_sent_data(MAX_SENT_DATA) .max_recv_data(MAX_RECV_DATA) @@ -197,30 +228,8 @@ async fn verifier(soc .await .unwrap(); - let VerifierOutput { - server_name, - transcript, - transcript_commitments, - } = verifier.verify(&VerifyConfig::default()).await.unwrap(); - + let output = verifier.verify(&VerifyConfig::default()).await.unwrap(); verifier.close().await.unwrap(); - let transcript = transcript.unwrap(); - - let ServerName::Dns(server_name) = server_name.unwrap(); - - assert_eq!(server_name.as_str(), SERVER_DOMAIN); - assert!(!transcript.is_complete()); - assert_eq!( - transcript.sent_authed().iter_ranges().next().unwrap(), - 0..10 - ); - assert_eq!( - transcript.received_authed().iter_ranges().next().unwrap(), - 0..10 - ); - assert!(matches!( - transcript_commitments[0], - TranscriptCommitment::Encoding(_) - )); + output }