Skip to content

Commit 8263a9e

Browse files
committed
fix(core,tlsn): let the verifier size the transcript it recorded
The prover's `ProveRequestMsg` carried a whole `PartialTranscript`, which states how long each direction is. Converting it allocates that many bytes while the message is still being parsed: let mut sent = vec![0; compressed.sent_total]; Nothing bounds those totals. The validation just above only checks that the revealed index fits inside the declared total, and an empty index fits inside any total, so a message with no authenticated data may name any length. The verifier does compare the declared length against the recorded session, but in `accept()` -- one message after this allocation has already run. A `vec![0; n]` that cannot be satisfied reaches `handle_alloc_error`, which aborts the process rather than returning an error, so one prover ends every session on the verifier. The length is redundant: the verifier recorded the session and already checks the prover's number against its own. So the prover no longer sends it. A new `TranscriptReveal` carries what the prover contributes -- the authenticated bytes and their ranges -- and the verifier builds the transcript with `into_partial`, passing the lengths it measured. There is no field in which to state a length, so none can be acted on before it is checked, and the length-mismatch branch in `verify` is removed because the mismatch is no longer representable. The bounds check that remains, that the revealed ranges fit the recorded length, runs in `into_partial` and returns an error. `TranscriptReveal` is `CompressedPartialTranscript` without the two totals, so its conversions delegate to the existing sibling impls rather than restating the byte-copy loops. This changes the prove message, so prover and verifier must upgrade together. The version handshake already enforces that: a mismatched pair is refused on the first message, before the prove message is sent. Signed-off-by: xgreenx <xgreenx9999@gmail.com> Assisted-by: Claude Opus 5 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
1 parent 0fe3c32 commit 8263a9e

5 files changed

Lines changed: 200 additions & 22 deletions

File tree

crates/core/src/transcript.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,88 @@ impl From<CompressedPartialTranscript> for PartialTranscript {
247247
}
248248
}
249249

