Skip to content

Commit f16d723

Browse files
authored
refactor(core): DoS mitigation and additional validation (#648)
* add encoding proof validation * check that merkle tree indices are not out of bounds * limit opened plaintext hash data * add test * formatting * bump commitment tree size cap to 30 bits * remove unnecessary test * fix stray lines
1 parent 9253ada commit f16d723

File tree

6 files changed

+112
-21
lines changed

6 files changed

+112
-21
lines changed

crates/core/src/connection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,11 @@ impl ServerCertData {
314314
return Err(CertificateVerificationError::InvalidServerEphemeralKey);
315315
}
316316

317-
// Verify server name
317+
// Verify server name.
318318
let server_name = tls_core::dns::ServerName::try_from(server_name.as_ref())
319319
.map_err(|_| CertificateVerificationError::InvalidIdentity(server_name.clone()))?;
320320

321-
// Verify server certificate
321+
// Verify server certificate.
322322
let cert_chain = self
323323
.certs
324324
.clone()

crates/core/src/merkle.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ impl MerkleError {
1919
#[derive(Clone, Serialize, Deserialize)]
2020
pub(crate) struct MerkleProof {
2121
alg: HashAlgId,
22-
tree_len: usize,
22+
leaf_count: usize,
2323
proof: rs_merkle::MerkleProof<Hash>,
2424
}
2525

@@ -55,13 +55,18 @@ impl MerkleProof {
5555
root.value,
5656
&indices,
5757
&leaves,
58-
self.tree_len,
58+
self.leaf_count,
5959
) {
6060
return Err(MerkleError::new("invalid merkle proof"));
6161
}
6262

6363
Ok(())
6464
}
65+
66+
/// Returns the leaf count of the Merkle tree associated with the proof.
67+
pub(crate) fn leaf_count(&self) -> usize {
68+
self.leaf_count
69+
}
6570
}
6671

