Skip to content

Commit f2dae8c

Browse files
committed
feat(client-lib): enhanced security check when verifying with cache enabled
By not using the cache until we cross an epoch boundary to make sure that the avk in certificate to check was included in a certificate from the previous epoch.
1 parent 99f52a4 commit f2dae8c

File tree

1 file changed

+95
-36
lines changed
  • mithril-client/src/certificate_client

1 file changed

+95
-36
lines changed

mithril-client/src/certificate_client/verify.rs

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl MithrilCertificateVerifier {
104104
Ok(None)
105105
}
106106

107-
async fn verify_one(
107+
async fn verify_with_cache(
108108
&self,
109109
certificate_chain_validation_id: &str,
110110
certificate: CertificateToVerify,
@@ -123,23 +123,25 @@ impl MithrilCertificateVerifier {
123123
hash: previous_hash,
124124
}))
125125
} else {
126-
self.verify_not_cached_certificate(certificate_chain_validation_id, certificate)
127-
.await
126+
let certificate = match certificate {
127+
CertificateToVerify::Downloaded { certificate } => certificate,
128+
CertificateToVerify::ToDownload { hash } => {
129+
self.retriever.get_certificate_details(&hash).await?
130+
}
131+
};
132+
133+
let previous_certificate = self
134+
.verify_without_cache(certificate_chain_validation_id, certificate)
135+
.await?;
136+
Ok(previous_certificate.map(Into::into))
128137
}
129138
}
130139

131-
async fn verify_not_cached_certificate(
140+
async fn verify_without_cache(
132141
&self,
133142
certificate_chain_validation_id: &str,
134-
certificate: CertificateToVerify,
135-
) -> MithrilResult<Option<CertificateToVerify>> {
136-
let certificate = match certificate {
137-
CertificateToVerify::Downloaded { certificate } => certificate,
138-
CertificateToVerify::ToDownload { hash } => {
139-
self.retriever.get_certificate_details(&hash).await?
140-
}
141-
};
142-
143+
certificate: Certificate,
144+
) -> MithrilResult<Option<Certificate>> {
143145
let previous_certificate = self
144146
.internal_verifier
145147
.verify_certificate(&certificate, &self.genesis_verification_key)
@@ -162,7 +164,7 @@ impl MithrilCertificateVerifier {
162164
})
163165
.await;
164166

165-
Ok(previous_certificate.map(|cert| CertificateToVerify::Downloaded { certificate: cert }))
167+
Ok(previous_certificate)
166168
}
167169
}
168170

@@ -182,6 +184,12 @@ impl CertificateToVerify {
182184
}
183185
}
184186

187+
impl From<Certificate> for CertificateToVerify {
188+
fn from(value: Certificate) -> Self {
189+
Self::Downloaded { certificate: value }
190+
}
191+
}
192+
185193
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
186194
#[cfg_attr(not(target_family = "wasm"), async_trait)]
187195
impl CertificateVerifier for MithrilCertificateVerifier {
@@ -196,22 +204,36 @@ impl CertificateVerifier for MithrilCertificateVerifier {
196204
})
197205
.await;
198206

199-
// always validate the given certificate even if it was cached
200-
let mut current_certificate = self
201-
.verify_not_cached_certificate(
202-
&certificate_chain_validation_id,
203-
CertificateToVerify::Downloaded {
204-
certificate: certificate.clone().try_into()?,
205-
},
206-
)
207-
.await?;
207+
// Validate certificates without cache until we cross an epoch boundary
208+
// This is necessary to ensure that the AVK of the current epoch was included
209+
// in a certificate in the previous epoch.
210+
let start_epoch = certificate.epoch;
211+
let mut current_certificate: Option<Certificate> = Some(certificate.clone().try_into()?);
212+
loop {
213+
match current_certificate {
214+
None => break,
215+
Some(next) => {
216+
current_certificate = self
217+
.verify_without_cache(&certificate_chain_validation_id, next)
218+
.await?;
219+
220+
let has_crossed_epoch_boundary = current_certificate
221+
.as_ref()
222+
.is_some_and(|c| c.epoch != start_epoch);
223+
if has_crossed_epoch_boundary {
224+
break;
225+
}
226+
}
227+
}
228+
}
208229

