use std::{collections::HashSet, fmt};
use serde::{Deserialize, Serialize};
use utils::range::ToRangeSet;
use crate::{
attestation::Body,
hash::Blinded,
index::Index,
transcript::{
commit::TranscriptCommitmentKind,
encoding::{EncodingProof, EncodingProofError, EncodingTree},
hash::{PlaintextHashProof, PlaintextHashProofError, PlaintextHashSecret},
Direction, Idx, PartialTranscript, Transcript,
},
CryptoProvider,
};
#[derive(Clone, Serialize, Deserialize)]
pub struct TranscriptProof {
encoding_proof: Option<EncodingProof>,
hash_proofs: Vec<PlaintextHashProof>,
}
opaque_debug::implement!(TranscriptProof);
impl TranscriptProof {
pub fn verify_with_provider(
self,
provider: &CryptoProvider,
attestation_body: &Body,
) -> Result<PartialTranscript, TranscriptProofError> {
let info = attestation_body.connection_info();
let mut transcript = PartialTranscript::new(
info.transcript_length.sent as usize,
info.transcript_length.received as usize,
);
if let Some(proof) = self.encoding_proof {
let commitment = attestation_body.encoding_commitment().ok_or_else(|| {
TranscriptProofError::new(
ErrorKind::Encoding,
"contains an encoding proof but attestation is missing encoding commitment",
)
})?;
let seq = proof.verify_with_provider(provider, &info.transcript_length, commitment)?;
transcript.union_transcript(&seq);
}
for proof in self.hash_proofs {
let commitment = attestation_body
.plaintext_hashes()
.get_by_field_id(proof.commitment_id())
.map(|field| &field.data)
.ok_or_else(|| {
TranscriptProofError::new(
ErrorKind::Hash,
format!("contains a hash opening but attestation is missing corresponding commitment (id: {})", proof.commitment_id()),
)
})?;
let (direction, seq) = proof.verify(&provider.hash, commitment)?;
transcript.union_subsequence(direction, &seq);
}
Ok(transcript)
}
}
#[derive(Debug, thiserror::Error)]
pub struct TranscriptProofError {
kind: ErrorKind,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl TranscriptProofError {
fn new<E>(kind: ErrorKind, source: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self {
kind,
source: Some(source.into()),
}
}
}
#[derive(Debug)]
enum ErrorKind {
Encoding,
Hash,
}
impl fmt::Display for TranscriptProofError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("transcript proof error: ")?;
match self.kind {
ErrorKind::Encoding => f.write_str("encoding error")?,
ErrorKind::Hash => f.write_str("hash error")?,
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
}
Ok(())
}
}
impl From<EncodingProofError> for TranscriptProofError {
fn from(e: EncodingProofError) -> Self {
TranscriptProofError::new(ErrorKind::Encoding, e)
}
}
impl From<PlaintextHashProofError> for TranscriptProofError {
fn from(e: PlaintextHashProofError) -> Self {
TranscriptProofError::new(ErrorKind::Hash, e)
}
}
#[derive(Debug)]
pub struct TranscriptProofBuilder<'a> {
default_kind: TranscriptCommitmentKind,
transcript: &'a Transcript,
encoding_tree: Option<&'a EncodingTree>,
plaintext_hashes: &'a Index<PlaintextHashSecret>,
encoding_proof_idxs: HashSet<(Direction, Idx)>,
hash_proofs: Vec<PlaintextHashProof>,
}
impl<'a> TranscriptProofBuilder<'a> {
pub(crate) fn new(
transcript: &'a Transcript,
encoding_tree: Option<&'a EncodingTree>,
plaintext_hashes: &'a Index<PlaintextHashSecret>,
) -> Self {
Self {
default_kind: TranscriptCommitmentKind::Encoding,
transcript,
encoding_tree,
plaintext_hashes,
encoding_proof_idxs: HashSet::default(),
hash_proofs: Vec::new(),
}
}
pub fn default_kind(&mut self, kind: TranscriptCommitmentKind) -> &mut Self {
self.default_kind = kind;
self
}
pub fn reveal_with_kind(
&mut self,
ranges: &dyn ToRangeSet<usize>,
direction: Direction,
kind: TranscriptCommitmentKind,
) -> Result<&mut Self, TranscriptProofBuilderError> {
let idx = Idx::new(ranges.to_range_set());
if idx.end() > self.transcript.len_of_direction(direction) {
return Err(TranscriptProofBuilderError::new(
BuilderErrorKind::Index,
format!(
"range is out of bounds of the transcript ({}): {} > {}",
direction,
idx.end(),
self.transcript.len_of_direction(direction)
),
));
}
match kind {
TranscriptCommitmentKind::Encoding => {
let Some(encoding_tree) = self.encoding_tree else {
return Err(TranscriptProofBuilderError::new(
BuilderErrorKind::MissingCommitment,
"encoding tree is missing",
));
};
let dir_idx = (direction, idx);
if !encoding_tree.contains(&dir_idx) {
return Err(TranscriptProofBuilderError::new(
BuilderErrorKind::MissingCommitment,
format!(
"encoding commitment is missing for ranges in {} transcript",
direction
),
));
}
self.encoding_proof_idxs.insert(dir_idx);
}
TranscriptCommitmentKind::Hash { .. } => {
let Some(PlaintextHashSecret {
direction,
commitment,
blinder,
..
}) = self.plaintext_hashes.get_by_transcript_idx(&idx)
else {
return Err(TranscriptProofBuilderError::new(
BuilderErrorKind::MissingCommitment,
format!(
"hash commitment is missing for ranges in {} transcript",
direction
),
));
};
let (_, data) = self
.transcript
.get(*direction, &idx)
.expect("subsequence was checked to be in transcript")
.into_parts();
self.hash_proofs.push(PlaintextHashProof::new(
Blinded::new_with_blinder(data, blinder.clone()),
*commitment,
));
}
}
Ok(self)
}
pub fn reveal(
&mut self,
ranges: &dyn ToRangeSet<usize>,
direction: Direction,
) -> Result<&mut Self, TranscriptProofBuilderError> {
self.reveal_with_kind(ranges, direction, self.default_kind)
}
pub fn reveal_sent(
&mut self,
ranges: &dyn ToRangeSet<usize>,
) -> Result<&mut Self, TranscriptProofBuilderError> {
self.reveal(ranges, Direction::Sent)
}
pub fn reveal_recv(
&mut self,
ranges: &dyn ToRangeSet<usize>,
) -> Result<&mut Self, TranscriptProofBuilderError> {
self.reveal(ranges, Direction::Received)
}
pub fn build(self) -> Result<TranscriptProof, TranscriptProofBuilderError> {
let encoding_proof = if !self.encoding_proof_idxs.is_empty() {
let encoding_tree = self.encoding_tree.expect("encoding tree is present");
let proof = encoding_tree
.proof(self.transcript, self.encoding_proof_idxs.iter())
.expect("subsequences were checked to be in tree");
Some(proof)
} else {
None
};
Ok(TranscriptProof {
encoding_proof,
hash_proofs: self.hash_proofs,
})
}
}
#[derive(Debug, thiserror::Error)]
pub struct TranscriptProofBuilderError {
kind: BuilderErrorKind,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl TranscriptProofBuilderError {
fn new<E>(kind: BuilderErrorKind, source: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self {
kind,
source: Some(source.into()),
}
}
}
#[derive(Debug)]
enum BuilderErrorKind {
Index,
MissingCommitment,
}
impl fmt::Display for TranscriptProofBuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("transcript proof builder error: ")?;
match self.kind {
BuilderErrorKind::Index => f.write_str("index error")?,
BuilderErrorKind::MissingCommitment => f.write_str("commitment error")?,
}
if let Some(source) = &self.source {
write!(f, " caused by: {}", source)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::{
fixtures::{
attestation_fixture, encoder_seed, encoding_provider, request_fixture,
ConnectionFixture, RequestFixture,
},
hash::Blake3,
signing::SignatureAlgId,
};
use super::*;
#[test]
fn test_reveal_range_out_of_bounds() {
let transcript = Transcript::new(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
);
let index = Index::default();
let mut builder = TranscriptProofBuilder::new(&transcript, None, &index);
let err = builder.reveal(&(10..15), Direction::Sent).err().unwrap();
assert!(matches!(err.kind, BuilderErrorKind::Index));
let err = builder
.reveal(&(10..15), Direction::Received)
.err()
.unwrap();
assert!(matches!(err.kind, BuilderErrorKind::Index));
}
#[test]
fn test_reveal_missing_encoding_tree() {
let transcript = Transcript::new(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
);
let index = Index::default();
let mut builder = TranscriptProofBuilder::new(&transcript, None, &index);
let err = builder.reveal_recv(&(9..11)).err().unwrap();
assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
}
#[test]
fn test_reveal_missing_encoding_commitment_range() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture { encoding_tree, .. } = request_fixture(
transcript.clone(),
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection,
Blake3::default(),
);
let index = Index::default();
let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree), &index);
let err = builder.reveal_recv(&(0..11)).err().unwrap();
assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
}
#[test]
fn test_verify_missing_encoding_commitment() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let connection = ConnectionFixture::tlsnotary(transcript.length());
let RequestFixture {
mut request,
encoding_tree,
} = request_fixture(
transcript.clone(),
encoding_provider(GET_WITH_HEADER, OK_JSON),
connection.clone(),
Blake3::default(),
);
let index = Index::default();
let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree), &index);
builder.reveal_recv(&(0..transcript.len().1)).unwrap();
let transcript_proof = builder.build().unwrap();
request.encoding_commitment_root = None;
let attestation = attestation_fixture(
request,
connection,
SignatureAlgId::SECP256K1,
encoder_seed().to_vec(),
);
let provider = CryptoProvider::default();
let err = transcript_proof
.verify_with_provider(&provider, &attestation.body)
.err()
.unwrap();
assert!(matches!(err.kind, ErrorKind::Encoding));
}
}