250+
/// The data a prover reveals from a transcript.
251+
///
252+
/// Unlike [`PartialTranscript`], this does not carry the length of either
253+
/// direction. The receiver supplies the lengths it recorded when converting
254+
/// with [`TranscriptReveal::into_partial`], so a peer cannot influence the size
255+
/// of the allocation.
256+
#[derive(Debug, Clone, Serialize, Deserialize)]
257+
#[cfg_attr(test, derive(PartialEq))]
258+
#[serde(try_from = "validation::TranscriptRevealUnchecked")]
259+
pub struct TranscriptReveal {
260+
/// Sent data which has been authenticated.
261+
sent_authed: Vec<u8>,
262+
/// Received data which has been authenticated.
263+
received_authed: Vec<u8>,
264+
/// Index of `sent_authed`.
265+
sent_idx: RangeSet<usize>,
266+
/// Index of `received_authed`.
267+
recv_idx: RangeSet<usize>,
268+
}
269+
270+
impl TranscriptReveal {
271+
/// Returns the index of sent data which have been authenticated.
272+
pub fn sent_authed(&self) -> &RangeSet<usize> {
273+
&self.sent_idx
274+
}
275+
276+
/// Returns the index of received data which have been authenticated.
277+
pub fn received_authed(&self) -> &RangeSet<usize> {
278+
&self.recv_idx
279+
}
280+
281+
/// Converts into a partial transcript of the given lengths.
282+
///
283+
/// The lengths are the receiver's own, so the transcript is sized by what
284+
/// the receiver recorded rather than by anything the reveal declares.
285+
///
286+
/// # Arguments
287+
///
288+
/// * `sent_len` - The length of the sent data.
289+
/// * `recv_len` - The length of the received data.
290+
pub fn into_partial(
291+
self,
292+
sent_len: usize,
293+
recv_len: usize,
294+
) -> Result<PartialTranscript, InvalidTranscriptReveal> {
295+
if self.sent_idx.end().unwrap_or(0) > sent_len
296+
|| self.recv_idx.end().unwrap_or(0) > recv_len
297+
{
298+
return Err(InvalidTranscriptReveal(
299+
"revealed ranges do not fit the transcript",
300+
));
301+
}
302+
303+
Ok(CompressedPartialTranscript {
304+
sent_authed: self.sent_authed,
305+
received_authed: self.received_authed,
306+
sent_idx: self.sent_idx,
307+
recv_idx: self.recv_idx,
308+
sent_total: sent_len,
309+
recv_total: recv_len,
310+
}
311+
.into())
312+
}
313+
}
314+
315+
impl From<PartialTranscript> for TranscriptReveal {
316+
fn from(uncompressed: PartialTranscript) -> Self {
317+
let compressed = CompressedPartialTranscript::from(uncompressed);
318+
Self {
319+
sent_authed: compressed.sent_authed,
320+
received_authed: compressed.received_authed,
321+
sent_idx: compressed.sent_idx,
322+
recv_idx: compressed.recv_idx,
323+
}
324+
}
325+
}
326+
327+
/// Invalid transcript reveal error.
328+
#[derive(Debug, thiserror::Error)]
329+
#[error("invalid transcript reveal: {0}")]
330+
pub struct InvalidTranscriptReveal(&'static str);
331+
250332
impl PartialTranscript {
251333
/// Creates a new partial transcript initalized to all 0s.
252334
///
@@ -578,6 +660,38 @@ mod validation {
578660
}
579661
}
580662

663+
#[derive(Debug, Deserialize)]
664+
#[cfg_attr(test, derive(Serialize))]
665+
pub(super) struct TranscriptRevealUnchecked {
666+
sent_authed: Vec<u8>,
667+
received_authed: Vec<u8>,
668+
sent_idx: RangeSet<usize>,
669+
recv_idx: RangeSet<usize>,
670+
}
671+
672+
impl TryFrom<TranscriptRevealUnchecked> for TranscriptReveal {
673+
type Error = InvalidTranscriptReveal;
674+
675+
fn try_from(unchecked: TranscriptRevealUnchecked) -> Result<Self, Self::Error> {
676+
// Whether the ranges fit the session is checked in `into_partial`,
677+
// by the party that recorded it.
678+
if unchecked.sent_authed.len() != unchecked.sent_idx.len()
679+
|| unchecked.received_authed.len() != unchecked.recv_idx.len()
680+
{
681+
return Err(InvalidTranscriptReveal(
682+
"lengths of index and data don't match",
683+
));
684+
}
685+
686+
Ok(Self {
687+
sent_authed: unchecked.sent_authed,
688+
received_authed: unchecked.received_authed,
689+
sent_idx: unchecked.sent_idx,
690+
recv_idx: unchecked.recv_idx,
691+
})
692+
}
693+
}
694+
581695
#[cfg(test)]
582696
mod tests {
583697
use rstest::{fixture, rstest};
@@ -636,6 +750,37 @@ mod validation {
636750
bincode::deserialize(&bytes);
637751
assert!(transcript.is_err());
638752
}
753+
754+
#[fixture]
755+
fn transcript_reveal() -> TranscriptRevealUnchecked {
756+
TranscriptRevealUnchecked {
757+
received_authed: vec![1, 2, 3, 11, 12, 13],
758+
sent_authed: vec![4, 5, 6, 14, 15, 16],
759+
recv_idx: RangeSet::from([1..4, 11..14]),
760+
sent_idx: RangeSet::from([4..7, 14..17]),
761+
}
762+
}
763+
764+
#[rstest]
765+
fn test_transcript_reveal_valid(transcript_reveal: TranscriptRevealUnchecked) {
766+
let bytes = bincode::serialize(&transcript_reveal).unwrap();
767+
let reveal: Result<TranscriptReveal, Box<bincode::ErrorKind>> =
768+
bincode::deserialize(&bytes);
769+
assert!(reveal.is_ok());
770+
}
771+
772+
#[rstest]
773+
// Expect to fail since the index and data lengths do not match.
774+
fn test_transcript_reveal_invalid_lengths(
775+
mut transcript_reveal: TranscriptRevealUnchecked,
776+
) {
777+
transcript_reveal.sent_authed.extend([1]);
778+
779+
let bytes = bincode::serialize(&transcript_reveal).unwrap();
780+
let reveal: Result<TranscriptReveal, Box<bincode::ErrorKind>> =
781+
bincode::deserialize(&bytes);
782+
assert!(reveal.is_err());
783+
}
639784
}
640785
}
641786