230+
let mut current_certificate = current_certificate.map(Into::into);
209231
loop {
210232
match current_certificate {
211233
None => break,
212234
Some(next) => {
213235
current_certificate = self
214-
.verify_one(&certificate_chain_validation_id, next)
236+
.verify_with_cache(&certificate_chain_validation_id, next)
215237
.await?
216238
}
217239
}
@@ -304,6 +326,7 @@ mod tests {
304326
#[cfg(feature = "unstable")]
305327
mod cache {
306328
use chrono::TimeDelta;
329+
use mithril_common::test_utils::CertificateChainingMethod;
307330
use mockall::predicate::eq;
308331

309332
use crate::aggregator_client::MockAggregatorHTTPClient;
@@ -349,7 +372,7 @@ mod tests {
349372
);
350373

351374
verifier
352-
.verify_one(
375+
.verify_with_cache(
353376
"certificate_chain_validation_id",
354377
CertificateToVerify::Downloaded {
355378
certificate: genesis_certificate.clone(),
@@ -385,7 +408,7 @@ mod tests {
385408
);
386409

387410
verifier
388-
.verify_one(
411+
.verify_with_cache(
389412
"certificate_chain_validation_id",
390413
CertificateToVerify::Downloaded {
391414
certificate: certificate.clone(),
@@ -429,28 +452,64 @@ mod tests {
429452
}
430453

431454
#[tokio::test]
432-
async fn verification_of_first_certificate_of_a_chain_should_not_use_cache() {
455+
async fn verification_of_certificates_should_not_use_cache_until_crossing_an_epoch_boundary(
456+
) {
457+
// Chain produced:
458+
// Cert epoch 3 n°6 - parent cert n°5
459+
// Cert epoch 3 n°5 - parent cert n°4
460+
// Cert epoch 2 n°4 - parent cert n°3
461+
// Cert epoch 2 n°3 - parent cert n°2
462+
// Cert epoch 2 n°2 - parent cert n°1
463+
// Cert epoch 1 n°1 - genesis
433464
let (chain, verifier) = CertificateChainBuilder::new()
434-
.with_total_certificates(2)
435-
.with_certificates_per_epoch(1)
465+
.with_total_certificates(6)
466+
.with_certificates_per_epoch(3)
467+
.with_certificate_chaining_method(CertificateChainingMethod::ToDirectPredecessor)
436468
.build();
469+
437470
let first_certificate = chain.first().unwrap();
438471
let genesis_certificate = chain.last().unwrap();
439472
assert!(genesis_certificate.is_genesis());
440473

474+
// The two certificates on the last epoch plus the last of the second epoch must be
475+
// fetched from the network as we need to cross an epoch boundary
476+
let certificates_that_must_not_be_fetched_from_cache =
477+
chain[..3].iter().cloned().collect::<Vec<_>>();
478+
let certificate_that_can_be_fetched_from_cache = chain[2..5].to_vec();
479+
441480
let mut cache = MockCertificateVerifierCache::new();
442-
cache.expect_get_previous_hash().returning(|_| Ok(None));
481+
482+
for certificate in &certificates_that_must_not_be_fetched_from_cache {
483+
cache
484+
.expect_get_previous_hash()
485+
.with(eq(certificate.hash.clone()))
486+
.never();
487+
}
488+
for certificate in certificate_that_can_be_fetched_from_cache {
489+
let previous_hash = certificate.previous_hash.clone();
490+
cache
491+
.expect_get_previous_hash()
492+
.with(eq(certificate.hash.clone()))
493+
.return_once(|_| Ok(Some(previous_hash)));
494+
}
443495
cache
444496
.expect_get_previous_hash()
445-
.with(eq(first_certificate.hash.clone()))
446-
.never();
497+
.with(eq(genesis_certificate.hash.clone()))
498+
.returning(|_| Ok(None));
499+
447500
cache
448501
.expect_store_validated_certificate()
449502
.returning(|_, _| Ok(()));
450503

451504
let certificate_client = CertificateClientTestBuilder::default()
452505
.config_aggregator_client_mock(|mock| {
453-
mock.expect_certificate_chain(chain.clone());
506+
mock.expect_certificate_chain(
507+
[
508+
certificates_that_must_not_be_fetched_from_cache,
509+
vec![genesis_certificate.clone()],
510+
]
511+
.concat(),
512+
);
454513
})
455514
.with_genesis_verification_key(verifier.to_verification_key())
456515
.with_verifier_cache(Arc::new(cache))
@@ -470,14 +529,14 @@ mod tests {
470529
.build();
471530
let last_certificate_hash = chain.first().unwrap().hash.clone();
472531

473-
// All certificates are cached except the last and the genesis (we always fetch the both)
532+
// All certificates are cached except the last two (to cross an epoch boundary) and the genesis (we always fetch the both)
474533
let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
475-
.with_items_from_chain(&chain[1..4]);
534+
.with_items_from_chain(&chain[2..4]);
476535

477536
let certificate_client = CertificateClientTestBuilder::default()
478537
.config_aggregator_client_mock(|mock| {
479538
mock.expect_certificate_chain(
480-
[chain[0..2].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
539+
[chain[0..3].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
481540
)
482541
})
483542
.with_genesis_verification_key(verifier.to_verification_key())

0 commit comments

Comments
 (0)