Skip to content

Commit 51b3641

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 baa1cb6 commit 51b3641

File tree

1 file changed

+92
-43
lines changed
  • mithril-client/src/certificate_client

1 file changed

+92
-43
lines changed

mithril-client/src/certificate_client/verify.rs

Lines changed: 92 additions & 43 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_enabled(
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 chaining is correct
209+
let start_epoch = certificate.epoch;
210+
let mut current_certificate: Option<Certificate> = Some(certificate.clone().try_into()?);
211+
loop {
212+
match current_certificate {
213+
None => break,
214+
Some(next) => {
215+
current_certificate = self
216+
.verify_without_cache(&certificate_chain_validation_id, next)
217+
.await?;
218+
219+
let has_crossed_epoch_boundary = current_certificate
220+
.as_ref()
221+
.is_some_and(|c| c.epoch != start_epoch);
222+
if has_crossed_epoch_boundary {
223+
break;
224+
}
225+
}
226+
}
227+
}
208228

229+
let mut current_certificate: Option<CertificateToVerify> =
230+
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_enabled(&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_enabled(
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_enabled(
389412
"certificate_chain_validation_id",
390413
CertificateToVerify::Downloaded {
391414
certificate: certificate.clone(),
@@ -429,31 +452,57 @@ 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+
// Scenario:
458+
// | Certificate | epoch | Parent | Can use cache to | Should be fully |
459+
// | | | | get parent hash | Verified |
460+
// |------------:|------:|---------------:|------------------|-----------------|
461+
// | n°6 | 3 | n°5 | No | Yes |
462+
// | n°5 | 3 | n°4 | No | Yes |
463+
// | n°4 | 2 | n°3 | Yes | Yes |
464+
// | n°3 | 2 | n°2 | Yes | No |
465+
// | n°2 | 2 | n°1 | Yes | No |
466+
// | n°1 | 1 | None (genesis) | Yes | Yes |
433467
let (chain, verifier) = CertificateChainBuilder::new()
434-
.with_total_certificates(2)
435-
.with_certificates_per_epoch(1)
468+
.with_total_certificates(6)
469+
.with_certificates_per_epoch(3)
470+
.with_certificate_chaining_method(CertificateChainingMethod::Sequential)
436471
.build();
472+
437473
let first_certificate = chain.first().unwrap();
438474
let genesis_certificate = chain.last().unwrap();
439475
assert!(genesis_certificate.is_genesis());
440476

441-
let mut cache = MockCertificateVerifierCache::new();
442-
cache.expect_get_previous_hash().returning(|_| Ok(None));
443-
cache
444-
.expect_get_previous_hash()
445-
.with(eq(first_certificate.hash.clone()))
446-
.never();
447-
cache
448-
.expect_store_validated_certificate()
449-
.returning(|_, _| Ok(()));
477+
let certificates_that_must_be_fully_verified =
478+
[chain[..3].to_vec(), vec![genesis_certificate.clone()]].concat();
479+
let certificates_which_parents_can_be_fetched_from_cache = chain[2..5].to_vec();
480+
481+
let cache = {
482+
let mut mock = MockCertificateVerifierCache::new();
483+
484+
for certificate in certificates_which_parents_can_be_fetched_from_cache {
485+
let previous_hash = certificate.previous_hash.clone();
486+
mock.expect_get_previous_hash()
487+
.with(eq(certificate.hash.clone()))
488+
.return_once(|_| Ok(Some(previous_hash)))
489+
.once();
490+
}
491+
mock.expect_get_previous_hash()
492+
.with(eq(genesis_certificate.hash.clone()))
493+
.returning(|_| Ok(None));
494+
mock.expect_store_validated_certificate()
495+
.returning(|_, _| Ok(()));
496+
497+
Arc::new(mock)
498+
};
450499

451500
let certificate_client = CertificateClientTestBuilder::default()
452501
.config_aggregator_client_mock(|mock| {
453-
mock.expect_certificate_chain(chain.clone());
502+
mock.expect_certificate_chain(certificates_that_must_be_fully_verified);
454503
})
455504
.with_genesis_verification_key(verifier.to_verification_key())
456-
.with_verifier_cache(Arc::new(cache))
505+
.with_verifier_cache(cache)
457506
.build();
458507

459508
certificate_client
@@ -470,14 +519,14 @@ mod tests {
470519
.build();
471520
let last_certificate_hash = chain.first().unwrap().hash.clone();
472521

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

477526
let certificate_client = CertificateClientTestBuilder::default()
478527
.config_aggregator_client_mock(|mock| {
479528
mock.expect_certificate_chain(
480-
[chain[0..2].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
529+
[chain[0..3].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
481530
)
482531
})
483532
.with_genesis_verification_key(verifier.to_verification_key())

0 commit comments

Comments
 (0)