diff --git a/transcript-core/Cargo.toml b/transcript-core/Cargo.toml new file mode 100644 index 0000000000..0e5bd5216e --- /dev/null +++ b/transcript-core/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "transcript-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +p256 = { version = "0.10", features = ["ecdsa"]} +thiserror = "1" +rs_merkle = "1.2.0" +serde = { version = "1.0", features = ["derive"] } +bincode = "1.3.3" + +[features] +# This feature allows other crates to modify private fields of some types for testing purposes +expose_setters_for_testing = [] + +[dev-dependencies] +tlsn-mpc-core = { path = "../mpc/mpc-core" } +tlsn-mpc-circuits = { path = "../mpc/mpc-circuits" } +tlsn-tls-circuits = { path = "../tls/tls-circuits" } +hex = "0.4" +rstest = "0.12" +rand_chacha = "0.3" +rand = "0.8" +rand_core = "0.6" + diff --git a/transcript-core/src/commitment.rs b/transcript-core/src/commitment.rs new file mode 100644 index 0000000000..84894f91f4 --- /dev/null +++ b/transcript-core/src/commitment.rs @@ -0,0 +1,209 @@ +use super::{error::Error, HashCommitment, LabelSeed}; +use serde::Serialize; + +/// A User's commitment to a portion of the notarized data +#[derive(Serialize, Clone, Default)] +pub struct Commitment { + /// This commitment's index in `commitments` of [crate::document::Document] + id: u32, + typ: CommitmentType, + direction: Direction, + /// The index of this commitment in the Merkle tree of commitments + merkle_tree_index: u32, + /// The actual commitment + commitment: HashCommitment, + /// The absolute byte ranges within the notarized data. The committed data + /// is located in those ranges. Ranges do not overlap. + ranges: Vec, +} + +impl Commitment { + pub fn new( + id: u32, + typ: CommitmentType, + direction: Direction, + commitment: HashCommitment, + ranges: Vec, + merkle_tree_index: u32, + ) -> Self { + Self { + id, + typ, + direction, + commitment, + ranges, + merkle_tree_index, + } + } + + pub fn id(&self) -> u32 { + self.id + } + + pub fn typ(&self) -> &CommitmentType { + &self.typ + } + + pub fn direction(&self) -> &Direction { + &self.direction + } + + pub fn merkle_tree_index(&self) -> u32 { + self.merkle_tree_index + } + + pub fn commitment(&self) -> [u8; 32] { + self.commitment + } + + pub fn ranges(&self) -> &Vec { + &self.ranges + } + + #[cfg(test)] + pub fn set_id(&mut self, id: u32) { + self.id = id; + } + + #[cfg(test)] + pub fn set_typ(&mut self, typ: CommitmentType) { + self.typ = typ; + } + + #[cfg(any(feature = "expose_setters_for_testing", test))] + pub fn set_ranges(&mut self, ranges: Vec) { + self.ranges = ranges; + } + + #[cfg(test)] + pub fn set_merkle_tree_index(&mut self, merkle_tree_index: u32) { + self.merkle_tree_index = merkle_tree_index; + } + + #[cfg(test)] + pub fn set_commitment(&mut self, commitment: [u8; 32]) { + self.commitment = commitment; + } +} + +#[derive(Clone, PartialEq, Serialize, Default)] +#[allow(non_camel_case_types)] +pub enum CommitmentType { + #[default] + // A blake3 digest of the garbled circuit's active labels. The labels are generated from a PRG seed. + // For more details on the protocol used to generate this commitment, see + // https://github.com/tlsnotary/docs-mdbook/blob/main/src/protocol/notarization/public_data_commitment.md + labels_blake3, +} + +/// Various supported types of commitment opening +#[derive(Serialize, Clone)] +pub enum CommitmentOpening { + LabelsBlake3(LabelsBlake3Opening), +} + +/// A validated opening for the commitment type [CommitmentType::labels_blake3] +#[derive(Serialize, Clone, Default)] +pub struct LabelsBlake3Opening { + /// This commitment opening's index in `commitment_openings` of [crate::document::Document]. + /// The [Commitment] corresponding to this opening has the same id. + id: u32, + /// The actual opening of the commitment + opening: Vec, + /// All our commitments are `salt`ed by appending 16 random bytes + salt: Vec, + /// A PRG seeds from which to generate garbled circuit active labels, see + /// [crate::commitment::CommitmentType::labels_blake3]. + /// It must match `label_seed` in [crate::document::Document]. + label_seed: LabelSeed, +} + +impl LabelsBlake3Opening { + pub fn new(id: u32, opening: Vec, salt: Vec, label_seed: LabelSeed) -> Self { + Self { + id, + opening, + salt, + label_seed, + } + } + + pub fn id(&self) -> u32 { + self.id + } + pub fn opening(&self) -> &Vec { + &self.opening + } + + pub fn salt(&self) -> &Vec { + &self.salt + } + + pub fn label_seed(&self) -> &LabelSeed { + &self.label_seed + } + + #[cfg(any(feature = "expose_setters_for_testing", test))] + pub fn set_id(&mut self, id: u32) { + self.id = id; + } + + pub fn set_opening(&mut self, opening: Vec) { + self.opening = opening; + } + + #[cfg(any(feature = "expose_setters_for_testing", test))] + pub fn set_label_seed(&mut self, label_seed: LabelSeed) { + self.label_seed = label_seed; + } + + #[cfg(any(feature = "expose_setters_for_testing", test))] + pub fn set_salt(&mut self, salt: Vec) { + self.salt = salt; + } +} + +#[derive(Serialize, Clone, PartialEq, Default, Debug)] +/// A TLS transcript consists of a stream of bytes which were `Sent` to the server +/// and a stream of bytes which were `Received` from the server . The User creates +/// separate commitments to bytes in each direction. +pub enum Direction { + #[default] + Sent, + Received, +} + +#[derive(Serialize, Clone, Debug, PartialEq)] +/// A non-empty half-open range [start, end). Range bounds are ascending i.e. start < end +pub struct TranscriptRange { + start: u32, + end: u32, +} + +impl TranscriptRange { + pub fn new(start: u32, end: u32) -> Result { + // empty ranges are not allowed + if start >= end { + return Err(Error::RangeInvalid); + } + Ok(Self { start, end }) + } + + pub fn start(&self) -> u32 { + self.start + } + + pub fn end(&self) -> u32 { + self.end + } + + #[cfg(test)] + pub fn len(&self) -> u32 { + self.end - self.start + } + + #[cfg(any(feature = "expose_setters_for_testing", test))] + pub fn new_unchecked(start: u32, end: u32) -> Self { + Self { start, end } + } +} diff --git a/transcript-core/src/document.rs b/transcript-core/src/document.rs new file mode 100644 index 0000000000..ce3ee57ba1 --- /dev/null +++ b/transcript-core/src/document.rs @@ -0,0 +1,105 @@ +use crate::{ + commitment::{Commitment, CommitmentOpening}, + merkle::MerkleProof, + tls_handshake::TLSHandshake, + LabelSeed, +}; +use serde::Serialize; + +/// Notarization document. This is the form in which the document is received +/// by the Verifier from the User. +#[derive(Serialize, Clone)] +pub struct Document { + version: u8, + tls_handshake: TLSHandshake, + /// Notary's signature over the [crate::signed::Signed] portion of this doc + signature: Option>, + + /// A PRG seeds from which to generate garbled circuit active labels, see + /// [crate::commitment::CommitmentType::labels_blake3] + label_seed: LabelSeed, + + /// The root of the Merkle tree of all the commitments. The User must prove that each one of the + /// `commitments` is included in the Merkle tree. + /// This approach allows the User to hide from the Notary the exact amount of commitments thus + /// increasing User privacy against the Notary. + /// The root was made known to the Notary before the Notary opened his garbled circuits + /// to the User. + merkle_root: [u8; 32], + + /// The total leaf count in the Merkle tree of commitments. Provided by the User to the Verifier + /// to enable merkle proof verification. + merkle_tree_leaf_count: u32, + + /// A proof that all [commitments] are the leaves of the Merkle tree + merkle_multi_proof: MerkleProof, + + /// User's commitments to various portions of the notarized data, sorted ascendingly by id + commitments: Vec, + + /// Openings for the commitments, sorted ascendingly by id + commitment_openings: Vec, +} + +impl Document { + /// Creates a new document + pub fn new( + version: u8, + tls_handshake: TLSHandshake, + signature: Option>, + label_seed: LabelSeed, + merkle_root: [u8; 32], + merkle_tree_leaf_count: u32, + merkle_multi_proof: MerkleProof, + commitments: Vec, + commitment_openings: Vec, + ) -> Self { + Self { + version, + tls_handshake, + signature, + label_seed, + merkle_root, + merkle_tree_leaf_count, + merkle_multi_proof, + commitments, + commitment_openings, + } + } + + pub fn version(&self) -> u8 { + self.version + } + + pub fn tls_handshake(&self) -> &TLSHandshake { + &self.tls_handshake + } + + pub fn signature(&self) -> &Option> { + &self.signature + } + + pub fn label_seed(&self) -> &LabelSeed { + &self.label_seed + } + + pub fn merkle_root(&self) -> &[u8; 32] { + &self.merkle_root + } + + pub fn merkle_tree_leaf_count(&self) -> u32 { + self.merkle_tree_leaf_count + } + + pub fn merkle_multi_proof(&self) -> &MerkleProof { + &self.merkle_multi_proof + } + + pub fn commitments(&self) -> &Vec { + &self.commitments + } + + pub fn commitment_openings(&self) -> &Vec { + &self.commitment_openings + } +} diff --git a/transcript-core/src/error.rs b/transcript-core/src/error.rs new file mode 100644 index 0000000000..7dd7b6b5cc --- /dev/null +++ b/transcript-core/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum Error { + #[error("An internal error occured")] + InternalError, + #[error("An internal error during serialization or deserialization")] + SerializationError, + #[error("Error during signature verification")] + SignatureVerificationError, + #[error("Attempted to create an invalid range")] + RangeInvalid, +} diff --git a/transcript-core/src/lib.rs b/transcript-core/src/lib.rs new file mode 100644 index 0000000000..440f5d7d99 --- /dev/null +++ b/transcript-core/src/lib.rs @@ -0,0 +1,15 @@ +//! This crate contains types associated with the notarized transcript + +pub mod commitment; +pub mod document; +pub mod error; +pub mod merkle; +pub mod pubkey; +pub mod signed; +pub mod tls_handshake; + +pub type HashCommitment = [u8; 32]; + +/// A PRG seeds from which to generate garbled circuit active labels, see +/// [crate::commitment::CommitmentType::labels_blake3] +pub type LabelSeed = [u8; 32]; diff --git a/transcript-core/src/merkle.rs b/transcript-core/src/merkle.rs new file mode 100644 index 0000000000..6bde7cd1bc --- /dev/null +++ b/transcript-core/src/merkle.rs @@ -0,0 +1,29 @@ +use rs_merkle::{algorithms, proof_serializers, MerkleProof as MerkleProof_rs_merkle}; +use serde::{ser::Serializer, Serialize}; + +/// A wrapper around rs_merkle's MerkleProof with an added Clone impl +/// and serde serializer +#[derive(Serialize)] +pub struct MerkleProof( + #[serde(serialize_with = "merkle_proof_serialize")] + pub MerkleProof_rs_merkle, +); + +impl Clone for MerkleProof { + fn clone(&self) -> Self { + let bytes = self.0.to_bytes(); + Self(MerkleProof_rs_merkle::::from_bytes(&bytes).unwrap()) + } +} + +/// Serialize the rs_merkle's MerkleProof type using its native `serialize` method +fn merkle_proof_serialize( + proof: &MerkleProof_rs_merkle, + serializer: S, +) -> Result +where + S: Serializer, +{ + let bytes = proof.serialize::(); + serializer.serialize_bytes(&bytes) +} diff --git a/transcript-core/src/pubkey.rs b/transcript-core/src/pubkey.rs new file mode 100644 index 0000000000..7dec07e7ed --- /dev/null +++ b/transcript-core/src/pubkey.rs @@ -0,0 +1,138 @@ +use p256::{ + self, + ecdsa::{signature::Verifier, Signature}, + EncodedPoint, +}; + +use crate::error::Error; + +pub enum KeyType { + P256, +} + +/// A public key used by the Notary to sign the notarization session +pub enum PubKey { + P256(p256::ecdsa::VerifyingKey), +} + +impl PubKey { + /// Constructs pubkey from bytes + pub fn from_bytes(typ: KeyType, bytes: &[u8]) -> Result { + match typ { + KeyType::P256 => { + let point = match EncodedPoint::from_bytes(bytes) { + Ok(point) => point, + Err(_) => return Err(Error::InternalError), + }; + let vk = match p256::ecdsa::VerifyingKey::from_encoded_point(&point) { + Ok(vk) => vk, + Err(_) => return Err(Error::InternalError), + }; + Ok(PubKey::P256(vk)) + } + #[allow(unreachable_patterns)] + _ => Err(Error::InternalError), + } + } + + /// Verifies a signature `sig` for the message `msg` + pub fn verify_signature(&self, msg: &[u8], sig: &[u8]) -> Result<(), Error> { + match *self { + PubKey::P256(key) => { + let signature = match Signature::from_der(sig) { + Ok(sig) => sig, + Err(_) => return Err(Error::SignatureVerificationError), + }; + match key.verify(msg, &signature) { + Ok(_) => Ok(()), + Err(_) => Err(Error::SignatureVerificationError), + } + } + #[allow(unreachable_patterns)] + _ => Err(Error::InternalError), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::pubkey::{KeyType, PubKey}; + use p256::{ + self, + ecdsa::{signature::Signer, SigningKey, VerifyingKey}, + }; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha12Rng; + use rstest::{fixture, rstest}; + + #[fixture] + // Create a valid (piblic key, message, signature) tuple + pub fn create_key_msg_sig() -> (PubKey, Vec, Vec) { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + + let signing_key = SigningKey::random(&mut rng); + let verifying_key = VerifyingKey::from(&signing_key); + let encoded = verifying_key.to_encoded_point(true); + let pubkey_bytes = encoded.as_bytes(); + let key = PubKey::from_bytes(KeyType::P256, pubkey_bytes).unwrap(); + + let msg: [u8; 16] = rng.gen(); + + let signature = signing_key.sign(&msg); + let sig_der = signature.to_der(); + let signature = sig_der.as_bytes(); + + (key, msg.to_vec(), signature.to_vec()) + } + + #[rstest] + // Expect verify_signature() to fail because the public key is wrong + fn test_verify_signature_fail_wrong_key(create_key_msg_sig: (PubKey, Vec, Vec)) { + let msg = create_key_msg_sig.1; + let sig = create_key_msg_sig.2; + + // generate the wrong pubkey + let mut rng = ChaCha12Rng::from_seed([1; 32]); + + let signing_key = SigningKey::random(&mut rng); + let verifying_key = VerifyingKey::from(&signing_key); + let encoded = verifying_key.to_encoded_point(true); + let pubkey_bytes = encoded.as_bytes(); + let key = PubKey::from_bytes(KeyType::P256, pubkey_bytes).unwrap(); + + assert!( + key.verify_signature(&msg, &sig).err().unwrap() == Error::SignatureVerificationError + ); + } + + #[rstest] + // Expect verify_signature() to fail because the message is wrong + fn test_verify_signature_fail_wrong_msg(create_key_msg_sig: (PubKey, Vec, Vec)) { + let key = create_key_msg_sig.0; + let sig = create_key_msg_sig.2; + + // generate the wrong msg + let mut rng = ChaCha12Rng::from_seed([1; 32]); + let msg: [u8; 16] = rng.gen(); + + assert!( + key.verify_signature(&msg, &sig).err().unwrap() == Error::SignatureVerificationError + ); + } + + #[rstest] + // Expect verify_signature() to fail because the signature is wrong + fn test_verify_signature_fail_wrong_sig(create_key_msg_sig: (PubKey, Vec, Vec)) { + let key = create_key_msg_sig.0; + let msg = create_key_msg_sig.1; + let mut sig = create_key_msg_sig.2; + + // corrupt a byte of signature + sig[10] = sig[10].checked_add(1).unwrap_or(0); + + assert!( + key.verify_signature(&msg, &sig).err().unwrap() == Error::SignatureVerificationError + ); + } +} diff --git a/transcript-core/src/signed.rs b/transcript-core/src/signed.rs new file mode 100644 index 0000000000..71b9b450e9 --- /dev/null +++ b/transcript-core/src/signed.rs @@ -0,0 +1,82 @@ +use crate::{error::Error, tls_handshake::EphemeralECPubkey, HashCommitment, LabelSeed}; +use serde::Serialize; + +#[derive(Clone, Serialize, Default)] +/// TLS handshake-related data which is signed by Notary +pub struct SignedHandshake { + /// notarization time against which the TLS Certificate validity is checked + time: u64, + /// ephemeral pubkey for ECDH key exchange + ephemeral_ec_pubkey: EphemeralECPubkey, + /// User's commitment to [crate::tls_handshake::HandshakeData] + handshake_commitment: HashCommitment, +} + +impl SignedHandshake { + pub fn new( + time: u64, + ephemeral_ec_pubkey: EphemeralECPubkey, + handshake_commitment: HashCommitment, + ) -> Self { + Self { + time, + ephemeral_ec_pubkey, + handshake_commitment, + } + } + + pub fn time(&self) -> u64 { + self.time + } + + pub fn ephemeral_ec_pubkey(&self) -> &EphemeralECPubkey { + &self.ephemeral_ec_pubkey + } + + pub fn handshake_commitment(&self) -> &HashCommitment { + &self.handshake_commitment + } + + #[cfg(any(feature = "expose_setters_for_testing", test))] + pub fn set_handshake_commitment(&mut self, handshake_commitment: HashCommitment) { + self.handshake_commitment = handshake_commitment; + } +} + +/// All the data which the Notary signs +/// (see also comments to the fields with the same name in [crate::document::Document]) +#[derive(Clone, Serialize)] +pub struct Signed { + tls: SignedHandshake, + /// PRG seed from which garbled circuit labels are generated + label_seed: LabelSeed, + /// Merkle root of all the commitments + merkle_root: [u8; 32], +} + +impl Signed { + /// Creates a new struct to be signed by the Notary + pub fn new(tls: SignedHandshake, label_seed: LabelSeed, merkle_root: [u8; 32]) -> Self { + Self { + tls, + label_seed, + merkle_root, + } + } + + pub fn serialize(self) -> Result, Error> { + bincode::serialize(&self).map_err(|_| Error::SerializationError) + } + + pub fn tls(&self) -> &SignedHandshake { + &self.tls + } + + pub fn label_seed(&self) -> &LabelSeed { + &self.label_seed + } + + pub fn merkle_root(&self) -> &[u8; 32] { + &self.merkle_root + } +} diff --git a/transcript-core/src/tls_handshake.rs b/transcript-core/src/tls_handshake.rs new file mode 100644 index 0000000000..0447dac101 --- /dev/null +++ b/transcript-core/src/tls_handshake.rs @@ -0,0 +1,141 @@ +use crate::{error::Error, signed::SignedHandshake}; +use serde::Serialize; + +/// TLSHandshake contains all the info needed to verify the authenticity of the TLS handshake +#[derive(Serialize, Default, Clone)] +pub struct TLSHandshake { + signed_handshake: SignedHandshake, + handshake_data: HandshakeData, +} + +impl TLSHandshake { + pub fn new(signed_handshake: SignedHandshake, handshake_data: HandshakeData) -> Self { + Self { + signed_handshake, + handshake_data, + } + } + + pub fn signed_handshake(&self) -> &SignedHandshake { + &self.signed_handshake + } + + pub fn handshake_data(&self) -> &HandshakeData { + &self.handshake_data + } +} + +/// an x509 certificate in DER format +pub type CertDER = Vec; + +/// Misc TLS handshake data which the User committed to before the User and the Notary engaged in 2PC +/// to compute the TLS session keys +/// +/// The User should not reveal `tls_cert_chain` because the Notary would learn the webserver name +/// from it. The User also should not reveal `sig_ke_params` to the Notary, because +/// for ECDSA sigs it is possible to derive the pubkey from the sig and then use that pubkey to find out +/// the identity of the webserver. +// +/// Note that there is no need to commit to the ephemeral key because it will be signed explicitely +/// by the Notary +#[derive(Serialize, Clone, Default)] +pub struct HandshakeData { + tls_cert_chain: Vec, + sig_ke_params: ServerSignature, + client_random: Vec, + server_random: Vec, +} + +impl HandshakeData { + pub fn new( + tls_cert_chain: Vec, + sig_ke_params: ServerSignature, + client_random: Vec, + server_random: Vec, + ) -> Self { + Self { + tls_cert_chain, + sig_ke_params, + client_random, + server_random, + } + } + + pub fn serialize(&self) -> Result, Error> { + bincode::serialize(&self).map_err(|_| Error::SerializationError) + } + + pub fn tls_cert_chain(&self) -> &Vec { + &self.tls_cert_chain + } + + pub fn sig_ke_params(&self) -> &ServerSignature { + &self.sig_ke_params + } + + pub fn client_random(&self) -> &Vec { + &self.client_random + } + + pub fn server_random(&self) -> &Vec { + &self.server_random + } +} + +/// Types of the ephemeral EC pubkey currently supported by TLSNotary +#[derive(Clone, Serialize, Default)] +pub enum EphemeralECPubkeyType { + #[default] + P256, +} + +/// The ephemeral EC public key (part of the TLS key exchange parameters) +#[derive(Clone, Serialize, Default)] +pub struct EphemeralECPubkey { + typ: EphemeralECPubkeyType, + pubkey: Vec, +} + +impl EphemeralECPubkey { + pub fn new(typ: EphemeralECPubkeyType, pubkey: Vec) -> Self { + Self { typ, pubkey } + } + + pub fn typ(&self) -> &EphemeralECPubkeyType { + &self.typ + } + + pub fn pubkey(&self) -> &Vec { + &self.pubkey + } +} + +/// Algorithms that can be used for signing the TLS key exchange parameters +#[derive(Clone, Serialize, Default)] +#[allow(non_camel_case_types)] +pub enum KEParamsSigAlg { + #[default] + RSA_PKCS1_2048_8192_SHA256, + ECDSA_P256_SHA256, +} + +/// A server's signature over the TLS key exchange parameters +#[derive(Serialize, Clone, Default)] +pub struct ServerSignature { + alg: KEParamsSigAlg, + sig: Vec, +} + +impl ServerSignature { + pub fn new(alg: KEParamsSigAlg, sig: Vec) -> Self { + Self { alg, sig } + } + + pub fn alg(&self) -> &KEParamsSigAlg { + &self.alg + } + + pub fn sig(&self) -> &Vec { + &self.sig + } +} diff --git a/verifier/transcript-verifier/Cargo.toml b/verifier/transcript-verifier/Cargo.toml new file mode 100644 index 0000000000..bd0e19384d --- /dev/null +++ b/verifier/transcript-verifier/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "transcript-verifier" +version = "0.1.0" +edition = "2021" +resolver = "2" + +[dependencies] +transcript-core = { path = "../../transcript-core"} +blake3 = "1.3.3" +p256 = { version = "0.10", features = ["ecdsa"]} +webpki = { version = "0.22.0", features = ["alloc"]} +webpki-roots = "0.22.5" +rand_chacha = "0.3" +rand = "0.8" +rand_core = "0.6" +thiserror = "1" +x509-parser = "0.14.0" +rs_merkle = "1.2.0" +serde = { version = "1.0", features = ["derive"] } +bincode = "1.3.3" + +[dev-dependencies] +transcript-core = { path = "../../transcript-core", features = ["expose_setters_for_testing"] } +tlsn-mpc-core = { path = "../../mpc/mpc-core" } +tlsn-mpc-circuits = { path = "../../mpc/mpc-circuits" } +tlsn-tls-circuits = { path = "../../tls/tls-circuits" } +hex = "0.4" +rstest = "0.12" + diff --git a/verifier/transcript-verifier/src/commitment.rs b/verifier/transcript-verifier/src/commitment.rs new file mode 100644 index 0000000000..4fbd692c2d --- /dev/null +++ b/verifier/transcript-verifier/src/commitment.rs @@ -0,0 +1,203 @@ +use crate::{error::Error, utils::compute_label_commitment}; +use serde::Serialize; +use transcript_core::{ + commitment::{CommitmentOpening, CommitmentType, Direction, TranscriptRange}, + HashCommitment, +}; + +/// A validated User's commitment to a portion of the notarized data +#[derive(Serialize, Clone, Default)] +pub struct Commitment { + /// This commitment's index in `commitments` of [super::UncheckedDoc] + id: u32, + typ: CommitmentType, + direction: Direction, + /// The index of this commitment in the Merkle tree of commitments + merkle_tree_index: u32, + /// The actual commitment + commitment: HashCommitment, + /// The absolute byte ranges within the notarized data. The committed data + /// is located in those ranges. Ranges do not overlap but may be adjacent. + ranges: Vec, +} + +impl Commitment { + /// Verifies this commitment against the opening + pub fn verify(&self, opening: &CommitmentOpening) -> Result<(), Error> { + let expected = match self.typ { + CommitmentType::labels_blake3 => { + let opening = match opening { + CommitmentOpening::LabelsBlake3(opening) => opening, + // will never happen since we checked that commitment and opening types match + #[allow(unreachable_patterns)] + _ => return Err(Error::InternalError), + }; + + compute_label_commitment( + opening.opening(), + &self.ranges, + opening.label_seed(), + opening.salt(), + )? + } + #[allow(unreachable_patterns)] + _ => return Err(Error::InternalError), + }; + + if expected != self.commitment { + return Err(Error::CommitmentVerificationFailed); + } + + Ok(()) + } + + pub fn id(&self) -> u32 { + self.id + } + + pub fn typ(&self) -> &CommitmentType { + &self.typ + } + + pub fn direction(&self) -> &Direction { + &self.direction + } + + pub fn merkle_tree_index(&self) -> u32 { + self.merkle_tree_index + } + + pub fn commitment(&self) -> [u8; 32] { + self.commitment + } + + pub fn ranges(&self) -> &Vec { + &self.ranges + } + + #[cfg(test)] + pub fn set_id(&mut self, id: u32) { + self.id = id; + } + + #[cfg(test)] + pub fn set_ranges(&mut self, ranges: Vec) { + self.ranges = ranges; + } + + #[cfg(test)] + pub fn set_merkle_tree_index(&mut self, merkle_tree_index: u32) { + self.merkle_tree_index = merkle_tree_index; + } + + #[cfg(test)] + pub fn set_commitment(&mut self, commitment: [u8; 32]) { + self.commitment = commitment; + } +} + +impl std::convert::From for Commitment { + fn from(c: transcript_core::commitment::Commitment) -> Self { + Commitment { + id: c.id(), + typ: c.typ().clone(), + direction: c.direction().clone(), + merkle_tree_index: c.merkle_tree_index(), + commitment: c.commitment(), + ranges: c.ranges().clone(), + } + } +} + +#[cfg(test)] +pub mod test { + use crate::{ + commitment::{Commitment, TranscriptRange}, + doc::validated::test::validated_doc, + error::Error, + }; + use rstest::{fixture, rstest}; + use transcript_core::commitment::CommitmentOpening; + + #[fixture] + // Returns a correct label commitment / opening pair + fn get_pair() -> (Commitment, CommitmentOpening) { + let doc = validated_doc(); + ( + doc.commitments()[0].clone(), + doc.commitment_openings()[0].clone(), + ) + } + + #[rstest] + // Expect verify() to succeed + fn verify_success(get_pair: (Commitment, CommitmentOpening)) { + let (commitment, opening) = get_pair; + assert!(commitment.verify(&opening).is_ok()) + } + + #[rstest] + // Expect verify() to fail because an opening byte is incorrect + fn verify_fail_wrong_opening(get_pair: (Commitment, CommitmentOpening)) { + let (commitment, opening) = get_pair; + let mut opening = match opening { + CommitmentOpening::LabelsBlake3(opening) => opening, + }; + let mut old_bytes = opening.opening().clone(); + // corrupt one byte + old_bytes[0] = old_bytes[0].checked_add(1).unwrap_or(0); + opening.set_opening(old_bytes); + + let opening = CommitmentOpening::LabelsBlake3(opening); + + assert!(commitment.verify(&opening).err().unwrap() == Error::CommitmentVerificationFailed) + } + + #[rstest] + // Expect verify() to fail because commitment range is incorrect + fn verify_fail_wrong_range(get_pair: (Commitment, CommitmentOpening)) { + let (mut commitment, opening) = get_pair; + + let mut ranges = commitment.ranges().clone(); + ranges[0] = TranscriptRange::new(ranges[0].start() + 1, ranges[0].end() + 1).unwrap(); + commitment.set_ranges(ranges); + + assert!(commitment.verify(&opening).err().unwrap() == Error::CommitmentVerificationFailed) + } + + #[rstest] + // Expect verify() to fail because label_seed is incorrect + fn verify_fail_wrong_seed(get_pair: (Commitment, CommitmentOpening)) { + let (commitment, opening) = get_pair; + + let mut opening = match opening { + CommitmentOpening::LabelsBlake3(opening) => opening, + }; + let mut seed = *opening.label_seed(); + // corrupt one byte + seed[0] = seed[0].checked_add(1).unwrap_or(0); + opening.set_label_seed(seed); + + let opening = CommitmentOpening::LabelsBlake3(opening); + + assert!(commitment.verify(&opening).err().unwrap() == Error::CommitmentVerificationFailed) + } + + #[rstest] + // Expect verify() to fail because salt is incorrect + fn verify_fail_wrong_salt(get_pair: (Commitment, CommitmentOpening)) { + let (commitment, opening) = get_pair; + + let mut opening = match opening { + CommitmentOpening::LabelsBlake3(opening) => opening, + }; + let mut salt = opening.salt().clone(); + // corrupt one byte + salt[0] = salt[0].checked_add(1).unwrap_or(0); + opening.set_salt(salt); + + let opening = CommitmentOpening::LabelsBlake3(opening); + + assert!(commitment.verify(&opening).err().unwrap() == Error::CommitmentVerificationFailed) + } +} diff --git a/verifier/transcript-verifier/src/doc/checks.rs b/verifier/transcript-verifier/src/doc/checks.rs new file mode 100644 index 0000000000..3d9cf71f20 --- /dev/null +++ b/verifier/transcript-verifier/src/doc/checks.rs @@ -0,0 +1,276 @@ +//! Methods performing various validation checks on the [crate::doc::unchecked::UncheckedDoc] + +use crate::{doc::unchecked::UncheckedDoc, error::Error, utils::overlapping_range}; +use transcript_core::commitment::{CommitmentOpening, CommitmentType}; + +/// Condition checked: at least one commitment is present +pub(super) fn check_at_least_one_commitment_present(unchecked: &UncheckedDoc) -> Result<(), Error> { + if unchecked.commitments().is_empty() { + return Err(Error::ValidationCheckError( + "check_at_least_one_commitment_present".to_string(), + )); + } + Ok(()) +} + +/// Condition checked: each [commitment, opening] pair has their id incremental and ascending. The types +/// of commitment and opening match. +pub(super) fn check_commitment_and_opening_pairs(unchecked: &UncheckedDoc) -> Result<(), Error> { + // ids start from 0 an increment + // (note that we already checked that commitment vec and opening vec have the same length) + for i in 0..unchecked.commitment_openings().len() { + let commitment = &unchecked.commitments()[i]; + let opening = &unchecked.commitment_openings()[i]; + + // extract the opening variant's id + let opening_id = match opening { + CommitmentOpening::LabelsBlake3(ref opening) => opening.id(), + }; + + // ids must match + if !(commitment.id() == (i as u32) && opening_id == (i as u32)) { + return Err(Error::ValidationCheckError( + "check_commitment_and_opening_pairs".to_string(), + )); + } + + // types must match + if matches!(commitment.typ(), &CommitmentType::labels_blake3) + && !matches!(opening, CommitmentOpening::LabelsBlake3(_)) + { + return Err(Error::ValidationCheckError( + "check_commitment_and_opening_pairs".to_string(), + )); + } + } + + Ok(()) +} + +/// Condition checked: commitment count equals opening count +pub(super) fn check_commitment_and_opening_count_equal( + unchecked: &UncheckedDoc, +) -> Result<(), Error> { + if unchecked.commitments().len() != unchecked.commitment_openings().len() { + return Err(Error::ValidationCheckError( + "check_commitment_and_opening_count_equal".to_string(), + )); + } + Ok(()) +} + +/// Condition checked: ranges inside one commitment are non-empty, valid, ascending, non-overlapping +pub(super) fn check_ranges_inside_each_commitment(unchecked: &UncheckedDoc) -> Result<(), Error> { + for c in unchecked.commitments() { + let len = c.ranges().len(); + // at least one range is expected + if len == 0 { + return Err(Error::ValidationCheckError( + "check_ranges_inside_each_commitment".to_string(), + )); + } + + for r in c.ranges() { + // ranges must be valid + if r.end() <= r.start() { + return Err(Error::ValidationCheckError( + "check_ranges_inside_each_commitment".to_string(), + )); + } + } + + // ranges must not overlap and must be ascending relative to each other + for pair in c.ranges().windows(2) { + if pair[1].start() < pair[0].end() { + return Err(Error::ValidationCheckError( + "check_ranges_inside_each_commitment".to_string(), + )); + } + } + } + + Ok(()) +} + +/// Condition checked: the total amount of committed data is less than [super::MAX_TOTAL_COMMITTED_DATA] +pub(super) fn check_max_total_committed_data(unchecked: &UncheckedDoc) -> Result<(), Error> { + // Make sure the grand total in all commitments' ranges is not too large + let mut total_committed = 0u64; + for commitment in unchecked.commitments() { + for r in commitment.ranges() { + total_committed += (r.end() - r.start()) as u64; + if total_committed > super::MAX_TOTAL_COMMITTED_DATA { + return Err(Error::ValidationCheckError( + "check_max_total_committed_data".to_string(), + )); + } + } + } + Ok(()) +} + +/// Condition checked: the length of each opening equals the amount of committed data in the ranges of the +/// corresponding commitment +pub(super) fn check_commitment_sizes(unchecked: &UncheckedDoc) -> Result<(), Error> { + // Make sure each opening's size matches the committed size + for opening in unchecked.commitment_openings() { + let (opening_id, opening_bytes) = match opening { + CommitmentOpening::LabelsBlake3(ref opening) => (opening.id(), opening.opening()), + }; + + // total committed bytes in all ranges of the commitment corresponding to the opening + let mut total_in_ranges = 0u64; + for r in unchecked.commitments()[opening_id as usize].ranges() { + total_in_ranges += (r.end() - r.start()) as u64; + } + if opening_bytes.len() as u64 != total_in_ranges { + return Err(Error::ValidationCheckError( + "check_commitment_sizes".to_string(), + )); + } + } + Ok(()) +} + +/// Condition checked: the amount of commitments is less than [super::MAX_COMMITMENT_COUNT] +pub(super) fn check_commitment_count(unchecked: &UncheckedDoc) -> Result<(), Error> { + if unchecked.commitments().len() >= super::MAX_COMMITMENT_COUNT as usize { + return Err(Error::ValidationCheckError( + "check_commitment_count".to_string(), + )); + } + Ok(()) +} + +/// Condition checked: each Merkle tree index is both unique and also ascending between commitments. +/// Index must not be higher than the index of the last leaf of the tree. +pub(super) fn check_merkle_tree_indices(unchecked: &UncheckedDoc) -> Result<(), Error> { + let indices: Vec = unchecked + .commitments() + .iter() + .map(|c| c.merkle_tree_index()) + .collect(); + for pair in indices.windows(2) { + if pair[0] >= pair[1] { + return Err(Error::ValidationCheckError( + "check_merkle_tree_indices".to_string(), + )); + } + } + + for idx in indices { + if idx >= unchecked.merkle_tree_leaf_count() { + return Err(Error::ValidationCheckError( + "check_merkle_tree_indices".to_string(), + )); + } + } + Ok(()) +} + +/// Makes sure that if two or more commitments contain overlapping ranges, the openings +/// corresponding to those ranges match exactly. Otherwise, if the openings don't match, +/// returns an error. +pub(super) fn check_overlapping_openings(unchecked: &UncheckedDoc) -> Result<(), Error> { + // Note: using an existing lib to find multi-range overlap would incur the need to audit + // that lib for correctness. Instead, since checking for overlap of two ranges is cheap, we use + // a naive way where we compare each range to all other ranges. + // This naive way will have redundancy in computation but it will be easy to audit. + + for needle_c in unchecked.commitments().iter() { + // Naming convention: we use the prefix "needle" to indicate the range that we are + // looking for (and to indicate the associates offsets, commitments and openings). + // Likewise the prefix "haystack" indicates _where_ we are searching. + + // byte offset in the opening; always positioned at the beginning of the range + let mut needle_offset = 0u32; + + for needle_range in needle_c.ranges() { + for haystack_c in unchecked.commitments().iter() { + if needle_c.id() == haystack_c.id() { + // don't search within the same commitment + continue; + } + + // byte offset in the opening; always positioned at the beginning of the range + let mut haystack_offset = 0u32; + // will be set to true when overlap is found + let mut overlap_was_found = false; + + for haystack_range in haystack_c.ranges() { + match overlapping_range(needle_range, haystack_range) { + Some(ov_range) => { + // the bytesize of the overlap + let overlap_size = ov_range.end() - ov_range.start(); + + // Find position (in the openings) from which the overlap starts. The + // offsets are already pointing to the beginning of the range, we just + // need to add the offset **within** the range. + let needle_ov_start = + needle_offset + (ov_range.start() - needle_range.start()); + let haystack_ov_start = + haystack_offset + (ov_range.start() - haystack_range.start()); + + // get the openings which overlapped + let needle_o = &unchecked.commitment_openings()[needle_c.id() as usize]; + let haystack_o = + &unchecked.commitment_openings()[haystack_c.id() as usize]; + + let needle_o_bytes = match needle_o { + CommitmentOpening::LabelsBlake3(opening) => opening.opening(), + }; + let haystack_o_bytes = match haystack_o { + CommitmentOpening::LabelsBlake3(opening) => opening.opening(), + }; + + if needle_o_bytes[needle_ov_start as usize + ..(needle_ov_start + overlap_size) as usize] + != haystack_o_bytes[haystack_ov_start as usize + ..(haystack_ov_start + overlap_size) as usize] + { + return Err(Error::OverlappingOpeningsDontMatch); + } + + // even if set to true on prev iteration, it is ok to set again + overlap_was_found = true; + } + None => { + if overlap_was_found { + // An overlap was found in the previous range of the haystack + // but not in this range. There will be no overlap in any + // following haystack ranges of this commitment since all ranges + // within a commitment are sorted ascendingly relative to each other. + break; + } + // otherwise keep iterating + } + } + + // advance the offset to the beginning of the next range + haystack_offset += haystack_range.end() - haystack_range.start(); + } + } + // advance the offset to the beginning of the next range + needle_offset += needle_range.end() - needle_range.start(); + } + } + + Ok(()) +} + +/// Condition checked: openings of LabelsBlake3Opening type must have their label seed match the +/// label seed which the Notary signed +pub(super) fn check_labels_opening(unchecked: &UncheckedDoc) -> Result<(), Error> { + for opening in unchecked.commitment_openings() { + #[allow(irrefutable_let_patterns)] + if let CommitmentOpening::LabelsBlake3(opening) = opening { + if opening.label_seed() != unchecked.label_seed() { + return Err(Error::ValidationCheckError( + "check_labels_opening".to_string(), + )); + } + } + } + + Ok(()) +} diff --git a/verifier/transcript-verifier/src/doc/mod.rs b/verifier/transcript-verifier/src/doc/mod.rs new file mode 100644 index 0000000000..887e4b8d16 --- /dev/null +++ b/verifier/transcript-verifier/src/doc/mod.rs @@ -0,0 +1,15 @@ +//! Types associated with various stages of the notarization document +pub mod checks; +pub mod unchecked; +pub mod validated; +pub mod verified; + +/// The maximum total size of all committed data in one document. Used to prevent DoS +/// during verification. +/// (this will cause the verifier to hash up to a max of 1GB * 128 = 128GB of labels if the +/// commitment type is [transcript_core::commitment::CommitmentType::labels_blake3]) +const MAX_TOTAL_COMMITTED_DATA: u64 = 1000000000; + +/// The maximum count of commitments in one document. Used to prevent DoS since searching for +/// overlapping commitments in the naive way which we implemented has quadratic cost. +const MAX_COMMITMENT_COUNT: u16 = 1000; diff --git a/verifier/transcript-verifier/src/doc/unchecked.rs b/verifier/transcript-verifier/src/doc/unchecked.rs new file mode 100644 index 0000000000..bfb1498d95 --- /dev/null +++ b/verifier/transcript-verifier/src/doc/unchecked.rs @@ -0,0 +1,705 @@ +use crate::{commitment::Commitment, doc::checks, error::Error, tls_handshake::TLSHandshake}; +use serde::Serialize; +use transcript_core::{commitment::CommitmentOpening, merkle::MerkleProof, LabelSeed}; + +/// Notarization document in its unchecked form +#[derive(Serialize, Clone)] +pub struct UncheckedDoc { + /// All fields are exactly as in [crate::doc::verified::VerifiedDoc] + version: u8, + tls_handshake: TLSHandshake, + signature: Option>, + label_seed: LabelSeed, + merkle_root: [u8; 32], + merkle_tree_leaf_count: u32, + merkle_multi_proof: MerkleProof, + commitments: Vec, + commitment_openings: Vec, +} + +impl UncheckedDoc { + /// Validates the unchecked document + pub fn validate(&self) -> Result<(), Error> { + // Performs the following validation checks: + // + // - at least one commitment is present + checks::check_at_least_one_commitment_present(self)?; + + // - commitment count equals opening count + checks::check_commitment_and_opening_count_equal(self)?; + + // - each [commitment, opening] pair has their id incremental and ascending. The types of commitment + // and opening match. + checks::check_commitment_and_opening_pairs(self)?; + + // - ranges inside one commitment are non-empty, valid, ascending, non-overlapping, non-overflowing + checks::check_ranges_inside_each_commitment(self)?; + + // - the total amount of committed data is not more than [super::MAX_TOTAL_COMMITTED_DATA] + checks::check_max_total_committed_data(self)?; + + // - the length of each opening equals the amount of committed data in the ranges of the + // corresponding commitment + checks::check_commitment_sizes(self)?; + + // - the amount of commitments is not more than [super::MAX_COMMITMENT_COUNT] + checks::check_commitment_count(self)?; + + // - overlapping openings must match exactly + checks::check_overlapping_openings(self)?; + + // - each [merkle_tree_index] is both unique and also ascending between commitments + checks::check_merkle_tree_indices(self)?; + + // - openings of LabelsBlake3 type must have their label seed match the label seed which the + // Notary signed + checks::check_labels_opening(self)?; + + Ok(()) + } + + pub fn version(&self) -> u8 { + self.version + } + + pub fn tls_handshake(&self) -> &TLSHandshake { + &self.tls_handshake + } + + pub fn signature(&self) -> &Option> { + &self.signature + } + + pub fn label_seed(&self) -> &LabelSeed { + &self.label_seed + } + + pub fn merkle_root(&self) -> &[u8; 32] { + &self.merkle_root + } + + pub fn merkle_tree_leaf_count(&self) -> u32 { + self.merkle_tree_leaf_count + } + + pub fn merkle_multi_proof(&self) -> &MerkleProof { + &self.merkle_multi_proof + } + + pub fn commitments(&self) -> &Vec { + &self.commitments + } + + pub fn commitment_openings(&self) -> &Vec { + &self.commitment_openings + } + + #[cfg(test)] + pub fn set_commitments(&mut self, commitments: Vec) { + self.commitments = commitments; + } + + #[cfg(test)] + pub fn set_commitment_openings(&mut self, commitment_openings: Vec) { + self.commitment_openings = commitment_openings; + } + + #[cfg(test)] + pub fn set_signature(&mut self, signature: Option>) { + self.signature = signature; + } + + #[cfg(test)] + pub fn set_merkle_root(&mut self, merkle_root: [u8; 32]) { + self.merkle_root = merkle_root; + } +} + +/// Converts the user's [transcript_core::document::Document] type into a document with types which +/// can be validated and verified +impl std::convert::From for UncheckedDoc { + fn from(d: transcript_core::document::Document) -> Self { + // convert each user's commitment into a type which can be verifier + let commitments: Vec = d + .commitments() + .clone() + .into_iter() + .map(Commitment::from) + .collect(); + + UncheckedDoc { + version: d.version(), + tls_handshake: d.tls_handshake().clone().into(), + signature: d.signature().clone(), + label_seed: *d.label_seed(), + merkle_root: *d.merkle_root(), + merkle_tree_leaf_count: d.merkle_tree_leaf_count(), + merkle_multi_proof: d.merkle_multi_proof().clone(), + commitments, + commitment_openings: d.commitment_openings().clone(), + } + } +} + +#[cfg(test)] +pub mod test { + use super::*; + use crate::{ + doc::{unchecked::UncheckedDoc, MAX_COMMITMENT_COUNT, MAX_TOTAL_COMMITTED_DATA}, + test::{default_unchecked_doc, unchecked_doc}, + }; + use rstest::{fixture, rstest}; + use transcript_core::{ + commitment::{LabelsBlake3Opening, TranscriptRange}, + signed::Signed, + }; + + #[fixture] + // Returns an unchecked document which passes validation and the document's signed portion + pub fn unchecked_doc_valid_and_signed() -> (UncheckedDoc, Signed) { + let (doc, _, signed) = default_unchecked_doc(); + (doc, signed) + } + + #[fixture] + // Returns an unchecked document which passes validation + pub fn unchecked_doc_valid() -> UncheckedDoc { + let (doc, _) = unchecked_doc_valid_and_signed(); + doc + } + + #[fixture] + // Returns a set of valid documents which pass validation. Each document contains overlapping + // commitments. Each document's commitments overlap in a unique way. + fn unchecked_docs_valid_overlap() -> Vec { + let mut docs = Vec::new(); + + // overlap on the left of one of the ranges of comm2 + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(14, 18).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // overlap on the right of one of the ranges of comm2 + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(0, 8).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // one of the ranges of comm2 is fully enveloped by one of the range of comm1 + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(6, 10).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // one of the ranges of comm2 is fully enveloped by one of the range of comm1 + // and the ranges' start bounds match + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(5, 10).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // one of the ranges of comm2 is fully enveloped by one of the range of comm1 + // and the ranges' end bounds match + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(6, 15).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // one of the ranges of comm2 fully envelops one of the range of comm1 + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(3, 17).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // one of the ranges of comm2 fully envelops one of the range of comm1 + // and the ranges' start bounds match + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(5, 17).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // one of the ranges of comm2 fully envelops one of the range of comm1 + // and the ranges' end bounds match + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(3, 15).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + // a range from comm1 matches exactly a range from comm2 + let comm1_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + ]; + let comm2_ranges = vec![ + TranscriptRange::new(5, 15).unwrap(), + TranscriptRange::new(23, 24).unwrap(), + ]; + docs.push(unchecked_doc(vec![comm1_ranges, comm2_ranges]).0); + + docs + } + + #[rstest] + // Expect validation to succeed for a document with non-overlapping commitments + fn validate_success_non_overlapping(unchecked_doc_valid: UncheckedDoc) { + assert!(unchecked_doc_valid.validate().is_ok()) + } + + #[rstest] + // Expect validation to succeed when document has overlapping commitments + fn validate_success_overlapping(unchecked_docs_valid_overlap: Vec) { + for doc in unchecked_docs_valid_overlap { + assert!(doc.validate().is_ok()) + } + } + + #[rstest] + // Expect validation to fail on check_at_least_one_commitment_present() + fn validate_fail_on_check_at_least_one_commitment_present( + mut unchecked_doc_valid: UncheckedDoc, + ) { + // insert empty commitments vec + unchecked_doc_valid.set_commitments(Vec::new()); + assert!( + unchecked_doc_valid.validate().err().unwrap() + == Error::ValidationCheckError("check_at_least_one_commitment_present".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_commitment_and_opening_count_equal() + fn validate_fail_on_check_commitment_and_opening_count_equalt( + mut unchecked_doc_valid: UncheckedDoc, + ) { + // append an extra commitment + let mut original_comms = unchecked_doc_valid.commitments().to_vec(); + original_comms.push(Commitment::default()); + + unchecked_doc_valid.set_commitments(original_comms); + assert!( + unchecked_doc_valid.validate().err().unwrap() + == Error::ValidationCheckError( + "check_commitment_and_opening_count_equal".to_string() + ) + ); + } + + #[rstest] + // Expect validation to fail on check_commitment_and_opening_count_equal() + fn validate_fail_on_check_commitment_and_opening_count_equal( + mut unchecked_doc_valid: UncheckedDoc, + ) { + // append an extra commitment + let mut original_comms = unchecked_doc_valid.commitments().to_vec(); + original_comms.push(Commitment::default()); + + unchecked_doc_valid.set_commitments(original_comms); + assert!( + unchecked_doc_valid.validate().err().unwrap() + == Error::ValidationCheckError( + "check_commitment_and_opening_count_equal".to_string() + ) + ); + } + + #[rstest] + // Expect validation to fail on check_commitment_and_opening_pairs() + fn validate_fail_on_check_commitment_and_opening_pairs(unchecked_doc_valid: UncheckedDoc) { + // ------------------- Change ids so that they don't start from 0 anymore. Keep them in + // incrementing order. + + let mut doc1 = unchecked_doc_valid.clone(); + + // change commitment ids + let mut new_commitments = doc1.commitments().to_vec(); + new_commitments[0].set_id(1); + new_commitments[1].set_id(2); + doc1.set_commitments(new_commitments); + + // change opening ids + let original_openings = doc1.commitment_openings().to_vec(); + let new_openings = original_openings + .iter() + .enumerate() + .map(|(idx, opening)| match opening { + CommitmentOpening::LabelsBlake3(ref opening) => { + let mut new_opening = opening.clone(); + new_opening.set_id(idx as u32 + 1); + CommitmentOpening::LabelsBlake3(new_opening) + } + }) + .collect(); + doc1.set_commitment_openings(new_openings); + + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_commitment_and_opening_pairs".to_string()) + ); + + // ---------------Modify ids to not be incremental + + let mut doc2 = unchecked_doc_valid.clone(); + + // change 2nd commitment id + let mut commitments = doc2.commitments().to_vec(); + commitments[1].set_id(2); + doc2.set_commitments(commitments); + + // change 2nd opening id + let original_openings = doc2.commitment_openings().to_vec(); + let new_opening = match original_openings[1] { + CommitmentOpening::LabelsBlake3(ref opening) => { + let mut new_opening = opening.clone(); + new_opening.set_id(2); + CommitmentOpening::LabelsBlake3(new_opening) + } + }; + + doc2.set_commitment_openings(vec![original_openings[0].clone(), new_opening]); + + assert!( + doc2.validate().err().unwrap() + == Error::ValidationCheckError("check_commitment_and_opening_pairs".to_string()) + ); + + // --------------- Modify commitment id so that commitment-opening pair ids don't match + + let mut doc3 = unchecked_doc_valid; + // change 2nd commitment id + let mut commitments = doc3.commitments().to_vec(); + commitments[1].set_id(2); + doc3.set_commitments(commitments); + + assert!( + doc3.validate().err().unwrap() + == Error::ValidationCheckError("check_commitment_and_opening_pairs".to_string()) + ); + } + + #[ignore = "second opening type not yet implemented"] + #[rstest] + // Expect validation to fail on check_commitment_and_opening_pairs() + fn validate_fail_on_check_commitment_and_opening_pairs2(unchecked_doc_valid: UncheckedDoc) { + // ---------------Modify opening type so that it doesn't match the commitment type + + let mut doc4 = unchecked_doc_valid; + + let original_openings = doc4.commitment_openings().to_vec(); + + // change 1st opening type + + // When a second opening type is implemented, use it here + let new_opening = CommitmentOpening::LabelsBlake3(LabelsBlake3Opening::default()); + + doc4.set_commitment_openings(vec![new_opening, original_openings[1].clone()]); + + assert!( + doc4.validate().err().unwrap() + == Error::ValidationCheckError("check_commitment_and_opening_pairs".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_ranges_inside_each_commitment() + fn validate_fail_on_check_ranges_inside_each_commitment(unchecked_doc_valid: UncheckedDoc) { + //-------------- Change ranges to be empty + let mut doc1 = unchecked_doc_valid.clone(); + + let mut new_commitments = doc1.commitments().to_vec(); + new_commitments[0].set_ranges(Vec::new()); + + doc1.set_commitments(new_commitments); + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_ranges_inside_each_commitment".to_string()) + ); + + //-------------- Change range to be invalid + let mut doc2 = unchecked_doc_valid.clone(); + + let mut new_commitments = doc2.commitments().to_vec(); + let mut new_ranges = new_commitments[0].ranges().clone(); + + new_ranges[0] = TranscriptRange::new_unchecked(5, 5); + new_commitments[0].set_ranges(new_ranges); + + doc2.set_commitments(new_commitments); + assert!( + doc2.validate().err().unwrap() + == Error::ValidationCheckError("check_ranges_inside_each_commitment".to_string()) + ); + + //-------------- Change ranges to overlap + let mut doc3 = unchecked_doc_valid.clone(); + + let mut new_commitments = doc3.commitments().to_vec(); + + new_ranges = vec![ + TranscriptRange::new(5, 19).unwrap(), + TranscriptRange::new(18, 22).unwrap(), + ]; + new_commitments[0].set_ranges(new_ranges); + + doc3.set_commitments(new_commitments); + assert!( + doc3.validate().err().unwrap() + == Error::ValidationCheckError("check_ranges_inside_each_commitment".to_string()) + ); + + //-------------- Change ranges to not be ascending relative to each other + let mut doc4 = unchecked_doc_valid; + + let mut new_commitments = doc4.commitments().to_vec(); + + new_ranges = vec![ + TranscriptRange::new(20, 22).unwrap(), + TranscriptRange::new(5, 19).unwrap(), + ]; + new_commitments[0].set_ranges(new_ranges); + + doc4.set_commitments(new_commitments); + assert!( + doc4.validate().err().unwrap() + == Error::ValidationCheckError("check_ranges_inside_each_commitment".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_max_total_committed_data() + fn validate_fail_on_check_max_total_committed_data(unchecked_doc_valid: UncheckedDoc) { + //-------------- Change total committed data to be > MAX_TOTAL_COMMITMENT_SIZE + let mut doc1 = unchecked_doc_valid; + + let mut new_commitments = doc1.commitments().to_vec(); + let new_ranges = vec![ + TranscriptRange::new(0, MAX_TOTAL_COMMITTED_DATA as u32).unwrap(), + TranscriptRange::new( + MAX_TOTAL_COMMITTED_DATA as u32, + MAX_TOTAL_COMMITTED_DATA as u32 + 1, + ) + .unwrap(), + ]; + + new_commitments[0].set_ranges(new_ranges); + + doc1.set_commitments(new_commitments); + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_max_total_committed_data".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_commitment_sizes() + fn validate_fail_on_check_commitment_sizes(unchecked_doc_valid: UncheckedDoc) { + //-------------- Change commitment range sizes to not correspond to the opening size + + let mut doc1 = unchecked_doc_valid; + + let mut new_commitments = doc1.commitments().to_vec(); + let mut new_ranges = new_commitments[0].ranges().clone(); + new_ranges[1] = + TranscriptRange::new(new_ranges[1].start(), new_ranges[1].end() + 1).unwrap(); + + new_commitments[0].set_ranges(new_ranges); + + doc1.set_commitments(new_commitments); + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_commitment_sizes".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_commitment_count() + fn validate_fail_on_check_commitment_count(unchecked_doc_valid: UncheckedDoc) { + //-------------- Change commitment count to be too high + + let mut doc1 = unchecked_doc_valid; + + let original_commitments = doc1.commitments().to_vec(); + + let new_commitments: Vec = (0..MAX_COMMITMENT_COUNT + 1) + .map(|i| { + let mut new = original_commitments[0].clone(); + // set correct id + new.set_id(i as u32); + new + }) + .collect(); + + // the amount of openings must be adjusted to match the new amount of commitments + let original_openings = doc1.commitment_openings().to_vec(); + + let new_openings: Vec = (0..MAX_COMMITMENT_COUNT + 1) + .map(|i| match original_openings[0] { + CommitmentOpening::LabelsBlake3(ref opening) => { + let mut new_opening = opening.clone(); + // set correct id + new_opening.set_id(i as u32); + CommitmentOpening::LabelsBlake3(new_opening) + } + }) + .collect(); + + doc1.set_commitments(new_commitments); + doc1.set_commitment_openings(new_openings); + + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_commitment_count".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_overlapping_openings() + fn validate_fail_on_check_overlapping_openings( + unchecked_docs_valid_overlap: Vec, + ) { + for mut doc in unchecked_docs_valid_overlap { + // modify openings such that the overlapping bytes will not match: + // first opening's bytes are set to all 'a's + // second opening's bytes are set to all 'b's + + let openings = doc.commitment_openings(); + let new_opening1 = match openings[0] { + CommitmentOpening::LabelsBlake3(ref opening) => { + let mut new_opening = opening.clone(); + // set new opening bytes + new_opening.set_opening(vec![b'a'; opening.opening().len()]); + CommitmentOpening::LabelsBlake3(new_opening) + } + }; + + let new_opening2 = match openings[1] { + CommitmentOpening::LabelsBlake3(ref opening) => { + let mut new_opening = opening.clone(); + // set new opening bytes + new_opening.set_opening(vec![b'b'; opening.opening().len()]); + CommitmentOpening::LabelsBlake3(new_opening) + } + }; + + doc.set_commitment_openings(vec![new_opening1, new_opening2]); + assert!(doc.validate().err().unwrap() == Error::OverlappingOpeningsDontMatch); + } + } + + #[rstest] + // Expect validation to fail on check_merkle_tree_indices() + fn validate_fail_on_check_merkle_tree_indicess(unchecked_doc_valid: UncheckedDoc) { + //-------------- Change merkle tree index on one commitment so that indices + // are not unique anymore + + let mut doc1 = unchecked_doc_valid.clone(); + + let mut commitments = doc1.commitments().to_vec(); + + let comm1_index = commitments[0].merkle_tree_index(); + commitments[1].set_merkle_tree_index(comm1_index); + + doc1.set_commitments(commitments); + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_merkle_tree_indices".to_string()) + ); + + //-------------- Switch commitment indices around so that indices are not ascending anymore + + let mut doc2 = unchecked_doc_valid.clone(); + + let mut commitments = doc2.commitments().to_vec(); + + let comm1_index = commitments[0].merkle_tree_index(); + let comm2_index = commitments[1].merkle_tree_index(); + commitments[0].set_merkle_tree_index(comm2_index); + commitments[1].set_merkle_tree_index(comm1_index); + + doc2.set_commitments(commitments); + assert!( + doc2.validate().err().unwrap() + == Error::ValidationCheckError("check_merkle_tree_indices".to_string()) + ); + + //-------------- Set index to be larger than the index of the last leaf in the tree + let mut doc3 = unchecked_doc_valid; + + let mut commitments = doc3.commitments().to_vec(); + commitments[0].set_merkle_tree_index(doc3.merkle_tree_leaf_count()); + + doc3.set_commitments(commitments); + assert!( + doc3.validate().err().unwrap() + == Error::ValidationCheckError("check_merkle_tree_indices".to_string()) + ); + } + + #[rstest] + // Expect validation to fail on check_labels_opening() + fn validate_fail_on_check_labels_opening(unchecked_doc_valid: UncheckedDoc) { + //-------------- Modify label_seed in the opening so that it doesn't match + // label_seed of the document + + let mut doc1 = unchecked_doc_valid; + + let openings = doc1.commitment_openings(); + + let new_opening1 = match openings[0] { + CommitmentOpening::LabelsBlake3(ref opening) => { + let mut new_opening = opening.clone(); + let mut seed = *opening.label_seed(); + // modify the seed's byte + seed[0] = seed[0].checked_add(1).unwrap_or(0); + new_opening.set_label_seed(seed); + CommitmentOpening::LabelsBlake3(new_opening) + } + }; + + doc1.set_commitment_openings(vec![new_opening1, openings[1].clone()]); + assert!( + doc1.validate().err().unwrap() + == Error::ValidationCheckError("check_labels_opening".to_string()) + ); + } +} diff --git a/verifier/transcript-verifier/src/doc/validated.rs b/verifier/transcript-verifier/src/doc/validated.rs new file mode 100644 index 0000000000..be07d583bc --- /dev/null +++ b/verifier/transcript-verifier/src/doc/validated.rs @@ -0,0 +1,428 @@ +use crate::{ + commitment::Commitment, doc::unchecked::UncheckedDoc, error::Error, tls_handshake::TLSHandshake, +}; +use std::collections::HashMap; +use transcript_core::{ + commitment::{CommitmentOpening, CommitmentType}, + merkle::MerkleProof, + signed::Signed, + LabelSeed, +}; + +/// Notarization document in its validated form (not yet verified) +pub(crate) struct ValidatedDoc { + /// All fields are exactly as in [crate::doc::verified::VerifiedDoc] + version: u8, + tls_handshake: TLSHandshake, + signature: Option>, + label_seed: LabelSeed, + merkle_root: [u8; 32], + merkle_tree_leaf_count: u32, + merkle_multi_proof: MerkleProof, + commitments: Vec, + commitment_openings: Vec, +} + +impl ValidatedDoc { + /// Returns a new [ValidatedDoc] after performing all validation checks + pub(crate) fn from_unchecked(unchecked: UncheckedDoc) -> Result { + unchecked.validate()?; + + // Make sure the Notary's signature is present. + // (If the Verifier IS also the Notary then the signature is NOT needed. `ValidatedDoc` + // should be created with `from_unchecked_with_signed_data()` instead.) + + if unchecked.signature().is_none() { + return Err(Error::SignatureExpected); + } + + Ok(Self { + version: unchecked.version(), + tls_handshake: unchecked.tls_handshake().clone(), + signature: unchecked.signature().clone(), + label_seed: *unchecked.label_seed(), + merkle_root: *unchecked.merkle_root(), + merkle_tree_leaf_count: unchecked.merkle_tree_leaf_count(), + merkle_multi_proof: unchecked.merkle_multi_proof().clone(), + commitments: unchecked.commitments().clone(), + commitment_openings: unchecked.commitment_openings().clone(), + }) + } + + /// Returns a new [ValidatedDoc] after performing all validation checks and adding the signed data. + /// `signed_data` (despite its name) is not actually signed because it was created locally by + /// the calling Verifier who had acted as the Notary during notarization. + pub(crate) fn from_unchecked_with_signed_data( + unchecked: UncheckedDoc, + signed_data: Signed, + ) -> Result { + unchecked.validate()?; + + // Make sure the Notary's signature is NOT present. + // (If the Verifier is NOT the Notary then the Notary's signature IS needed. `ValidatedDoc` + // should be created with `from_unchecked()` instead.) + + if unchecked.signature().is_some() { + return Err(Error::SignatureNotExpected); + } + + // insert `signed_data` which we had created locally + + let tls_handshake = TLSHandshake::new( + signed_data.tls().clone(), + unchecked.tls_handshake().handshake_data().clone(), + ); + let label_seed = *signed_data.label_seed(); + let merkle_root = *signed_data.merkle_root(); + + Ok(Self { + version: unchecked.version(), + tls_handshake, + signature: unchecked.signature().clone(), + label_seed, + merkle_root, + merkle_tree_leaf_count: unchecked.merkle_tree_leaf_count(), + merkle_multi_proof: unchecked.merkle_multi_proof().clone(), + commitments: unchecked.commitments().clone(), + commitment_openings: unchecked.commitment_openings().clone(), + }) + } + + /// Verifies the document. This includes verifying: + /// - the TLS document + /// - the inclusion of commitments in the Merkle tree + /// - each commitment + pub(crate) fn verify(&self, dns_name: &str) -> Result<(), Error> { + self.tls_handshake.verify(dns_name)?; + + self.verify_merkle_proofs()?; + + self.verify_commitments()?; + + Ok(()) + } + + /// Verifies that each commitment is present in the Merkle tree. + /// + /// Note that we already checked in [crate::doc::checks::check_merkle_tree_indices] that indices + /// are unique and ascending + fn verify_merkle_proofs(&self) -> Result<(), Error> { + // collect all merkle tree leaf indices and corresponding hashes + let (leaf_indices, leaf_hashes): (Vec, Vec<[u8; 32]>) = self + .commitments + .iter() + .map(|c| (c.merkle_tree_index() as usize, c.commitment())) + .unzip(); + + // verify the inclusion of multiple leaves + if !self.merkle_multi_proof.0.verify( + self.merkle_root, + &leaf_indices, + &leaf_hashes, + self.merkle_tree_leaf_count as usize, + ) { + return Err(Error::MerkleProofVerificationFailed); + } + + Ok(()) + } + + /// Verifies commitments to notarized data + fn verify_commitments(&self) -> Result<(), Error> { + self.verify_label_commitments()?; + + // verify any other types of commitments here + + Ok(()) + } + + /// Verifies each garbled circuit label commitment against its opening + fn verify_label_commitments(&self) -> Result<(), Error> { + // map each opening to its id + let mut openings_ids: HashMap = HashMap::new(); + for o in &self.commitment_openings { + let opening_id = match o { + CommitmentOpening::LabelsBlake3(opening) => opening.id(), + }; + openings_ids.insert(opening_id as usize, o); + } + + for commitment in &self.commitments { + // we only need label commitments + if commitment.typ() == &CommitmentType::labels_blake3 { + // get a corresponding opening + let opening = match openings_ids.get(&(commitment.id() as usize)) { + Some(opening) => opening, + // should never happen since we already checked that each opening has a + // corresponding commitment in validate() of [crate::doc::unchecked::UncheckedDoc] + _ => return Err(Error::InternalError), + }; + // verify + commitment.verify(opening)?; + } + } + + Ok(()) + } + + pub fn version(&self) -> u8 { + self.version + } + + pub fn tls_handshake(&self) -> &TLSHandshake { + &self.tls_handshake + } + + pub fn signature(&self) -> &Option> { + &self.signature + } + + pub fn label_seed(&self) -> &LabelSeed { + &self.label_seed + } + + pub fn merkle_root(&self) -> &[u8; 32] { + &self.merkle_root + } + + pub fn merkle_tree_leaf_count(&self) -> u32 { + self.merkle_tree_leaf_count + } + + pub fn merkle_multi_proof(&self) -> &MerkleProof { + &self.merkle_multi_proof + } + + pub fn commitments(&self) -> &Vec { + &self.commitments + } + + pub fn commitment_openings(&self) -> &Vec { + &self.commitment_openings + } + + #[cfg(test)] + pub fn set_commitments(&mut self, commitments: Vec) { + self.commitments = commitments; + } + + #[cfg(test)] + pub fn set_merkle_tree_leaf_count(&mut self, merkle_tree_leaf_count: u32) { + self.merkle_tree_leaf_count = merkle_tree_leaf_count; + } + + #[cfg(test)] + pub fn set_merkle_root(&mut self, merkle_root: [u8; 32]) { + self.merkle_root = merkle_root; + } + + #[cfg(test)] + pub fn set_merkle_multi_proof(&mut self, merkle_multi_proof: MerkleProof) { + self.merkle_multi_proof = merkle_multi_proof; + } + + #[cfg(test)] + pub fn set_signature(&mut self, signature: Option>) { + self.signature = signature; + } +} + +/// Extracts relevant fields from [ValidatedDoc]. Those are the fields +/// which the Notary signs. +impl std::convert::From<&ValidatedDoc> for Signed { + fn from(doc: &ValidatedDoc) -> Self { + Signed::new( + doc.tls_handshake().signed_handshake().clone(), + *doc.label_seed(), + *doc.merkle_root(), + ) + } +} + +#[cfg(test)] +pub mod test { + use super::*; + use crate::{ + doc::unchecked::test::{unchecked_doc_valid, unchecked_doc_valid_and_signed}, + test::default_unchecked_doc, + }; + use rstest::{fixture, rstest}; + + #[fixture] + // Returns a validated document + pub(crate) fn validated_doc() -> ValidatedDoc { + validated_doc_and_signed().0 + } + + #[fixture] + // Returns a validated document and its signed portion + fn validated_doc_and_signed() -> (ValidatedDoc, Signed) { + let (unchecked_doc, _, signed) = default_unchecked_doc(); + + (ValidatedDoc::from_unchecked(unchecked_doc).unwrap(), signed) + } + + #[rstest] + // Expect from_unchecked() to fail since no signature is present + fn test_from_unchecked_fail_no_sig() { + let mut unchecked_doc = unchecked_doc_valid(); + unchecked_doc.set_signature(None); + + assert!( + ValidatedDoc::from_unchecked(unchecked_doc).err().unwrap() == Error::SignatureExpected + ); + } + + #[rstest] + // Expect from_unchecked_with_signed_data() to succeed + fn test_from_unchecked_with_signed_data_success() { + let (mut unchecked_doc, signed) = unchecked_doc_valid_and_signed(); + // remove signature, it is not expected to be present + unchecked_doc.set_signature(None); + + // corrupt some part of the doc which was signed, e.g. merkle_root + let mut merkle_root = *unchecked_doc.merkle_root(); + merkle_root[0] = merkle_root[0].checked_add(1).unwrap_or(0); + unchecked_doc.set_merkle_root(merkle_root); + + // the signed portion will replace the corrupted portion and the document will be verified + // successfully + assert!(ValidatedDoc::from_unchecked_with_signed_data(unchecked_doc, signed).is_ok()); + } + + #[rstest] + // Expect from_unchecked_with_signed_data() to fail because a signature is present in the + // document + fn test_from_unchecked_with_signed_fail_sig_present() { + // by default `unchecked_doc` has a signature + let (unchecked_doc, signed) = unchecked_doc_valid_and_signed(); + assert!( + ValidatedDoc::from_unchecked_with_signed_data(unchecked_doc, signed) + .err() + .unwrap() + == Error::SignatureNotExpected + ); + } + + #[rstest] + // Expect verify_merkle_proofs() to succeed + fn verify_merkle_proofs_success(validated_doc: ValidatedDoc) { + assert!(validated_doc.verify_merkle_proofs().is_ok()) + } + + #[rstest] + // Expect verify_merkle_proofs() to fail since one of the commitment's merkle tree index is wrong + fn verify_merkle_proofs_fail_wrong_index(mut validated_doc: ValidatedDoc) { + let mut commitments = validated_doc.commitments().clone(); + let old = commitments[0].merkle_tree_index(); + commitments[0].set_merkle_tree_index(old + 1); + validated_doc.set_commitments(commitments); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } + + #[rstest] + // Expect verify_merkle_proofs() to fail since correct hashes are swapped, i.e now the hashes + // corresponding to indices are incorrect + fn verify_merkle_proofs_fail_wrong_hash(mut validated_doc: ValidatedDoc) { + let mut commitments = validated_doc.commitments().clone(); + let hash1 = commitments[0].commitment(); + let hash2 = commitments[1].commitment(); + commitments[0].set_commitment(hash2); + commitments[1].set_commitment(hash1); + validated_doc.set_commitments(commitments); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } + + #[rstest] + // Expect verify_merkle_proofs() to fail since an extra (correct) leaf was provided + // which is not covered by the proof + fn verify_merkle_proofs_fail_extra_leaf(mut validated_doc: ValidatedDoc) { + let mut commitments = validated_doc.commitments().clone(); + + let mut new_commitment = commitments[0].clone(); + new_commitment.set_merkle_tree_index(1); + new_commitment.set_commitment(crate::test::DUMMY_HASH); + // During validation the tree indices between commitments are checked to be ascending. + // Since this test skipped the validation, we make sure now that indices are ascending. + commitments.splice(1..1, [new_commitment]); + + validated_doc.set_commitments(commitments); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } + + #[rstest] + // Expect verify_merkle_proofs() to fail since a leaf which was covered by the proof + // is missing + fn verify_merkle_proofs_fail_missing_leaf(mut validated_doc: ValidatedDoc) { + let mut commitments = validated_doc.commitments().clone(); + commitments.pop(); + + validated_doc.set_commitments(commitments); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } + + #[rstest] + // Expect verify_merkle_proofs() to fail because of the wrong merkle root + fn verify_merkle_proofs_fail_wrong_root(mut validated_doc: ValidatedDoc) { + let mut old = *validated_doc.merkle_root(); + // corrupt one byte + old[0] = old[0].checked_add(1).unwrap_or(0); + validated_doc.set_merkle_root(old); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } + + #[rstest] + // Expect verify_merkle_proofs() to fail because of the wrong merkle proof + fn verify_merkle_proofs_fail_wrong_proof(mut validated_doc: ValidatedDoc) { + use rs_merkle::{algorithms, MerkleProof as MerkleProof_rs_merkle}; + + let old_proof = validated_doc.merkle_multi_proof().clone(); + let mut bytes = old_proof.0.to_bytes(); + // corrupt one byte + bytes[0] = bytes[0].checked_add(1).unwrap_or(0); + + validated_doc.set_merkle_multi_proof(MerkleProof( + MerkleProof_rs_merkle::::from_bytes(&bytes).unwrap(), + )); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } + + // Ignored for now due to a panic in rs_merkle + // https://github.com/antouhou/rs-merkle/issues/20 + #[ignore = "waiting for a panic in rs_merkle to be fixed"] + #[rstest] + // Expect verify_merkle_proofs() to fail since a wrong count of leaves in the tree is + // provided + fn verify_merkle_proofs_fail_wrong_leaf_count(mut validated_doc: ValidatedDoc) { + validated_doc.set_merkle_tree_leaf_count(validated_doc.merkle_tree_leaf_count() + 1); + + assert!( + validated_doc.verify_merkle_proofs().err().unwrap() + == Error::MerkleProofVerificationFailed + ); + } +} diff --git a/verifier/transcript-verifier/src/doc/verified.rs b/verifier/transcript-verifier/src/doc/verified.rs new file mode 100644 index 0000000000..b3cd1bd15e --- /dev/null +++ b/verifier/transcript-verifier/src/doc/verified.rs @@ -0,0 +1,176 @@ +use crate::{ + commitment::Commitment, doc::validated::ValidatedDoc, error::Error, tls_handshake::TLSHandshake, +}; +use serde::Serialize; +use transcript_core::{ + commitment::CommitmentOpening, merkle::MerkleProof, pubkey::PubKey, signed::Signed, LabelSeed, +}; + +#[derive(Serialize)] +/// A validated and verified notarization document +pub struct VerifiedDoc { + version: u8, + tls_handshake: TLSHandshake, + /// Notary's signature over the [Signed] portion of this doc + signature: Option>, + + /// A PRG seeds from which to generate garbled circuit active labels, see + /// [transcript_core::commitment::CommitmentType::labels_blake3] + label_seed: LabelSeed, + + /// The root of the Merkle tree of all the commitments. The User must prove that each one of the + /// `commitments` is included in the Merkle tree. + /// This approach allows the User to hide from the Notary the exact amount of commitments thus + /// increasing User privacy against the Notary. + /// The root was made known to the Notary before the Notary opened his garbled circuits + /// to the User. + merkle_root: [u8; 32], + + /// The total leaf count in the Merkle tree of commitments. Provided by the User to the Verifier + /// to enable merkle proof verification. + merkle_tree_leaf_count: u32, + + /// A proof that all [commitments] are the leaves of the Merkle tree + merkle_multi_proof: MerkleProof, + + /// User's commitments to various portions of the notarized data, sorted ascendingly by id + commitments: Vec, + + /// Openings for the commitments, sorted ascendingly by id + commitment_openings: Vec, +} + +impl VerifiedDoc { + /// Creates a new [VerifiedDoc] from [ValidatedDoc] + pub(crate) fn from_validated( + validated_doc: ValidatedDoc, + dns_name: &str, + trusted_pubkey: Option, + ) -> Result { + // verify Notary's signature, if any + match (validated_doc.signature(), &trusted_pubkey) { + (Some(sig), Some(pubkey)) => { + verify_doc_signature(pubkey, sig, signed_data(&validated_doc))?; + } + // no pubkey and no signature (this Verifier was also the Notary), do not verify + (None, None) => {} + // either pubkey or signature is missing + _ => { + return Err(Error::NoPubkeyOrSignature); + } + } + + // verify the document + validated_doc.verify(dns_name)?; + + Ok(Self { + version: validated_doc.version(), + tls_handshake: validated_doc.tls_handshake().clone(), + signature: validated_doc.signature().clone(), + label_seed: *validated_doc.label_seed(), + merkle_root: *validated_doc.merkle_root(), + merkle_tree_leaf_count: validated_doc.merkle_tree_leaf_count(), + merkle_multi_proof: validated_doc.merkle_multi_proof().clone(), + commitments: validated_doc.commitments().clone(), + commitment_openings: validated_doc.commitment_openings().clone(), + }) + } + + pub fn tls_handshake(&self) -> &TLSHandshake { + &self.tls_handshake + } + + pub fn commitments(&self) -> &Vec { + &self.commitments + } + + pub fn commitment_openings(&self) -> &Vec { + &self.commitment_openings + } +} + +/// Verifies Notary's signature on that part of the document which was signed +pub(crate) fn verify_doc_signature(pubkey: &PubKey, sig: &[u8], msg: Signed) -> Result<(), Error> { + let msg = msg.serialize().map_err(Error::from)?; + pubkey.verify_signature(&msg, sig)?; + + Ok(()) +} + +/// Extracts the necessary fields from the [ValidatedDoc] into a [Signed] +/// struct and returns it +pub(crate) fn signed_data(doc: &ValidatedDoc) -> Signed { + doc.into() +} + +#[cfg(test)] +pub mod test { + use super::*; + use crate::test::default_unchecked_doc; + use rstest::{fixture, rstest}; + use transcript_core::pubkey::{KeyType, PubKey}; + + #[fixture] + // Returns a signed validated document and the pubkey used to sign it + pub(crate) fn signed_validated_doc_and_pubkey() -> (ValidatedDoc, PubKey) { + let (doc, pubkey_bytes, _) = default_unchecked_doc(); + // Initially the Verifier may store the Notary's pubkey as bytes. Converts it into + // PubKey type + let trusted_pubkey = PubKey::from_bytes(KeyType::P256, &pubkey_bytes).unwrap(); + (ValidatedDoc::from_unchecked(doc).unwrap(), trusted_pubkey) + } + + #[rstest] + // Expect from_validated() to succeed with the correct signature and a pubkey provided + fn test_from_validated_success_with_sig_and_pubkey( + signed_validated_doc_and_pubkey: (ValidatedDoc, PubKey), + ) { + let validated_doc = signed_validated_doc_and_pubkey.0; + let trusted_pubkey = signed_validated_doc_and_pubkey.1; + + assert!( + VerifiedDoc::from_validated(validated_doc, "tlsnotary.org", Some(trusted_pubkey)) + .is_ok() + ); + } + + #[rstest] + // Expect from_validated() to succeed when there is no signature and no pubkey provided + fn test_from_validated_success_no_sig_and_pubkey( + signed_validated_doc_and_pubkey: (ValidatedDoc, PubKey), + ) { + let mut validated_doc = signed_validated_doc_and_pubkey.0; + validated_doc.set_signature(None); + + assert!(VerifiedDoc::from_validated(validated_doc, "tlsnotary.org", None).is_ok()); + } + + #[rstest] + // Expect from_validated() to fail when there is no signature but the pubkey is provided + fn test_from_validated_fail_no_sig(signed_validated_doc_and_pubkey: (ValidatedDoc, PubKey)) { + let mut validated_doc = signed_validated_doc_and_pubkey.0; + let trusted_pubkey = signed_validated_doc_and_pubkey.1; + + validated_doc.set_signature(None); + + assert!( + VerifiedDoc::from_validated(validated_doc, "tlsnotary.org", Some(trusted_pubkey)) + .err() + .unwrap() + == Error::NoPubkeyOrSignature + ); + } + + #[rstest] + // Expect from_validated() to fail when there is signature but no pubkey is provided + fn test_from_validated_fail_no_pubkey(signed_validated_doc_and_pubkey: (ValidatedDoc, PubKey)) { + let validated_doc = signed_validated_doc_and_pubkey.0; + + assert!( + VerifiedDoc::from_validated(validated_doc, "tlsnotary.org", None) + .err() + .unwrap() + == Error::NoPubkeyOrSignature + ); + } +} diff --git a/verifier/transcript-verifier/src/error.rs b/verifier/transcript-verifier/src/error.rs new file mode 100644 index 0000000000..17b23f34cc --- /dev/null +++ b/verifier/transcript-verifier/src/error.rs @@ -0,0 +1,52 @@ +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum Error { + #[error("Can't verify the document because either signature or pubkey were not provided")] + NoPubkeyOrSignature, + #[error("The document is expected to contain a signature")] + SignatureExpected, + #[error("The document is NOT expected to contain a signature")] + SignatureNotExpected, + #[error("x509-parser error: {0}")] + X509ParserError(String), + #[error("webpki error: {0}")] + WebpkiError(String), + #[error("Certificate chain was empty")] + EmptyCertificateChain, + #[error("End entity must not be a certificate authority")] + EndEntityIsCA, + #[error("Key exchange data was signed using an unknown curve")] + UnknownCurveInKeyExchange, + #[error("Key exchange data was signed using an unknown algorithm")] + UnknownSigningAlgorithmInKeyExchange, + #[error("Commitment verification failed")] + CommitmentVerificationFailed, + #[error("Error while performing validation check in: {0}")] + ValidationCheckError(String), + #[error("Failed to verify a Merkle proof")] + MerkleProofVerificationFailed, + #[error("Overlapping openings don't match")] + OverlappingOpeningsDontMatch, + #[error("Failed while checking committed TLS")] + CommittedTLSCheckFailed, + #[error("An internal error occured")] + InternalError, + #[error("An internal error during serialization or deserialization")] + SerializationError, + #[error("Error during signature verification")] + SignatureVerificationError, + #[error("Attempted to create an invalid range")] + RangeInvalid, +} + +use transcript_core::error::Error as CoreError; + +impl std::convert::From for Error { + fn from(e: CoreError) -> Self { + match e { + CoreError::InternalError => Error::InternalError, + CoreError::RangeInvalid => Error::RangeInvalid, + CoreError::SerializationError => Error::SerializationError, + CoreError::SignatureVerificationError => Error::SignatureVerificationError, + } + } +} diff --git a/verifier/transcript-verifier/src/label_encoder.rs b/verifier/transcript-verifier/src/label_encoder.rs new file mode 100644 index 0000000000..7110fd17e5 --- /dev/null +++ b/verifier/transcript-verifier/src/label_encoder.rs @@ -0,0 +1,104 @@ +//! Adapted from tlsn/mpc/mpc-core, except [encode() in](ChaChaEncoder) was modified to encode 1 bit +//! at a time +use rand::{CryptoRng, Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use std::ops::BitXor; +use transcript_core::LabelSeed; + +const DELTA_STREAM_ID: u64 = u64::MAX; +/// PLAINTEXT_STREAM_ID must match the id of the plaintext input in tls/tls-circuits/src/c6.rs +const PLAINTEXT_STREAM_ID: u64 = 4; + +#[derive(Clone, Copy)] +pub struct Block(u128); + +impl Block { + #[inline] + pub fn new(b: u128) -> Self { + Self(b) + } + + #[inline] + pub fn random(rng: &mut R) -> Self { + Self::new(rng.gen()) + } + + #[inline] + pub fn set_lsb(&mut self) { + self.0 |= 1; + } + + #[inline] + pub fn inner(&self) -> u128 { + self.0 + } +} + +impl BitXor for Block { + type Output = Self; + + #[inline] + fn bitxor(self, other: Self) -> Self::Output { + Self(self.0 ^ other.0) + } +} + +/// Global binary offset used by the Free-XOR technique to create wire label +/// pairs where W_1 = W_0 ^ Delta. +/// +/// In accordance with the (p&p) permute-and-point technique, the LSB of delta is set to 1 so +/// the permute bit LSB(W_1) = LSB(W_0) ^ 1 +#[derive(Clone, Copy)] +pub struct Delta(Block); + +impl Delta { + /// Creates new random Delta + pub(crate) fn random(rng: &mut R) -> Self { + let mut block = Block::random(rng); + block.set_lsb(); + Self(block) + } + + /// Returns the inner block + #[inline] + pub(crate) fn into_inner(self) -> Block { + self.0 + } +} + +/// Encodes wires into labels using the ChaCha algorithm. +pub struct ChaChaEncoder { + rng: ChaCha20Rng, + delta: Delta, +} + +impl ChaChaEncoder { + /// Creates a new encoder with the provided seed + /// + /// * `seed` - 32-byte seed for ChaChaRng + pub fn new(seed: LabelSeed) -> Self { + let mut rng = ChaCha20Rng::from_seed(seed); + + // Stream id u64::MAX is reserved to generate delta. + // This way there is only ever 1 delta per seed + rng.set_stream(DELTA_STREAM_ID); + let delta = Delta::random(&mut rng); + + Self { rng, delta } + } + + /// Encodes one bit of plaintext into two labels + /// + /// * `pos` - The position of a bit which needs to be encoded + pub fn encode(&mut self, pos: usize) -> [Block; 2] { + self.rng.set_stream(PLAINTEXT_STREAM_ID); + + // jump to the multiple-of-128 bit offset (128 bits is the size of one label) + // (the argument to `set_word_pos()` is a 32-bit word) + self.rng.set_word_pos((pos as u128) * 4); + + let zero_label = Block::random(&mut self.rng); + + [zero_label, zero_label ^ self.delta.into_inner()] + } +} diff --git a/verifier/transcript-verifier/src/lib.rs b/verifier/transcript-verifier/src/lib.rs new file mode 100644 index 0000000000..c484c0cd28 --- /dev/null +++ b/verifier/transcript-verifier/src/lib.rs @@ -0,0 +1,329 @@ +mod commitment; +mod doc; +mod error; +mod label_encoder; +mod tls_handshake; +mod utils; +pub mod verified_transcript; +mod webpki_utils; + +use crate::{ + doc::{unchecked::UncheckedDoc, validated::ValidatedDoc, verified::VerifiedDoc}, + error::Error, + verified_transcript::VerifiedTranscript, +}; +use transcript_core::{document::Document, pubkey::PubKey, signed::Signed}; + +/// Verifier of the notarization document. The document contains commitments to the TLS +/// transcript. +/// +/// Once the verification succeeds, an application level (e.g. HTTP, JSON) parser can +/// parse the resulting transcript [crate::verified_transcript::VerifiedTranscript] +pub struct TranscriptVerifier {} + +impl TranscriptVerifier { + /// Creates a new TranscriptVerifier + pub fn new() -> Self { + Self {} + } + + /// Verifies that the notarization document resulted from notarizing data from a TLS server with the + /// DNS name `dns_name`. Also verifies the Notary's signature (if any). + /// + /// IMPORTANT: + /// if the notarized application data type is HTTP, the checks below will not be sufficient. You must + /// also check on the HTTP parser's level against domain fronting. + /// + /// * document - The notarization document to be validated and verified + /// * dns_name - A DNS name. Must be exactly as it appears in the server's TLS certificate. + /// * signed - If this Verifier acted as the Notary, he provides his [Signed] struct + /// * trusted_pubkey - A trusted Notary's pubkey (if this Verifier acted as the Notary then no + /// pubkey needs to be provided) + pub fn verify( + &self, + document: Document, + dns_name: &str, + trusted_pubkey: Option, + signed: Option, + ) -> Result { + // convert the user's document into a document with types which can be validated + // and verified + let unchecked_doc = UncheckedDoc::from(document); + + // validate the document + let validated_doc = match signed { + None => ValidatedDoc::from_unchecked(unchecked_doc)?, + Some(signed) => ValidatedDoc::from_unchecked_with_signed_data(unchecked_doc, signed)?, + }; + + // verify the document + let verified_doc = VerifiedDoc::from_validated(validated_doc, dns_name, trusted_pubkey)?; + + // extract the verified transcript + let verified_transcript = VerifiedTranscript::from_verified_doc(verified_doc, dns_name); + + Ok(verified_transcript) + } +} + +#[cfg(test)] +mod test { + use crate::doc::unchecked::UncheckedDoc; + use blake3::Hasher; + use transcript_core::{ + commitment::{ + CommitmentOpening, CommitmentType, Direction, LabelsBlake3Opening, TranscriptRange, + }, + document::Document, + merkle::MerkleProof, + signed::{Signed, SignedHandshake}, + tls_handshake::{ + EphemeralECPubkey, EphemeralECPubkeyType, HandshakeData, KEParamsSigAlg, + ServerSignature, TLSHandshake, + }, + HashCommitment, LabelSeed, + }; + + use mpc_circuits::Value; + use mpc_core::garble::{ChaChaEncoder, Encoder, Label}; + use p256::ecdsa::{signature::Signer, SigningKey, VerifyingKey}; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha12Rng; + use rs_merkle::{algorithms::Sha256, MerkleTree}; + use tls_circuits::c6; + + // the leaves of the tree with indices [1..8] will have a dummy value + pub const DUMMY_HASH: [u8; 32] = [0u8; 32]; + + // unix time when the cert chain was valid + pub const TIME: u64 = 1671637529; + + // plaintext padded to a multiple of 16 bytes + pub const DEFAULT_PLAINTEXT: [u8; 48] = *b"This important data will be notarized..........."; + + /// Returns default ranges which are used to construct the default document + pub fn default_ranges() -> Vec { + vec![ + // sent data commitment's ranges + TranscriptRange::new(5, 20).unwrap(), + TranscriptRange::new(20, 22).unwrap(), + // received data commitment's ranges + TranscriptRange::new(0, 2).unwrap(), + TranscriptRange::new(15, 20).unwrap(), + ] + } + + /// Constructs a default signed unchecked document with the provided commitments. Returns the doc, the pubkey + /// used to sign it, and the Signed portion of the doc. + pub fn default_unchecked_doc() -> (UncheckedDoc, Vec, Signed) { + let ranges = default_ranges(); + let comm1_ranges = vec![ranges[0].clone(), ranges[1].clone()]; + let comm2_ranges = vec![ranges[2].clone(), ranges[3].clone()]; + unchecked_doc(vec![comm1_ranges, comm2_ranges]) + } + + /// Constructs a signed unchecked document with the provided commitment ranges. Returns the doc, + /// the pubkey used to sign it, and the Signed portion of the doc. + pub fn unchecked_doc( + // 2 ranges for the first commitment and 2 ranges for the second commitment + commitment_ranges: Vec>, + ) -> (UncheckedDoc, Vec, Signed) { + if commitment_ranges.len() != 2 { + panic!("two commitments are expected") + } + let mut rng = ChaCha12Rng::from_seed([0; 32]); + + // -------- After the webserver sends the Server Key Exchange message (during the TLS handshake), + // the tls-client module provides the following TLS data: + + /// end entity cert + static EE: &[u8] = include_bytes!("testdata/tlsnotary.org/ee.der"); + // intermediate cert + static INTER: &[u8] = include_bytes!("testdata/tlsnotary.org/inter.der"); + // certificate authority cert + static CA: &[u8] = include_bytes!("testdata/tlsnotary.org/ca.der"); + let cert_chain = vec![CA.to_vec(), INTER.to_vec(), EE.to_vec()]; + + // data taken from an actual network trace captured with `tcpdump host tlsnotary.org -w out.pcap` + // (see testdata/key_exchange/README for details) + + let client_random = + hex::decode("ac3808970faf996d38864e205c6b787a1d05f681654a5d2a3c87f7dd2f13332e") + .unwrap(); + let server_random = + hex::decode("8abf9a0c4b3b9694edac3d19e8eb7a637bfa8fe5644bd9f1444f574e47524401") + .unwrap(); + let ephemeral_pubkey = hex::decode("04521e456448e6156026bb1392e0a689c051a84d67d353ab755fce68a2e9fba68d09393fa6485db84517e16d9855ce5ba3ec2293f2e511d1e315570531722e9788").unwrap(); + let sig = hex::decode("337aa65793562550f6de0a9c792b5f531a96bb78f65a2063f710bfb99e11c791e13d35c798b50eea1351c14efc526009c7836e888206cebde7135130a1fbc049d42e1d1ed05c10f0d108b9540f049ac24fe1076d391b9da3d4e60b5cb8f341bda993f6002873847be744c1955ff575b2d833694fb8a432898c5ac55752e2bddcee4c07371335e1a6581694df43c6eb0ce8da4cdd497c205607b573f9c5d17c951e0a71fbf967c4bff53fc37c597b2f5656478fefb780e8f37bd8409985dd980eda4f254c7dce76dc69e66ed27c0f2c93b53a6dfd7b27359e1589a30d483725e92305766c62d6cad2c0142d3a3c4a2272e6d81eda2886ef12028167f83b3c33ea").unwrap(); + + let server_sig = ServerSignature::new(KEParamsSigAlg::RSA_PKCS1_2048_8192_SHA256, sig); + + let ephemeral_pubkey = + EphemeralECPubkey::new(EphemeralECPubkeyType::P256, ephemeral_pubkey); + + // -------- Using the above data, the User computes [HandshakeData] and sends a commitment to + // the Notary + + let handshake_data = + HandshakeData::new(cert_chain, server_sig, client_random, server_random); + let handshake_commitment = blake3(&handshake_data.serialize().unwrap()); + + // -------- The Notary generates garbled circuit's labels from a PRG seed (label_seed which + // was passed in). + + // ---------- After the notarization session is over and after the Notary revealed his label_seed: + + let label_seed: LabelSeed = rng.gen(); + let mut enc = ChaChaEncoder::new(label_seed); + + // encoder works only on the `Input` type. This is the only way to obtain it + // c6 is the AES encryption circuit, input with id == 4 is the plaintext + let input = c6().input(4).unwrap(); + + // since `input` is a 16-byte value, encode one 16-byte chunk at a time + let active_labels: Vec