@@ -684,6 +829,39 @@ mod tests {
684829
assert_eq!(partial_transcript, deserialized_transcript);
685830
}
686831

832+
#[rstest]
833+
// A reveal rebuilds the transcript it came from, given the recorded lengths.
834+
fn test_transcript_reveal_round_trip(partial_transcript: PartialTranscript) {
835+
let sent_len = partial_transcript.len_sent();
836+
let recv_len = partial_transcript.len_received();
837+
let reveal = TranscriptReveal::from(partial_transcript.clone());
838+
let rebuilt = reveal.into_partial(sent_len, recv_len).unwrap();
839+
assert_eq!(rebuilt, partial_transcript);
840+
}
841+
842+
#[rstest]
843+
// The reveal carries no length, so the same disclosure serializes
844+
// identically regardless of how long the transcript was.
845+
fn test_transcript_reveal_serialization_length_independent() {
846+
let reveal_of = |len: usize| {
847+
let mut sent = vec![0xffu8; len];
848+
sent[1..4].copy_from_slice(&[1, 2, 3]);
849+
let transcript = Transcript::new(sent, vec![0xeeu8; len]);
850+
let partial = transcript.to_partial(RangeSet::from(1..4), RangeSet::default());
851+
bincode::serialize(&TranscriptReveal::from(partial)).unwrap()
852+
};
853+
assert_eq!(reveal_of(16), reveal_of(4096));
854+
}
855+
856+
#[rstest]
857+
// Expect an error since the reveal does not fit the recorded length.
858+
fn test_transcript_reveal_into_partial_out_of_bounds(partial_transcript: PartialTranscript) {
859+
let recv_len = partial_transcript.len_received();
860+
let reveal = TranscriptReveal::from(partial_transcript);
861+
let short = reveal.sent_authed().end().unwrap() - 1;
862+
assert!(reveal.into_partial(short, recv_len).is_err());
863+
}
864+
687865
#[rstest]
688866
fn test_transcript_to_partial_success(transcript: Transcript) {
689867
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));

crates/tlsn/src/msg.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66
use tlsn_core::{
77
config::{prove::ProveRequest, tls_commit::TlsCommitConfig},
88
connection::{HandshakeData, ServerName},
9-
transcript::PartialTranscript,
9+
transcript::TranscriptReveal,
1010
};
1111

1212
#[derive(Debug, Serialize, Deserialize)]
@@ -19,7 +19,7 @@ pub(crate) struct TlsCommitRequestMsg {
1919
pub(crate) struct ProveRequestMsg {
2020
pub(crate) request: ProveRequest,
2121
pub(crate) handshake: Option<(ServerName, HandshakeData)>,
22-
pub(crate) transcript: Option<PartialTranscript>,
22+
pub(crate) transcript: Option<TranscriptReveal>,
2323
}
2424

2525
#[derive(Debug, Serialize, Deserialize)]

crates/tlsn/src/prover.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use tlsn_core::{
3535
prove::ProveConfig, prover::ProverConfig, tls::TlsClientConfig, tls_commit::TlsCommitConfig,
3636
},
3737
connection::{HandshakeData, ServerName},
38-
transcript::{TlsTranscript, Transcript},
38+
transcript::{TlsTranscript, Transcript, TranscriptReveal},
3939
};
4040
use tlsn_mux::{Handle, Stream};
4141
use tracing::{Span, debug, info_span, instrument};
@@ -555,14 +555,14 @@ impl Prover<state::Committed> {
555555
)
556556
});
557557

