use anyhow::{anyhow, Context};
use async_trait::async_trait;
use hex::ToHex;
use slog::{debug, Logger};
use std::sync::Arc;
use thiserror::Error;
use super::CertificateRetriever;
use crate::crypto_helper::{
ProtocolAggregateVerificationKey, ProtocolGenesisError, ProtocolGenesisVerificationKey,
ProtocolMultiSignature,
};
use crate::entities::{
Certificate, CertificateSignature, ProtocolMessage, ProtocolMessagePartKey, ProtocolParameters,
};
use crate::StdResult;
#[cfg(test)]
use mockall::automock;
#[derive(Error, Debug)]
pub enum CertificateVerifierError {
#[error("multi signature verification failed: '{0}'")]
VerifyMultiSignature(String),
#[error("certificate genesis error")]
CertificateGenesis(#[from] ProtocolGenesisError),
#[error("certificate hash unmatch error")]
CertificateHashUnmatch,
#[error("certificate chain previous hash unmatch error")]
CertificateChainPreviousHashUnmatch,
#[error("certificate chain AVK unmatch error")]
CertificateChainAVKUnmatch,
#[error("certificate chain infinite loop error")]
CertificateChainInfiniteLoop,
#[error("can't validate genesis certificate: given certificate isn't a genesis certificate")]
InvalidGenesisCertificateProvided,
}
#[cfg_attr(test, automock)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
pub trait CertificateVerifier: Send + Sync {
async fn verify_genesis_certificate(
&self,
genesis_certificate: &Certificate,
genesis_verification_key: &ProtocolGenesisVerificationKey,
) -> StdResult<()>;
async fn verify_certificate(
&self,
certificate: &Certificate,
genesis_verification_key: &ProtocolGenesisVerificationKey,
) -> StdResult<Option<Certificate>>;
async fn verify_certificate_chain(
&self,
certificate: Certificate,
genesis_verification_key: &ProtocolGenesisVerificationKey,
) -> StdResult<()> {
let mut certificate = certificate;
while let Some(previous_certificate) = self
.verify_certificate(&certificate, genesis_verification_key)
.await?
{
certificate = previous_certificate;
}
Ok(())
}
fn verify_protocol_message(
&self,
protocol_message: &ProtocolMessage,
certificate: &Certificate,
) -> bool {
protocol_message.compute_hash() == certificate.signed_message
}
}
pub struct MithrilCertificateVerifier {
logger: Logger,
certificate_retriever: Arc<dyn CertificateRetriever>,
}
impl MithrilCertificateVerifier {
pub fn new(logger: Logger, certificate_retriever: Arc<dyn CertificateRetriever>) -> Self {
debug!(logger, "New MithrilCertificateVerifier created");
Self {
logger,
certificate_retriever,
}
}
fn verify_multi_signature(
&self,
message: &[u8],
multi_signature: &ProtocolMultiSignature,
aggregate_verification_key: &ProtocolAggregateVerificationKey,
protocol_parameters: &ProtocolParameters,
) -> Result<(), CertificateVerifierError> {
debug!(
self.logger,
"Verify multi signature for {:?}",
message.encode_hex::<String>()
);
multi_signature
.verify(
message,
aggregate_verification_key,
&protocol_parameters.to_owned().into(),
)
.map_err(|e| CertificateVerifierError::VerifyMultiSignature(e.to_string()))
}
async fn verify_standard_certificate(
&self,
certificate: &Certificate,
signature: &ProtocolMultiSignature,
) -> StdResult<Option<Certificate>> {
self.verify_multi_signature(
certificate.signed_message.as_bytes(),
signature,
&certificate.aggregate_verification_key,
&certificate.metadata.protocol_parameters,
)?;
let previous_certificate = self
.certificate_retriever
.get_certificate_details(&certificate.previous_hash)
.await
.map_err(|e| anyhow!(e))
.with_context(|| "Can not retrieve previous certificate during verification")?;
if previous_certificate.hash != certificate.previous_hash {
return Err(anyhow!(
CertificateVerifierError::CertificateChainPreviousHashUnmatch
));
}
let current_certificate_avk: String = certificate
.aggregate_verification_key
.to_json_hex()
.with_context(|| {
format!(
"avk to string conversion error for certificate: `{}`",
certificate.hash
)
})?;
let previous_certificate_avk: String = previous_certificate
.aggregate_verification_key
.to_json_hex()
.with_context(|| {
format!(
"avk to string conversion error for previous certificate: `{}`",
certificate.hash
)
})?;
let valid_certificate_has_different_epoch_as_previous =
|next_aggregate_verification_key: &str| -> bool {
next_aggregate_verification_key == current_certificate_avk
&& previous_certificate.epoch != certificate.epoch
};
let valid_certificate_has_same_epoch_as_previous = || -> bool {
previous_certificate_avk == current_certificate_avk
&& previous_certificate.epoch == certificate.epoch
};
match previous_certificate
.protocol_message
.get_message_part(&ProtocolMessagePartKey::NextAggregateVerificationKey)
{
Some(next_aggregate_verification_key)
if valid_certificate_has_different_epoch_as_previous(
next_aggregate_verification_key,
) =>
{
Ok(Some(previous_certificate.to_owned()))
}
Some(_) if valid_certificate_has_same_epoch_as_previous() => {
Ok(Some(previous_certificate.to_owned()))
}
None => Ok(None),
_ => {
debug!(
self.logger,
"Previous certificate {:#?}", previous_certificate
);
Err(anyhow!(
CertificateVerifierError::CertificateChainAVKUnmatch
))
}
}
}
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl CertificateVerifier for MithrilCertificateVerifier {
async fn verify_genesis_certificate(
&self,
genesis_certificate: &Certificate,
genesis_verification_key: &ProtocolGenesisVerificationKey,
) -> StdResult<()> {
let genesis_signature = match &genesis_certificate.signature {
CertificateSignature::GenesisSignature(signature) => Ok(signature),
_ => Err(CertificateVerifierError::InvalidGenesisCertificateProvided),
}?;
genesis_verification_key
.verify(
genesis_certificate.signed_message.as_bytes(),
genesis_signature,
)
.with_context(|| "Certificate verifier failed verifying a genesis certificate")?;
Ok(())
}
async fn verify_certificate(
&self,
certificate: &Certificate,
genesis_verification_key: &ProtocolGenesisVerificationKey,
) -> StdResult<Option<Certificate>> {
debug!(
self.logger,
"Verifying certificate";
"certificate_hash" => &certificate.hash,
"certificate_previous_hash" => &certificate.previous_hash,
"certificate_epoch" => ?certificate.epoch,
"certificate_signed_entity_type" => ?certificate.signed_entity_type(),
);
certificate
.hash
.eq(&certificate.compute_hash())
.then(|| certificate.hash.clone())
.ok_or(CertificateVerifierError::CertificateHashUnmatch)?;
if certificate.is_chaining_to_itself() {
Err(anyhow!(
CertificateVerifierError::CertificateChainInfiniteLoop
))
} else {
match &certificate.signature {
CertificateSignature::GenesisSignature(_signature) => {
self.verify_genesis_certificate(certificate, genesis_verification_key)
.await?;
Ok(None)
}
CertificateSignature::MultiSignature(_, signature) => {
self.verify_standard_certificate(certificate, signature)
.await
}
}
}
}
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use mockall::mock;
use slog_scope;
use super::CertificateRetriever;
use super::*;
use crate::certificate_chain::CertificateRetrieverError;
use crate::crypto_helper::{tests_setup::*, ProtocolClerk};
use crate::test_utils::MithrilFixtureBuilder;
mock! {
pub CertificateRetrieverImpl { }
#[async_trait]
impl CertificateRetriever for CertificateRetrieverImpl {
async fn get_certificate_details(
&self,
certificate_hash: &str,
) -> Result<Certificate, CertificateRetrieverError>;
}
}
#[test]
fn test_verify_multi_signature_ok() {
let protocol_parameters = setup_protocol_parameters();
let fixture = MithrilFixtureBuilder::default()
.with_signers(5)
.with_protocol_parameters(protocol_parameters.into())
.build();
let signers = fixture.signers_fixture();
let message_hash = setup_message().compute_hash().as_bytes().to_vec();
let single_signatures = signers
.iter()
.filter_map(|s| s.protocol_signer.sign(&message_hash))
.collect::<Vec<_>>();
let first_signer = &signers[0].protocol_signer;
let clerk = ProtocolClerk::from_signer(first_signer);
let aggregate_verification_key = clerk.compute_avk().into();
let multi_signature = clerk
.aggregate(&single_signatures, &message_hash)
.unwrap()
.into();
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(MockCertificateRetrieverImpl::new()),
);
let message_tampered = message_hash[1..].to_vec();
assert!(
verifier
.verify_multi_signature(
&message_tampered,
&multi_signature,
&aggregate_verification_key,
&fixture.protocol_parameters(),
)
.is_err(),
"multi signature verification should have failed"
);
verifier
.verify_multi_signature(
&message_hash,
&multi_signature,
&aggregate_verification_key,
&fixture.protocol_parameters(),
)
.expect("multi signature verification should have succeeded");
}
#[tokio::test]
async fn test_verify_certificate_ok_different_epochs() {
let total_certificates = 5;
let certificates_per_epoch = 1;
let (fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let fake_certificate1 = fake_certificates[0].clone();
let fake_certificate2 = fake_certificates[1].clone();
let mut mock_certificate_retriever = MockCertificateRetrieverImpl::new();
mock_certificate_retriever
.expect_get_certificate_details()
.returning(move |_| Ok(fake_certificate2.clone()))
.times(1);
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let verify = verifier
.verify_certificate(&fake_certificate1, &genesis_verifier.to_verification_key())
.await;
verify.expect("unexpected error");
}
#[tokio::test]
async fn test_verify_certificate_ok_same_epoch() {
let total_certificates = 5;
let certificates_per_epoch = 2;
let (fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let fake_certificate1 = fake_certificates[0].clone();
let fake_certificate2 = fake_certificates[1].clone();
let mut mock_certificate_retriever = MockCertificateRetrieverImpl::new();
mock_certificate_retriever
.expect_get_certificate_details()
.returning(move |_| Ok(fake_certificate2.clone()))
.times(1);
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let verify = verifier
.verify_certificate(&fake_certificate1, &genesis_verifier.to_verification_key())
.await;
verify.expect("unexpected error");
}
#[tokio::test]
async fn test_verify_certificate_ko_certificate_chain_previous_hash_unmatch() {
let total_certificates = 5;
let certificates_per_epoch = 1;
let (fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let fake_certificate1 = fake_certificates[0].clone();
let mut fake_certificate2 = fake_certificates[1].clone();
fake_certificate2.previous_hash = "another-hash".to_string();
fake_certificate2.hash = fake_certificate2.compute_hash();
let mut mock_certificate_retriever = MockCertificateRetrieverImpl::new();
mock_certificate_retriever
.expect_get_certificate_details()
.returning(move |_| Ok(fake_certificate2.clone()))
.times(1);
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let error = verifier
.verify_certificate(&fake_certificate1, &genesis_verifier.to_verification_key())
.await
.expect_err("verify_certificate_chain should fail");
let error = error
.downcast_ref::<CertificateVerifierError>()
.expect("Can not downcast to `CertificateVerifierError`.");
assert!(
matches!(
error,
CertificateVerifierError::CertificateChainPreviousHashUnmatch
),
"unexpected error type: {error:?}"
);
}
#[tokio::test]
async fn test_verify_certificate_ko_certificate_chain_avk_unmatch() {
let total_certificates = 5;
let certificates_per_epoch = 1;
let (fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let mut fake_certificate1 = fake_certificates[0].clone();
let mut fake_certificate2 = fake_certificates[1].clone();
fake_certificate2.protocol_message.set_message_part(
ProtocolMessagePartKey::NextAggregateVerificationKey,
"another-avk".to_string(),
);
fake_certificate2.hash = fake_certificate2.compute_hash();
fake_certificate1
.previous_hash
.clone_from(&fake_certificate2.hash);
fake_certificate1.hash = fake_certificate1.compute_hash();
let mut mock_certificate_retriever = MockCertificateRetrieverImpl::new();
mock_certificate_retriever
.expect_get_certificate_details()
.returning(move |_| Ok(fake_certificate2.clone()))
.times(1);
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let error = verifier
.verify_certificate(&fake_certificate1, &genesis_verifier.to_verification_key())
.await
.expect_err("verify_certificate_chain should fail");
let error = error
.downcast_ref::<CertificateVerifierError>()
.expect("Can not downcast to `CertificateVerifierError`.");
assert!(
matches!(error, CertificateVerifierError::CertificateChainAVKUnmatch),
"unexpected error type: {error:?}"
);
}
#[tokio::test]
async fn test_verify_certificate_ko_certificate_hash_not_matching() {
let total_certificates = 5;
let certificates_per_epoch = 1;
let (fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let mut fake_certificate1 = fake_certificates[0].clone();
fake_certificate1.hash = "another-hash".to_string();
let mock_certificate_retriever = MockCertificateRetrieverImpl::new();
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let error = verifier
.verify_certificate(&fake_certificate1, &genesis_verifier.to_verification_key())
.await
.expect_err("verify_certificate_chain should fail");
let error = error
.downcast_ref::<CertificateVerifierError>()
.expect("Can not downcast to `CertificateVerifierError`.");
assert!(
matches!(error, CertificateVerifierError::CertificateHashUnmatch),
"unexpected error type: {error:?}"
);
}
#[tokio::test]
async fn test_verify_certificate_chain_ok() {
let total_certificates = 15;
let certificates_per_epoch = 2;
let (fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let mut mock_certificate_retriever = MockCertificateRetrieverImpl::new();
let certificate_to_verify = fake_certificates[0].clone();
for fake_certificate in fake_certificates.into_iter().skip(1) {
mock_certificate_retriever
.expect_get_certificate_details()
.returning(move |_| Ok(fake_certificate.clone()))
.times(1);
}
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let verify = verifier
.verify_certificate_chain(
certificate_to_verify,
&genesis_verifier.to_verification_key(),
)
.await;
verify.expect("unexpected error");
}
#[tokio::test]
async fn test_verify_certificate_chain_ko() {
let total_certificates = 15;
let certificates_per_epoch = 2;
let (mut fake_certificates, genesis_verifier) =
setup_certificate_chain(total_certificates, certificates_per_epoch);
let index_certificate_fail = (total_certificates / 2) as usize;
fake_certificates[index_certificate_fail].hash = "tampered-hash".to_string();
let mut mock_certificate_retriever = MockCertificateRetrieverImpl::new();
let certificate_to_verify = fake_certificates[0].clone();
for fake_certificate in fake_certificates
.into_iter()
.skip(1)
.take(index_certificate_fail)
{
mock_certificate_retriever
.expect_get_certificate_details()
.returning(move |_| Ok(fake_certificate.clone()))
.times(1);
}
let verifier = MithrilCertificateVerifier::new(
slog_scope::logger(),
Arc::new(mock_certificate_retriever),
);
let error = verifier
.verify_certificate_chain(
certificate_to_verify,
&genesis_verifier.to_verification_key(),
)
.await
.expect_err("verify_certificate_chain should fail");
let error = error
.downcast_ref::<CertificateVerifierError>()
.expect("Can not downcast to `CertificateVerifierError`.");
assert!(
matches!(
error,
CertificateVerifierError::CertificateChainPreviousHashUnmatch
),
"unexpected error type: {error:?}"
);
}
}