6772
#[derive(Clone)]
@@ -118,15 +123,21 @@ impl MerkleTree {
118123
/// # Panics
119124
///
120125
/// - If the provided indices are not unique and sorted.
126+
/// - If the provided indices are out of bounds.
121127
pub(crate) fn proof(&self, indices: &[usize]) -> MerkleProof {
122128
assert!(
123129
indices.windows(2).all(|w| w[0] < w[1]),
124130
"indices must be unique and sorted"
125131
);
126132

133+
assert!(
134+
*indices.last().unwrap() < self.tree.leaves_len(),
135+
"one or more provided indices are out of bounds"
136+
);
137+
127138
MerkleProof {
128139
alg: self.alg,
129-
tree_len: self.tree.leaves_len(),
140+
leaf_count: self.tree.leaves_len(),
130141
proof: self.tree.proof(indices),
131142
}
132143
}
@@ -213,6 +224,21 @@ mod test {
213224
_ = tree.proof(&[2, 4, 3]);
214225
}
215226

227+
#[rstest]
228+
#[case::sha2(Sha256::default())]
229+
#[case::blake3(Blake3::default())]
230+
#[case::keccak(Keccak256::default())]
231+
#[should_panic]
232+
fn test_proof_fail_index_out_of_bounds<H: HashAlgorithm>(#[case] hasher: H) {
233+
let mut tree = MerkleTree::new(hasher.id());
234+
235+
let leaves = leaves(&hasher, [T(0), T(1), T(2), T(3), T(4)]);
236+
237+
tree.insert(&hasher, leaves.clone());
238+
239+
_ = tree.proof(&[2, 3, 4, 6]);
240+
}
241+
216242
#[rstest]
217243
#[case::sha2(Sha256::default())]
218244
#[case::blake3(Blake3::default())]
@@ -257,12 +283,20 @@ mod test {
257283

258284
tree.insert(&hasher, leaves.clone());
259285

260-
let mut proof = tree.proof(&[2, 3, 4]);
286+
let mut proof1 = tree.proof(&[2, 3, 4]);
287+
let mut proof2 = proof1.clone();
261288

262-
proof.tree_len += 1;
289+
// Increment the `leaf_count` field.
290+
proof1.leaf_count += 1;
291+
// Decrement the `leaf_count` field.
292+
proof2.leaf_count -= 1;
263293

264294
// Fail because leaf count is wrong.
265-
assert!(proof
295+
assert!(proof1
296+
.verify(&hasher, &tree.root(), choose_leaves([2, 3, 4], &leaves))
297+
.is_err());
298+
299+
assert!(proof2
266300
.verify(&hasher, &tree.root(), choose_leaves([2, 3, 4], &leaves))
267301
.is_err());
268302
}

crates/core/src/transcript/commit.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ use crate::{
1010
transcript::{Direction, Idx, Transcript},
1111
};
1212

13+
/// The maximum allowed total bytelength of committed data for a single
14+
/// commitment kind. Used to prevent DoS during verification. (May cause the
15+
/// verifier to hash up to a max of 1GB * 128 = 128GB of data for certain kinds
16+
/// of encoding commitments.)
17+
///
18+
/// This value must not exceed bcs's MAX_SEQUENCE_LENGTH limit (which is (1 <<
19+
/// 31) - 1 by default)
20+
pub(crate) const MAX_TOTAL_COMMITTED_DATA: usize = 1_000_000_000;
21+
1322
/// Kind of transcript commitment.
1423
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1524
pub enum TranscriptCommitmentKind {

crates/core/src/transcript/encoding.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@ use serde::{Deserialize, Serialize};
1717

1818
use crate::hash::{impl_domain_separator, TypedHash};
1919

20-
/// The maximum allowed total bytelength of committed data for a single
21-
/// commitment kind. Used to prevent DoS during verification. (May cause the
22-
/// verifier to hash up to a max of 1GB * 128 = 128GB of data for certain kinds
23-
/// of encoding commitments.)
24-
///
25-
/// This value must not exceed bcs's MAX_SEQUENCE_LENGTH limit (which is (1 <<
26-
/// 31) - 1 by default)
27-
const MAX_TOTAL_COMMITTED_DATA: usize = 1_000_000_000;
28-
2920
/// Transcript encoding commitment.
3021
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3122
pub struct EncodingCommitment {

crates/core/src/transcript/encoding/proof.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ use crate::{
77
hash::{Blinded, Blinder, HashAlgorithmExt, HashProviderError},
88
merkle::{MerkleError, MerkleProof},
99
transcript::{
10-
encoding::{
11-
new_encoder, tree::EncodingLeaf, Encoder, EncodingCommitment, MAX_TOTAL_COMMITTED_DATA,
12-
},
10+
commit::MAX_TOTAL_COMMITTED_DATA,
11+
encoding::{new_encoder, tree::EncodingLeaf, Encoder, EncodingCommitment},
1312
Direction, PartialTranscript, Subsequence,
1413
},
1514
CryptoProvider,
@@ -27,6 +26,7 @@ opaque_debug::implement!(Opening);
2726

2827
/// An encoding commitment proof.
2928
#[derive(Debug, Clone, Serialize, Deserialize)]
29+
#[serde(try_from = "validation::EncodingProofUnchecked")]
3030
pub struct EncodingProof {
3131
/// The proof of inclusion of the commitment(s) in the Merkle tree of
3232
/// commitments.
@@ -177,6 +177,52 @@ impl From<MerkleError> for EncodingProofError {
177177
}
178178
}
179179

180+
/// Invalid encoding proof error.
181+
#[derive(Debug, thiserror::Error)]
182+
#[error("invalid encoding proof: {0}")]
183+
pub struct InvalidEncodingProof(&'static str);
184+
185+
mod validation {
186+
use super::*;
187+
188+
/// The maximum allowed height of the Merkle tree of encoding commitments.
189+
///
190+
/// The statistical security parameter (SSP) of the encoding commitment
191+
/// protocol is calculated as "the number of uniformly random bits in a
192+
/// single bit's encoding minus `MAX_HEIGHT`".
193+
///
194+
/// For example, a bit encoding used in garbled circuits typically has 127
195+
/// uniformly random bits, hence when using it in the encoding
196+
/// commitment protocol, the SSP is 127 - 30 = 97 bits.
197+
///
198+
/// Leaving this validation here as a fail-safe in case we ever start
199+
/// using shorter encodings.
200+
const MAX_HEIGHT: usize = 30;
201+
202+
#[derive(Debug, Deserialize)]
203+
pub(super) struct EncodingProofUnchecked {
204+
inclusion_proof: MerkleProof,
205+
openings: HashMap<usize, Opening>,
206+
}
207+
208+
impl TryFrom<EncodingProofUnchecked> for EncodingProof {
209+
type Error = InvalidEncodingProof;
210+
211+
fn try_from(unchecked: EncodingProofUnchecked) -> Result<Self, Self::Error> {
212+
if unchecked.inclusion_proof.leaf_count() > 1 << MAX_HEIGHT {
213+
return Err(InvalidEncodingProof(
214+
"the height of the tree exceeds the maximum allowed",
215+
));
216+
}
217+
218+
Ok(Self {
219+
inclusion_proof: unchecked.inclusion_proof,
220+
openings: unchecked.openings,
221+
})
222+
}
223+
}
224+
}
225+
180226
#[cfg(test)]
181227
mod test {
182228
use tlsn_data_fixtures::http::{request::POST_JSON, response::OK_JSON};

crates/core/src/transcript/proof.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
attestation::Body,
99
index::Index,
1010
transcript::{
11-
commit::TranscriptCommitmentKind,
11+
commit::{TranscriptCommitmentKind, MAX_TOTAL_COMMITTED_DATA},
1212
encoding::{EncodingProof, EncodingProofError, EncodingTree},
1313
hash::{PlaintextHashProof, PlaintextHashProofError, PlaintextHashSecret},
1414
Direction, Idx, PartialTranscript, Transcript,
@@ -59,6 +59,8 @@ impl TranscriptProof {
5959
}
6060

6161
// Verify hash openings.
62+
let mut total_opened = 0u128;
63+
6264
for proof in self.hash_proofs {
6365
let commitment = attestation_body
6466
.plaintext_hashes()
@@ -71,6 +73,15 @@ impl TranscriptProof {
7173
)
7274
})?;
7375

76+
// Make sure the amount of data being proved is bounded.
77+
total_opened += commitment.idx.len() as u128;
78+
if total_opened > MAX_TOTAL_COMMITTED_DATA as u128 {
79+
return Err(TranscriptProofError::new(
80+
ErrorKind::Hash,
81+
"exceeded maximum allowed data",
82+
))?;
83+
}
84+
7485
let (direction, seq) = proof.verify(&provider.hash, commitment)?;
7586
transcript.union_subsequence(direction, &seq);
7687
}

0 commit comments

Comments
 (0)