558-
let partial_transcript = config
559-
.reveal()
560-
.map(|(sent, recv)| transcript.to_partial(sent.clone(), recv.clone()));
558+
let reveal = config.reveal().map(|(sent, recv)| {
559+
TranscriptReveal::from(transcript.to_partial(sent.clone(), recv.clone()))
560+
});
561561

562562
let msg = ProveRequestMsg {
563563
request: config.to_request(),
564564
handshake,
565-
transcript: partial_transcript,
565+
transcript: reveal,
566566
};
567567

568568
ctx.io_mut().send(msg).await.map_err(|e| {

crates/tlsn/src/verifier/state.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use mpc_tls::SessionKeys;
66
use tlsn_core::{
77
config::prove::ProveRequest,
88
connection::{HandshakeData, ServerName},
9-
transcript::{PartialTranscript, TlsTranscript},
9+
transcript::{TlsTranscript, TranscriptReveal},
1010
};
1111

1212
use tlsn_core::config::tls_commit::TlsCommitConfig;
@@ -62,7 +62,7 @@ pub struct Verify {
6262
pub(crate) tls_transcript: TlsTranscript,
6363
pub(crate) request: ProveRequest,
6464
pub(crate) handshake: Option<(ServerName, HandshakeData)>,
65-
pub(crate) transcript: Option<PartialTranscript>,
65+
pub(crate) transcript: Option<TranscriptReveal>,
6666
}
6767

6868
opaque_debug::implement!(Verify);

crates/tlsn/src/verifier/verify.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use tlsn_core::{
99
connection::{HandshakeData, ServerName},
1010
transcript::{
1111
ContentType, Direction, PartialTranscript, Record, TlsTranscript, TranscriptCommitment,
12+
TranscriptReveal,
1213
},
1314
webpki::ServerCertVerifier,
1415
};
@@ -27,37 +28,36 @@ pub(crate) async fn verify<T: Vm<Binary> + Send + Sync>(
2728
tls_transcript: &TlsTranscript,
2829
request: ProveRequest,
2930
handshake: Option<(ServerName, HandshakeData)>,
30-
transcript: Option<PartialTranscript>,
31+
transcript: Option<TranscriptReveal>,
3132
) -> Result<VerifierOutput> {
3233
let ciphertext_sent = collect_ciphertext(tls_transcript.sent());
3334
let ciphertext_recv = collect_ciphertext(tls_transcript.recv());
3435

3536
let transcript = if let Some((auth_sent, auth_recv)) = request.reveal() {
36-
let Some(transcript) = transcript else {
37+
let Some(reveal) = transcript else {
3738
return Err(Error::internal().with_msg(
3839
"verification failed: prover requested to reveal data but did not send transcript",
3940
));
4041
};
4142

42-
if transcript.len_sent() != ciphertext_sent.len()
43-
|| transcript.len_received() != ciphertext_recv.len()
44-
{
45-
return Err(
46-
Error::internal().with_msg("verification failed: transcript length mismatch")
47-
);
48-
}
49-
50-
if transcript.sent_authed() != auth_sent {
43+
if reveal.sent_authed() != auth_sent {
5144
return Err(Error::internal().with_msg("verification failed: sent auth data mismatch"));
5245
}
5346

54-
if transcript.received_authed() != auth_recv {
47+
if reveal.received_authed() != auth_recv {
5548
return Err(
5649
Error::internal().with_msg("verification failed: received auth data mismatch")
5750
);
5851
}
5952

60-
transcript
53+
// Sized by the lengths this party recorded.
54+
reveal
55+
.into_partial(ciphertext_sent.len(), ciphertext_recv.len())
56+
.map_err(|e| {
57+
Error::internal()
58+
.with_msg("verification failed: transcript reveal does not fit the session")
59+
.with_source(e)
60+
})?
6161
} else {
6262
PartialTranscript::new(ciphertext_sent.len(), ciphertext_recv.len())
6363
};

0 commit comments

Comments
 (0)