mirror of
https://github.com/tlsnotary/tlsn.git
synced 2026-01-10 22:08:09 -05:00
Compare commits
15 Commits
interactiv
...
blake3_exa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6c5c13aa | ||
|
|
4ab73bdfb5 | ||
|
|
a8fa57f2cb | ||
|
|
d25fb320d4 | ||
|
|
0539268da7 | ||
|
|
427b2896b5 | ||
|
|
89d1e594d1 | ||
|
|
b4380f021e | ||
|
|
8a823d18ec | ||
|
|
7bcfc56bd8 | ||
|
|
2909d5ebaa | ||
|
|
7918494ccc | ||
|
|
92dd47b376 | ||
|
|
5474a748ce | ||
|
|
92da5adc24 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -18,10 +18,10 @@ env:
|
||||
# We need a higher number of parallel rayon tasks than the default (which is 4)
|
||||
# in order to prevent a deadlock, c.f.
|
||||
# - https://github.com/tlsnotary/tlsn/issues/548
|
||||
# - https://github.com/privacy-scaling-explorations/mpz/issues/178
|
||||
# - https://github.com/privacy-ethereum/mpz/issues/178
|
||||
# 32 seems to be big enough for the foreseeable future
|
||||
RAYON_NUM_THREADS: 32
|
||||
RUST_VERSION: 1.89.0
|
||||
RUST_VERSION: 1.90.0
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: clippy
|
||||
|
||||
1
.github/workflows/rustdoc.yml
vendored
1
.github/workflows/rustdoc.yml
vendored
@@ -23,7 +23,6 @@ jobs:
|
||||
- name: "rustdoc"
|
||||
run: crates/wasm/build-docs.sh
|
||||
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
|
||||
2869
Cargo.lock
generated
2869
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@@ -66,19 +66,19 @@ tlsn-harness-runner = { path = "crates/harness/runner" }
|
||||
tlsn-wasm = { path = "crates/wasm" }
|
||||
tlsn = { path = "crates/tlsn" }
|
||||
|
||||
mpz-circuits = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-memory-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-common = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-vm-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-garble = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-garble-core = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-ole = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-ot = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-share-conversion = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-fields = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-zk = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-hash = { git = "https://github.com/privacy-scaling-explorations/mpz", rev = "6432a43" }
|
||||
mpz-circuits = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-memory-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-common = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-vm-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-garble = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-garble-core = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-ole = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-ot = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-share-conversion = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-fields = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-zk = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
mpz-hash = { git = "https://github.com/privacy-ethereum/mpz", rev = "70348c1" }
|
||||
|
||||
rangeset = { version = "0.2" }
|
||||
serio = { version = "0.2" }
|
||||
@@ -86,6 +86,7 @@ spansy = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6168663" }
|
||||
uid-mux = { version = "0.2" }
|
||||
websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6168663" }
|
||||
|
||||
aead = { version = "0.4" }
|
||||
aes = { version = "0.8" }
|
||||
aes-gcm = { version = "0.9" }
|
||||
anyhow = { version = "1.0" }
|
||||
@@ -109,7 +110,7 @@ elliptic-curve = { version = "0.13" }
|
||||
enum-try-as-inner = { version = "0.1" }
|
||||
env_logger = { version = "0.10" }
|
||||
futures = { version = "0.3" }
|
||||
futures-rustls = { version = "0.26" }
|
||||
futures-rustls = { version = "0.25" }
|
||||
generic-array = { version = "0.14" }
|
||||
ghash = { version = "0.5" }
|
||||
hex = { version = "0.4" }
|
||||
|
||||
@@ -23,9 +23,9 @@ thiserror = { workspace = true }
|
||||
tiny-keccak = { workspace = true, features = ["keccak"] }
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-primitives = { version = "0.8.22", default-features = false }
|
||||
alloy-signer = { version = "0.12", default-features = false }
|
||||
alloy-signer-local = { version = "0.12", default-features = false }
|
||||
alloy-primitives = { version = "1.3.1", default-features = false }
|
||||
alloy-signer = { version = "1.0", default-features = false }
|
||||
alloy-signer-local = { version = "1.0", default-features = false }
|
||||
rand06-compat = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tlsn-core = { workspace = true, features = ["fixtures"] }
|
||||
|
||||
@@ -13,7 +13,13 @@ workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
fixtures = ["dep:hex", "dep:tlsn-data-fixtures"]
|
||||
fixtures = [
|
||||
"dep:hex",
|
||||
"dep:tlsn-data-fixtures",
|
||||
"dep:aead",
|
||||
"dep:aes-gcm",
|
||||
"dep:generic-array",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
tlsn-data-fixtures = { workspace = true, optional = true }
|
||||
@@ -21,6 +27,9 @@ tlsn-tls-core = { workspace = true, features = ["serde"] }
|
||||
tlsn-utils = { workspace = true }
|
||||
rangeset = { workspace = true, features = ["serde"] }
|
||||
|
||||
aead = { workspace = true, features = ["alloc"], optional = true }
|
||||
aes-gcm = { workspace = true, optional = true }
|
||||
generic-array = { workspace = true, optional = true }
|
||||
bimap = { version = "0.6", features = ["serde"] }
|
||||
blake3 = { workspace = true }
|
||||
hex = { workspace = true, optional = true }
|
||||
@@ -39,9 +48,12 @@ webpki-roots = { workspace = true }
|
||||
rustls-webpki = { workspace = true, features = ["ring"] }
|
||||
rustls-pki-types = { workspace = true }
|
||||
itybity = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
aead = { workspace = true, features = ["alloc"] }
|
||||
aes-gcm = { workspace = true }
|
||||
generic-array = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
|
||||
16
crates/core/src/display.rs
Normal file
16
crates/core/src/display.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use rangeset::RangeSet;
|
||||
|
||||
pub(crate) struct FmtRangeSet<'a>(pub &'a RangeSet<usize>);
|
||||
|
||||
impl<'a> std::fmt::Display for FmtRangeSet<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("{")?;
|
||||
for range in self.0.iter_ranges() {
|
||||
write!(f, "{}..{}", range.start, range.end)?;
|
||||
if range.end < self.0.end().unwrap_or(0) {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
}
|
||||
f.write_str("}")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Fixtures for testing
|
||||
|
||||
mod provider;
|
||||
pub mod transcript;
|
||||
|
||||
pub use provider::FixtureEncodingProvider;
|
||||
|
||||
|
||||
199
crates/core/src/fixtures/transcript.rs
Normal file
199
crates/core/src/fixtures/transcript.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
//! Transcript fixtures for testing.
|
||||
|
||||
use aead::Payload as AeadPayload;
|
||||
use aes_gcm::{aead::Aead, Aes128Gcm, NewAead};
|
||||
use generic_array::GenericArray;
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
use tls_core::msgs::{
|
||||
base::Payload,
|
||||
codec::Codec,
|
||||
enums::{ContentType, HandshakeType, ProtocolVersion},
|
||||
handshake::{HandshakeMessagePayload, HandshakePayload},
|
||||
message::{OpaqueMessage, PlainMessage},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connection::{TranscriptLength, VerifyData},
|
||||
fixtures::ConnectionFixture,
|
||||
transcript::{Record, TlsTranscript},
|
||||
};
|
||||
|
||||
/// The key used for encryption of the sent and received transcript.
|
||||
pub const KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
|
||||
/// The iv used for encryption of the sent and received transcript.
|
||||
pub const IV: [u8; 4] = [1, 3, 3, 7];
|
||||
|
||||
/// The record size in bytes.
|
||||
pub const RECORD_SIZE: usize = 512;
|
||||
|
||||
/// Creates a transript fixture for testing.
|
||||
pub fn transcript_fixture(sent: &[u8], recv: &[u8]) -> TlsTranscript {
|
||||
TranscriptGenerator::new(KEY, IV).generate(sent, recv)
|
||||
}
|
||||
|
||||
struct TranscriptGenerator {
|
||||
key: [u8; 16],
|
||||
iv: [u8; 4],
|
||||
}
|
||||
|
||||
impl TranscriptGenerator {
|
||||
fn new(key: [u8; 16], iv: [u8; 4]) -> Self {
|
||||
Self { key, iv }
|
||||
}
|
||||
|
||||
fn generate(&self, sent: &[u8], recv: &[u8]) -> TlsTranscript {
|
||||
let mut rng = StdRng::from_seed([1; 32]);
|
||||
|
||||
let transcript_len = TranscriptLength {
|
||||
sent: sent.len() as u32,
|
||||
received: recv.len() as u32,
|
||||
};
|
||||
let tlsn = ConnectionFixture::tlsnotary(transcript_len);
|
||||
|
||||
let time = tlsn.connection_info.time;
|
||||
let version = tlsn.connection_info.version;
|
||||
let server_cert_chain = tlsn.server_cert_data.certs;
|
||||
let server_signature = tlsn.server_cert_data.sig;
|
||||
let cert_binding = tlsn.server_cert_data.binding;
|
||||
|
||||
let cf_vd: [u8; 12] = rng.random();
|
||||
let sf_vd: [u8; 12] = rng.random();
|
||||
|
||||
let verify_data = VerifyData {
|
||||
client_finished: cf_vd.to_vec(),
|
||||
server_finished: sf_vd.to_vec(),
|
||||
};
|
||||
|
||||
let sent = self.gen_records(cf_vd, sent);
|
||||
let recv = self.gen_records(sf_vd, recv);
|
||||
|
||||
TlsTranscript::new(
|
||||
time,
|
||||
version,
|
||||
Some(server_cert_chain),
|
||||
Some(server_signature),
|
||||
cert_binding,
|
||||
verify_data,
|
||||
sent,
|
||||
recv,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn gen_records(&self, vd: [u8; 12], plaintext: &[u8]) -> Vec<Record> {
|
||||
let mut records = Vec::new();
|
||||
|
||||
let handshake = self.gen_handshake(vd);
|
||||
records.push(handshake);
|
||||
|
||||
for (seq, msg) in (1_u64..).zip(plaintext.chunks(RECORD_SIZE)) {
|
||||
let record = self.gen_app_data(seq, msg);
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
records
|
||||
}
|
||||
|
||||
fn gen_app_data(&self, seq: u64, plaintext: &[u8]) -> Record {
|
||||
assert!(
|
||||
plaintext.len() <= 1 << 14,
|
||||
"plaintext len per record must be smaller than 2^14 bytes"
|
||||
);
|
||||
|
||||
let explicit_nonce: [u8; 8] = seq.to_be_bytes();
|
||||
let msg = PlainMessage {
|
||||
typ: ContentType::ApplicationData,
|
||||
version: ProtocolVersion::TLSv1_2,
|
||||
payload: Payload::new(plaintext),
|
||||
};
|
||||
let opaque = aes_gcm_encrypt(self.key, self.iv, seq, explicit_nonce, &msg);
|
||||
|
||||
let mut payload = opaque.payload.0;
|
||||
let mut ciphertext = payload.split_off(8);
|
||||
let tag = ciphertext.split_off(ciphertext.len() - 16);
|
||||
|
||||
Record {
|
||||
seq,
|
||||
typ: ContentType::ApplicationData,
|
||||
plaintext: Some(plaintext.to_vec()),
|
||||
explicit_nonce: explicit_nonce.to_vec(),
|
||||
ciphertext,
|
||||
tag: Some(tag),
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_handshake(&self, vd: [u8; 12]) -> Record {
|
||||
let seq = 0_u64;
|
||||
let explicit_nonce = seq.to_be_bytes();
|
||||
|
||||
let mut plaintext = Vec::new();
|
||||
|
||||
let payload = Payload(vd.to_vec());
|
||||
let hs_payload = HandshakePayload::Finished(payload);
|
||||
let handshake_message = HandshakeMessagePayload {
|
||||
typ: HandshakeType::Finished,
|
||||
payload: hs_payload,
|
||||
};
|
||||
handshake_message.encode(&mut plaintext);
|
||||
|
||||
let msg = PlainMessage {
|
||||
typ: ContentType::Handshake,
|
||||
version: ProtocolVersion::TLSv1_2,
|
||||
payload: Payload::new(plaintext.clone()),
|
||||
};
|
||||
|
||||
let opaque = aes_gcm_encrypt(self.key, self.iv, seq, explicit_nonce, &msg);
|
||||
let mut payload = opaque.payload.0;
|
||||
let mut ciphertext = payload.split_off(8);
|
||||
let tag = ciphertext.split_off(ciphertext.len() - 16);
|
||||
|
||||
Record {
|
||||
seq,
|
||||
typ: ContentType::Handshake,
|
||||
plaintext: Some(plaintext),
|
||||
explicit_nonce: explicit_nonce.to_vec(),
|
||||
ciphertext,
|
||||
tag: Some(tag),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn aes_gcm_encrypt(
|
||||
key: [u8; 16],
|
||||
iv: [u8; 4],
|
||||
seq: u64,
|
||||
explicit_nonce: [u8; 8],
|
||||
msg: &PlainMessage,
|
||||
) -> OpaqueMessage {
|
||||
let mut aad = [0u8; 13];
|
||||
|
||||
aad[..8].copy_from_slice(&seq.to_be_bytes());
|
||||
aad[8] = msg.typ.get_u8();
|
||||
aad[9..11].copy_from_slice(&msg.version.get_u16().to_be_bytes());
|
||||
aad[11..13].copy_from_slice(&(msg.payload.0.len() as u16).to_be_bytes());
|
||||
let payload = AeadPayload {
|
||||
msg: &msg.payload.0,
|
||||
aad: &aad,
|
||||
};
|
||||
|
||||
let mut nonce = [0u8; 12];
|
||||
nonce[..4].copy_from_slice(&iv);
|
||||
nonce[4..].copy_from_slice(&explicit_nonce);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
let cipher = Aes128Gcm::new_from_slice(&key).unwrap();
|
||||
|
||||
// ciphertext will have the MAC appended
|
||||
let ciphertext = cipher.encrypt(nonce, payload).unwrap();
|
||||
|
||||
// prepend the explicit nonce
|
||||
let mut nonce_ct_mac = vec![0u8; 0];
|
||||
nonce_ct_mac.extend(explicit_nonce.iter());
|
||||
nonce_ct_mac.extend(ciphertext.iter());
|
||||
|
||||
OpaqueMessage {
|
||||
typ: msg.typ,
|
||||
version: msg.version,
|
||||
payload: Payload::new(nonce_ct_mac),
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,11 @@ impl Hash {
|
||||
len: value.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a byte slice of the hash value.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.value[..self.len]
|
||||
}
|
||||
}
|
||||
|
||||
impl rs_merkle::Hash for Hash {
|
||||
|
||||
@@ -11,15 +11,17 @@ pub mod hash;
|
||||
pub mod merkle;
|
||||
pub mod transcript;
|
||||
pub mod webpki;
|
||||
pub use rangeset;
|
||||
pub(crate) mod display;
|
||||
|
||||
use rangeset::ToRangeSet;
|
||||
use rangeset::{RangeSet, ToRangeSet, UnionMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connection::{HandshakeData, ServerName},
|
||||
transcript::{
|
||||
Direction, Idx, PartialTranscript, Transcript, TranscriptCommitConfig,
|
||||
TranscriptCommitRequest, TranscriptCommitment, TranscriptSecret,
|
||||
Direction, PartialTranscript, Transcript, TranscriptCommitConfig, TranscriptCommitRequest,
|
||||
TranscriptCommitment, TranscriptSecret,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,7 +29,7 @@ use crate::{
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProveConfig {
|
||||
server_identity: bool,
|
||||
transcript: Option<PartialTranscript>,
|
||||
reveal: Option<(RangeSet<usize>, RangeSet<usize>)>,
|
||||
transcript_commit: Option<TranscriptCommitConfig>,
|
||||
}
|
||||
|
||||
@@ -42,9 +44,9 @@ impl ProveConfig {
|
||||
self.server_identity
|
||||
}
|
||||
|
||||
/// Returns the transcript to be proven.
|
||||
pub fn transcript(&self) -> Option<&PartialTranscript> {
|
||||
self.transcript.as_ref()
|
||||
/// Returns the ranges of the transcript to be revealed.
|
||||
pub fn reveal(&self) -> Option<&(RangeSet<usize>, RangeSet<usize>)> {
|
||||
self.reveal.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the transcript commitment configuration.
|
||||
@@ -58,8 +60,7 @@ impl ProveConfig {
|
||||
pub struct ProveConfigBuilder<'a> {
|
||||
transcript: &'a Transcript,
|
||||
server_identity: bool,
|
||||
reveal_sent: Idx,
|
||||
reveal_recv: Idx,
|
||||
reveal: Option<(RangeSet<usize>, RangeSet<usize>)>,
|
||||
transcript_commit: Option<TranscriptCommitConfig>,
|
||||
}
|
||||
|
||||
@@ -69,8 +70,7 @@ impl<'a> ProveConfigBuilder<'a> {
|
||||
Self {
|
||||
transcript,
|
||||
server_identity: false,
|
||||
reveal_sent: Idx::default(),
|
||||
reveal_recv: Idx::default(),
|
||||
reveal: None,
|
||||
transcript_commit: None,
|
||||
}
|
||||
}
|
||||
@@ -93,22 +93,24 @@ impl<'a> ProveConfigBuilder<'a> {
|
||||
direction: Direction,
|
||||
ranges: &dyn ToRangeSet<usize>,
|
||||
) -> Result<&mut Self, ProveConfigBuilderError> {
|
||||
let idx = Idx::new(ranges.to_range_set());
|
||||
let idx = ranges.to_range_set();
|
||||
|
||||
if idx.end() > self.transcript.len_of_direction(direction) {
|
||||
if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
|
||||
return Err(ProveConfigBuilderError(
|
||||
ProveConfigBuilderErrorRepr::IndexOutOfBounds {
|
||||
direction,
|
||||
actual: idx.end(),
|
||||
actual: idx.end().unwrap_or(0),
|
||||
len: self.transcript.len_of_direction(direction),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (sent, recv) = self.reveal.get_or_insert_default();
|
||||
match direction {
|
||||
Direction::Sent => self.reveal_sent.union_mut(&idx),
|
||||
Direction::Received => self.reveal_recv.union_mut(&idx),
|
||||
Direction::Sent => sent.union_mut(&idx),
|
||||
Direction::Received => recv.union_mut(&idx),
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
@@ -130,18 +132,9 @@ impl<'a> ProveConfigBuilder<'a> {
|
||||
|
||||
/// Builds the configuration.
|
||||
pub fn build(self) -> Result<ProveConfig, ProveConfigBuilderError> {
|
||||
let transcript = if !self.reveal_sent.is_empty() || !self.reveal_recv.is_empty() {
|
||||
Some(
|
||||
self.transcript
|
||||
.to_partial(self.reveal_sent, self.reveal_recv),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ProveConfig {
|
||||
server_identity: self.server_identity,
|
||||
transcript,
|
||||
reveal: self.reveal,
|
||||
transcript_commit: self.transcript_commit,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ mod tls;
|
||||
|
||||
use std::{fmt, ops::Range};
|
||||
|
||||
use rangeset::{Difference, IndexRanges, RangeSet, Subset, ToRangeSet, Union, UnionMut};
|
||||
use rangeset::{Difference, IndexRanges, RangeSet, Union};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::connection::TranscriptLength;
|
||||
@@ -39,6 +39,7 @@ pub use proof::{
|
||||
TranscriptProof, TranscriptProofBuilder, TranscriptProofBuilderError, TranscriptProofError,
|
||||
};
|
||||
pub use tls::{Record, TlsTranscript};
|
||||
pub use tls_core::msgs::enums::ContentType;
|
||||
|
||||
/// A transcript contains the plaintext of all application data communicated
|
||||
/// between the Prover and the Server.
|
||||
@@ -95,18 +96,18 @@ impl Transcript {
|
||||
|
||||
/// Returns the subsequence of the transcript with the provided index,
|
||||
/// returning `None` if the index is out of bounds.
|
||||
pub fn get(&self, direction: Direction, idx: &Idx) -> Option<Subsequence> {
|
||||
pub fn get(&self, direction: Direction, idx: &RangeSet<usize>) -> Option<Subsequence> {
|
||||
let data = match direction {
|
||||
Direction::Sent => &self.sent,
|
||||
Direction::Received => &self.received,
|
||||
};
|
||||
|
||||
if idx.end() > data.len() {
|
||||
if idx.end().unwrap_or(0) > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Subsequence::new(idx.clone(), data.index_ranges(&idx.0))
|
||||
Subsequence::new(idx.clone(), data.index_ranges(idx))
|
||||
.expect("data is same length as index"),
|
||||
)
|
||||
}
|
||||
@@ -121,7 +122,11 @@ impl Transcript {
|
||||
///
|
||||
/// * `sent_idx` - The indices of the sent data to include.
|
||||
/// * `recv_idx` - The indices of the received data to include.
|
||||
pub fn to_partial(&self, sent_idx: Idx, recv_idx: Idx) -> PartialTranscript {
|
||||
pub fn to_partial(
|
||||
&self,
|
||||
sent_idx: RangeSet<usize>,
|
||||
recv_idx: RangeSet<usize>,
|
||||
) -> PartialTranscript {
|
||||
let mut sent = vec![0; self.sent.len()];
|
||||
let mut received = vec![0; self.received.len()];
|
||||
|
||||
@@ -156,9 +161,9 @@ pub struct PartialTranscript {
|
||||
/// Data received by the Prover from the Server.
|
||||
received: Vec<u8>,
|
||||
/// Index of `sent` which have been authenticated.
|
||||
sent_authed_idx: Idx,
|
||||
sent_authed_idx: RangeSet<usize>,
|
||||
/// Index of `received` which have been authenticated.
|
||||
received_authed_idx: Idx,
|
||||
received_authed_idx: RangeSet<usize>,
|
||||
}
|
||||
|
||||
/// `PartialTranscript` in a compressed form.
|
||||
@@ -170,9 +175,9 @@ pub struct CompressedPartialTranscript {
|
||||
/// Received data which has been authenticated.
|
||||
received_authed: Vec<u8>,
|
||||
/// Index of `sent_authed`.
|
||||
sent_idx: Idx,
|
||||
sent_idx: RangeSet<usize>,
|
||||
/// Index of `received_authed`.
|
||||
recv_idx: Idx,
|
||||
recv_idx: RangeSet<usize>,
|
||||
/// Total bytelength of sent data in the original partial transcript.
|
||||
sent_total: usize,
|
||||
/// Total bytelength of received data in the original partial transcript.
|
||||
@@ -184,10 +189,10 @@ impl From<PartialTranscript> for CompressedPartialTranscript {
|
||||
Self {
|
||||
sent_authed: uncompressed
|
||||
.sent
|
||||
.index_ranges(&uncompressed.sent_authed_idx.0),
|
||||
.index_ranges(&uncompressed.sent_authed_idx),
|
||||
received_authed: uncompressed
|
||||
.received
|
||||
.index_ranges(&uncompressed.received_authed_idx.0),
|
||||
.index_ranges(&uncompressed.received_authed_idx),
|
||||
sent_idx: uncompressed.sent_authed_idx,
|
||||
recv_idx: uncompressed.received_authed_idx,
|
||||
sent_total: uncompressed.sent.len(),
|
||||
@@ -237,8 +242,8 @@ impl PartialTranscript {
|
||||
Self {
|
||||
sent: vec![0; sent_len],
|
||||
received: vec![0; received_len],
|
||||
sent_authed_idx: Idx::default(),
|
||||
received_authed_idx: Idx::default(),
|
||||
sent_authed_idx: RangeSet::default(),
|
||||
received_authed_idx: RangeSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,10 +264,10 @@ impl PartialTranscript {
|
||||
}
|
||||
|
||||
/// Returns whether the index is in bounds of the transcript.
|
||||
pub fn contains(&self, direction: Direction, idx: &Idx) -> bool {
|
||||
pub fn contains(&self, direction: Direction, idx: &RangeSet<usize>) -> bool {
|
||||
match direction {
|
||||
Direction::Sent => idx.end() <= self.sent.len(),
|
||||
Direction::Received => idx.end() <= self.received.len(),
|
||||
Direction::Sent => idx.end().unwrap_or(0) <= self.sent.len(),
|
||||
Direction::Received => idx.end().unwrap_or(0) <= self.received.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,23 +294,23 @@ impl PartialTranscript {
|
||||
}
|
||||
|
||||
/// Returns the index of sent data which have been authenticated.
|
||||
pub fn sent_authed(&self) -> &Idx {
|
||||
pub fn sent_authed(&self) -> &RangeSet<usize> {
|
||||
&self.sent_authed_idx
|
||||
}
|
||||
|
||||
/// Returns the index of received data which have been authenticated.
|
||||
pub fn received_authed(&self) -> &Idx {
|
||||
pub fn received_authed(&self) -> &RangeSet<usize> {
|
||||
&self.received_authed_idx
|
||||
}
|
||||
|
||||
/// Returns the index of sent data which haven't been authenticated.
|
||||
pub fn sent_unauthed(&self) -> Idx {
|
||||
Idx(RangeSet::from(0..self.sent.len()).difference(&self.sent_authed_idx.0))
|
||||
pub fn sent_unauthed(&self) -> RangeSet<usize> {
|
||||
(0..self.sent.len()).difference(&self.sent_authed_idx)
|
||||
}
|
||||
|
||||
/// Returns the index of received data which haven't been authenticated.
|
||||
pub fn received_unauthed(&self) -> Idx {
|
||||
Idx(RangeSet::from(0..self.received.len()).difference(&self.received_authed_idx.0))
|
||||
pub fn received_unauthed(&self) -> RangeSet<usize> {
|
||||
(0..self.received.len()).difference(&self.received_authed_idx)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the authenticated data in the transcript.
|
||||
@@ -315,7 +320,7 @@ impl PartialTranscript {
|
||||
Direction::Received => (&self.received, &self.received_authed_idx),
|
||||
};
|
||||
|
||||
authed.0.iter().map(|i| data[i])
|
||||
authed.iter().map(|i| data[i])
|
||||
}
|
||||
|
||||
/// Unions the authenticated data of this transcript with another.
|
||||
@@ -337,8 +342,7 @@ impl PartialTranscript {
|
||||
|
||||
for range in other
|
||||
.sent_authed_idx
|
||||
.0
|
||||
.difference(&self.sent_authed_idx.0)
|
||||
.difference(&self.sent_authed_idx)
|
||||
.iter_ranges()
|
||||
{
|
||||
self.sent[range.clone()].copy_from_slice(&other.sent[range]);
|
||||
@@ -346,8 +350,7 @@ impl PartialTranscript {
|
||||
|
||||
for range in other
|
||||
.received_authed_idx
|
||||
.0
|
||||
.difference(&self.received_authed_idx.0)
|
||||
.difference(&self.received_authed_idx)
|
||||
.iter_ranges()
|
||||
{
|
||||
self.received[range.clone()].copy_from_slice(&other.received[range]);
|
||||
@@ -399,12 +402,12 @@ impl PartialTranscript {
|
||||
pub fn set_unauthed_range(&mut self, value: u8, direction: Direction, range: Range<usize>) {
|
||||
match direction {
|
||||
Direction::Sent => {
|
||||
for range in range.difference(&self.sent_authed_idx.0).iter_ranges() {
|
||||
for range in range.difference(&self.sent_authed_idx).iter_ranges() {
|
||||
self.sent[range].fill(value);
|
||||
}
|
||||
}
|
||||
Direction::Received => {
|
||||
for range in range.difference(&self.received_authed_idx.0).iter_ranges() {
|
||||
for range in range.difference(&self.received_authed_idx).iter_ranges() {
|
||||
self.received[range].fill(value);
|
||||
}
|
||||
}
|
||||
@@ -433,130 +436,19 @@ impl fmt::Display for Direction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transcript index.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Idx(RangeSet<usize>);
|
||||
|
||||
impl Idx {
|
||||
/// Creates a new index builder.
|
||||
pub fn builder() -> IdxBuilder {
|
||||
IdxBuilder::default()
|
||||
}
|
||||
|
||||
/// Creates an empty index.
|
||||
pub fn empty() -> Self {
|
||||
Self(RangeSet::default())
|
||||
}
|
||||
|
||||
/// Creates a new transcript index.
|
||||
pub fn new(ranges: impl Into<RangeSet<usize>>) -> Self {
|
||||
Self(ranges.into())
|
||||
}
|
||||
|
||||
/// Returns the start of the index.
|
||||
pub fn start(&self) -> usize {
|
||||
self.0.min().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the end of the index, non-inclusive.
|
||||
pub fn end(&self) -> usize {
|
||||
self.0.end().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the values in the index.
|
||||
pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the ranges of the index.
|
||||
pub fn iter_ranges(&self) -> impl Iterator<Item = Range<usize>> + '_ {
|
||||
self.0.iter_ranges()
|
||||
}
|
||||
|
||||
/// Returns the number of values in the index.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns whether the index is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of disjoint ranges in the index.
|
||||
pub fn count(&self) -> usize {
|
||||
self.0.len_ranges()
|
||||
}
|
||||
|
||||
pub(crate) fn as_range_set(&self) -> &RangeSet<usize> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns the union of this index with another.
|
||||
pub(crate) fn union(&self, other: &Idx) -> Idx {
|
||||
Idx(self.0.union(&other.0))
|
||||
}
|
||||
|
||||
/// Unions this index with another.
|
||||
pub(crate) fn union_mut(&mut self, other: &Idx) {
|
||||
self.0.union_mut(&other.0);
|
||||
}
|
||||
|
||||
/// Returns the difference between `self` and `other`.
|
||||
pub(crate) fn difference(&self, other: &Idx) -> Idx {
|
||||
Idx(self.0.difference(&other.0))
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is a subset of `other`.
|
||||
pub(crate) fn is_subset(&self, other: &Idx) -> bool {
|
||||
self.0.is_subset(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Idx {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Idx([")?;
|
||||
let count = self.0.len_ranges();
|
||||
for (i, range) in self.0.iter_ranges().enumerate() {
|
||||
write!(f, "{}..{}", range.start, range.end)?;
|
||||
if i < count - 1 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
}
|
||||
f.write_str("])")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for [`Idx`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdxBuilder(RangeSet<usize>);
|
||||
|
||||
impl IdxBuilder {
|
||||
/// Unions ranges.
|
||||
pub fn union(self, ranges: &dyn ToRangeSet<usize>) -> Self {
|
||||
IdxBuilder(self.0.union(&ranges.to_range_set()))
|
||||
}
|
||||
|
||||
/// Builds the index.
|
||||
pub fn build(self) -> Idx {
|
||||
Idx(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transcript subsequence.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "validation::SubsequenceUnchecked")]
|
||||
pub struct Subsequence {
|
||||
/// Index of the subsequence.
|
||||
idx: Idx,
|
||||
idx: RangeSet<usize>,
|
||||
/// Data of the subsequence.
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Subsequence {
|
||||
/// Creates a new subsequence.
|
||||
pub fn new(idx: Idx, data: Vec<u8>) -> Result<Self, InvalidSubsequence> {
|
||||
pub fn new(idx: RangeSet<usize>, data: Vec<u8>) -> Result<Self, InvalidSubsequence> {
|
||||
if idx.len() != data.len() {
|
||||
return Err(InvalidSubsequence(
|
||||
"index length does not match data length",
|
||||
@@ -567,7 +459,7 @@ impl Subsequence {
|
||||
}
|
||||
|
||||
/// Returns the index of the subsequence.
|
||||
pub fn index(&self) -> &Idx {
|
||||
pub fn index(&self) -> &RangeSet<usize> {
|
||||
&self.idx
|
||||
}
|
||||
|
||||
@@ -583,7 +475,7 @@ impl Subsequence {
|
||||
}
|
||||
|
||||
/// Returns the inner parts of the subsequence.
|
||||
pub fn into_parts(self) -> (Idx, Vec<u8>) {
|
||||
pub fn into_parts(self) -> (RangeSet<usize>, Vec<u8>) {
|
||||
(self.idx, self.data)
|
||||
}
|
||||
|
||||
@@ -611,7 +503,7 @@ mod validation {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct SubsequenceUnchecked {
|
||||
idx: Idx,
|
||||
idx: RangeSet<usize>,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
@@ -633,8 +525,8 @@ mod validation {
|
||||
pub(super) struct CompressedPartialTranscriptUnchecked {
|
||||
sent_authed: Vec<u8>,
|
||||
received_authed: Vec<u8>,
|
||||
sent_idx: Idx,
|
||||
recv_idx: Idx,
|
||||
sent_idx: RangeSet<usize>,
|
||||
recv_idx: RangeSet<usize>,
|
||||
sent_total: usize,
|
||||
recv_total: usize,
|
||||
}
|
||||
@@ -651,8 +543,8 @@ mod validation {
|
||||
));
|
||||
}
|
||||
|
||||
if unchecked.sent_idx.end() > unchecked.sent_total
|
||||
|| unchecked.recv_idx.end() > unchecked.recv_total
|
||||
if unchecked.sent_idx.end().unwrap_or(0) > unchecked.sent_total
|
||||
|| unchecked.recv_idx.end().unwrap_or(0) > unchecked.recv_total
|
||||
{
|
||||
return Err(InvalidCompressedPartialTranscript(
|
||||
"ranges are not in bounds of the data",
|
||||
@@ -681,8 +573,8 @@ mod validation {
|
||||
CompressedPartialTranscriptUnchecked {
|
||||
received_authed: vec![1, 2, 3, 11, 12, 13],
|
||||
sent_authed: vec![4, 5, 6, 14, 15, 16],
|
||||
recv_idx: Idx(RangeSet::new(&[1..4, 11..14])),
|
||||
sent_idx: Idx(RangeSet::new(&[4..7, 14..17])),
|
||||
recv_idx: RangeSet::from([1..4, 11..14]),
|
||||
sent_idx: RangeSet::from([4..7, 14..17]),
|
||||
sent_total: 20,
|
||||
recv_total: 20,
|
||||
}
|
||||
@@ -721,7 +613,6 @@ mod validation {
|
||||
// Change the total to be less than the last range's end bound.
|
||||
let end = partial_transcript
|
||||
.sent_idx
|
||||
.0
|
||||
.iter_ranges()
|
||||
.next_back()
|
||||
.unwrap()
|
||||
@@ -753,31 +644,25 @@ mod tests {
|
||||
|
||||
#[fixture]
|
||||
fn partial_transcript() -> PartialTranscript {
|
||||
transcript().to_partial(
|
||||
Idx::new(RangeSet::new(&[1..4, 6..9])),
|
||||
Idx::new(RangeSet::new(&[2..5, 7..10])),
|
||||
)
|
||||
transcript().to_partial(RangeSet::from([1..4, 6..9]), RangeSet::from([2..5, 7..10]))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_transcript_get_subsequence(transcript: Transcript) {
|
||||
let subseq = transcript
|
||||
.get(Direction::Received, &Idx(RangeSet::from([0..4, 7..10])))
|
||||
.get(Direction::Received, &RangeSet::from([0..4, 7..10]))
|
||||
.unwrap();
|
||||
assert_eq!(subseq.data, vec![0, 1, 2, 3, 7, 8, 9]);
|
||||
|
||||
let subseq = transcript
|
||||
.get(Direction::Sent, &Idx(RangeSet::from([0..4, 9..12])))
|
||||
.get(Direction::Sent, &RangeSet::from([0..4, 9..12]))
|
||||
.unwrap();
|
||||
assert_eq!(subseq.data, vec![0, 1, 2, 3, 9, 10, 11]);
|
||||
|
||||
let subseq = transcript.get(
|
||||
Direction::Received,
|
||||
&Idx(RangeSet::from([0..4, 7..10, 11..13])),
|
||||
);
|
||||
let subseq = transcript.get(Direction::Received, &RangeSet::from([0..4, 7..10, 11..13]));
|
||||
assert_eq!(subseq, None);
|
||||
|
||||
let subseq = transcript.get(Direction::Sent, &Idx(RangeSet::from([0..4, 7..10, 11..13])));
|
||||
let subseq = transcript.get(Direction::Sent, &RangeSet::from([0..4, 7..10, 11..13]));
|
||||
assert_eq!(subseq, None);
|
||||
}
|
||||
|
||||
@@ -790,7 +675,7 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
fn test_transcript_to_partial_success(transcript: Transcript) {
|
||||
let partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
assert_eq!(partial.sent_unsafe(), [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(
|
||||
partial.received_unsafe(),
|
||||
@@ -801,29 +686,30 @@ mod tests {
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_transcript_to_partial_failure(transcript: Transcript) {
|
||||
let _ = transcript.to_partial(Idx::new(0..14), Idx::new(3..7));
|
||||
let _ = transcript.to_partial(RangeSet::from(0..14), RangeSet::from(3..7));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_contains(transcript: Transcript) {
|
||||
let partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
assert!(partial.contains(Direction::Sent, &Idx::new([0..5, 7..10])));
|
||||
assert!(!partial.contains(Direction::Received, &Idx::new([4..6, 7..13])))
|
||||
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
assert!(partial.contains(Direction::Sent, &RangeSet::from([0..5, 7..10])));
|
||||
assert!(!partial.contains(Direction::Received, &RangeSet::from([4..6, 7..13])))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_unauthed(transcript: Transcript) {
|
||||
let partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
assert_eq!(partial.sent_unauthed(), Idx::new(2..12));
|
||||
assert_eq!(partial.received_unauthed(), Idx::new([0..3, 7..12]));
|
||||
let partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
assert_eq!(partial.sent_unauthed(), RangeSet::from(2..12));
|
||||
assert_eq!(partial.received_unauthed(), RangeSet::from([0..3, 7..12]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_union_success(transcript: Transcript) {
|
||||
// Non overlapping ranges.
|
||||
let mut simple_partial = transcript.to_partial(Idx::new(0..2), Idx::new(3..7));
|
||||
let mut simple_partial = transcript.to_partial(RangeSet::from(0..2), RangeSet::from(3..7));
|
||||
|
||||
let other_simple_partial = transcript.to_partial(Idx::new(3..5), Idx::new(1..2));
|
||||
let other_simple_partial =
|
||||
transcript.to_partial(RangeSet::from(3..5), RangeSet::from(1..2));
|
||||
|
||||
simple_partial.union_transcript(&other_simple_partial);
|
||||
|
||||
@@ -835,12 +721,16 @@ mod tests {
|
||||
simple_partial.received_unsafe(),
|
||||
[0, 1, 0, 3, 4, 5, 6, 0, 0, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(simple_partial.sent_authed(), &Idx::new([0..2, 3..5]));
|
||||
assert_eq!(simple_partial.received_authed(), &Idx::new([1..2, 3..7]));
|
||||
assert_eq!(simple_partial.sent_authed(), &RangeSet::from([0..2, 3..5]));
|
||||
assert_eq!(
|
||||
simple_partial.received_authed(),
|
||||
&RangeSet::from([1..2, 3..7])
|
||||
);
|
||||
|
||||
// Overwrite with another partial transcript.
|
||||
|
||||
let another_simple_partial = transcript.to_partial(Idx::new(1..4), Idx::new(6..9));
|
||||
let another_simple_partial =
|
||||
transcript.to_partial(RangeSet::from(1..4), RangeSet::from(6..9));
|
||||
|
||||
simple_partial.union_transcript(&another_simple_partial);
|
||||
|
||||
@@ -852,13 +742,17 @@ mod tests {
|
||||
simple_partial.received_unsafe(),
|
||||
[0, 1, 0, 3, 4, 5, 6, 7, 8, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(simple_partial.sent_authed(), &Idx::new(0..5));
|
||||
assert_eq!(simple_partial.received_authed(), &Idx::new([1..2, 3..9]));
|
||||
assert_eq!(simple_partial.sent_authed(), &RangeSet::from(0..5));
|
||||
assert_eq!(
|
||||
simple_partial.received_authed(),
|
||||
&RangeSet::from([1..2, 3..9])
|
||||
);
|
||||
|
||||
// Overlapping ranges.
|
||||
let mut overlap_partial = transcript.to_partial(Idx::new(4..6), Idx::new(3..7));
|
||||
let mut overlap_partial = transcript.to_partial(RangeSet::from(4..6), RangeSet::from(3..7));
|
||||
|
||||
let other_overlap_partial = transcript.to_partial(Idx::new(3..5), Idx::new(5..9));
|
||||
let other_overlap_partial =
|
||||
transcript.to_partial(RangeSet::from(3..5), RangeSet::from(5..9));
|
||||
|
||||
overlap_partial.union_transcript(&other_overlap_partial);
|
||||
|
||||
@@ -870,13 +764,16 @@ mod tests {
|
||||
overlap_partial.received_unsafe(),
|
||||
[0, 0, 0, 3, 4, 5, 6, 7, 8, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(overlap_partial.sent_authed(), &Idx::new([3..5, 4..6]));
|
||||
assert_eq!(overlap_partial.received_authed(), &Idx::new([3..7, 5..9]));
|
||||
assert_eq!(overlap_partial.sent_authed(), &RangeSet::from([3..5, 4..6]));
|
||||
assert_eq!(
|
||||
overlap_partial.received_authed(),
|
||||
&RangeSet::from([3..7, 5..9])
|
||||
);
|
||||
|
||||
// Equal ranges.
|
||||
let mut equal_partial = transcript.to_partial(Idx::new(4..6), Idx::new(3..7));
|
||||
let mut equal_partial = transcript.to_partial(RangeSet::from(4..6), RangeSet::from(3..7));
|
||||
|
||||
let other_equal_partial = transcript.to_partial(Idx::new(4..6), Idx::new(3..7));
|
||||
let other_equal_partial = transcript.to_partial(RangeSet::from(4..6), RangeSet::from(3..7));
|
||||
|
||||
equal_partial.union_transcript(&other_equal_partial);
|
||||
|
||||
@@ -888,13 +785,15 @@ mod tests {
|
||||
equal_partial.received_unsafe(),
|
||||
[0, 0, 0, 3, 4, 5, 6, 0, 0, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(equal_partial.sent_authed(), &Idx::new(4..6));
|
||||
assert_eq!(equal_partial.received_authed(), &Idx::new(3..7));
|
||||
assert_eq!(equal_partial.sent_authed(), &RangeSet::from(4..6));
|
||||
assert_eq!(equal_partial.received_authed(), &RangeSet::from(3..7));
|
||||
|
||||
// Subset ranges.
|
||||
let mut subset_partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let mut subset_partial =
|
||||
transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
|
||||
let other_subset_partial = transcript.to_partial(Idx::new(6..9), Idx::new(5..6));
|
||||
let other_subset_partial =
|
||||
transcript.to_partial(RangeSet::from(6..9), RangeSet::from(5..6));
|
||||
|
||||
subset_partial.union_transcript(&other_subset_partial);
|
||||
|
||||
@@ -906,30 +805,32 @@ mod tests {
|
||||
subset_partial.received_unsafe(),
|
||||
[0, 0, 0, 3, 4, 5, 6, 7, 8, 9, 10, 0]
|
||||
);
|
||||
assert_eq!(subset_partial.sent_authed(), &Idx::new(4..10));
|
||||
assert_eq!(subset_partial.received_authed(), &Idx::new(3..11));
|
||||
assert_eq!(subset_partial.sent_authed(), &RangeSet::from(4..10));
|
||||
assert_eq!(subset_partial.received_authed(), &RangeSet::from(3..11));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_partial_transcript_union_failure(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
|
||||
let other_transcript = Transcript::new(
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
);
|
||||
|
||||
let other_partial = other_transcript.to_partial(Idx::new(6..9), Idx::new(5..6));
|
||||
let other_partial = other_transcript.to_partial(RangeSet::from(6..9), RangeSet::from(5..6));
|
||||
|
||||
partial.union_transcript(&other_partial);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_union_subseq_success(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let sent_seq = Subsequence::new(Idx::new([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let recv_seq = Subsequence::new(Idx::new([0..4, 5..7]), [0, 1, 2, 3, 5, 6].into()).unwrap();
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
let sent_seq =
|
||||
Subsequence::new(RangeSet::from([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let recv_seq =
|
||||
Subsequence::new(RangeSet::from([0..4, 5..7]), [0, 1, 2, 3, 5, 6].into()).unwrap();
|
||||
|
||||
partial.union_subsequence(Direction::Sent, &sent_seq);
|
||||
partial.union_subsequence(Direction::Received, &recv_seq);
|
||||
@@ -939,30 +840,31 @@ mod tests {
|
||||
partial.received_unsafe(),
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0]
|
||||
);
|
||||
assert_eq!(partial.sent_authed(), &Idx::new([0..3, 4..10]));
|
||||
assert_eq!(partial.received_authed(), &Idx::new(0..11));
|
||||
assert_eq!(partial.sent_authed(), &RangeSet::from([0..3, 4..10]));
|
||||
assert_eq!(partial.received_authed(), &RangeSet::from(0..11));
|
||||
|
||||
// Overwrite with another subseq.
|
||||
let other_sent_seq = Subsequence::new(Idx::new(0..3), [3, 2, 1].into()).unwrap();
|
||||
let other_sent_seq = Subsequence::new(RangeSet::from(0..3), [3, 2, 1].into()).unwrap();
|
||||
|
||||
partial.union_subsequence(Direction::Sent, &other_sent_seq);
|
||||
assert_eq!(partial.sent_unsafe(), [3, 2, 1, 0, 4, 5, 6, 7, 8, 9, 0, 0]);
|
||||
assert_eq!(partial.sent_authed(), &Idx::new([0..3, 4..10]));
|
||||
assert_eq!(partial.sent_authed(), &RangeSet::from([0..3, 4..10]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_partial_transcript_union_subseq_failure(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..11));
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..11));
|
||||
|
||||
let sent_seq = Subsequence::new(Idx::new([0..3, 13..15]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let sent_seq =
|
||||
Subsequence::new(RangeSet::from([0..3, 13..15]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
|
||||
partial.union_subsequence(Direction::Sent, &sent_seq);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_partial_transcript_set_unauthed_range(transcript: Transcript) {
|
||||
let mut partial = transcript.to_partial(Idx::new(4..10), Idx::new(3..7));
|
||||
let mut partial = transcript.to_partial(RangeSet::from(4..10), RangeSet::from(3..7));
|
||||
|
||||
partial.set_unauthed_range(7, Direction::Sent, 2..5);
|
||||
partial.set_unauthed_range(5, Direction::Sent, 0..2);
|
||||
@@ -979,13 +881,13 @@ mod tests {
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_subsequence_new_invalid_len() {
|
||||
let _ = Subsequence::new(Idx::new([0..3, 5..8]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let _ = Subsequence::new(RangeSet::from([0..3, 5..8]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[should_panic]
|
||||
fn test_subsequence_copy_to_invalid_len() {
|
||||
let seq = Subsequence::new(Idx::new([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
let seq = Subsequence::new(RangeSet::from([0..3, 5..7]), [0, 1, 2, 5, 6].into()).unwrap();
|
||||
|
||||
let mut data: [u8; 3] = [0, 1, 2];
|
||||
seq.copy_to(&mut data);
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
transcript::{
|
||||
encoding::{EncodingCommitment, EncodingTree},
|
||||
hash::{PlaintextHash, PlaintextHashSecret},
|
||||
Direction, Idx, Transcript,
|
||||
Direction, RangeSet, Transcript,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ pub struct TranscriptCommitConfig {
|
||||
encoding_hash_alg: HashAlgId,
|
||||
has_encoding: bool,
|
||||
has_hash: bool,
|
||||
commits: Vec<((Direction, Idx), TranscriptCommitmentKind)>,
|
||||
commits: Vec<((Direction, RangeSet<usize>), TranscriptCommitmentKind)>,
|
||||
}
|
||||
|
||||
impl TranscriptCommitConfig {
|
||||
@@ -96,7 +96,7 @@ impl TranscriptCommitConfig {
|
||||
}
|
||||
|
||||
/// Returns an iterator over the encoding commitment indices.
|
||||
pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, Idx)> {
|
||||
pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>)> {
|
||||
self.commits.iter().filter_map(|(idx, kind)| match kind {
|
||||
TranscriptCommitmentKind::Encoding => Some(idx),
|
||||
_ => None,
|
||||
@@ -104,7 +104,7 @@ impl TranscriptCommitConfig {
|
||||
}
|
||||
|
||||
/// Returns an iterator over the hash commitment indices.
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = (&(Direction, Idx), &HashAlgId)> {
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = (&(Direction, RangeSet<usize>), &HashAlgId)> {
|
||||
self.commits.iter().filter_map(|(idx, kind)| match kind {
|
||||
TranscriptCommitmentKind::Hash { alg } => Some((idx, alg)),
|
||||
_ => None,
|
||||
@@ -134,7 +134,7 @@ pub struct TranscriptCommitConfigBuilder<'a> {
|
||||
has_encoding: bool,
|
||||
has_hash: bool,
|
||||
default_kind: TranscriptCommitmentKind,
|
||||
commits: HashSet<((Direction, Idx), TranscriptCommitmentKind)>,
|
||||
commits: HashSet<((Direction, RangeSet<usize>), TranscriptCommitmentKind)>,
|
||||
}
|
||||
|
||||
impl<'a> TranscriptCommitConfigBuilder<'a> {
|
||||
@@ -175,15 +175,15 @@ impl<'a> TranscriptCommitConfigBuilder<'a> {
|
||||
direction: Direction,
|
||||
kind: TranscriptCommitmentKind,
|
||||
) -> Result<&mut Self, TranscriptCommitConfigBuilderError> {
|
||||
let idx = Idx::new(ranges.to_range_set());
|
||||
let idx = ranges.to_range_set();
|
||||
|
||||
if idx.end() > self.transcript.len_of_direction(direction) {
|
||||
if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
|
||||
return Err(TranscriptCommitConfigBuilderError::new(
|
||||
ErrorKind::Index,
|
||||
format!(
|
||||
"range is out of bounds of the transcript ({}): {} > {}",
|
||||
direction,
|
||||
idx.end(),
|
||||
idx.end().unwrap_or(0),
|
||||
self.transcript.len_of_direction(direction)
|
||||
),
|
||||
));
|
||||
@@ -290,7 +290,7 @@ impl fmt::Display for TranscriptCommitConfigBuilderError {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TranscriptCommitRequest {
|
||||
encoding: bool,
|
||||
hash: Vec<(Direction, Idx, HashAlgId)>,
|
||||
hash: Vec<(Direction, RangeSet<usize>, HashAlgId)>,
|
||||
}
|
||||
|
||||
impl TranscriptCommitRequest {
|
||||
@@ -305,7 +305,7 @@ impl TranscriptCommitRequest {
|
||||
}
|
||||
|
||||
/// Returns an iterator over the hash commitments.
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = &(Direction, Idx, HashAlgId)> {
|
||||
pub fn iter_hash(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>, HashAlgId)> {
|
||||
self.hash.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
transcript::{
|
||||
commit::MAX_TOTAL_COMMITTED_DATA,
|
||||
encoding::{new_encoder, Encoder, EncodingCommitment},
|
||||
Direction, Idx,
|
||||
Direction,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub(super) struct Opening {
|
||||
pub(super) direction: Direction,
|
||||
pub(super) idx: Idx,
|
||||
pub(super) idx: RangeSet<usize>,
|
||||
pub(super) blinder: Blinder,
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl EncodingProof {
|
||||
commitment: &EncodingCommitment,
|
||||
sent: &[u8],
|
||||
recv: &[u8],
|
||||
) -> Result<(Idx, Idx), EncodingProofError> {
|
||||
) -> Result<(RangeSet<usize>, RangeSet<usize>), EncodingProofError> {
|
||||
let hasher = provider.get(&commitment.root.alg)?;
|
||||
|
||||
let encoder = new_encoder(&commitment.secret);
|
||||
@@ -89,13 +89,13 @@ impl EncodingProof {
|
||||
};
|
||||
|
||||
// Make sure the ranges are within the bounds of the transcript.
|
||||
if idx.end() > data.len() {
|
||||
if idx.end().unwrap_or(0) > data.len() {
|
||||
return Err(EncodingProofError::new(
|
||||
ErrorKind::Proof,
|
||||
format!(
|
||||
"index out of bounds of the transcript ({}): {} > {}",
|
||||
direction,
|
||||
idx.end(),
|
||||
idx.end().unwrap_or(0),
|
||||
data.len()
|
||||
),
|
||||
));
|
||||
@@ -111,7 +111,7 @@ impl EncodingProof {
|
||||
// present in the merkle tree.
|
||||
leaves.push((*id, hasher.hash(&expected_leaf)));
|
||||
|
||||
auth.union_mut(idx.as_range_set());
|
||||
auth.union_mut(idx);
|
||||
}
|
||||
|
||||
// Verify that the expected hashes are present in the merkle tree.
|
||||
@@ -121,7 +121,7 @@ impl EncodingProof {
|
||||
// data is authentic.
|
||||
inclusion_proof.verify(hasher, &commitment.root, leaves)?;
|
||||
|
||||
Ok((Idx(auth_sent), Idx(auth_recv)))
|
||||
Ok((auth_sent, auth_recv))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ mod test {
|
||||
hash::Blake3,
|
||||
transcript::{
|
||||
encoding::{EncoderSecret, EncodingTree},
|
||||
Idx, Transcript,
|
||||
Transcript,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,8 +249,8 @@ mod test {
|
||||
fn new_encoding_fixture(secret: EncoderSecret) -> EncodingFixture {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..OK_JSON.len()));
|
||||
|
||||
let provider = encoding_provider(transcript.sent(), transcript.received());
|
||||
let tree = EncodingTree::new(&Blake3::default(), [&idx_0, &idx_1], &provider).unwrap();
|
||||
@@ -317,7 +317,7 @@ mod test {
|
||||
|
||||
let Opening { idx, .. } = proof.openings.values_mut().next().unwrap();
|
||||
|
||||
*idx = Idx::new([0..3, 13..15]);
|
||||
*idx = RangeSet::from([0..3, 13..15]);
|
||||
|
||||
let err = proof
|
||||
.verify_with_provider(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bimap::BiMap;
|
||||
use rangeset::{RangeSet, UnionMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@@ -11,7 +12,7 @@ use crate::{
|
||||
proof::{EncodingProof, Opening},
|
||||
EncodingProvider,
|
||||
},
|
||||
Direction, Idx,
|
||||
Direction,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +23,7 @@ pub enum EncodingTreeError {
|
||||
#[error("index is out of bounds of the transcript")]
|
||||
OutOfBounds {
|
||||
/// The index.
|
||||
index: Idx,
|
||||
index: RangeSet<usize>,
|
||||
/// The transcript length.
|
||||
transcript_length: usize,
|
||||
},
|
||||
@@ -30,13 +31,13 @@ pub enum EncodingTreeError {
|
||||
#[error("encoding provider is missing an encoding for an index")]
|
||||
MissingEncoding {
|
||||
/// The index which is missing.
|
||||
index: Idx,
|
||||
index: RangeSet<usize>,
|
||||
},
|
||||
/// Index is missing from the tree.
|
||||
#[error("index is missing from the tree")]
|
||||
MissingLeaf {
|
||||
/// The index which is missing.
|
||||
index: Idx,
|
||||
index: RangeSet<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,11 +50,11 @@ pub struct EncodingTree {
|
||||
blinders: Vec<Blinder>,
|
||||
/// Mapping between the index of a leaf and the transcript index it
|
||||
/// corresponds to.
|
||||
idxs: BiMap<usize, (Direction, Idx)>,
|
||||
idxs: BiMap<usize, (Direction, RangeSet<usize>)>,
|
||||
/// Union of all transcript indices in the sent direction.
|
||||
sent_idx: Idx,
|
||||
sent_idx: RangeSet<usize>,
|
||||
/// Union of all transcript indices in the received direction.
|
||||
received_idx: Idx,
|
||||
received_idx: RangeSet<usize>,
|
||||
}
|
||||
|
||||
opaque_debug::implement!(EncodingTree);
|
||||
@@ -68,15 +69,15 @@ impl EncodingTree {
|
||||
/// * `provider` - The encoding provider.
|
||||
pub fn new<'idx>(
|
||||
hasher: &dyn HashAlgorithm,
|
||||
idxs: impl IntoIterator<Item = &'idx (Direction, Idx)>,
|
||||
idxs: impl IntoIterator<Item = &'idx (Direction, RangeSet<usize>)>,
|
||||
provider: &dyn EncodingProvider,
|
||||
) -> Result<Self, EncodingTreeError> {
|
||||
let mut this = Self {
|
||||
tree: MerkleTree::new(hasher.id()),
|
||||
blinders: Vec::new(),
|
||||
idxs: BiMap::new(),
|
||||
sent_idx: Idx::empty(),
|
||||
received_idx: Idx::empty(),
|
||||
sent_idx: RangeSet::default(),
|
||||
received_idx: RangeSet::default(),
|
||||
};
|
||||
|
||||
let mut leaves = Vec::new();
|
||||
@@ -138,7 +139,7 @@ impl EncodingTree {
|
||||
/// * `idxs` - The transcript indices to prove.
|
||||
pub fn proof<'idx>(
|
||||
&self,
|
||||
idxs: impl Iterator<Item = &'idx (Direction, Idx)>,
|
||||
idxs: impl Iterator<Item = &'idx (Direction, RangeSet<usize>)>,
|
||||
) -> Result<EncodingProof, EncodingTreeError> {
|
||||
let mut openings = HashMap::new();
|
||||
for dir_idx in idxs {
|
||||
@@ -171,11 +172,11 @@ impl EncodingTree {
|
||||
}
|
||||
|
||||
/// Returns whether the tree contains the given transcript index.
|
||||
pub fn contains(&self, idx: &(Direction, Idx)) -> bool {
|
||||
pub fn contains(&self, idx: &(Direction, RangeSet<usize>)) -> bool {
|
||||
self.idxs.contains_right(idx)
|
||||
}
|
||||
|
||||
pub(crate) fn idx(&self, direction: Direction) -> &Idx {
|
||||
pub(crate) fn idx(&self, direction: Direction) -> &RangeSet<usize> {
|
||||
match direction {
|
||||
Direction::Sent => &self.sent_idx,
|
||||
Direction::Received => &self.received_idx,
|
||||
@@ -183,7 +184,7 @@ impl EncodingTree {
|
||||
}
|
||||
|
||||
/// Returns the committed transcript indices.
|
||||
pub(crate) fn transcript_indices(&self) -> impl Iterator<Item = &(Direction, Idx)> {
|
||||
pub(crate) fn transcript_indices(&self) -> impl Iterator<Item = &(Direction, RangeSet<usize>)> {
|
||||
self.idxs.right_values()
|
||||
}
|
||||
}
|
||||
@@ -200,7 +201,7 @@ mod tests {
|
||||
|
||||
fn new_tree<'seq>(
|
||||
transcript: &Transcript,
|
||||
idxs: impl Iterator<Item = &'seq (Direction, Idx)>,
|
||||
idxs: impl Iterator<Item = &'seq (Direction, RangeSet<usize>)>,
|
||||
) -> Result<EncodingTree, EncodingTreeError> {
|
||||
let provider = encoding_provider(transcript.sent(), transcript.received());
|
||||
|
||||
@@ -211,8 +212,8 @@ mod tests {
|
||||
fn test_encoding_tree() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..OK_JSON.len()));
|
||||
|
||||
let tree = new_tree(&transcript, [&idx_0, &idx_1].into_iter()).unwrap();
|
||||
|
||||
@@ -243,10 +244,10 @@ mod tests {
|
||||
fn test_encoding_tree_multiple_ranges() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..1));
|
||||
let idx_1 = (Direction::Sent, Idx::new(1..POST_JSON.len()));
|
||||
let idx_2 = (Direction::Received, Idx::new(0..1));
|
||||
let idx_3 = (Direction::Received, Idx::new(1..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..1));
|
||||
let idx_1 = (Direction::Sent, RangeSet::from(1..POST_JSON.len()));
|
||||
let idx_2 = (Direction::Received, RangeSet::from(0..1));
|
||||
let idx_3 = (Direction::Received, RangeSet::from(1..OK_JSON.len()));
|
||||
|
||||
let tree = new_tree(&transcript, [&idx_0, &idx_1, &idx_2, &idx_3].into_iter()).unwrap();
|
||||
|
||||
@@ -273,11 +274,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut expected_auth_sent = Idx::default();
|
||||
let mut expected_auth_sent = RangeSet::default();
|
||||
expected_auth_sent.union_mut(&idx_0.1);
|
||||
expected_auth_sent.union_mut(&idx_1.1);
|
||||
|
||||
let mut expected_auth_recv = Idx::default();
|
||||
let mut expected_auth_recv = RangeSet::default();
|
||||
expected_auth_recv.union_mut(&idx_2.1);
|
||||
expected_auth_recv.union_mut(&idx_3.1);
|
||||
|
||||
@@ -289,9 +290,9 @@ mod tests {
|
||||
fn test_encoding_tree_proof_missing_leaf() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..4));
|
||||
let idx_2 = (Direction::Received, Idx::new(4..OK_JSON.len()));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len()));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..4));
|
||||
let idx_2 = (Direction::Received, RangeSet::from(4..OK_JSON.len()));
|
||||
|
||||
let tree = new_tree(&transcript, [&idx_0, &idx_1].into_iter()).unwrap();
|
||||
|
||||
@@ -305,8 +306,8 @@ mod tests {
|
||||
fn test_encoding_tree_out_of_bounds() {
|
||||
let transcript = Transcript::new(POST_JSON, OK_JSON);
|
||||
|
||||
let idx_0 = (Direction::Sent, Idx::new(0..POST_JSON.len() + 1));
|
||||
let idx_1 = (Direction::Received, Idx::new(0..OK_JSON.len() + 1));
|
||||
let idx_0 = (Direction::Sent, RangeSet::from(0..POST_JSON.len() + 1));
|
||||
let idx_1 = (Direction::Received, RangeSet::from(0..OK_JSON.len() + 1));
|
||||
|
||||
let result = new_tree(&transcript, [&idx_0].into_iter()).unwrap_err();
|
||||
assert!(matches!(result, EncodingTreeError::MissingEncoding { .. }));
|
||||
@@ -321,7 +322,7 @@ mod tests {
|
||||
|
||||
let result = EncodingTree::new(
|
||||
&Blake3::default(),
|
||||
[(Direction::Sent, Idx::new(0..8))].iter(),
|
||||
[(Direction::Sent, RangeSet::from(0..8))].iter(),
|
||||
&provider,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
hash::{Blinder, HashAlgId, HashAlgorithm, TypedHash},
|
||||
transcript::{Direction, Idx},
|
||||
transcript::{Direction, RangeSet},
|
||||
};
|
||||
|
||||
/// Hashes plaintext with a blinder.
|
||||
@@ -23,7 +23,7 @@ pub struct PlaintextHash {
|
||||
/// Direction of the plaintext.
|
||||
pub direction: Direction,
|
||||
/// Index of plaintext.
|
||||
pub idx: Idx,
|
||||
pub idx: RangeSet<usize>,
|
||||
/// The hash of the data.
|
||||
pub hash: TypedHash,
|
||||
}
|
||||
@@ -34,7 +34,7 @@ pub struct PlaintextHashSecret {
|
||||
/// Direction of the plaintext.
|
||||
pub direction: Direction,
|
||||
/// Index of plaintext.
|
||||
pub idx: Idx,
|
||||
pub idx: RangeSet<usize>,
|
||||
/// The algorithm of the hash.
|
||||
pub alg: HashAlgId,
|
||||
/// Blinder for the hash.
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
//! Transcript proofs.
|
||||
|
||||
use rangeset::{Cover, ToRangeSet};
|
||||
use rangeset::{Cover, Difference, Subset, ToRangeSet, UnionMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, fmt};
|
||||
|
||||
use crate::{
|
||||
connection::TranscriptLength,
|
||||
display::FmtRangeSet,
|
||||
hash::{HashAlgId, HashProvider},
|
||||
transcript::{
|
||||
commit::{TranscriptCommitment, TranscriptCommitmentKind},
|
||||
encoding::{EncodingProof, EncodingProofError, EncodingTree},
|
||||
hash::{hash_plaintext, PlaintextHash, PlaintextHashSecret},
|
||||
Direction, Idx, PartialTranscript, Transcript, TranscriptSecret,
|
||||
Direction, PartialTranscript, RangeSet, Transcript, TranscriptSecret,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +22,9 @@ const DEFAULT_COMMITMENT_KINDS: &[TranscriptCommitmentKind] = &[
|
||||
TranscriptCommitmentKind::Hash {
|
||||
alg: HashAlgId::SHA256,
|
||||
},
|
||||
TranscriptCommitmentKind::Hash {
|
||||
alg: HashAlgId::BLAKE3,
|
||||
},
|
||||
TranscriptCommitmentKind::Encoding,
|
||||
];
|
||||
|
||||
@@ -77,8 +81,8 @@ impl TranscriptProof {
|
||||
));
|
||||
}
|
||||
|
||||
let mut total_auth_sent = Idx::default();
|
||||
let mut total_auth_recv = Idx::default();
|
||||
let mut total_auth_sent = RangeSet::default();
|
||||
let mut total_auth_recv = RangeSet::default();
|
||||
|
||||
// Verify encoding proof.
|
||||
if let Some(proof) = self.encoding_proof {
|
||||
@@ -120,7 +124,7 @@ impl TranscriptProof {
|
||||
Direction::Received => (self.transcript.received_unsafe(), &mut total_auth_recv),
|
||||
};
|
||||
|
||||
if idx.end() > plaintext.len() {
|
||||
if idx.end().unwrap_or(0) > plaintext.len() {
|
||||
return Err(TranscriptProofError::new(
|
||||
ErrorKind::Hash,
|
||||
"hash opening index is out of bounds",
|
||||
@@ -215,15 +219,15 @@ impl From<EncodingProofError> for TranscriptProofError {
|
||||
/// Union of ranges to reveal.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct QueryIdx {
|
||||
sent: Idx,
|
||||
recv: Idx,
|
||||
sent: RangeSet<usize>,
|
||||
recv: RangeSet<usize>,
|
||||
}
|
||||
|
||||
impl QueryIdx {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
sent: Idx::empty(),
|
||||
recv: Idx::empty(),
|
||||
sent: RangeSet::default(),
|
||||
recv: RangeSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +235,7 @@ impl QueryIdx {
|
||||
self.sent.is_empty() && self.recv.is_empty()
|
||||
}
|
||||
|
||||
fn union(&mut self, direction: &Direction, other: &Idx) {
|
||||
fn union(&mut self, direction: &Direction, other: &RangeSet<usize>) {
|
||||
match direction {
|
||||
Direction::Sent => self.sent.union_mut(other),
|
||||
Direction::Received => self.recv.union_mut(other),
|
||||
@@ -241,7 +245,12 @@ impl QueryIdx {
|
||||
|
||||
impl std::fmt::Display for QueryIdx {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "sent: {}, received: {}", self.sent, self.recv)
|
||||
write!(
|
||||
f,
|
||||
"sent: {}, received: {}",
|
||||
FmtRangeSet(&self.sent),
|
||||
FmtRangeSet(&self.recv)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +262,8 @@ pub struct TranscriptProofBuilder<'a> {
|
||||
transcript: &'a Transcript,
|
||||
encoding_tree: Option<&'a EncodingTree>,
|
||||
hash_secrets: Vec<&'a PlaintextHashSecret>,
|
||||
committed_sent: Idx,
|
||||
committed_recv: Idx,
|
||||
committed_sent: RangeSet<usize>,
|
||||
committed_recv: RangeSet<usize>,
|
||||
query_idx: QueryIdx,
|
||||
}
|
||||
|
||||
@@ -264,8 +273,8 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
transcript: &'a Transcript,
|
||||
secrets: impl IntoIterator<Item = &'a TranscriptSecret>,
|
||||
) -> Self {
|
||||
let mut committed_sent = Idx::empty();
|
||||
let mut committed_recv = Idx::empty();
|
||||
let mut committed_sent = RangeSet::default();
|
||||
let mut committed_recv = RangeSet::default();
|
||||
|
||||
let mut encoding_tree = None;
|
||||
let mut hash_secrets = Vec::new();
|
||||
@@ -323,15 +332,15 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
ranges: &dyn ToRangeSet<usize>,
|
||||
direction: Direction,
|
||||
) -> Result<&mut Self, TranscriptProofBuilderError> {
|
||||
let idx = Idx::new(ranges.to_range_set());
|
||||
let idx = ranges.to_range_set();
|
||||
|
||||
if idx.end() > self.transcript.len_of_direction(direction) {
|
||||
if idx.end().unwrap_or(0) > self.transcript.len_of_direction(direction) {
|
||||
return Err(TranscriptProofBuilderError::new(
|
||||
BuilderErrorKind::Index,
|
||||
format!(
|
||||
"range is out of bounds of the transcript ({}): {} > {}",
|
||||
direction,
|
||||
idx.end(),
|
||||
idx.end().unwrap_or(0),
|
||||
self.transcript.len_of_direction(direction)
|
||||
),
|
||||
));
|
||||
@@ -348,7 +357,10 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
let missing = idx.difference(committed);
|
||||
return Err(TranscriptProofBuilderError::new(
|
||||
BuilderErrorKind::MissingCommitment,
|
||||
format!("commitment is missing for ranges in {direction} transcript: {missing}"),
|
||||
format!(
|
||||
"commitment is missing for ranges in {direction} transcript: {}",
|
||||
FmtRangeSet(&missing)
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(self)
|
||||
@@ -403,25 +415,23 @@ impl<'a> TranscriptProofBuilder<'a> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (sent_dir_idxs, sent_uncovered) =
|
||||
uncovered_query_idx.sent.as_range_set().cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Sent),
|
||||
|(_, idx)| &idx.0,
|
||||
);
|
||||
let (sent_dir_idxs, sent_uncovered) = uncovered_query_idx.sent.cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Sent),
|
||||
|(_, idx)| idx,
|
||||
);
|
||||
// Uncovered ranges will be checked with ranges of the next
|
||||
// preferred commitment kind.
|
||||
uncovered_query_idx.sent = Idx(sent_uncovered);
|
||||
uncovered_query_idx.sent = sent_uncovered;
|
||||
|
||||
let (recv_dir_idxs, recv_uncovered) =
|
||||
uncovered_query_idx.recv.as_range_set().cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Received),
|
||||
|(_, idx)| &idx.0,
|
||||
);
|
||||
uncovered_query_idx.recv = Idx(recv_uncovered);
|
||||
let (recv_dir_idxs, recv_uncovered) = uncovered_query_idx.recv.cover_by(
|
||||
encoding_tree
|
||||
.transcript_indices()
|
||||
.filter(|(dir, _)| *dir == Direction::Received),
|
||||
|(_, idx)| idx,
|
||||
);
|
||||
uncovered_query_idx.recv = recv_uncovered;
|
||||
|
||||
let dir_idxs = sent_dir_idxs
|
||||
.into_iter()
|
||||
@@ -439,25 +449,23 @@ 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,
|
||||
);
|
||||
let (sent_hashes, sent_uncovered) = uncovered_query_idx.sent.cover_by(
|
||||
self.hash_secrets.iter().filter(|hash| {
|
||||
hash.direction == Direction::Sent && &hash.alg == alg
|
||||
}),
|
||||
|hash| &hash.idx,
|
||||
);
|
||||
// Uncovered ranges will be checked with ranges of the next
|
||||
// preferred commitment kind.
|
||||
uncovered_query_idx.sent = Idx(sent_uncovered);
|
||||
uncovered_query_idx.sent = 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);
|
||||
let (recv_hashes, recv_uncovered) = uncovered_query_idx.recv.cover_by(
|
||||
self.hash_secrets.iter().filter(|hash| {
|
||||
hash.direction == Direction::Received && &hash.alg == alg
|
||||
}),
|
||||
|hash| &hash.idx,
|
||||
);
|
||||
uncovered_query_idx.recv = recv_uncovered;
|
||||
|
||||
transcript_proof.hash_secrets.extend(
|
||||
sent_hashes
|
||||
@@ -577,7 +585,7 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_verify_missing_encoding_commitment_root() {
|
||||
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
|
||||
let idxs = vec![(Direction::Received, Idx::new(0..transcript.len().1))];
|
||||
let idxs = vec![(Direction::Received, RangeSet::from(0..transcript.len().1))];
|
||||
let encoding_tree = EncodingTree::new(
|
||||
&Blake3::default(),
|
||||
&idxs,
|
||||
@@ -632,15 +640,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_reveal_with_hash_commitment() {
|
||||
#[case::sha256(HashAlgId::SHA256)]
|
||||
#[case::blake3(HashAlgId::BLAKE3)]
|
||||
fn test_reveal_with_hash_commitment(#[case] alg: HashAlgId) {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
let provider = HashProvider::default();
|
||||
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
|
||||
|
||||
let direction = Direction::Sent;
|
||||
let idx = Idx::new(0..10);
|
||||
let idx = RangeSet::from(0..10);
|
||||
let blinder: Blinder = rng.random();
|
||||
let alg = HashAlgId::SHA256;
|
||||
let hasher = provider.get(&alg).unwrap();
|
||||
|
||||
let commitment = PlaintextHash {
|
||||
@@ -678,15 +687,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_reveal_with_inconsistent_hash_commitment() {
|
||||
#[case::sha256(HashAlgId::SHA256)]
|
||||
#[case::blake3(HashAlgId::BLAKE3)]
|
||||
fn test_reveal_with_inconsistent_hash_commitment(#[case] alg: HashAlgId) {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
let provider = HashProvider::default();
|
||||
let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON);
|
||||
|
||||
let direction = Direction::Sent;
|
||||
let idx = Idx::new(0..10);
|
||||
let idx = RangeSet::from(0..10);
|
||||
let blinder: Blinder = rng.random();
|
||||
let alg = HashAlgId::SHA256;
|
||||
let hasher = provider.get(&alg).unwrap();
|
||||
|
||||
let commitment = PlaintextHash {
|
||||
@@ -894,10 +904,10 @@ mod tests {
|
||||
match kind {
|
||||
BuilderErrorKind::Cover { uncovered, .. } => {
|
||||
if !uncovered_sent_rangeset.is_empty() {
|
||||
assert_eq!(uncovered.sent, Idx(uncovered_sent_rangeset));
|
||||
assert_eq!(uncovered.sent, uncovered_sent_rangeset);
|
||||
}
|
||||
if !uncovered_recv_rangeset.is_empty() {
|
||||
assert_eq!(uncovered.recv, Idx(uncovered_recv_rangeset));
|
||||
assert_eq!(uncovered.recv, uncovered_recv_rangeset);
|
||||
}
|
||||
}
|
||||
_ => panic!("unexpected error kind: {kind:?}"),
|
||||
|
||||
@@ -18,13 +18,13 @@ spansy = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
dotenv = { version = "0.15.0" }
|
||||
futures = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
hyper = { workspace = true, features = ["client", "http1"] }
|
||||
hyper-util = { workspace = true, features = ["full"] }
|
||||
k256 = { workspace = true, features = ["ecdsa"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"rt",
|
||||
@@ -37,7 +37,25 @@ tokio = { workspace = true, features = [
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
noir = { git = "https://github.com/zkmopro/noir-rs", tag = "v1.0.0-beta.8", features = ["barretenberg"] }
|
||||
blake3 = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "interactive"
|
||||
path = "interactive/interactive.rs"
|
||||
|
||||
[[example]]
|
||||
name = "interactive_zk"
|
||||
path = "interactive_zk/interactive_zk.rs"
|
||||
|
||||
[[example]]
|
||||
name = "attestation_prove"
|
||||
path = "attestation/prove.rs"
|
||||
|
||||
[[example]]
|
||||
name = "attestation_present"
|
||||
path = "attestation/present.rs"
|
||||
|
||||
[[example]]
|
||||
name = "attestation_verify"
|
||||
path = "attestation/verify.rs"
|
||||
|
||||
@@ -5,4 +5,4 @@ This folder contains examples demonstrating how to use the TLSNotary protocol.
|
||||
* [Interactive](./interactive/README.md): Interactive Prover and Verifier session without a trusted notary.
|
||||
* [Attestation](./attestation/README.md): Performing a simple notarization with a trusted notary.
|
||||
|
||||
Refer to <https://docs.tlsnotary.org/quick_start/index.html> for a quick start guide to using TLSNotary with these examples.
|
||||
Refer to <https://tlsnotary.org/docs/quick_start> for a quick start guide to using TLSNotary with these examples.
|
||||
164
crates/examples/attestation/README.md
Normal file
164
crates/examples/attestation/README.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Attestation Example
|
||||
|
||||
|
||||
This example demonstrates a **TLSNotary attestation workflow**: notarizing data from a server with a trusted third party (Notary), then creating verifiable presentations with selective disclosure of sensitive information to a Verifier.
|
||||
|
||||
## 🔍 How It Works
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as Prover
|
||||
participant N as MPC-TLS<br/>Verifier
|
||||
participant S as Server<br/>Fixture
|
||||
participant V as Attestation<br/>Verifier
|
||||
|
||||
Note over P,S: 1. Notarization Phase
|
||||
P->>N: Establish MPC-TLS connection
|
||||
P->>S: Request (MPC-TLS)
|
||||
S->>P: Response (MPC-TLS)
|
||||
N->>P: Issue signed attestation
|
||||
|
||||
Note over P: 2. Presentation Phase
|
||||
P->>P: Create redacted presentation
|
||||
|
||||
Note over P,V: 3. Verification Phase
|
||||
P->>V: Share presentation
|
||||
V->>V: Verify attestation signature
|
||||
```
|
||||
|
||||
### The Three-Step Process
|
||||
|
||||
1. **🔐 Notarize**: Prover collaborates with Notary to create an authenticated TLS session and obtain a signed attestation
|
||||
2. **✂️ Present**: Prover creates a selective presentation, choosing which data to reveal or redact
|
||||
3. **✅ Verify**: Anyone can verify the presentation's authenticity using the Notary's public key
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Step 1: Notarize Data
|
||||
|
||||
**Start the test server** (from repository root):
|
||||
```bash
|
||||
RUST_LOG=info PORT=4000 cargo run --bin tlsn-server-fixture
|
||||
```
|
||||
|
||||
**Run the notarization** (in a new terminal):
|
||||
```bash
|
||||
RUST_LOG=info SERVER_PORT=4000 cargo run --release --example attestation_prove
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
Notarization completed successfully!
|
||||
The attestation has been written to `example-json.attestation.tlsn` and the corresponding secrets to `example-json.secrets.tlsn`.
|
||||
```
|
||||
|
||||
### Step 2: Create Verifiable Presentation
|
||||
|
||||
**Generate a redacted presentation:**
|
||||
```bash
|
||||
cargo run --release --example attestation_present
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
Presentation built successfully!
|
||||
The presentation has been written to `example-json.presentation.tlsn`.
|
||||
```
|
||||
|
||||
> 💡 **Tip**: You can create multiple presentations from the same attestation, each with different redactions!
|
||||
|
||||
### Step 3: Verify the Presentation
|
||||
|
||||
**Verify the presentation:**
|
||||
```bash
|
||||
cargo run --release --example attestation_verify
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
Verifying presentation with {key algorithm} key: { hex encoded key }
|
||||
|
||||
**Ask yourself, do you trust this key?**
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Successfully verified that the data below came from a session with test-server.io at { time }.
|
||||
Note that the data which the Prover chose not to disclose are shown as X.
|
||||
|
||||
Data sent:
|
||||
|
||||
GET /formats/json HTTP/1.1
|
||||
host: test-server.io
|
||||
accept: */*
|
||||
accept-encoding: identity
|
||||
connection: close
|
||||
user-agent: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
Data received:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
content-type: application/json
|
||||
content-length: 722
|
||||
connection: close
|
||||
date: Mon, 08 Sep 2025 09:18:29 GMT
|
||||
|
||||
XXXXXX1234567890XXXXXXXXXXXXXXXXXXXXXXXXJohn DoeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1.2XX
|
||||
```
|
||||
|
||||
## 🎯 Use Cases & Examples
|
||||
|
||||
### JSON Data (Default)
|
||||
Perfect for API responses, configuration data, or structured information:
|
||||
```bash
|
||||
# All three steps use JSON by default
|
||||
SERVER_PORT=4000 cargo run --release --example attestation_prove
|
||||
cargo run --release --example attestation_present
|
||||
cargo run --release --example attestation_verify
|
||||
```
|
||||
|
||||
### HTML Content
|
||||
Ideal for web pages, forms, or any HTML-based data:
|
||||
```bash
|
||||
# Notarize HTML content
|
||||
SERVER_PORT=4000 cargo run --release --example attestation_prove -- html
|
||||
cargo run --release --example attestation_present -- html
|
||||
cargo run --release --example attestation_verify -- html
|
||||
```
|
||||
|
||||
### Authenticated/Private Data
|
||||
For APIs requiring authentication tokens, cookies, or private access:
|
||||
```bash
|
||||
# Notarize private data with authentication
|
||||
SERVER_PORT=4000 cargo run --release --example attestation_prove -- authenticated
|
||||
cargo run --release --example attestation_present -- authenticated
|
||||
cargo run --release --example attestation_verify -- authenticated
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
For detailed logging and troubleshooting:
|
||||
```bash
|
||||
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example attestation_prove
|
||||
```
|
||||
|
||||
### Generated Files
|
||||
|
||||
After running the examples, you'll find:
|
||||
- **`*.attestation.tlsn`**: The cryptographically signed attestation from the Notary
|
||||
- **`*.secrets.tlsn`**: Cryptographic secrets needed to create presentations
|
||||
- **`*.presentation.tlsn`**: The verifiable presentation with your chosen redactions
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### Trust Model
|
||||
- ✅ **Notary Key**: The presentation includes the Notary's verifying key - The verifier must trust this key
|
||||
- ✅ **Data Authenticity**: Cryptographically guaranteed that data came from the specified server
|
||||
- ✅ **Tamper Evidence**: Any modification to the presentation will fail verification
|
||||
- ⚠️ **Notary Trust**: The verifier must trust the Notary not to collude with the Prover
|
||||
|
||||
### Production Deployment
|
||||
- 🏭 **Independent Notary**: Use a trusted third-party Notary service (not a local one)
|
||||
- 🔒 **Key Management**: Implement proper Notary key distribution and verification
|
||||
- 📋 **Audit Trail**: Maintain logs of notarization and verification events
|
||||
- 🔄 **Key Rotation**: Plan for Notary key updates and migration
|
||||
|
||||
> ⚠️ **Demo Notice**: This example uses a local test server and local Notary for demonstration. In production, use trusted third-party Notary services and real server endpoints.
|
||||
117
crates/examples/attestation/present.rs
Normal file
117
crates/examples/attestation/present.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
// This example demonstrates how to build a verifiable presentation from an
|
||||
// attestation and the corresponding connection secrets. See the `prove.rs`
|
||||
// example to learn how to acquire an attestation from a Notary.
|
||||
|
||||
use clap::Parser;
|
||||
use hyper::header;
|
||||
|
||||
use tlsn::attestation::{presentation::Presentation, Attestation, CryptoProvider, Secrets};
|
||||
use tlsn_examples::ExampleType;
|
||||
use tlsn_formats::http::HttpTranscript;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// What data to notarize
|
||||
#[clap(default_value_t, value_enum)]
|
||||
example_type: ExampleType,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
|
||||
create_presentation(&args.example_type).await
|
||||
}
|
||||
|
||||
async fn create_presentation(example_type: &ExampleType) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let attestation_path = tlsn_examples::get_file_path(example_type, "attestation");
|
||||
let secrets_path = tlsn_examples::get_file_path(example_type, "secrets");
|
||||
|
||||
// Read attestation from disk.
|
||||
let attestation: Attestation = bincode::deserialize(&std::fs::read(attestation_path)?)?;
|
||||
|
||||
// Read secrets from disk.
|
||||
let secrets: Secrets = bincode::deserialize(&std::fs::read(secrets_path)?)?;
|
||||
|
||||
// Parse the HTTP transcript.
|
||||
let transcript = HttpTranscript::parse(secrets.transcript())?;
|
||||
|
||||
// Build a transcript proof.
|
||||
let mut builder = secrets.transcript_proof_builder();
|
||||
|
||||
// Here is where we reveal all or some of the parts we committed in `prove.rs`
|
||||
// previously.
|
||||
let request = &transcript.requests[0];
|
||||
// Reveal the structure of the request without the headers or body.
|
||||
builder.reveal_sent(&request.without_data())?;
|
||||
// Reveal the request target.
|
||||
builder.reveal_sent(&request.request.target)?;
|
||||
// Reveal all request headers except the values of User-Agent and Authorization.
|
||||
for header in &request.headers {
|
||||
if !(header
|
||||
.name
|
||||
.as_str()
|
||||
.eq_ignore_ascii_case(header::USER_AGENT.as_str())
|
||||
|| header
|
||||
.name
|
||||
.as_str()
|
||||
.eq_ignore_ascii_case(header::AUTHORIZATION.as_str()))
|
||||
{
|
||||
builder.reveal_sent(header)?;
|
||||
} else {
|
||||
builder.reveal_sent(&header.without_value())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal only parts of the response.
|
||||
let response = &transcript.responses[0];
|
||||
// Reveal the structure of the response without the headers or body.
|
||||
builder.reveal_recv(&response.without_data())?;
|
||||
// Reveal all response headers.
|
||||
for header in &response.headers {
|
||||
builder.reveal_recv(header)?;
|
||||
}
|
||||
|
||||
let content = &response.body.as_ref().unwrap().content;
|
||||
match content {
|
||||
tlsn_formats::http::BodyContent::Json(json) => {
|
||||
// For experimentation, reveal the entire response or just a selection.
|
||||
let reveal_all = false;
|
||||
if reveal_all {
|
||||
builder.reveal_recv(response)?;
|
||||
} else {
|
||||
builder.reveal_recv(json.get("id").unwrap())?;
|
||||
builder.reveal_recv(json.get("information.name").unwrap())?;
|
||||
builder.reveal_recv(json.get("meta.version").unwrap())?;
|
||||
}
|
||||
}
|
||||
tlsn_formats::http::BodyContent::Unknown(span) => {
|
||||
builder.reveal_recv(span)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let transcript_proof = builder.build()?;
|
||||
|
||||
// Use default crypto provider to build the presentation.
|
||||
let provider = CryptoProvider::default();
|
||||
|
||||
let mut builder = attestation.presentation_builder(&provider);
|
||||
|
||||
builder
|
||||
.identity_proof(secrets.identity_proof())
|
||||
.transcript_proof(transcript_proof);
|
||||
|
||||
let presentation: Presentation = builder.build()?;
|
||||
|
||||
let presentation_path = tlsn_examples::get_file_path(example_type, "presentation");
|
||||
|
||||
// Write the presentation to disk.
|
||||
std::fs::write(&presentation_path, bincode::serialize(&presentation)?)?;
|
||||
|
||||
println!("Presentation built successfully!");
|
||||
println!("The presentation has been written to `{presentation_path}`.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
396
crates/examples/attestation/prove.rs
Normal file
396
crates/examples/attestation/prove.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
// This example demonstrates how to use the Prover to acquire an attestation for
|
||||
// an HTTP request sent to a server fixture. The attestation and secrets are
|
||||
// saved to disk.
|
||||
|
||||
use std::env;
|
||||
|
||||
use clap::Parser;
|
||||
use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, Request, StatusCode};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use spansy::Spanned;
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
sync::oneshot::{self, Receiver, Sender},
|
||||
};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::info;
|
||||
|
||||
use tlsn::{
|
||||
attestation::{
|
||||
request::{Request as AttestationRequest, RequestConfig},
|
||||
signing::Secp256k1Signer,
|
||||
Attestation, AttestationConfig, CryptoProvider, Secrets,
|
||||
},
|
||||
config::{
|
||||
CertificateDer, PrivateKeyDer, ProtocolConfig, ProtocolConfigValidator, RootCertStore,
|
||||
},
|
||||
connection::{ConnectionInfo, HandshakeData, ServerName, TranscriptLength},
|
||||
prover::{state::Committed, ProveConfig, Prover, ProverConfig, ProverOutput, TlsConfig},
|
||||
transcript::{ContentType, TranscriptCommitConfig},
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tlsn_examples::ExampleType;
|
||||
use tlsn_formats::http::{DefaultHttpCommitter, HttpCommit, HttpTranscript};
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::{CA_CERT_DER, CLIENT_CERT_DER, CLIENT_KEY_DER, SERVER_DOMAIN};
|
||||
|
||||
// Setting of the application server.
|
||||
const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// What data to notarize.
|
||||
#[clap(default_value_t, value_enum)]
|
||||
example_type: ExampleType,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let args = Args::parse();
|
||||
let (uri, extra_headers) = match args.example_type {
|
||||
ExampleType::Json => ("/formats/json", vec![]),
|
||||
ExampleType::Html => ("/formats/html", vec![]),
|
||||
ExampleType::Authenticated => ("/protected", vec![("Authorization", "random_auth_token")]),
|
||||
};
|
||||
|
||||
let (notary_socket, prover_socket) = tokio::io::duplex(1 << 23);
|
||||
let (request_tx, request_rx) = oneshot::channel();
|
||||
let (attestation_tx, attestation_rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
notary(notary_socket, request_rx, attestation_tx)
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
prover(
|
||||
prover_socket,
|
||||
request_tx,
|
||||
attestation_rx,
|
||||
uri,
|
||||
extra_headers,
|
||||
&args.example_type,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prover<S: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: S,
|
||||
req_tx: Sender<AttestationRequest>,
|
||||
resp_rx: Receiver<Attestation>,
|
||||
uri: &str,
|
||||
extra_headers: Vec<(&str, &str)>,
|
||||
example_type: &ExampleType,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let server_host: String = env::var("SERVER_HOST").unwrap_or("127.0.0.1".into());
|
||||
let server_port: u16 = env::var("SERVER_PORT")
|
||||
.map(|port| port.parse().expect("port should be valid integer"))
|
||||
.unwrap_or(DEFAULT_FIXTURE_PORT);
|
||||
|
||||
// Create a root certificate store with the server-fixture's self-signed
|
||||
// certificate. This is only required for offline testing with the
|
||||
// server-fixture.
|
||||
let mut tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
// (Optional) Set up TLS client authentication if required by the server.
|
||||
.client_auth((
|
||||
vec![CertificateDer(CLIENT_CERT_DER.to_vec())],
|
||||
PrivateKeyDer(CLIENT_KEY_DER.to_vec()),
|
||||
));
|
||||
|
||||
let tls_config = tls_config_builder.build().unwrap();
|
||||
|
||||
// Set up protocol configuration for prover.
|
||||
let mut prover_config_builder = ProverConfig::builder();
|
||||
prover_config_builder
|
||||
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
// We must configure the amount of data we expect to exchange beforehand, which will
|
||||
// be preprocessed prior to the connection. Reducing these limits will improve
|
||||
// performance.
|
||||
.max_sent_data(tlsn_examples::MAX_SENT_DATA)
|
||||
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
|
||||
.build()?,
|
||||
);
|
||||
|
||||
let prover_config = prover_config_builder.build()?;
|
||||
|
||||
// Create a new prover and perform necessary setup.
|
||||
let prover = Prover::new(prover_config).setup(socket.compat()).await?;
|
||||
|
||||
// Open a TCP connection to the server.
|
||||
let client_socket = tokio::net::TcpStream::connect((server_host, server_port)).await?;
|
||||
|
||||
// Bind the prover to the server connection.
|
||||
// The returned `mpc_tls_connection` is an MPC TLS connection to the server: all
|
||||
// data written to/read from it will be encrypted/decrypted using MPC with
|
||||
// the notary.
|
||||
let (mpc_tls_connection, prover_fut) = prover.connect(client_socket.compat()).await?;
|
||||
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
|
||||
|
||||
// Spawn the prover task to be run concurrently in the background.
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// Attach the hyper HTTP client to the connection.
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
|
||||
|
||||
// Spawn the HTTP task to be run concurrently in the background.
|
||||
tokio::spawn(connection);
|
||||
|
||||
// Build a simple HTTP request with common headers.
|
||||
let request_builder = Request::builder()
|
||||
.uri(uri)
|
||||
.header("Host", SERVER_DOMAIN)
|
||||
.header("Accept", "*/*")
|
||||
// Using "identity" instructs the Server not to use compression for its HTTP response.
|
||||
// TLSNotary tooling does not support compression.
|
||||
.header("Accept-Encoding", "identity")
|
||||
.header("Connection", "close")
|
||||
.header("User-Agent", USER_AGENT);
|
||||
let mut request_builder = request_builder;
|
||||
for (key, value) in extra_headers {
|
||||
request_builder = request_builder.header(key, value);
|
||||
}
|
||||
let request = request_builder.body(Empty::<Bytes>::new())?;
|
||||
|
||||
info!("Starting an MPC TLS connection with the server");
|
||||
|
||||
// Send the request to the server and wait for the response.
|
||||
let response = request_sender.send_request(request).await?;
|
||||
|
||||
info!("Got a response from the server: {}", response.status());
|
||||
|
||||
assert!(response.status() == StatusCode::OK);
|
||||
|
||||
// The prover task should be done now, so we can await it.
|
||||
let mut prover = prover_task.await??;
|
||||
|
||||
// Parse the HTTP transcript.
|
||||
let transcript = HttpTranscript::parse(prover.transcript())?;
|
||||
|
||||
let body_content = &transcript.responses[0].body.as_ref().unwrap().content;
|
||||
let body = String::from_utf8_lossy(body_content.span().as_bytes());
|
||||
|
||||
match body_content {
|
||||
tlsn_formats::http::BodyContent::Json(_json) => {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(&body)?;
|
||||
info!("{}", serde_json::to_string_pretty(&parsed)?);
|
||||
}
|
||||
tlsn_formats::http::BodyContent::Unknown(_span) => {
|
||||
info!("{}", &body);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Commit to the transcript.
|
||||
let mut builder = TranscriptCommitConfig::builder(prover.transcript());
|
||||
|
||||
// This commits to various parts of the transcript separately (e.g. request
|
||||
// headers, response headers, response body and more). See https://docs.tlsnotary.org//protocol/commit_strategy.html
|
||||
// for other strategies that can be used to generate commitments.
|
||||
DefaultHttpCommitter::default().commit_transcript(&mut builder, &transcript)?;
|
||||
|
||||
let transcript_commit = builder.build()?;
|
||||
|
||||
// Build an attestation request.
|
||||
let mut builder = RequestConfig::builder();
|
||||
|
||||
builder.transcript_commit(transcript_commit);
|
||||
|
||||
// Optionally, add an extension to the attestation if the notary supports it.
|
||||
// builder.extension(Extension {
|
||||
// id: b"example.name".to_vec(),
|
||||
// value: b"Bobert".to_vec(),
|
||||
// });
|
||||
|
||||
let request_config = builder.build()?;
|
||||
|
||||
let (attestation, secrets) = notarize(&mut prover, &request_config, req_tx, resp_rx).await?;
|
||||
|
||||
// Write the attestation to disk.
|
||||
let attestation_path = tlsn_examples::get_file_path(example_type, "attestation");
|
||||
let secrets_path = tlsn_examples::get_file_path(example_type, "secrets");
|
||||
|
||||
tokio::fs::write(&attestation_path, bincode::serialize(&attestation)?).await?;
|
||||
|
||||
// Write the secrets to disk.
|
||||
tokio::fs::write(&secrets_path, bincode::serialize(&secrets)?).await?;
|
||||
|
||||
println!("Notarization completed successfully!");
|
||||
println!(
|
||||
"The attestation has been written to `{attestation_path}` and the \
|
||||
corresponding secrets to `{secrets_path}`."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notarize(
|
||||
prover: &mut Prover<Committed>,
|
||||
config: &RequestConfig,
|
||||
request_tx: Sender<AttestationRequest>,
|
||||
attestation_rx: Receiver<Attestation>,
|
||||
) -> Result<(Attestation, Secrets), Box<dyn std::error::Error>> {
|
||||
let mut builder = ProveConfig::builder(prover.transcript());
|
||||
|
||||
if let Some(config) = config.transcript_commit() {
|
||||
builder.transcript_commit(config.clone());
|
||||
}
|
||||
|
||||
let disclosure_config = builder.build()?;
|
||||
|
||||
let ProverOutput {
|
||||
transcript_commitments,
|
||||
transcript_secrets,
|
||||
..
|
||||
} = prover.prove(&disclosure_config).await?;
|
||||
|
||||
// Build an attestation request.
|
||||
let mut builder = AttestationRequest::builder(config);
|
||||
|
||||
builder
|
||||
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
|
||||
.handshake_data(HandshakeData {
|
||||
certs: prover
|
||||
.tls_transcript()
|
||||
.server_cert_chain()
|
||||
.expect("server cert chain is present")
|
||||
.to_vec(),
|
||||
sig: prover
|
||||
.tls_transcript()
|
||||
.server_signature()
|
||||
.expect("server signature is present")
|
||||
.clone(),
|
||||
binding: prover.tls_transcript().certificate_binding().clone(),
|
||||
})
|
||||
.transcript(prover.transcript().clone())
|
||||
.transcript_commitments(transcript_secrets, transcript_commitments);
|
||||
|
||||
let (request, secrets) = builder.build(&CryptoProvider::default())?;
|
||||
|
||||
// Send attestation request to notary.
|
||||
request_tx
|
||||
.send(request.clone())
|
||||
.map_err(|_| "notary is not receiving attestation request".to_string())?;
|
||||
|
||||
// Receive attestation from notary.
|
||||
let attestation = attestation_rx
|
||||
.await
|
||||
.map_err(|err| format!("notary did not respond with attestation: {err}"))?;
|
||||
|
||||
// Check the attestation is consistent with the Prover's view.
|
||||
request.validate(&attestation)?;
|
||||
|
||||
Ok((attestation, secrets))
|
||||
}
|
||||
|
||||
async fn notary<S: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: S,
|
||||
request_rx: Receiver<AttestationRequest>,
|
||||
attestation_tx: Sender<Attestation>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set up Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(tlsn_examples::MAX_SENT_DATA)
|
||||
.max_recv_data(tlsn_examples::MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Create a root certificate store with the server-fixture's self-signed
|
||||
// certificate. This is only required for offline testing with the
|
||||
// server-fixture.
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut verifier = Verifier::new(verifier_config)
|
||||
.setup(socket.compat())
|
||||
.await?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
let VerifierOutput {
|
||||
transcript_commitments,
|
||||
..
|
||||
} = verifier.verify(&VerifyConfig::default()).await?;
|
||||
|
||||
let tls_transcript = verifier.tls_transcript().clone();
|
||||
|
||||
verifier.close().await?;
|
||||
|
||||
let sent_len = tls_transcript
|
||||
.sent()
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let ContentType::ApplicationData = record.typ {
|
||||
Some(record.ciphertext.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum::<usize>();
|
||||
|
||||
let recv_len = tls_transcript
|
||||
.recv()
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let ContentType::ApplicationData = record.typ {
|
||||
Some(record.ciphertext.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum::<usize>();
|
||||
|
||||
// Receive attestation request from prover.
|
||||
let request = request_rx.await?;
|
||||
|
||||
// Load a dummy signing key.
|
||||
let signing_key = k256::ecdsa::SigningKey::from_bytes(&[1u8; 32].into())?;
|
||||
let signer = Box::new(Secp256k1Signer::new(&signing_key.to_bytes())?);
|
||||
let mut provider = CryptoProvider::default();
|
||||
provider.signer.set_signer(signer);
|
||||
|
||||
// Build an attestation.
|
||||
let mut att_config_builder = AttestationConfig::builder();
|
||||
att_config_builder.supported_signature_algs(Vec::from_iter(provider.signer.supported_algs()));
|
||||
let att_config = att_config_builder.build()?;
|
||||
|
||||
let mut builder = Attestation::builder(&att_config).accept_request(request)?;
|
||||
builder
|
||||
.connection_info(ConnectionInfo {
|
||||
time: tls_transcript.time(),
|
||||
version: (*tls_transcript.version()),
|
||||
transcript_length: TranscriptLength {
|
||||
sent: sent_len as u32,
|
||||
received: recv_len as u32,
|
||||
},
|
||||
})
|
||||
.server_ephemeral_key(tls_transcript.server_ephemeral_key().clone())
|
||||
.transcript_commitments(transcript_commitments);
|
||||
|
||||
let attestation = builder.build(&provider)?;
|
||||
|
||||
// Send attestation to prover.
|
||||
attestation_tx
|
||||
.send(attestation)
|
||||
.map_err(|_| "prover is not receiving attestation".to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
96
crates/examples/attestation/verify.rs
Normal file
96
crates/examples/attestation/verify.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
// This example demonstrates how to verify a presentation. See `present.rs` for
|
||||
// an example of how to build a presentation from an attestation and connection
|
||||
// secrets.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use tlsn::{
|
||||
attestation::{
|
||||
presentation::{Presentation, PresentationOutput},
|
||||
signing::VerifyingKey,
|
||||
CryptoProvider,
|
||||
},
|
||||
config::{CertificateDer, RootCertStore},
|
||||
verifier::ServerCertVerifier,
|
||||
};
|
||||
use tlsn_examples::ExampleType;
|
||||
use tlsn_server_fixture_certs::CA_CERT_DER;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// What data to notarize.
|
||||
#[clap(default_value_t, value_enum)]
|
||||
example_type: ExampleType,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
|
||||
verify_presentation(&args.example_type).await
|
||||
}
|
||||
|
||||
async fn verify_presentation(example_type: &ExampleType) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Read the presentation from disk.
|
||||
let presentation_path = tlsn_examples::get_file_path(example_type, "presentation");
|
||||
|
||||
let presentation: Presentation = bincode::deserialize(&std::fs::read(presentation_path)?)?;
|
||||
|
||||
// Create a crypto provider accepting the server-fixture's self-signed
|
||||
// root certificate.
|
||||
//
|
||||
// This is only required for offline testing with the server-fixture. In
|
||||
// production, use `CryptoProvider::default()` instead.
|
||||
let root_cert_store = RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
};
|
||||
let crypto_provider = CryptoProvider {
|
||||
cert: ServerCertVerifier::new(&root_cert_store)?,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let VerifyingKey {
|
||||
alg,
|
||||
data: key_data,
|
||||
} = presentation.verifying_key();
|
||||
|
||||
println!(
|
||||
"Verifying presentation with {alg} key: {}\n\n**Ask yourself, do you trust this key?**\n",
|
||||
hex::encode(key_data)
|
||||
);
|
||||
|
||||
// Verify the presentation.
|
||||
let PresentationOutput {
|
||||
server_name,
|
||||
connection_info,
|
||||
transcript,
|
||||
// extensions, // Optionally, verify any custom extensions from prover/notary.
|
||||
..
|
||||
} = presentation.verify(&crypto_provider).unwrap();
|
||||
|
||||
// The time at which the connection was started.
|
||||
let time = chrono::DateTime::UNIX_EPOCH + Duration::from_secs(connection_info.time);
|
||||
let server_name = server_name.unwrap();
|
||||
let mut partial_transcript = transcript.unwrap();
|
||||
// Set the unauthenticated bytes so they are distinguishable.
|
||||
partial_transcript.set_unauthed(b'X');
|
||||
|
||||
let sent = String::from_utf8_lossy(partial_transcript.sent_unsafe());
|
||||
let recv = String::from_utf8_lossy(partial_transcript.received_unsafe());
|
||||
|
||||
println!("-------------------------------------------------------------------");
|
||||
println!(
|
||||
"Successfully verified that the data below came from a session with {server_name} at {time}.",
|
||||
);
|
||||
println!("Note that the data which the Prover chose not to disclose are shown as X.\n");
|
||||
println!("Data sent:\n");
|
||||
println!("{sent}\n");
|
||||
println!("Data received:\n");
|
||||
println!("{recv}\n");
|
||||
println!("-------------------------------------------------------------------");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,7 +10,6 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{CertificateDer, ProtocolConfig, ProtocolConfigValidator, RootCertStore},
|
||||
connection::ServerName,
|
||||
@@ -19,7 +18,7 @@ use tlsn::{
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
use tlsn_server_fixture_certs::{CA_CERT_DER, SERVER_DOMAIN};
|
||||
|
||||
const SECRET: &str = "TLSNotary's private key 🤡";
|
||||
|
||||
|
||||
5
crates/examples/interactive_zk/.gitignore
vendored
Normal file
5
crates/examples/interactive_zk/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
!noir/target/
|
||||
# Ignore everything inside noir/target
|
||||
noir/target/*
|
||||
# Except noir.json
|
||||
!noir/target/noir.json
|
||||
167
crates/examples/interactive_zk/README.md
Normal file
167
crates/examples/interactive_zk/README.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Interactive Zero-Knowledge Age Verification with TLSNotary
|
||||
|
||||
This example demonstrates **privacy-preserving age verification** using TLSNotary and zero-knowledge proofs. It allows a prover to demonstrate they are 18+ years old without revealing their actual birth date or any other personal information.
|
||||
|
||||
## 🔍 How It Works (simplified overview)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Tax Server<br/>(fixture)
|
||||
participant P as Prover
|
||||
participant V as Verifier
|
||||
|
||||
P->>S: Request tax data (with auth token) (MPC-TLS)
|
||||
S->>P: Tax data including `date_of_birth` (MPC-TLS)
|
||||
P->>V: Share transcript with redactions
|
||||
P->>V: Commit to blinded hash of birth date
|
||||
P->>P: Generate ZK proof of age ≥ 18
|
||||
P->>V: Send ZK proof
|
||||
V->>V: Verify transcript & ZK proof
|
||||
V->>V: ✅ Confirm: Prover is 18+ (no birth date revealed)
|
||||
```
|
||||
|
||||
### The Process
|
||||
|
||||
1. **MPC-TLS Session**: The Prover fetches tax information containing their birth date, while the Verifier jointly verifies the TLS session to ensure the data comes from the authentic server.
|
||||
2. **Selective Disclosure**:
|
||||
* The authorization token is **redacted**: the Verifier sees the plaintext request but not the token.
|
||||
* The birth date is **committed** as a blinded hash: the Verifier cannot see the date, but the Prover is cryptographically bound to it.
|
||||
(Depending on the use case more data can be redacted or revealed)
|
||||
3. **Zero-Knowledge Proof**: The Prover generates a ZK proof that the committed birth date corresponds to an age ≥ 18.
|
||||
4. **Verification**: The Verifier checks both the TLS transcript and the ZK proof, confirming age ≥ 18 without learning the actual date of birth.
|
||||
|
||||
|
||||
### Example Data
|
||||
|
||||
The tax server returns data like this:
|
||||
```json
|
||||
{
|
||||
"tax_year": 2024,
|
||||
"taxpayer": {
|
||||
"idnr": "12345678901",
|
||||
"first_name": "Max",
|
||||
"last_name": "Mustermann",
|
||||
"date_of_birth": "1985-03-12",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 Zero-Knowledge Proof Details
|
||||
|
||||
The ZK circuit proves: **"I know a birth date that hashes to the committed value AND indicates I am 18+ years old"**
|
||||
|
||||
**Public Inputs:**
|
||||
- ✅ Verification date
|
||||
- ✅ Committed blinded hash of birth date
|
||||
|
||||
**Private Inputs (Hidden):**
|
||||
- 🔒 Actual birth date plaintext
|
||||
- 🔒 Random blinder used in hash commitment
|
||||
|
||||
**What the Verifier Learns:**
|
||||
- ✅ The prover is 18+ years old
|
||||
- ✅ The birth date is authentic (from the MPC-TLS session)
|
||||
|
||||
Everything else remains private.
|
||||
|
||||
## 🏃 Run the Example
|
||||
|
||||
1. **Start the test server** (from repository root):
|
||||
```bash
|
||||
RUST_LOG=info PORT=4000 cargo run --bin tlsn-server-fixture
|
||||
```
|
||||
|
||||
2. **Run the age verification** (in a new terminal):
|
||||
```bash
|
||||
SERVER_PORT=4000 cargo run --release --example interactive_zk
|
||||
```
|
||||
|
||||
3. **For detailed logs**:
|
||||
```bash
|
||||
RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example interactive_zk
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
Successfully verified https://test-server.io:4000/elster
|
||||
Age verified in ZK: 18+ ✅
|
||||
|
||||
Verified sent data:
|
||||
GET https://test-server.io:4000/elster HTTP/1.1
|
||||
host: test-server.io
|
||||
connection: close
|
||||
authorization: 🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈
|
||||
|
||||
Verified received data:
|
||||
🙈🙈🙈🙈🙈🙈🙈🙈[truncated for brevity]...🙈🙈🙈🙈🙈"tax_year":2024🙈🙈🙈🙈🙈...
|
||||
```
|
||||
|
||||
> 💡 **Note**: In this demo, both Prover and Verifier run on the same machine. In production, they would operate on separate systems.
|
||||
> 💡 **Note**: This demo assumes that the tax server serves correct data, and that only the submitter of the tax data has access to the specified page.
|
||||
|
||||
## 🛠 Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
interactive_zk/
|
||||
├── prover.rs # Prover implementation
|
||||
├── verifier.rs # Verifier implementation
|
||||
├── types.rs # Shared types
|
||||
└── interactive_zk.rs # Main example runner
|
||||
├── noir/ # Zero-knowledge circuit
|
||||
│ ├── src/main.n # Noir circuit code
|
||||
│ ├── target/ # Compiled circuit artifacts
|
||||
│ └── Nargo.toml # Noir project config
|
||||
│ └── Prover.toml # Example input for `nargo execute`
|
||||
│ └── generate_test_data.rs # Rust script to generate Noir test data
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Noir Circuit Commands
|
||||
|
||||
We use [Mopro's `noir_rs`](https://zkmopro.org/docs/crates/noir-rs/) for ZK proof generation. The **circuit is pre-compiled and ready to use**. You don't need to install Noir tools to run the example. But if you want to change or test the circuit in isolation, you can use the following instructions.
|
||||
|
||||
Before you proceed, we recommend to double check that your Noir tooling matches the versions used in Mopro's `noir_rs`:
|
||||
```sh
|
||||
# Install correct Noir and BB versions (important for compatibility!)
|
||||
noirup --version 1.0.0-beta.8
|
||||
bbup -v 1.0.0-nightly.20250723
|
||||
```
|
||||
|
||||
If you don't have `noirup` and `bbup` installed yet, check [Noir's Quick Start](https://noir-lang.org/docs/getting_started/quick_start).
|
||||
|
||||
To compile the circuit, go to the `noir` folder and run `nargo compile`.
|
||||
|
||||
To check and experiment with the Noir circuit, you can use these commands:
|
||||
|
||||
* Execute Circuit: Compile the circuit and run it with sample data from `Prover.toml`:
|
||||
```sh
|
||||
nargo execute
|
||||
```
|
||||
* Generate Verification Key: Create the verification key needed to verify proofs
|
||||
```sh
|
||||
bb write_vk -b ./target/noir.json -o ./target
|
||||
```
|
||||
* Generate Proof: Create a zero-knowledge proof using the circuit and witness data.
|
||||
```sh
|
||||
bb prove --bytecode_path ./target/noir.json --witness_path ./target/noir.gz -o ./target
|
||||
```
|
||||
* Verify Proof: Verify that a proof is valid using the verification key.
|
||||
```sh
|
||||
bb verify -k ./target/vk -p ./target/proof
|
||||
```
|
||||
* Run the Noir tests:
|
||||
```sh
|
||||
nargo test --show-output
|
||||
```
|
||||
To create extra tests, you can use `./generate_test_data.rs` to help with generating correct blinders and hashes.
|
||||
|
||||
## 📚 Learn More
|
||||
|
||||
- [TLSNotary Documentation](https://docs.tlsnotary.org/)
|
||||
- [Noir Language Guide](https://noir-lang.org/)
|
||||
- [Zero-Knowledge Proofs Explained](https://ethereum.org/en/zero-knowledge-proofs/)
|
||||
- [Mopro ZK Toolkit](https://zkmopro.org/)
|
||||
59
crates/examples/interactive_zk/interactive_zk.rs
Normal file
59
crates/examples/interactive_zk/interactive_zk.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
mod prover;
|
||||
mod types;
|
||||
mod verifier;
|
||||
|
||||
use prover::prover;
|
||||
use std::{
|
||||
env,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
use verifier::verifier;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let server_host: String = env::var("SERVER_HOST").unwrap_or("127.0.0.1".into());
|
||||
let server_port: u16 = env::var("SERVER_PORT")
|
||||
.map(|port| port.parse().expect("port should be valid integer"))
|
||||
.unwrap_or(DEFAULT_FIXTURE_PORT);
|
||||
|
||||
// We use SERVER_DOMAIN here to make sure it matches the domain in the test
|
||||
// server's certificate.
|
||||
let uri = format!("https://{SERVER_DOMAIN}:{server_port}/elster");
|
||||
let server_ip: IpAddr = server_host
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid IP address '{}': {}", server_host, e))?;
|
||||
let server_addr = SocketAddr::from((server_ip, server_port));
|
||||
|
||||
// Connect prover and verifier.
|
||||
let (prover_socket, verifier_socket) = tokio::io::duplex(1 << 23);
|
||||
let (prover_extra_socket, verifier_extra_socket) = tokio::io::duplex(1 << 23);
|
||||
|
||||
let (_, transcript) = tokio::try_join!(
|
||||
prover(prover_socket, prover_extra_socket, &server_addr, &uri),
|
||||
verifier(verifier_socket, verifier_extra_socket)
|
||||
)?;
|
||||
|
||||
println!("---");
|
||||
println!("Successfully verified {}", &uri);
|
||||
println!("Age verified in ZK: 18+ ✅\n");
|
||||
|
||||
println!(
|
||||
"Verified sent data:\n{}",
|
||||
bytes_to_redacted_string(transcript.sent_unsafe())
|
||||
);
|
||||
println!(
|
||||
"Verified received data:\n{}",
|
||||
bytes_to_redacted_string(transcript.received_unsafe())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render redacted bytes as `🙈`.
|
||||
pub fn bytes_to_redacted_string(bytes: &[u8]) -> String {
|
||||
String::from_utf8_lossy(bytes).replace('\0', "🙈")
|
||||
}
|
||||
7
crates/examples/interactive_zk/noir/Nargo.toml
Normal file
7
crates/examples/interactive_zk/noir/Nargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "noir"
|
||||
type = "bin"
|
||||
authors = [""]
|
||||
|
||||
[dependencies]
|
||||
date = { tag = "v0.5.4", git = "https://github.com/madztheo/noir-date.git" }
|
||||
8
crates/examples/interactive_zk/noir/Prover.toml
Normal file
8
crates/examples/interactive_zk/noir/Prover.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
blinder = [108, 93, 120, 205, 15, 35, 159, 124, 243, 96, 22, 128, 16, 149, 219, 216]
|
||||
committed_hash = [186, 158, 101, 39, 49, 48, 26, 83, 242, 96, 10, 221, 121, 174, 62, 50, 136, 132, 232, 58, 25, 32, 66, 196, 99, 85, 66, 85, 255, 1, 202, 254]
|
||||
date_of_birth = "1985-03-12"
|
||||
|
||||
[proof_date]
|
||||
day = "29"
|
||||
month = "08"
|
||||
year = "2025"
|
||||
64
crates/examples/interactive_zk/noir/generate_test_data.rs
Executable file
64
crates/examples/interactive_zk/noir/generate_test_data.rs
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env -S cargo +nightly -Zscript
|
||||
---
|
||||
[package]
|
||||
name = "generate_test_data"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
blake3 = "1.5"
|
||||
rand = "0.8"
|
||||
chrono = "0.4"
|
||||
---
|
||||
use chrono::Datelike;
|
||||
use chrono::Local;
|
||||
use rand::RngCore;
|
||||
|
||||
fn main() {
|
||||
// 1. Birthdate string (fixed)
|
||||
let dob_str = "1985-03-12"; // 10 bytes long
|
||||
|
||||
let proof_date = Local::now().date_naive();
|
||||
let proof_year = proof_date.year();
|
||||
let proof_month = proof_date.month();
|
||||
let proof_day = proof_date.day();
|
||||
|
||||
// 2. Generate random 16-byte blinder
|
||||
let mut blinder = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut blinder);
|
||||
|
||||
// 3. Concatenate blinder + dob string bytes
|
||||
let mut preimage = Vec::with_capacity(26);
|
||||
preimage.extend_from_slice(dob_str.as_bytes());
|
||||
preimage.extend_from_slice(&blinder);
|
||||
|
||||
// 4. Hash it
|
||||
let hash = blake3::hash(&preimage);
|
||||
let hash = hash.as_bytes();
|
||||
|
||||
let blinder = blinder
|
||||
.iter()
|
||||
.map(|b| b.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let committed_hash = hash
|
||||
.iter()
|
||||
.map(|b| b.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
println!(
|
||||
"
|
||||
// Private input
|
||||
let date_of_birth = \"{dob_str}\";
|
||||
let blinder = [{blinder}];
|
||||
|
||||
// Public input
|
||||
let proof_date = date::Date {{ year: {proof_year}, month: {proof_month}, day: {proof_day} }};
|
||||
let committed_hash = [{committed_hash}];
|
||||
|
||||
main(proof_date, committed_hash, date_of_birth, blinder);
|
||||
"
|
||||
);
|
||||
}
|
||||
82
crates/examples/interactive_zk/noir/src/main.nr
Normal file
82
crates/examples/interactive_zk/noir/src/main.nr
Normal file
@@ -0,0 +1,82 @@
|
||||
use dep::date::Date;
|
||||
|
||||
fn main(
|
||||
// Public inputs
|
||||
proof_date: pub date::Date, // "2025-08-29"
|
||||
committed_hash: pub [u8; 32], // Hash of (blinder || dob string)
|
||||
// Private inputs
|
||||
date_of_birth: str<10>, // "1985-03-12"
|
||||
blinder: [u8; 16], // Random 16-byte blinder
|
||||
) {
|
||||
let is_18 = check_18(date_of_birth, proof_date);
|
||||
|
||||
let correct_hash = check_hash(date_of_birth, blinder, committed_hash);
|
||||
|
||||
assert(correct_hash);
|
||||
assert(is_18);
|
||||
}
|
||||
|
||||
fn check_18(date_of_birth: str<10>, proof_date: date::Date) -> bool {
|
||||
let dob = parse_birth_date(date_of_birth);
|
||||
let is_18 = dob.add_years(18).lt(proof_date);
|
||||
println(f"Is 18? {is_18}");
|
||||
is_18
|
||||
}
|
||||
|
||||
fn check_hash(date_of_birth: str<10>, blinder: [u8; 16], committed_hash: [u8; 32]) -> bool {
|
||||
let hash_input: [u8; 26] = make_hash_input(date_of_birth, blinder);
|
||||
let computed_hash = std::hash::blake3(hash_input);
|
||||
let correct_hash = computed_hash == committed_hash;
|
||||
println(f"Correct hash? {correct_hash}");
|
||||
correct_hash
|
||||
}
|
||||
|
||||
fn make_hash_input(dob: str<10>, blinder: [u8; 16]) -> [u8; 26] {
|
||||
let mut input: [u8; 26] = [0; 26];
|
||||
for i in 0..10 {
|
||||
input[i] = dob.as_bytes()[i];
|
||||
}
|
||||
for i in 0..16 {
|
||||
input[10 + i] = blinder[i];
|
||||
}
|
||||
input
|
||||
}
|
||||
|
||||
pub fn parse_birth_date(birth_date: str<10>) -> date::Date {
|
||||
let date: [u8; 10] = birth_date.as_bytes();
|
||||
let date_str: str<8> =
|
||||
[date[0], date[1], date[2], date[3], date[5], date[6], date[8], date[9]].as_str_unchecked();
|
||||
Date::from_str_long_year(date_str)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_is_over_18() {
|
||||
// Private input
|
||||
let date_of_birth = "1985-03-12";
|
||||
let blinder = [109, 224, 222, 179, 60, 44, 41, 65, 166, 94, 111, 216, 73, 231, 63, 83];
|
||||
|
||||
// Public input
|
||||
let proof_date = date::Date { year: 2025, month: 9, day: 26 };
|
||||
let committed_hash = [
|
||||
114, 34, 41, 235, 91, 156, 13, 57, 254, 112, 250, 35, 104, 217, 20, 182, 240, 170, 57, 39,
|
||||
187, 154, 14, 39, 91, 67, 50, 199, 149, 231, 78, 46,
|
||||
];
|
||||
|
||||
main(proof_date, committed_hash, date_of_birth, blinder);
|
||||
}
|
||||
|
||||
#[test(should_fail)]
|
||||
fn test_under_18() {
|
||||
// Private input
|
||||
let date_of_birth = "2010-08-01";
|
||||
let blinder = [160, 23, 57, 158, 141, 195, 155, 132, 109, 242, 48, 220, 70, 217, 229, 189];
|
||||
|
||||
// Public input
|
||||
let proof_date = date::Date { year: 2025, month: 8, day: 29 };
|
||||
let committed_hash = [
|
||||
16, 132, 194, 62, 232, 90, 157, 153, 4, 231, 1, 54, 226, 3, 87, 174, 129, 177, 80, 69, 37,
|
||||
222, 209, 91, 168, 156, 9, 109, 108, 144, 168, 109,
|
||||
];
|
||||
|
||||
main(proof_date, committed_hash, date_of_birth, blinder);
|
||||
}
|
||||
1
crates/examples/interactive_zk/noir/target/noir.json
Normal file
1
crates/examples/interactive_zk/noir/target/noir.json
Normal file
File diff suppressed because one or more lines are too long
379
crates/examples/interactive_zk/prover.rs
Normal file
379
crates/examples/interactive_zk/prover.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::types::received_commitments;
|
||||
|
||||
use super::types::ZKProofBundle;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, header, Request, StatusCode, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use k256::sha2::Digest;
|
||||
use noir::{
|
||||
barretenberg::{
|
||||
prove::prove_ultra_honk, srs::setup_srs_from_bytecode,
|
||||
verify::get_ultra_honk_verification_key,
|
||||
},
|
||||
witness::from_vec_str_to_witness_map,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use spansy::{
|
||||
http::{BodyContent, Requests, Responses},
|
||||
Spanned,
|
||||
};
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{CertificateDer, ProtocolConfig, RootCertStore},
|
||||
connection::ServerName,
|
||||
hash::HashAlgId,
|
||||
prover::{ProveConfig, ProveConfigBuilder, Prover, ProverConfig, TlsConfig},
|
||||
transcript::{
|
||||
hash::{PlaintextHash, PlaintextHashSecret},
|
||||
Direction, TranscriptCommitConfig, TranscriptCommitConfigBuilder, TranscriptCommitmentKind,
|
||||
TranscriptSecret,
|
||||
},
|
||||
};
|
||||
|
||||
use tlsn_examples::MAX_RECV_DATA;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use tlsn_examples::MAX_SENT_DATA;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(verifier_socket, verifier_extra_socket))]
|
||||
pub async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
verifier_socket: T,
|
||||
mut verifier_extra_socket: T,
|
||||
server_addr: &SocketAddr,
|
||||
uri: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let uri = uri.parse::<Uri>()?;
|
||||
|
||||
if uri.scheme().map(|s| s.as_str()) != Some("https") {
|
||||
return Err("URI must use HTTPS scheme".into());
|
||||
}
|
||||
|
||||
let server_domain = uri.authority().ok_or("URI must have authority")?.host();
|
||||
|
||||
// Create a root certificate store with the server-fixture's self-signed
|
||||
// certificate. This is only required for offline testing with the
|
||||
// server-fixture.
|
||||
let mut tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
});
|
||||
let tls_config = tls_config_builder.build()?;
|
||||
|
||||
// Set up protocol configuration for prover.
|
||||
let mut prover_config_builder = ProverConfig::builder();
|
||||
prover_config_builder
|
||||
.server_name(ServerName::Dns(server_domain.try_into()?))
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()?,
|
||||
);
|
||||
|
||||
let prover_config = prover_config_builder.build()?;
|
||||
|
||||
// Create prover and connect to verifier.
|
||||
//
|
||||
// Perform the setup phase with the verifier.
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_socket.compat())
|
||||
.await?;
|
||||
|
||||
// Connect to TLS Server.
|
||||
let tls_client_socket = tokio::net::TcpStream::connect(server_addr).await?;
|
||||
|
||||
// Pass server connection into the prover.
|
||||
let (mpc_tls_connection, prover_fut) = prover.connect(tls_client_socket.compat()).await?;
|
||||
|
||||
// Wrap the connection in a TokioIo compatibility layer to use it with hyper.
|
||||
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
|
||||
|
||||
// Spawn the Prover to run in the background.
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// MPC-TLS Handshake.
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
|
||||
|
||||
// Spawn the connection to run in the background.
|
||||
tokio::spawn(connection);
|
||||
|
||||
// MPC-TLS: Send Request and wait for Response.
|
||||
let request = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.header("Host", server_domain)
|
||||
.header("Connection", "close")
|
||||
.header(header::AUTHORIZATION, "Bearer random_auth_token")
|
||||
.method("GET")
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
|
||||
let response = request_sender.send_request(request).await?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(format!("MPC-TLS request failed with status {}", response.status()).into());
|
||||
}
|
||||
|
||||
// Create proof for the Verifier.
|
||||
let mut prover = prover_task.await??;
|
||||
|
||||
let transcript = prover.transcript().clone();
|
||||
let mut prove_config_builder = ProveConfig::builder(&transcript);
|
||||
|
||||
// Reveal the DNS name.
|
||||
prove_config_builder.server_identity();
|
||||
|
||||
let sent: &[u8] = transcript.sent();
|
||||
let received: &[u8] = transcript.received();
|
||||
let sent_len = sent.len();
|
||||
let recv_len = received.len();
|
||||
tracing::info!("Sent length: {}, Received length: {}", sent_len, recv_len);
|
||||
|
||||
// Reveal the entire HTTP request except for the authorization bearer token
|
||||
reveal_request(sent, &mut prove_config_builder)?;
|
||||
|
||||
// Create hash commitment for the date of birth field from the response
|
||||
let mut transcript_commitment_builder = TranscriptCommitConfig::builder(&transcript);
|
||||
transcript_commitment_builder.default_kind(TranscriptCommitmentKind::Hash {
|
||||
alg: HashAlgId::BLAKE3,
|
||||
});
|
||||
reveal_received(
|
||||
received,
|
||||
&mut prove_config_builder,
|
||||
&mut transcript_commitment_builder,
|
||||
)?;
|
||||
|
||||
let transcripts_commitment_config = transcript_commitment_builder.build()?;
|
||||
prove_config_builder.transcript_commit(transcripts_commitment_config);
|
||||
|
||||
let prove_config = prove_config_builder.build()?;
|
||||
|
||||
// MPC-TLS prove
|
||||
let prover_output = prover.prove(&prove_config).await?;
|
||||
prover.close().await?;
|
||||
|
||||
// Prove birthdate is more than 18 years ago.
|
||||
let received_commitments = received_commitments(&prover_output.transcript_commitments);
|
||||
let received_commitment = received_commitments
|
||||
.first()
|
||||
.ok_or("No received commitments found")?; // committed hash (of date of birth string)
|
||||
let received_secrets = received_secrets(&prover_output.transcript_secrets);
|
||||
let received_secret = received_secrets
|
||||
.first()
|
||||
.ok_or("No received secrets found")?; // hash blinder
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let proof_input = prepare_zk_proof_input(received, received_commitment, received_secret)?;
|
||||
let prepare_duration = start_time.elapsed();
|
||||
tracing::info!("🔢 prepare_zk_proof_input took: {:?}", prepare_duration);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let proof_bundle = generate_zk_proof(&proof_input)?;
|
||||
let generate_duration = start_time.elapsed();
|
||||
tracing::info!("⚡ generate_zk_proof took: {:?}", generate_duration);
|
||||
|
||||
// Sent zk proof bundle to verifier
|
||||
let serialized_proof = bincode::serialize(&proof_bundle)?;
|
||||
verifier_extra_socket.write_all(&serialized_proof).await?;
|
||||
verifier_extra_socket.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Reveal everything from the request, except for the authorization token.
|
||||
fn reveal_request(
|
||||
request: &[u8],
|
||||
builder: &mut ProveConfigBuilder<'_>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let reqs = Requests::new_from_slice(request).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let req = reqs.first().ok_or("No requests found")?;
|
||||
|
||||
if req.request.method.as_str() != "GET" {
|
||||
return Err(format!("Expected GET method, found {}", req.request.method.as_str()).into());
|
||||
}
|
||||
|
||||
let authorization_header = req
|
||||
.headers_with_name(header::AUTHORIZATION.as_str())
|
||||
.next()
|
||||
.ok_or("Authorization header not found")?;
|
||||
|
||||
let start_pos = authorization_header
|
||||
.span()
|
||||
.indices()
|
||||
.min()
|
||||
.ok_or("Could not find authorization header start position")?
|
||||
+ header::AUTHORIZATION.as_str().len()
|
||||
+ 2;
|
||||
let end_pos =
|
||||
start_pos + authorization_header.span().len() - header::AUTHORIZATION.as_str().len() - 2;
|
||||
|
||||
builder.reveal_sent(&(0..start_pos))?;
|
||||
builder.reveal_sent(&(end_pos..request.len()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reveal_received(
|
||||
received: &[u8],
|
||||
builder: &mut ProveConfigBuilder<'_>,
|
||||
transcript_commitment_builder: &mut TranscriptCommitConfigBuilder,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let resp = Responses::new_from_slice(received).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let response = resp.first().ok_or("No responses found")?;
|
||||
let body = response.body.as_ref().ok_or("Response body not found")?;
|
||||
|
||||
let BodyContent::Json(json) = &body.content else {
|
||||
return Err("Expected JSON body content".into());
|
||||
};
|
||||
|
||||
// reveal tax year
|
||||
let tax_year = json
|
||||
.get("tax_year")
|
||||
.ok_or("tax_year field not found in JSON")?;
|
||||
let start_pos = tax_year
|
||||
.span()
|
||||
.indices()
|
||||
.min()
|
||||
.ok_or("Could not find tax_year start position")?
|
||||
- 11;
|
||||
let end_pos = tax_year
|
||||
.span()
|
||||
.indices()
|
||||
.max()
|
||||
.ok_or("Could not find tax_year end position")?
|
||||
+ 1;
|
||||
builder.reveal_recv(&(start_pos..end_pos))?;
|
||||
|
||||
// commit to hash of date of birth
|
||||
let dob = json
|
||||
.get("taxpayer.date_of_birth")
|
||||
.ok_or("taxpayer.date_of_birth field not found in JSON")?;
|
||||
|
||||
transcript_commitment_builder.commit_recv(dob.span())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// extract secret from prover output
|
||||
fn received_secrets(transcript_secrets: &[TranscriptSecret]) -> Vec<&PlaintextHashSecret> {
|
||||
transcript_secrets
|
||||
.iter()
|
||||
.filter_map(|secret| match secret {
|
||||
TranscriptSecret::Hash(hash) if hash.direction == Direction::Received => Some(hash),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ZKProofInput {
|
||||
dob: Vec<u8>,
|
||||
proof_date: NaiveDate,
|
||||
blinder: Vec<u8>,
|
||||
committed_hash: Vec<u8>,
|
||||
}
|
||||
|
||||
// Verify that the blinded, committed hash is correct
|
||||
fn prepare_zk_proof_input(
|
||||
received: &[u8],
|
||||
received_commitment: &PlaintextHash,
|
||||
received_secret: &PlaintextHashSecret,
|
||||
) -> Result<ZKProofInput, Box<dyn std::error::Error>> {
|
||||
assert_eq!(received_commitment.direction, Direction::Received);
|
||||
assert_eq!(received_commitment.hash.alg, HashAlgId::BLAKE3);
|
||||
|
||||
let hash = &received_commitment.hash;
|
||||
|
||||
let dob_start = received_commitment
|
||||
.idx
|
||||
.min()
|
||||
.ok_or("No start index for DOB")?;
|
||||
let dob_end = received_commitment
|
||||
.idx
|
||||
.end()
|
||||
.ok_or("No end index for DOB")?;
|
||||
let dob = received[dob_start..dob_end].to_vec();
|
||||
let blinder = received_secret.blinder.as_bytes().to_vec();
|
||||
let committed_hash = hash.value.as_bytes().to_vec();
|
||||
let proof_date = Local::now().date_naive();
|
||||
|
||||
assert_eq!(received_secret.direction, Direction::Received);
|
||||
assert_eq!(received_secret.alg, HashAlgId::BLAKE3);
|
||||
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(&dob);
|
||||
hasher.update(&blinder);
|
||||
let computed_hash = hasher.finalize().as_bytes().to_vec();
|
||||
|
||||
if committed_hash != computed_hash.as_slice() {
|
||||
return Err("Computed hash does not match committed hash".into());
|
||||
}
|
||||
|
||||
Ok(ZKProofInput {
|
||||
dob,
|
||||
proof_date,
|
||||
committed_hash,
|
||||
blinder,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_zk_proof(
|
||||
proof_input: &ZKProofInput,
|
||||
) -> Result<ZKProofBundle, Box<dyn std::error::Error>> {
|
||||
tracing::info!("🔒 Generating ZK proof with Noir...");
|
||||
|
||||
const PROGRAM_JSON: &str = include_str!("./noir/target/noir.json");
|
||||
|
||||
// 1. Load bytecode from program.json
|
||||
let json: Value = serde_json::from_str(PROGRAM_JSON)?;
|
||||
let bytecode = json["bytecode"]
|
||||
.as_str()
|
||||
.ok_or("bytecode field not found in program.json")?;
|
||||
|
||||
let mut inputs: Vec<String> = vec![];
|
||||
inputs.push(proof_input.proof_date.day().to_string());
|
||||
inputs.push(proof_input.proof_date.month().to_string());
|
||||
inputs.push(proof_input.proof_date.year().to_string());
|
||||
inputs.extend(proof_input.committed_hash.iter().map(|b| b.to_string()));
|
||||
inputs.extend(proof_input.dob.iter().map(|b| b.to_string()));
|
||||
inputs.extend(proof_input.blinder.iter().map(|b| b.to_string()));
|
||||
|
||||
let proof_date = proof_input.proof_date.to_string();
|
||||
tracing::info!(
|
||||
"Public inputs : Proof date ({}) and committed hash ({})",
|
||||
proof_date,
|
||||
hex::encode(&proof_input.committed_hash)
|
||||
);
|
||||
tracing::info!(
|
||||
"Private inputs: Blinder ({}) and Date of Birth ({})",
|
||||
hex::encode(&proof_input.blinder),
|
||||
String::from_utf8_lossy(&proof_input.dob)
|
||||
);
|
||||
|
||||
tracing::debug!("Witness inputs {:?}", inputs);
|
||||
|
||||
let input_refs: Vec<&str> = inputs.iter().map(String::as_str).collect();
|
||||
let witness = from_vec_str_to_witness_map(input_refs)?;
|
||||
|
||||
// Setup SRS
|
||||
setup_srs_from_bytecode(bytecode, None, false)?;
|
||||
|
||||
// Verification key
|
||||
let vk = get_ultra_honk_verification_key(bytecode, false)?;
|
||||
|
||||
// Generate proof
|
||||
let proof = prove_ultra_honk(bytecode, witness.clone(), vk.clone(), false)?;
|
||||
tracing::info!("✅ Proof generated ({} bytes)", proof.len());
|
||||
|
||||
let proof_bundle = ZKProofBundle { vk, proof };
|
||||
Ok(proof_bundle)
|
||||
}
|
||||
21
crates/examples/interactive_zk/types.rs
Normal file
21
crates/examples/interactive_zk/types.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tlsn::transcript::{hash::PlaintextHash, Direction, TranscriptCommitment};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ZKProofBundle {
|
||||
pub vk: Vec<u8>,
|
||||
pub proof: Vec<u8>,
|
||||
}
|
||||
|
||||
// extract commitment from prover output
|
||||
pub fn received_commitments(
|
||||
transcript_commitments: &[TranscriptCommitment],
|
||||
) -> Vec<&PlaintextHash> {
|
||||
transcript_commitments
|
||||
.iter()
|
||||
.filter_map(|commitment| match commitment {
|
||||
TranscriptCommitment::Hash(hash) if hash.direction == Direction::Received => Some(hash),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
184
crates/examples/interactive_zk/verifier.rs
Normal file
184
crates/examples/interactive_zk/verifier.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use crate::types::received_commitments;
|
||||
|
||||
use super::types::ZKProofBundle;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use noir::barretenberg::verify::{get_ultra_honk_verification_key, verify_ultra_honk};
|
||||
use serde_json::Value;
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{CertificateDer, ProtocolConfigValidator, RootCertStore},
|
||||
connection::ServerName,
|
||||
hash::HashAlgId,
|
||||
transcript::{Direction, PartialTranscript},
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
use tlsn_examples::{MAX_RECV_DATA, MAX_SENT_DATA};
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(socket, extra_socket))]
|
||||
pub async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: T,
|
||||
mut extra_socket: T,
|
||||
) -> Result<PartialTranscript, Box<dyn std::error::Error>> {
|
||||
// Set up Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()?;
|
||||
|
||||
// Create a root certificate store with the server-fixture's self-signed
|
||||
// certificate. This is only required for offline testing with the
|
||||
// server-fixture.
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()?;
|
||||
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
|
||||
// Receive authenticated data.
|
||||
let VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
transcript_commitments,
|
||||
..
|
||||
} = verifier
|
||||
.verify(socket.compat(), &VerifyConfig::default())
|
||||
.await?;
|
||||
|
||||
let server_name = server_name.ok_or("Prover should have revealed server name")?;
|
||||
let transcript = transcript.ok_or("Prover should have revealed transcript data")?;
|
||||
|
||||
// Create hash commitment for the date of birth field from the response
|
||||
let sent = transcript.sent_unsafe().to_vec();
|
||||
let sent_data = String::from_utf8(sent.clone())
|
||||
.map_err(|e| format!("Verifier expected valid UTF-8 sent data: {}", e))?;
|
||||
|
||||
if !sent_data.contains(SERVER_DOMAIN) {
|
||||
return Err(format!(
|
||||
"Verification failed: Expected host {} not found in sent data",
|
||||
SERVER_DOMAIN
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check received data.
|
||||
let received_commitments = received_commitments(&transcript_commitments);
|
||||
let received_commitment = received_commitments
|
||||
.first()
|
||||
.ok_or("Missing received hash commitment")?;
|
||||
|
||||
assert!(received_commitment.direction == Direction::Received);
|
||||
assert!(received_commitment.hash.alg == HashAlgId::BLAKE3);
|
||||
|
||||
let committed_hash = &received_commitment.hash;
|
||||
|
||||
// Check Session info: server name.
|
||||
let ServerName::Dns(server_name) = server_name;
|
||||
if server_name.as_str() != SERVER_DOMAIN {
|
||||
return Err(format!(
|
||||
"Server name mismatch: expected {}, got {}",
|
||||
SERVER_DOMAIN,
|
||||
server_name.as_str()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Receive ZKProof information from prover
|
||||
let mut buf = Vec::new();
|
||||
extra_socket.read_to_end(&mut buf).await?;
|
||||
|
||||
if buf.is_empty() {
|
||||
return Err("No ZK proof data received from prover".into());
|
||||
}
|
||||
|
||||
let msg: ZKProofBundle = bincode::deserialize(&buf)
|
||||
.map_err(|e| format!("Failed to deserialize ZK proof bundle: {}", e))?;
|
||||
|
||||
// Verify zk proof
|
||||
const PROGRAM_JSON: &str = include_str!("./noir/target/noir.json");
|
||||
let json: Value = serde_json::from_str(PROGRAM_JSON)
|
||||
.map_err(|e| format!("Failed to parse Noir circuit: {}", e))?;
|
||||
|
||||
let bytecode = json["bytecode"]
|
||||
.as_str()
|
||||
.ok_or("Bytecode field missing in noir.json")?;
|
||||
|
||||
let vk = get_ultra_honk_verification_key(bytecode, false)
|
||||
.map_err(|e| format!("Failed to get verification key: {}", e))?;
|
||||
|
||||
if vk != msg.vk {
|
||||
return Err("Verification key mismatch between computed and provided by prover".into());
|
||||
}
|
||||
|
||||
let proof = msg.proof.clone();
|
||||
|
||||
// Validate proof has enough data.
|
||||
// The proof should start with the public inputs:
|
||||
// * We expect at least 3 * 32 bytes for the three date fields (day, month,
|
||||
// year)
|
||||
// * and 32*32 bytes for the hash
|
||||
let min_bytes = (32 + 3) * 32;
|
||||
if proof.len() < min_bytes {
|
||||
return Err(format!(
|
||||
"Proof too short: expected at least {} bytes, got {}",
|
||||
min_bytes,
|
||||
proof.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check that the proof date is correctly included in the proof
|
||||
let proof_date_day: u32 = u32::from_be_bytes(proof[28..32].try_into()?);
|
||||
let proof_date_month: u32 = u32::from_be_bytes(proof[60..64].try_into()?);
|
||||
let proof_date_year: i32 = i32::from_be_bytes(proof[92..96].try_into()?);
|
||||
let proof_date_from_proof =
|
||||
NaiveDate::from_ymd_opt(proof_date_year, proof_date_month, proof_date_day)
|
||||
.ok_or("Invalid proof date in proof")?;
|
||||
let today = Local::now().date_naive();
|
||||
if (today - proof_date_from_proof).num_days() < 0 {
|
||||
return Err(format!(
|
||||
"The proof date can only be today or in the past: provided {}, today {}",
|
||||
proof_date_from_proof, today
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check that the committed hash in the proof matches the hash from the
|
||||
// commitment
|
||||
let committed_hash_in_proof: Vec<u8> = proof
|
||||
.chunks(32)
|
||||
.skip(3) // skip the first 3 chunks
|
||||
.take(32)
|
||||
.map(|chunk| *chunk.last().unwrap_or(&0))
|
||||
.collect();
|
||||
let expected_hash = committed_hash.value.as_bytes().to_vec();
|
||||
if committed_hash_in_proof != expected_hash {
|
||||
tracing::error!(
|
||||
"❌ The hash in the proof does not match the committed hash in MPC-TLS: {} != {}",
|
||||
hex::encode(&committed_hash_in_proof),
|
||||
hex::encode(&expected_hash)
|
||||
);
|
||||
return Err("Hash in proof does not match committed hash in MPC-TLS".into());
|
||||
}
|
||||
tracing::info!(
|
||||
"✅ The hash in the proof matches the committed hash in MPC-TLS ({})",
|
||||
hex::encode(&expected_hash)
|
||||
);
|
||||
|
||||
// Finally verify the proof
|
||||
let is_valid = verify_ultra_honk(msg.proof, msg.vk)
|
||||
.map_err(|e| format!("ZKProof Verification failed: {}", e))?;
|
||||
if !is_valid {
|
||||
tracing::error!("❌ Age verification ZKProof failed to verify");
|
||||
return Err("Age verification ZKProof failed to verify".into());
|
||||
}
|
||||
tracing::info!("✅ Age verification ZKProof successfully verified");
|
||||
|
||||
Ok(transcript)
|
||||
}
|
||||
@@ -72,4 +72,5 @@ pub(crate) struct ServerFinishedVd {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct CloseConnection;
|
||||
|
||||
@@ -193,7 +193,7 @@ where
|
||||
};
|
||||
|
||||
// Divide by block length and round up.
|
||||
let block_count = input.len() / 16 + (input.len() % 16 != 0) as usize;
|
||||
let block_count = input.len() / 16 + !input.len().is_multiple_of(16) as usize;
|
||||
|
||||
if block_count > MAX_POWER {
|
||||
return Err(ErrorRepr::InputLength {
|
||||
@@ -282,11 +282,11 @@ fn build_ghash_data(mut aad: Vec<u8>, mut ciphertext: Vec<u8>) -> Vec<u8> {
|
||||
let len_block = ((associated_data_bitlen as u128) << 64) + (text_bitlen as u128);
|
||||
|
||||
// Pad data to be a multiple of 16 bytes.
|
||||
let aad_padded_block_count = (aad.len() / 16) + (aad.len() % 16 != 0) as usize;
|
||||
let aad_padded_block_count = (aad.len() / 16) + !aad.len().is_multiple_of(16) as usize;
|
||||
aad.resize(aad_padded_block_count * 16, 0);
|
||||
|
||||
let ciphertext_padded_block_count =
|
||||
(ciphertext.len() / 16) + (ciphertext.len() % 16 != 0) as usize;
|
||||
(ciphertext.len() / 16) + !ciphertext.len().is_multiple_of(16) as usize;
|
||||
ciphertext.resize(ciphertext_padded_block_count * 16, 0);
|
||||
|
||||
let mut data: Vec<u8> = Vec::with_capacity(aad.len() + ciphertext.len() + 16);
|
||||
|
||||
@@ -6,7 +6,7 @@ pub static SERVER_CERT_DER: &[u8] = include_bytes!("tls/test_server_cert.der");
|
||||
pub static SERVER_KEY_DER: &[u8] = include_bytes!("tls/test_server_private_key.der");
|
||||
/// The domain name bound to the server certificate.
|
||||
pub static SERVER_DOMAIN: &str = "test-server.io";
|
||||
/// A client certificate fixture PEM-encoded.
|
||||
pub static CLIENT_CERT: &[u8] = include_bytes!("tls/client_cert.pem");
|
||||
/// A client private key fixture PEM-encoded.
|
||||
pub static CLIENT_KEY: &[u8] = include_bytes!("tls/client_cert.key");
|
||||
/// A client certificate fixture.
|
||||
pub static CLIENT_CERT_DER: &[u8] = include_bytes!("tls/client_cert.der");
|
||||
/// A client private key fixture.
|
||||
pub static CLIENT_KEY_DER: &[u8] = include_bytes!("tls/client_cert_private_key.der");
|
||||
|
||||
@@ -33,5 +33,8 @@ openssl req -new -key client_cert.key -out client_cert.csr -subj "/C=US/ST=State
|
||||
# Sign the CSR with the root CA to create the end entity certificate (100 years validity)
|
||||
openssl x509 -req -in client_cert.csr -CA root_ca.crt -CAkey root_ca.key -CAcreateserial -out client_cert.crt -days 36525 -sha256 -extfile openssl.cnf -extensions v3_req
|
||||
|
||||
# Convert the end entity certificate to PEM format
|
||||
openssl x509 -in client_cert.crt -outform pem -out client_cert.pem
|
||||
# Convert the end entity certificate to DER format
|
||||
openssl x509 -in client_cert.crt -outform der -out client_cert.der
|
||||
|
||||
# Convert the end entity certificate private key to DER format
|
||||
openssl pkcs8 -topk8 -inform PEM -outform DER -in client_cert.key -out client_cert_private_key.der -nocrypt
|
||||
|
||||
BIN
crates/server-fixture/certs/src/tls/client_cert.der
Normal file
BIN
crates/server-fixture/certs/src/tls/client_cert.der
Normal file
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID2jCCAsKgAwIBAgIUG5JKIz/fbUDdpX1+TAw33mS+mWwwDQYJKoZIhvcNAQEL
|
||||
BQAwZTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
|
||||
MRIwEAYDVQQKDAl0bHNub3RhcnkxCzAJBgNVBAsMAklUMRYwFAYDVQQDDA10bHNu
|
||||
b3Rhcnkub3JnMCAXDTI1MDYxMDA3MTYxOVoYDzIxMjUwNjExMDcxNjE5WjBwMQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFU3RhdGUxDTALBgNVBAcMBENpdHkxEjAQBgNV
|
||||
BAoMCXRsc25vdGFyeTELMAkGA1UECwwCSVQxITAfBgNVBAMMGGNsaWVudC1hdXRo
|
||||
ZW50aWNhdGlvbi5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANsx
|
||||
Tf3JqWdAMGFzOwbO64vJ5fV/IPSrdBwKY/Fjef0REZC1Z/gGzmp0nnlaHZzZLtLS
|
||||
Z9kyfdUrL6PuG3HfP6wxhiaBpUay+1O9KZsuhkKSif4KMPjlYKm+oZLvD12Qj62r
|
||||
TFlui4+1wKgPrTGUUO6SQdoRxKU4nzuzRYRLyzDi0pO5YD9RLaruBj+IDEOVRW7d
|
||||
1uleheVMg61lbQle5Fo0c4I0Sif96Z+7aotj3j9F2lK52jaLpA1kvC3oLajfAT30
|
||||
BzpNLZTnWa1b5PRRxkuOYUXeNr+aNO90fL80K1YeIlea0f7qmKL9uDLtQbrqIJv5
|
||||
tBaf8Uf0UghtBm//kx8CAwEAAaN1MHMwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAw
|
||||
GQYDVR0RBBIwEIIOdGVzdC1zZXJ2ZXIuaW8wHQYDVR0OBBYEFH1qCgl04Y5i75aF
|
||||
cT0V3fn9423iMB8GA1UdIwQYMBaAFMmBciQ/DZlWROxwXH8IplmuHKbNMA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQB8Gvj3dsENAn0u6PS9uTFm46MaA9Dm+Fa+KbXuEHp3ADs2
|
||||
7m4Hb3eojM3yae93/v/stYn8IVcB5zWmMvg6WA6obe86muuB+SZeMC/AnSD8P4pm
|
||||
AzO3eTSR1s5Dr4O0qVPd2VP36e7NWXfojQg4W9t9UQtC64bVOaCDQvbe0xeWT+AR
|
||||
w0y7GwnuCr/8bisqQZS8+Er1JU3zxBEjQwMiMxlOWHnYtjGeA6pdWaeLp0E6Ss3x
|
||||
ecsTjmrLt6oY+BdfRSyWU4qVEOpuZLCeikUWXFzpxRX7NWYRtJUfVnoRWwuD2lzG
|
||||
LybzCW2qxwHJe4biGIfWKQ7Ne7DrwQwFxVRJxCm0
|
||||
-----END CERTIFICATE-----
|
||||
BIN
crates/server-fixture/certs/src/tls/client_cert_private_key.der
Normal file
BIN
crates/server-fixture/certs/src/tls/client_cert_private_key.der
Normal file
Binary file not shown.
37
crates/server-fixture/server/src/data/elster.json
Normal file
37
crates/server-fixture/server/src/data/elster.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"tax_year": 2024,
|
||||
"taxpayer": {
|
||||
"idnr": "12345678901",
|
||||
"first_name": "Max",
|
||||
"last_name": "Mustermann",
|
||||
"date_of_birth": "1985-03-12",
|
||||
"address": {
|
||||
"street": "Musterstraße 1",
|
||||
"postal_code": "10115",
|
||||
"city": "Berlin"
|
||||
}
|
||||
},
|
||||
"income": {
|
||||
"employment_income": 54200.00,
|
||||
"other_income": 1200.00,
|
||||
"capital_gains": 350.00
|
||||
},
|
||||
"deductions": {
|
||||
"pension_insurance": 4200.00,
|
||||
"health_insurance": 3600.00,
|
||||
"donations": 500.00,
|
||||
"work_related_expenses": 1100.00
|
||||
},
|
||||
"assessment": {
|
||||
"taxable_income": 49200.00,
|
||||
"income_tax": 9156.00,
|
||||
"solidarity_surcharge": 503.58,
|
||||
"total_tax": 9659.58,
|
||||
"prepaid_tax": 9500.00,
|
||||
"refund": 159.58
|
||||
},
|
||||
"submission": {
|
||||
"submitted_at": "2025-03-01T14:22:30Z",
|
||||
"submitted_by": "ElsterOnline-Portal"
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ fn app(state: AppState) -> Router {
|
||||
.route("/formats/json", get(json))
|
||||
.route("/formats/html", get(html))
|
||||
.route("/protected", get(protected_route))
|
||||
.route("/elster", get(elster_route))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(Arc::new(Mutex::new(state)))
|
||||
}
|
||||
@@ -196,6 +197,12 @@ async fn protected_route(_: AuthenticatedUser) -> Result<Json<Value>, StatusCode
|
||||
get_json_value(include_str!("data/protected_data.json"))
|
||||
}
|
||||
|
||||
async fn elster_route(_: AuthenticatedUser) -> Result<Json<Value>, StatusCode> {
|
||||
info!("Handling /elster");
|
||||
|
||||
get_json_value(include_str!("data/elster.json"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -886,6 +886,7 @@ async fn client_error_is_sticky() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[allow(clippy::no_effect)]
|
||||
#[allow(clippy::unnecessary_operation)]
|
||||
async fn client_is_send() {
|
||||
let (client, _) = make_pair(KeyType::Rsa).await;
|
||||
|
||||
@@ -415,7 +415,8 @@ pub(crate) fn pki_error(error: webpki::Error) -> Error {
|
||||
match error {
|
||||
BadDer | BadDerTime => Error::InvalidCertificateEncoding,
|
||||
InvalidSignatureForPublicKey => Error::InvalidCertificateSignature,
|
||||
UnsupportedSignatureAlgorithm | UnsupportedSignatureAlgorithmForPublicKey => {
|
||||
UnsupportedSignatureAlgorithmContext(_)
|
||||
| UnsupportedSignatureAlgorithmForPublicKeyContext(_) => {
|
||||
Error::InvalidCertificateSignatureType
|
||||
}
|
||||
e => Error::InvalidCertificateData(format!("invalid peer certificate: {e}")),
|
||||
@@ -475,12 +476,17 @@ fn verify_sig_using_any_alg(
|
||||
// we try them all.
|
||||
for alg in algs {
|
||||
match cert.verify_signature(*alg, message, sig) {
|
||||
Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKey) => continue,
|
||||
Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKeyContext(_)) => continue,
|
||||
res => return res,
|
||||
}
|
||||
}
|
||||
|
||||
Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKey)
|
||||
Err(webpki::Error::UnsupportedSignatureAlgorithmContext(
|
||||
webpki::UnsupportedSignatureAlgorithmContext {
|
||||
signature_algorithm_id: vec![],
|
||||
supported_algorithms: algs.iter().map(|alg| alg.signature_alg_id()).collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn verify_signed_struct(
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mpz_core::bitvec::BitVec;
|
||||
use mpz_hash::sha256::Sha256;
|
||||
use mpz_hash::{blake3::Blake3, sha256::Sha256};
|
||||
use mpz_memory_core::{
|
||||
DecodeFutureTyped, MemoryExt, Vector,
|
||||
binary::{Binary, U8},
|
||||
};
|
||||
use mpz_vm_core::{Vm, VmError, prelude::*};
|
||||
use rangeset::RangeSet;
|
||||
use tlsn_core::{
|
||||
hash::{Blinder, Hash, HashAlgId, TypedHash},
|
||||
transcript::{
|
||||
Direction, Idx,
|
||||
Direction,
|
||||
hash::{PlaintextHash, PlaintextHashSecret},
|
||||
},
|
||||
};
|
||||
@@ -26,7 +27,7 @@ pub(crate) struct HashCommitFuture {
|
||||
#[allow(clippy::type_complexity)]
|
||||
futs: Vec<(
|
||||
Direction,
|
||||
Idx,
|
||||
RangeSet<usize>,
|
||||
HashAlgId,
|
||||
DecodeFutureTyped<BitVec, Vec<u8>>,
|
||||
)>,
|
||||
@@ -60,7 +61,7 @@ impl HashCommitFuture {
|
||||
pub(crate) fn prove_hash(
|
||||
vm: &mut dyn Vm<Binary>,
|
||||
refs: &TranscriptRefs,
|
||||
idxs: impl IntoIterator<Item = (Direction, Idx, HashAlgId)>,
|
||||
idxs: impl IntoIterator<Item = (Direction, RangeSet<usize>, HashAlgId)>,
|
||||
) -> Result<(HashCommitFuture, Vec<PlaintextHashSecret>), HashCommitError> {
|
||||
let mut futs = Vec::new();
|
||||
let mut secrets = Vec::new();
|
||||
@@ -90,7 +91,7 @@ pub(crate) fn prove_hash(
|
||||
pub(crate) fn verify_hash(
|
||||
vm: &mut dyn Vm<Binary>,
|
||||
refs: &TranscriptRefs,
|
||||
idxs: impl IntoIterator<Item = (Direction, Idx, HashAlgId)>,
|
||||
idxs: impl IntoIterator<Item = (Direction, RangeSet<usize>, HashAlgId)>,
|
||||
) -> Result<HashCommitFuture, HashCommitError> {
|
||||
let mut futs = Vec::new();
|
||||
for (direction, idx, alg, hash_ref, blinder_ref) in
|
||||
@@ -106,14 +107,29 @@ pub(crate) fn verify_hash(
|
||||
Ok(HashCommitFuture { futs })
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Hasher {
|
||||
Sha256(Sha256),
|
||||
Blake3(Blake3),
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
idxs: impl IntoIterator<Item = (Direction, RangeSet<usize>, HashAlgId)>,
|
||||
) -> Result<
|
||||
Vec<(
|
||||
Direction,
|
||||
RangeSet<usize>,
|
||||
HashAlgId,
|
||||
Array<U8, 32>,
|
||||
Vector<U8>,
|
||||
)>,
|
||||
HashCommitError,
|
||||
> {
|
||||
let mut output = Vec::new();
|
||||
let mut hashers = HashMap::new();
|
||||
for (direction, idx, alg) in idxs {
|
||||
@@ -125,11 +141,11 @@ fn hash_commit_inner(
|
||||
|
||||
let hash = match alg {
|
||||
HashAlgId::SHA256 => {
|
||||
let mut hasher = if let Some(hasher) = hashers.get(&alg).cloned() {
|
||||
let mut hasher = if let Some(Hasher::Sha256(hasher)) = hashers.get(&alg).cloned() {
|
||||
hasher
|
||||
} else {
|
||||
let hasher = Sha256::new_with_init(vm).map_err(HashCommitError::hasher)?;
|
||||
hashers.insert(alg, hasher.clone());
|
||||
hashers.insert(alg, Hasher::Sha256(hasher.clone()));
|
||||
hasher
|
||||
};
|
||||
|
||||
@@ -139,6 +155,26 @@ fn hash_commit_inner(
|
||||
hasher.update(&blinder);
|
||||
hasher.finalize(vm).map_err(HashCommitError::hasher)?
|
||||
}
|
||||
HashAlgId::BLAKE3 => {
|
||||
let mut hasher = if let Some(Hasher::Blake3(hasher)) = hashers.get(&alg).cloned() {
|
||||
hasher
|
||||
} else {
|
||||
let hasher = Blake3::new(vm).map_err(HashCommitError::hasher)?;
|
||||
hashers.insert(alg, Hasher::Blake3(hasher.clone()));
|
||||
hasher
|
||||
};
|
||||
|
||||
for plaintext in refs.get(direction, &idx).expect("plaintext refs are valid") {
|
||||
hasher
|
||||
.update(vm, &plaintext)
|
||||
.map_err(HashCommitError::hasher)?;
|
||||
}
|
||||
hasher
|
||||
.update(vm, &blinder)
|
||||
.map_err(HashCommitError::hasher)?;
|
||||
hasher.finalize(vm).map_err(HashCommitError::hasher)?
|
||||
|
||||
}
|
||||
alg => {
|
||||
return Err(HashCommitError::unsupported_alg(alg));
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use mpz_memory_core::{
|
||||
binary::{Binary, U8},
|
||||
};
|
||||
use mpz_vm_core::{Vm, VmError};
|
||||
use rangeset::Intersection;
|
||||
use tlsn_core::transcript::{Direction, Idx, PartialTranscript};
|
||||
use rangeset::{Intersection, RangeSet};
|
||||
use tlsn_core::transcript::{Direction, PartialTranscript};
|
||||
|
||||
/// References to the application plaintext in the transcript.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@@ -38,7 +38,11 @@ impl TranscriptRefs {
|
||||
|
||||
/// Returns VM references for the given direction and index, otherwise
|
||||
/// `None` if the index is out of bounds.
|
||||
pub(crate) fn get(&self, direction: Direction, idx: &Idx) -> Option<Vec<Vector<U8>>> {
|
||||
pub(crate) fn get(
|
||||
&self,
|
||||
direction: Direction,
|
||||
idx: &RangeSet<usize>,
|
||||
) -> Option<Vec<Vector<U8>>> {
|
||||
if idx.is_empty() {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
@@ -83,8 +87,8 @@ impl TranscriptRefs {
|
||||
/// Decodes the transcript.
|
||||
pub(crate) fn decode_transcript(
|
||||
vm: &mut dyn Vm<Binary>,
|
||||
sent: &Idx,
|
||||
recv: &Idx,
|
||||
sent: &RangeSet<usize>,
|
||||
recv: &RangeSet<usize>,
|
||||
refs: &TranscriptRefs,
|
||||
) -> Result<(), VmError> {
|
||||
let sent_refs = refs.get(Direction::Sent, sent).expect("index is in bounds");
|
||||
@@ -149,7 +153,7 @@ mod tests {
|
||||
use mpz_memory_core::{FromRaw, Slice, Vector, binary::U8};
|
||||
use rangeset::RangeSet;
|
||||
use std::ops::Range;
|
||||
use tlsn_core::transcript::{Direction, Idx};
|
||||
use tlsn_core::transcript::Direction;
|
||||
|
||||
// TRANSCRIPT_REFS:
|
||||
//
|
||||
@@ -185,7 +189,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let vm_refs = transcript_refs
|
||||
.get(Direction::Sent, &idx_fixture())
|
||||
.get(Direction::Sent, &RangeSet::from(IDXS))
|
||||
.unwrap();
|
||||
|
||||
let expected_refs: Vec<Vector<U8>> = EXPECTED_REFS
|
||||
@@ -204,9 +208,4 @@ mod tests {
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
fn idx_fixture() -> Idx {
|
||||
let set = RangeSet::from(IDXS);
|
||||
Idx::builder().union(&set).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ use mpz_memory_core::{
|
||||
correlated::{Delta, Key, Mac},
|
||||
};
|
||||
use rand::Rng;
|
||||
use rangeset::RangeSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serio::{SinkExt, stream::IoStreamExt};
|
||||
use tlsn_core::{
|
||||
hash::HashAlgorithm,
|
||||
transcript::{
|
||||
Direction, Idx,
|
||||
Direction,
|
||||
encoding::{
|
||||
Encoder, EncoderSecret, EncodingCommitment, EncodingProvider, EncodingProviderError,
|
||||
EncodingTree, EncodingTreeError, new_encoder,
|
||||
@@ -117,7 +118,7 @@ pub(crate) async fn receive<'a>(
|
||||
hasher: &(dyn HashAlgorithm + Send + Sync),
|
||||
refs: &TranscriptRefs,
|
||||
f: impl Fn(Vector<U8>) -> &'a [Mac],
|
||||
idxs: impl IntoIterator<Item = &(Direction, Idx)>,
|
||||
idxs: impl IntoIterator<Item = &(Direction, RangeSet<usize>)>,
|
||||
) -> Result<(EncodingCommitment, EncodingTree), EncodingError> {
|
||||
// Set frame limit and add some extra bytes cushion room.
|
||||
let (sent, recv) = refs.len();
|
||||
|
||||
@@ -23,11 +23,11 @@ pub(crate) fn build_ghash_data(mut aad: Vec<u8>, mut ciphertext: Vec<u8>) -> Vec
|
||||
let len_block = ((associated_data_bitlen as u128) << 64) + (text_bitlen as u128);
|
||||
|
||||
// Pad data to be a multiple of 16 bytes.
|
||||
let aad_padded_block_count = (aad.len() / 16) + (aad.len() % 16 != 0) as usize;
|
||||
let aad_padded_block_count = (aad.len() / 16) + !aad.len().is_multiple_of(16) as usize;
|
||||
aad.resize(aad_padded_block_count * 16, 0);
|
||||
|
||||
let ciphertext_padded_block_count =
|
||||
(ciphertext.len() / 16) + (ciphertext.len() % 16 != 0) as usize;
|
||||
(ciphertext.len() / 16) + !ciphertext.len().is_multiple_of(16) as usize;
|
||||
ciphertext.resize(ciphertext_padded_block_count * 16, 0);
|
||||
|
||||
let mut data: Vec<u8> = Vec::with_capacity(aad.len() + ciphertext.len() + 16);
|
||||
|
||||
@@ -6,6 +6,7 @@ use tlsn_core::connection::{HandshakeData, ServerName};
|
||||
|
||||
/// Message sent from Prover to Verifier to prove the server identity.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct ServerIdentityProof {
|
||||
/// Server name.
|
||||
pub name: ServerName,
|
||||
|
||||
@@ -362,6 +362,7 @@ impl Prover<state::Committed> {
|
||||
ctx,
|
||||
vm,
|
||||
tls_transcript,
|
||||
transcript,
|
||||
transcript_refs,
|
||||
..
|
||||
} = &mut self.state;
|
||||
@@ -371,6 +372,14 @@ impl Prover<state::Committed> {
|
||||
transcript_secrets: Vec::new(),
|
||||
};
|
||||
|
||||
let partial_transcript = if let Some((sent, recv)) = config.reveal() {
|
||||
decode_transcript(vm, sent, recv, transcript_refs).map_err(ProverError::zk)?;
|
||||
|
||||
Some(transcript.to_partial(sent.clone(), recv.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let payload = ProvePayload {
|
||||
handshake: config.server_identity().then(|| {
|
||||
(
|
||||
@@ -388,7 +397,7 @@ impl Prover<state::Committed> {
|
||||
},
|
||||
)
|
||||
}),
|
||||
transcript: config.transcript().cloned(),
|
||||
transcript: partial_transcript,
|
||||
transcript_commit: config.transcript_commit().map(|config| config.to_request()),
|
||||
};
|
||||
|
||||
@@ -397,16 +406,6 @@ impl Prover<state::Committed> {
|
||||
.poll_with(ctx.io_mut().send(payload).map_err(ProverError::from))
|
||||
.await?;
|
||||
|
||||
if let Some(partial_transcript) = config.transcript() {
|
||||
decode_transcript(
|
||||
vm,
|
||||
partial_transcript.sent_authed(),
|
||||
partial_transcript.received_authed(),
|
||||
transcript_refs,
|
||||
)
|
||||
.map_err(ProverError::zk)?;
|
||||
}
|
||||
|
||||
let mut hash_commitments = None;
|
||||
if let Some(commit_config) = config.transcript_commit() {
|
||||
if commit_config.has_encoding() {
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::config::{NetworkSetting, ProtocolConfig, ProtocolConfigValidator};
|
||||
#[builder(pattern = "owned")]
|
||||
pub struct VerifierConfig {
|
||||
protocol_config_validator: ProtocolConfigValidator,
|
||||
#[builder(setter(strip_option))]
|
||||
#[builder(default, setter(strip_option))]
|
||||
root_store: Option<RootCertStore>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user