feat: SHA256 transcript commitments (#881)

* feat: SHA256 transcript commitments

* clippy
This commit is contained in:
sinu.eth
2025-05-22 09:10:21 -07:00
committed by GitHub
parent 8b1cac6fe0
commit ad530ca500
12 changed files with 630 additions and 92 deletions

View File

@@ -17,6 +17,7 @@ tlsn-cipher = { workspace = true }
mpz-core = { workspace = true }
mpz-common = { workspace = true }
mpz-memory-core = { workspace = true }
mpz-hash = { workspace = true }
mpz-vm-core = { workspace = true }
mpz-zk = { workspace = true }

View File

@@ -1,5 +1,7 @@
//! Plaintext commitment and proof of encryption.
pub mod hash;
use mpz_core::bitvec::BitVec;
use mpz_memory_core::{binary::Binary, DecodeFutureTyped};
use mpz_vm_core::{prelude::*, Vm};

View File

@@ -0,0 +1,197 @@
//! Plaintext hash commitments.
use std::collections::HashMap;
use mpz_core::bitvec::BitVec;
use mpz_hash::sha256::Sha256;
use mpz_memory_core::{
binary::{Binary, U8},
DecodeFutureTyped, MemoryExt, Vector,
};
use mpz_vm_core::{prelude::*, Vm, VmError};
use tlsn_core::{
hash::{Blinder, Hash, HashAlgId, TypedHash},
transcript::{
hash::{PlaintextHash, PlaintextHashSecret},
Direction, Idx,
},
};
use crate::{transcript::TranscriptRefs, Role};
/// Future which will resolve to the committed hash values.
#[derive(Debug)]
pub struct HashCommitFuture {
#[allow(clippy::type_complexity)]
futs: Vec<(
Direction,
Idx,
HashAlgId,
DecodeFutureTyped<BitVec, Vec<u8>>,
)>,
}
impl HashCommitFuture {
/// Tries to receive the value, returning an error if the value is not
/// ready.
pub fn try_recv(self) -> Result<Vec<PlaintextHash>, HashCommitError> {
let mut output = Vec::new();
for (direction, idx, alg, mut fut) in self.futs {
let hash = fut
.try_recv()
.map_err(|_| HashCommitError::decode())?
.ok_or_else(HashCommitError::decode)?;
output.push(PlaintextHash {
direction,
idx,
hash: TypedHash {
alg,
value: Hash::try_from(hash).map_err(HashCommitError::convert)?,
},
});
}
Ok(output)
}
}
/// Prove plaintext hash commitments.
pub fn prove_hash(
vm: &mut dyn Vm<Binary>,
refs: &TranscriptRefs,
idxs: impl IntoIterator<Item = (Direction, Idx, HashAlgId)>,
) -> Result<(HashCommitFuture, Vec<PlaintextHashSecret>), HashCommitError> {
let mut futs = Vec::new();
let mut secrets = Vec::new();
for (direction, idx, alg, hash_ref, blinder_ref) in
hash_commit_inner(vm, Role::Prover, refs, idxs)?
{
let blinder: Blinder = rand::random();
vm.assign(blinder_ref, blinder.as_bytes().to_vec())?;
vm.commit(blinder_ref)?;
let hash_fut = vm.decode(Vector::<U8>::from(hash_ref))?;
futs.push((direction, idx.clone(), alg, hash_fut));
secrets.push(PlaintextHashSecret {
direction,
idx,
blinder,
alg,
});
}
Ok((HashCommitFuture { futs }, secrets))
}
/// Verify plaintext hash commitments.
pub fn verify_hash(
vm: &mut dyn Vm<Binary>,
refs: &TranscriptRefs,
idxs: impl IntoIterator<Item = (Direction, Idx, HashAlgId)>,
) -> Result<HashCommitFuture, HashCommitError> {
let mut futs = Vec::new();
for (direction, idx, alg, hash_ref, blinder_ref) in
hash_commit_inner(vm, Role::Verifier, refs, idxs)?
{
vm.commit(blinder_ref)?;
let hash_fut = vm.decode(Vector::<U8>::from(hash_ref))?;
futs.push((direction, idx, alg, hash_fut));
}
Ok(HashCommitFuture { futs })
}
/// Commit plaintext hashes of the transcript.
#[allow(clippy::type_complexity)]
fn hash_commit_inner(
vm: &mut dyn Vm<Binary>,
role: Role,
refs: &TranscriptRefs,
idxs: impl IntoIterator<Item = (Direction, Idx, HashAlgId)>,
) -> Result<Vec<(Direction, Idx, HashAlgId, Array<U8, 32>, Vector<U8>)>, HashCommitError> {
let mut output = Vec::new();
let mut hashers = HashMap::new();
for (direction, idx, alg) in idxs {
let blinder = vm.alloc_vec::<U8>(16)?;
match role {
Role::Prover => vm.mark_private(blinder)?,
Role::Verifier => vm.mark_blind(blinder)?,
}
let hash = match alg {
HashAlgId::SHA256 => {
let mut hasher = if let Some(hasher) = hashers.get(&alg).cloned() {
hasher
} else {
let hasher = Sha256::new_with_init(vm).map_err(HashCommitError::hasher)?;
hashers.insert(alg, hasher.clone());
hasher
};
for plaintext in refs.get(direction, &idx).expect("plaintext refs are valid") {
hasher.update(&plaintext);
}
hasher.update(&blinder);
hasher.finalize(vm).map_err(HashCommitError::hasher)?
}
alg => {
return Err(HashCommitError::unsupported_alg(alg));
}
};
output.push((direction, idx, alg, hash, blinder));
}
Ok(output)
}
/// Error type for hash commitments.
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct HashCommitError(#[from] ErrorRepr);
impl HashCommitError {
fn decode() -> Self {
Self(ErrorRepr::Decode)
}
fn convert(e: &'static str) -> Self {
Self(ErrorRepr::Convert(e))
}
fn hasher<E>(e: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self(ErrorRepr::Hasher(e.into()))
}
fn unsupported_alg(alg: HashAlgId) -> Self {
Self(ErrorRepr::UnsupportedAlg { alg })
}
}
#[derive(Debug, thiserror::Error)]
#[error("hash commit error: {0}")]
enum ErrorRepr {
#[error("VM error: {0}")]
Vm(VmError),
#[error("failed to decode hash")]
Decode,
#[error("failed to convert hash: {0}")]
Convert(&'static str),
#[error("unsupported hash algorithm: {alg}")]
UnsupportedAlg { alg: HashAlgId },
#[error("hasher error: {0}")]
Hasher(Box<dyn std::error::Error + Send + Sync>),
}
impl From<VmError> for HashCommitError {
fn from(value: VmError) -> Self {
Self(ErrorRepr::Vm(value))
}
}

View File

@@ -100,7 +100,7 @@ impl Display for HashAlgId {
}
/// A typed hash value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TypedHash {
/// The algorithm of the hash.
pub alg: HashAlgId,
@@ -109,7 +109,7 @@ pub struct TypedHash {
}
/// A hash value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Hash {
// To avoid heap allocation, we use a fixed-size array.
// 64 bytes should be sufficient for most hash algorithms.
@@ -253,12 +253,13 @@ impl<T: HashAlgorithm + ?Sized> HashAlgorithmExt for T {}
/// A hash blinder.
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct Blinder([u8; 16]);
pub struct Blinder([u8; 16]);
opaque_debug::implement!(Blinder);
impl Blinder {
pub(crate) fn as_bytes(&self) -> &[u8] {
/// Returns the blinder as a byte slice.
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}

View File

@@ -35,17 +35,6 @@ impl Secrets {
/// Returns a transcript proof builder.
pub fn transcript_proof_builder(&self) -> TranscriptProofBuilder<'_> {
let encoding_secret = self
.transcript_commitment_secrets
.iter()
.find_map(|secret| {
#[allow(irrefutable_let_patterns)]
if let TranscriptSecret::Encoding(secret) = secret {
Some(secret)
} else {
None
}
});
TranscriptProofBuilder::new(&self.transcript, encoding_secret)
TranscriptProofBuilder::new(&self.transcript, &self.transcript_commitment_secrets)
}
}

View File

@@ -34,6 +34,7 @@
mod commit;
#[doc(hidden)]
pub mod encoding;
pub mod hash;
mod proof;
use std::{fmt, ops::Range};

View File

@@ -9,6 +9,7 @@ use crate::{
hash::{impl_domain_separator, HashAlgId},
transcript::{
encoding::{EncodingCommitment, EncodingTree},
hash::{PlaintextHash, PlaintextHashSecret},
Direction, Idx, Transcript,
},
};
@@ -50,6 +51,8 @@ impl fmt::Display for TranscriptCommitmentKind {
pub enum TranscriptCommitment {
/// Encoding commitment.
Encoding(EncodingCommitment),
/// Plaintext hash commitment.
Hash(PlaintextHash),
}
impl_domain_separator!(TranscriptCommitment);
@@ -60,6 +63,8 @@ impl_domain_separator!(TranscriptCommitment);
pub enum TranscriptSecret {
/// Encoding tree.
Encoding(EncodingTree),
/// Plaintext hash secret.
Hash(PlaintextHashSecret),
}
impl_domain_separator!(TranscriptSecret);
@@ -69,6 +74,7 @@ impl_domain_separator!(TranscriptSecret);
pub struct TranscriptCommitConfig {
encoding_hash_alg: HashAlgId,
has_encoding: bool,
has_hash: bool,
commits: Vec<((Direction, Idx), TranscriptCommitmentKind)>,
}
@@ -83,11 +89,16 @@ impl TranscriptCommitConfig {
&self.encoding_hash_alg
}
/// Returns whether the configuration has any encoding commitments.
/// Returns `true` if the configuration has any encoding commitments.
pub fn has_encoding(&self) -> bool {
self.has_encoding
}
/// Returns `true` if the configuration has any hash commitments.
pub fn has_hash(&self) -> bool {
self.has_hash
}
/// Returns an iterator over the encoding commitment indices.
pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, Idx)> {
self.commits.iter().filter_map(|(idx, kind)| match kind {
@@ -108,6 +119,10 @@ impl TranscriptCommitConfig {
pub fn to_request(&self) -> TranscriptCommitRequest {
TranscriptCommitRequest {
encoding: self.has_encoding,
hash: self
.iter_hash()
.map(|((dir, idx), alg)| (*dir, idx.clone(), *alg))
.collect(),
}
}
}
@@ -121,6 +136,7 @@ pub struct TranscriptCommitConfigBuilder<'a> {
transcript: &'a Transcript,
encoding_hash_alg: HashAlgId,
has_encoding: bool,
has_hash: bool,
default_kind: TranscriptCommitmentKind,
commits: HashSet<((Direction, Idx), TranscriptCommitmentKind)>,
}
@@ -132,6 +148,7 @@ impl<'a> TranscriptCommitConfigBuilder<'a> {
transcript,
encoding_hash_alg: HashAlgId::BLAKE3,
has_encoding: false,
has_hash: false,
default_kind: TranscriptCommitmentKind::Encoding,
commits: HashSet::default(),
}
@@ -176,8 +193,9 @@ impl<'a> TranscriptCommitConfigBuilder<'a> {
));
}
if let TranscriptCommitmentKind::Encoding = kind {
self.has_encoding = true;
match kind {
TranscriptCommitmentKind::Encoding => self.has_encoding = true,
TranscriptCommitmentKind::Hash { .. } => self.has_hash = true,
}
self.commits.insert(((direction, idx), kind));
@@ -228,6 +246,7 @@ impl<'a> TranscriptCommitConfigBuilder<'a> {
Ok(TranscriptCommitConfig {
encoding_hash_alg: self.encoding_hash_alg,
has_encoding: self.has_encoding,
has_hash: self.has_hash,
commits: Vec::from_iter(self.commits),
})
}
@@ -275,6 +294,7 @@ impl fmt::Display for TranscriptCommitConfigBuilderError {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptCommitRequest {
encoding: bool,
hash: Vec<(Direction, Idx, HashAlgId)>,
}
impl TranscriptCommitRequest {
@@ -282,6 +302,16 @@ impl TranscriptCommitRequest {
pub fn encoding(&self) -> bool {
self.encoding
}
/// Returns `true` if a hash commitment is requested.
pub fn has_hash(&self) -> bool {
!self.hash.is_empty()
}
/// Returns an iterator over the hash commitments.
pub fn iter_hash(&self) -> impl Iterator<Item = &(Direction, Idx, HashAlgId)> {
self.hash.iter()
}
}
#[cfg(test)]

View File

@@ -0,0 +1,46 @@
//! Plaintext hash commitments.
use serde::{Deserialize, Serialize};
use crate::{
hash::{impl_domain_separator, Blinder, HashAlgId, HashAlgorithm, TypedHash},
transcript::{Direction, Idx},
};
/// Hashes plaintext with a blinder.
///
/// By convention, plaintext is hashed as `H(msg | blinder)`.
pub fn hash_plaintext(hasher: &dyn HashAlgorithm, msg: &[u8], blinder: &Blinder) -> TypedHash {
TypedHash {
alg: hasher.id(),
value: hasher.hash_prefixed(msg, blinder.as_bytes()),
}
}
/// Hash of plaintext in the transcript.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PlaintextHash {
/// Direction of the plaintext.
pub direction: Direction,
/// Index of plaintext.
pub idx: Idx,
/// The hash of the data.
pub hash: TypedHash,
}
impl_domain_separator!(PlaintextHash);
/// Secret component of [`PlaintextHash`].
#[derive(Clone, Serialize, Deserialize)]
pub struct PlaintextHashSecret {
/// Direction of the plaintext.
pub direction: Direction,
/// Index of plaintext.
pub idx: Idx,
/// The algorithm of the hash.
pub alg: HashAlgId,
/// Blinder for the hash.
pub blinder: Blinder,
}
opaque_debug::implement!(PlaintextHashSecret);

View File

@@ -6,23 +6,31 @@ use std::{collections::HashSet, fmt};
use crate::{
connection::TranscriptLength,
hash::HashAlgId,
transcript::{
commit::{TranscriptCommitment, TranscriptCommitmentKind},
encoding::{EncodingProof, EncodingProofError, EncodingTree},
Direction, Idx, PartialTranscript, Transcript,
hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret},
Direction, Idx, PartialTranscript, Transcript, TranscriptSecret,
},
CryptoProvider,
};
/// Default commitment kinds in order of preference for building transcript
/// proofs.
const DEFAULT_COMMITMENT_KINDS: &[TranscriptCommitmentKind] = &[TranscriptCommitmentKind::Encoding];
const DEFAULT_COMMITMENT_KINDS: &[TranscriptCommitmentKind] = &[
TranscriptCommitmentKind::Hash {
alg: HashAlgId::SHA256,
},
TranscriptCommitmentKind::Encoding,
];
/// Proof of the contents of a transcript.
#[derive(Clone, Serialize, Deserialize)]
pub struct TranscriptProof {
transcript: PartialTranscript,
encoding_proof: Option<EncodingProof>,
hash_secrets: Vec<PlaintextHashSecret>,
}
opaque_debug::implement!(TranscriptProof);
@@ -42,7 +50,24 @@ impl TranscriptProof {
length: &TranscriptLength,
commitments: impl IntoIterator<Item = &'a TranscriptCommitment>,
) -> Result<PartialTranscript, TranscriptProofError> {
let commitments: Vec<_> = commitments.into_iter().collect();
let mut encoding_commitment = None;
let mut hash_commitments = HashSet::new();
// Index commitments.
for commitment in commitments {
match commitment {
TranscriptCommitment::Encoding(commitment) => {
if encoding_commitment.replace(commitment).is_some() {
return Err(TranscriptProofError::new(
ErrorKind::Encoding,
"multiple encoding commitments are present.",
));
}
}
TranscriptCommitment::Hash(plaintext_hash) => {
hash_commitments.insert(plaintext_hash);
}
}
}
if self.transcript.sent_unsafe().len() != length.sent as usize
|| self.transcript.received_unsafe().len() != length.received as usize
@@ -58,22 +83,12 @@ impl TranscriptProof {
// Verify encoding proof.
if let Some(proof) = self.encoding_proof {
let commitment = commitments
.iter()
.find_map(|commitment| {
#[allow(irrefutable_let_patterns)]
if let TranscriptCommitment::Encoding(encoding) = commitment {
Some(encoding)
} else {
None
}
})
.ok_or_else(|| {
TranscriptProofError::new(
ErrorKind::Encoding,
"contains an encoding proof but attestation is missing encoding commitment",
)
})?;
let commitment = encoding_commitment.ok_or_else(|| {
TranscriptProofError::new(
ErrorKind::Encoding,
"contains an encoding proof but missing encoding commitment",
)
})?;
let (auth_sent, auth_recv) = proof.verify_with_provider(
provider,
@@ -86,7 +101,53 @@ impl TranscriptProof {
total_auth_recv.union_mut(&auth_recv);
}
// TODO: Support hash openings.
let mut buffer = Vec::new();
for PlaintextHashSecret {
direction,
idx,
alg,
blinder,
} in self.hash_secrets
{
let hasher = provider.hash.get(&alg).map_err(|_| {
TranscriptProofError::new(
ErrorKind::Hash,
format!("hash opening has unknown algorithm: {alg}"),
)
})?;
let (plaintext, auth) = match direction {
Direction::Sent => (self.transcript.sent_unsafe(), &mut total_auth_sent),
Direction::Received => (self.transcript.received_unsafe(), &mut total_auth_recv),
};
if idx.end() > plaintext.len() {
return Err(TranscriptProofError::new(
ErrorKind::Hash,
"hash opening index is out of bounds",
));
}
buffer.clear();
for range in idx.iter_ranges() {
buffer.extend_from_slice(&plaintext[range]);
}
let expected = PlaintextHash {
direction,
idx,
hash: hash_plaintext(hasher, &buffer, &blinder),
};
if !hash_commitments.contains(&expected) {
return Err(TranscriptProofError::new(
ErrorKind::Hash,
"hash opening does not match any commitment",
));
}
auth.union_mut(&expected.idx);
}
// Assert that all the authenticated data are covered by the proof.
if &total_auth_sent != self.transcript.sent_authed()
@@ -124,7 +185,6 @@ impl TranscriptProofError {
#[derive(Debug)]
enum ErrorKind {
Encoding,
#[allow(dead_code)]
Hash,
Proof,
}
@@ -153,34 +213,6 @@ impl From<EncodingProofError> for TranscriptProofError {
}
}
/// Union of committed ranges of all commitment kinds.
#[derive(Debug)]
struct CommittedIdx {
sent: Idx,
recv: Idx,
}
impl CommittedIdx {
fn new(encoding_tree: Option<&EncodingTree>) -> Self {
let mut sent = Idx::default();
let mut recv = Idx::default();
if let Some(tree) = encoding_tree {
sent.union_mut(tree.idx(Direction::Sent));
recv.union_mut(tree.idx(Direction::Received));
}
Self { sent, recv }
}
fn idx(&self, direction: &Direction) -> &Idx {
match direction {
Direction::Sent => &self.sent,
Direction::Received => &self.recv,
}
}
}
/// Union of ranges to reveal.
#[derive(Clone, Debug, PartialEq)]
struct QueryIdx {
@@ -221,18 +253,47 @@ pub struct TranscriptProofBuilder<'a> {
commitment_kinds: Vec<TranscriptCommitmentKind>,
transcript: &'a Transcript,
encoding_tree: Option<&'a EncodingTree>,
committed_idx: CommittedIdx,
hash_secrets: Vec<&'a PlaintextHashSecret>,
committed_sent: Idx,
committed_recv: Idx,
query_idx: QueryIdx,
}
impl<'a> TranscriptProofBuilder<'a> {
/// Creates a new proof config builder.
pub(crate) fn new(transcript: &'a Transcript, encoding_tree: Option<&'a EncodingTree>) -> Self {
/// Creates a new proof builder.
pub(crate) fn new(
transcript: &'a Transcript,
secrets: impl IntoIterator<Item = &'a TranscriptSecret>,
) -> Self {
let mut committed_sent = Idx::empty();
let mut committed_recv = Idx::empty();
let mut encoding_tree = None;
let mut hash_secrets = Vec::new();
for secret in secrets {
match secret {
TranscriptSecret::Encoding(tree) => {
committed_sent.union_mut(tree.idx(Direction::Sent));
committed_recv.union_mut(tree.idx(Direction::Received));
encoding_tree = Some(tree);
}
TranscriptSecret::Hash(hash) => {
match hash.direction {
Direction::Sent => committed_sent.union_mut(&hash.idx),
Direction::Received => committed_recv.union_mut(&hash.idx),
}
hash_secrets.push(hash);
}
}
}
Self {
commitment_kinds: DEFAULT_COMMITMENT_KINDS.to_vec(),
transcript,
encoding_tree,
committed_idx: CommittedIdx::new(encoding_tree),
hash_secrets,
committed_sent,
committed_recv,
query_idx: QueryIdx::new(),
}
}
@@ -277,10 +338,15 @@ impl<'a> TranscriptProofBuilder<'a> {
));
}
if idx.is_subset(self.committed_idx.idx(&direction)) {
let committed = match direction {
Direction::Sent => &self.committed_sent,
Direction::Received => &self.committed_recv,
};
if idx.is_subset(committed) {
self.query_idx.union(&direction, &idx);
} else {
let missing = idx.difference(self.committed_idx.idx(&direction));
let missing = idx.difference(committed);
return Err(TranscriptProofBuilderError::new(
BuilderErrorKind::MissingCommitment,
format!("commitment is missing for ranges in {direction} transcript: {missing}"),
@@ -320,6 +386,7 @@ impl<'a> TranscriptProofBuilder<'a> {
.transcript
.to_partial(self.query_idx.sent.clone(), self.query_idx.recv.clone()),
encoding_proof: None,
hash_secrets: Vec::new(),
};
let mut uncovered_query_idx = self.query_idx.clone();
let mut commitment_kinds_iter = self.commitment_kinds.iter();
@@ -372,6 +439,39 @@ impl<'a> TranscriptProofBuilder<'a> {
);
}
}
TranscriptCommitmentKind::Hash { alg } => {
let (sent_hashes, sent_uncovered) =
uncovered_query_idx.sent.as_range_set().cover_by(
self.hash_secrets.iter().filter(|hash| {
hash.direction == Direction::Sent && &hash.alg == alg
}),
|hash| &hash.idx.0,
);
// Uncovered ranges will be checked with ranges of the next
// preferred commitment kind.
uncovered_query_idx.sent = Idx(sent_uncovered);
let (recv_hashes, recv_uncovered) =
uncovered_query_idx.recv.as_range_set().cover_by(
self.hash_secrets.iter().filter(|hash| {
hash.direction == Direction::Received && &hash.alg == alg
}),
|hash| &hash.idx.0,
);
uncovered_query_idx.recv = Idx(recv_uncovered);
transcript_proof.hash_secrets.extend(
sent_hashes
.into_iter()
.map(|s| PlaintextHashSecret::clone(s)),
);
transcript_proof.hash_secrets.extend(
recv_hashes
.into_iter()
.map(|s| PlaintextHashSecret::clone(s)),
);
}
#[allow(unreachable_patterns)]
kind => {
return Err(TranscriptProofBuilderError::new(
BuilderErrorKind::NotSupported,
@@ -463,13 +563,14 @@ impl fmt::Display for TranscriptProofBuilderError {
#[allow(clippy::single_range_in_vec_init)]
#[cfg(test)]
mod tests {
use rand::{Rng, SeedableRng};
use rangeset::RangeSet;
use rstest::rstest;
use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON};
use crate::{
fixtures::{encoding_provider, request_fixture, ConnectionFixture, RequestFixture},
hash::{Blake3, HashAlgId},
hash::{Blake3, Blinder, HashAlgId},
transcript::TranscriptCommitConfigBuilder,
};
@@ -488,7 +589,8 @@ mod tests {
Vec::new(),
);
let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree));
let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
builder.reveal_recv(&(0..transcript.len().1)).unwrap();
@@ -509,7 +611,7 @@ mod tests {
[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 mut builder = TranscriptProofBuilder::new(&transcript, None);
let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
let err = builder.reveal(&(10..15), Direction::Sent).unwrap_err();
assert!(matches!(err.kind, BuilderErrorKind::Index));
@@ -527,16 +629,106 @@ mod tests {
[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 mut builder = TranscriptProofBuilder::new(&transcript, None);
let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
let err = builder.reveal_recv(&(9..11)).unwrap_err();
assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment));
}
#[rstest]
fn test_reveal_with_hash_commitment() {
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let provider = CryptoProvider::default();
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let direction = Direction::Sent;
let idx = Idx::new(0..10);
let blinder: Blinder = rng.random();
let alg = HashAlgId::SHA256;
let hasher = provider.hash.get(&alg).unwrap();
let commitment = PlaintextHash {
direction,
idx: idx.clone(),
hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
};
let secret = PlaintextHashSecret {
direction,
idx: idx.clone(),
alg,
blinder,
};
let secrets = vec![TranscriptSecret::Hash(secret)];
let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
builder.reveal_sent(&(0..10)).unwrap();
let transcript_proof = builder.build().unwrap();
let partial_transcript = transcript_proof
.verify_with_provider(
&provider,
&transcript.length(),
&[TranscriptCommitment::Hash(commitment)],
)
.unwrap();
assert_eq!(
partial_transcript.sent_unsafe()[0..10],
transcript.sent()[0..10]
);
}
#[rstest]
fn test_reveal_with_inconsistent_hash_commitment() {
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let provider = CryptoProvider::default();
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let direction = Direction::Sent;
let idx = Idx::new(0..10);
let blinder: Blinder = rng.random();
let alg = HashAlgId::SHA256;
let hasher = provider.hash.get(&alg).unwrap();
let commitment = PlaintextHash {
direction,
idx: idx.clone(),
hash: hash_plaintext(hasher, &transcript.sent()[0..10], &blinder),
};
let secret = PlaintextHashSecret {
direction,
idx: idx.clone(),
alg,
// Use a different blinder to create an inconsistent commitment
blinder: rng.random(),
};
let secrets = vec![TranscriptSecret::Hash(secret)];
let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
builder.reveal_sent(&(0..10)).unwrap();
let transcript_proof = builder.build().unwrap();
let err = transcript_proof
.verify_with_provider(
&provider,
&transcript.length(),
&[TranscriptCommitment::Hash(commitment)],
)
.unwrap_err();
assert!(matches!(err.kind, ErrorKind::Hash));
}
#[rstest]
fn test_set_commitment_kinds_with_duplicates() {
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
let mut builder = TranscriptProofBuilder::new(&transcript, None);
let mut builder = TranscriptProofBuilder::new(&transcript, &[]);
builder.commitment_kinds(&[
TranscriptCommitmentKind::Hash {
alg: HashAlgId::SHA256,
@@ -621,7 +813,8 @@ mod tests {
)
.unwrap();
let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree));
let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
if success {
assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok());
@@ -693,7 +886,8 @@ mod tests {
)
.unwrap();
let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree));
let secrets = vec![TranscriptSecret::Encoding(encoding_tree)];
let mut builder = TranscriptProofBuilder::new(&transcript, &secrets);
builder.reveal_sent(&reveal_sent_rangeset).unwrap();
builder.reveal_recv(&reveal_recv_rangeset).unwrap();

View File

@@ -28,7 +28,7 @@ use tls_client::{ClientConnection, ServerName as TlsServerName};
use tls_client_async::{bind_client, TlsConnection};
use tls_core::msgs::enums::ContentType;
use tlsn_common::{
commit::commit_records,
commit::{commit_records, hash::prove_hash},
context::build_mt_context,
encoding,
mux::attach_mux,
@@ -43,7 +43,7 @@ use tlsn_core::{
TranscriptLength,
},
request::{Request, RequestConfig},
transcript::{Transcript, TranscriptCommitment, TranscriptSecret},
transcript::{Direction, Transcript, TranscriptCommitment, TranscriptSecret},
ProvePayload, Secrets,
};
use tlsn_deap::Deap;
@@ -376,6 +376,7 @@ impl Prover<state::Committed> {
.map_err(ProverError::zk)?;
}
let mut hash_commitments = None;
if let Some(commit_config) = config.transcript_commit() {
if commit_config.has_encoding() {
let hasher = self
@@ -406,13 +407,36 @@ impl Prover<state::Committed> {
.push(TranscriptSecret::Encoding(tree));
}
// TODO: Other commitment types.
if commit_config.has_hash() {
hash_commitments = Some(
prove_hash(
vm,
transcript_refs,
commit_config
.iter_hash()
.map(|((dir, idx), alg)| (*dir, idx.clone(), *alg)),
)
.map_err(ProverError::commit)?,
);
}
}
mux_fut
.poll_with(vm.execute_all(ctx).map_err(ProverError::zk))
.await?;
if let Some((hash_fut, hash_secrets)) = hash_commitments {
let hash_commitments = hash_fut.try_recv().map_err(ProverError::commit)?;
for (commitment, secret) in hash_commitments.into_iter().zip(hash_secrets) {
output
.transcript_commitments
.push(TranscriptCommitment::Hash(commitment));
output
.transcript_secrets
.push(TranscriptSecret::Hash(secret));
}
}
Ok(output)
}
@@ -432,6 +456,23 @@ impl Prover<state::Committed> {
let mut builder = ProveConfig::builder(self.transcript());
if let Some(config) = config.transcript_commit() {
// Temporarily, we reject attestation requests which contain hash commitments to
// subsets of the transcript. We do this because we want to preserve the
// obliviousness of the reference notary, and hash commitments currently leak
// the ranges which are being committed.
for ((direction, idx), _) in config.iter_hash() {
let len = match direction {
Direction::Sent => self.transcript().sent().len(),
Direction::Received => self.transcript().received().len(),
};
if idx.start() > 0 || idx.end() < len || idx.count() != 1 {
return Err(ProverError::attestation(
"hash commitments to subsets of the transcript are currently not supported in attestation requests",
));
}
}
builder.transcript_commit(config.clone());
}

View File

@@ -1,6 +1,10 @@
use tls_core::{anchors::RootCertStore, verify::WebPkiVerifier};
use tlsn_common::config::{ProtocolConfig, ProtocolConfigValidator};
use tlsn_core::{transcript::Idx, CryptoProvider, ProveConfig, VerifierOutput, VerifyConfig};
use tlsn_core::{
hash::HashAlgId,
transcript::{Idx, TranscriptCommitConfig, TranscriptCommitment, TranscriptCommitmentKind},
CryptoProvider, ProveConfig, VerifierOutput, VerifyConfig,
};
use tlsn_prover::{Prover, ProverConfig};
use tlsn_server_fixture::bind;
use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN};
@@ -31,7 +35,7 @@ async fn verify() {
VerifierOutput {
server_name,
transcript,
..
transcript_commitments,
},
) = tokio::join!(prover(socket_0), verifier(socket_1));
@@ -47,6 +51,11 @@ async fn verify() {
&Idx::new(2..transcript.len_received())
);
assert_eq!(server_name.as_str(), SERVER_DOMAIN);
assert!(transcript_commitments
.iter()
.any(|commitment| matches!(commitment, TranscriptCommitment::Hash { .. })));
println!("{:?}", transcript_commitments);
}
#[instrument(skip(notary_socket))]
@@ -96,7 +105,11 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(notary_socke
tokio::spawn(connection);
let request = Request::builder()
.uri(format!("https://{}", SERVER_DOMAIN))
.uri(format!(
"https://{}/bytes?size={recv}",
SERVER_DOMAIN,
recv = MAX_RECV_DATA - 256
))
.header("Host", SERVER_DOMAIN)
.header("Connection", "close")
.method("GET")
@@ -107,8 +120,7 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(notary_socke
assert!(response.status() == StatusCode::OK);
let payload = response.into_body().collect().await.unwrap().to_bytes();
println!("{:?}", &String::from_utf8_lossy(&payload));
let _ = response.into_body().collect().await.unwrap().to_bytes();
let _ = server_task.await.unwrap();
@@ -116,6 +128,17 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(notary_socke
let (sent_len, recv_len) = prover.transcript().len();
let mut builder = TranscriptCommitConfig::builder(prover.transcript());
builder.default_kind(TranscriptCommitmentKind::Hash {
alg: HashAlgId::SHA256,
});
builder.commit_sent(&(0..sent_len)).unwrap();
builder.commit_recv(&(0..recv_len)).unwrap();
let transcript_commit = builder.build().unwrap();
let mut builder = ProveConfig::builder(prover.transcript());
builder
@@ -123,7 +146,8 @@ async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(notary_socke
.reveal_sent(&(0..sent_len - 1))
.unwrap()
.reveal_recv(&(2..recv_len))
.unwrap();
.unwrap()
.transcript_commit(transcript_commit);
let config = builder.build().unwrap();

View File

@@ -23,7 +23,7 @@ use mpz_vm_core::prelude::*;
use serio::{stream::IoStreamExt, SinkExt};
use tls_core::msgs::enums::ContentType;
use tlsn_common::{
commit::commit_records,
commit::{commit_records, hash::verify_hash},
config::ProtocolConfig,
context::build_mt_context,
encoding,
@@ -382,6 +382,7 @@ impl Verifier<state::Committed> {
}
let mut transcript_commitments = Vec::new();
let mut hash_commitments = None;
if let Some(commit_config) = transcript_commit {
if commit_config.encoding() {
let commitment = mux_fut
@@ -396,7 +397,12 @@ impl Verifier<state::Committed> {
transcript_commitments.push(TranscriptCommitment::Encoding(commitment));
}
// TODO: Other commitment types.
if commit_config.has_hash() {
hash_commitments = Some(
verify_hash(vm, transcript_refs, commit_config.iter_hash().cloned())
.map_err(VerifierError::verify)?,
);
}
}
mux_fut
@@ -409,6 +415,12 @@ impl Verifier<state::Committed> {
.map_err(VerifierError::verify)?;
}
if let Some(hash_commitments) = hash_commitments {
for commitment in hash_commitments.try_recv().map_err(VerifierError::verify)? {
transcript_commitments.push(TranscriptCommitment::Hash(commitment));
}
}
Ok(VerifierOutput {
server_name,
transcript,