#[cfg(any(
feature = "certified-subnet-catalog-host",
feature = "cmc-host",
feature = "icrc-host"
))]
use candid::Principal;
#[cfg(any(
feature = "certified-subnet-catalog-host",
all(test, any(feature = "cmc-host", feature = "icrc-host"))
))]
use ic_agent::Certificate;
#[cfg(any(
feature = "certified-subnet-catalog-host",
feature = "cmc-host",
feature = "icrc-host"
))]
use ic_agent::{Agent, hash_tree::HashTree};
#[cfg(any(feature = "cmc-host", feature = "icrc-host"))]
const MAX_CERTIFIED_DATA_DEPTH: u8 = 64;
#[cfg(any(feature = "certified-subnet-catalog-host", feature = "ic-state-host"))]
pub const MAX_CERTIFICATE_TIME_SKEW_SECONDS: u64 = 5 * 60;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CertifiedDataError {
#[cfg(any(
feature = "certified-subnet-catalog-host",
feature = "cmc-host",
feature = "icrc-host"
))]
Authentication {
reason: String,
},
Invalid {
reason: String,
},
}
#[cfg(any(feature = "cmc-host", feature = "icrc-host"))]
pub fn authenticate_canister_hash_tree(
agent: &Agent,
canister: &Principal,
encoded_certificate: &[u8],
encoded_hash_tree: &[u8],
certified_data_owner: &str,
) -> Result<HashTree<Vec<u8>>, CertifiedDataError> {
crate::certificate::authenticate_hash_tree(
agent,
*canister,
encoded_certificate,
encoded_hash_tree,
crate::certificate::CertificateEvidenceLimits {
bytes: crate::agent::MAX_IC_AGENT_RESPONSE_BODY_BYTES,
depth: MAX_CERTIFIED_DATA_DEPTH,
},
)
.map_err(|error| match error {
crate::certificate::CertificateVerificationError::Invalid(error) => {
CertifiedDataError::Authentication {
reason: error.to_string(),
}
}
crate::certificate::CertificateVerificationError::WitnessMismatch => {
invalid_certified_data(format!(
"hash-tree digest does not match the {certified_data_owner} certified_data value"
))
}
error => invalid_certified_data(format!("certificate evidence is invalid: {error}")),
})
}
#[cfg(feature = "certified-subnet-catalog-host")]
pub fn authenticate_canister_tree(
agent: &Agent,
canister: &Principal,
certificate: &Certificate,
hash_tree: &HashTree<Vec<u8>>,
certified_data_owner: &str,
) -> Result<(), CertifiedDataError> {
agent
.verify(certificate, *canister)
.map_err(|error| CertifiedDataError::Authentication {
reason: error.to_string(),
})?;
verify_canister_tree(certificate, canister, hash_tree, certified_data_owner)
}
#[cfg(all(test, any(feature = "cmc-host", feature = "icrc-host")))]
pub fn verify_canister_hash_tree(
certificate: &Certificate,
canister: &Principal,
encoded_hash_tree: &[u8],
certified_data_owner: &str,
) -> Result<HashTree<Vec<u8>>, CertifiedDataError> {
let hash_tree = ic_host_tools::certificate::decode_hash_tree(
encoded_hash_tree,
ic_host_tools::certificate::DecodeLimits {
bytes: crate::agent::MAX_IC_AGENT_RESPONSE_BODY_BYTES,
depth: MAX_CERTIFIED_DATA_DEPTH,
},
)
.map_err(|error| invalid_certified_data(format!("hash-tree CBOR is invalid: {error}")))?;
verify_canister_tree(certificate, canister, &hash_tree, certified_data_owner)?;
Ok(hash_tree)
}
#[cfg(any(
feature = "certified-subnet-catalog-host",
all(test, any(feature = "cmc-host", feature = "icrc-host"))
))]
pub fn verify_canister_tree(
certificate: &Certificate,
canister: &Principal,
hash_tree: &HashTree<Vec<u8>>,
certified_data_owner: &str,
) -> Result<(), CertifiedDataError> {
crate::certificate::verify_witness(certificate, *canister, hash_tree).map_err(|_| {
invalid_certified_data(format!(
"hash-tree digest does not match the {certified_data_owner} certified_data value"
))
})
}
#[cfg(any(feature = "certified-subnet-catalog-host", feature = "ic-state-host"))]
pub fn validate_certificate_time(
observed_at_unix_secs: u64,
certificate_time_nanos: u64,
) -> Result<(), CertifiedDataError> {
let certificate_time = certificate_time_nanos / 1_000_000_000;
let minimum = observed_at_unix_secs.saturating_sub(MAX_CERTIFICATE_TIME_SKEW_SECONDS);
let maximum = observed_at_unix_secs.saturating_add(MAX_CERTIFICATE_TIME_SKEW_SECONDS);
if !(minimum..=maximum).contains(&certificate_time) {
return Err(invalid_certified_data(format!(
"certificate time {certificate_time} is outside the accepted {MAX_CERTIFICATE_TIME_SKEW_SECONDS}-second skew around collection time {observed_at_unix_secs}"
)));
}
Ok(())
}
fn invalid_certified_data(reason: impl Into<String>) -> CertifiedDataError {
CertifiedDataError::Invalid {
reason: reason.into(),
}
}
#[cfg(all(test, any(feature = "cmc-host", feature = "icrc-host")))]
mod tests {
use super::*;
use ic_agent::hash_tree::{label, leaf};
#[test]
fn oversized_evidence_refuses_before_authentication() {
let agent = Agent::builder()
.with_url("https://icp-api.io")
.build()
.unwrap();
let certificate = vec![0; crate::agent::MAX_IC_AGENT_RESPONSE_BODY_BYTES];
let error = authenticate_canister_hash_tree(
&agent,
&Principal::anonymous(),
&certificate,
&[0],
"fixture",
)
.unwrap_err();
assert!(matches!(error, CertifiedDataError::Invalid { reason }
if reason.contains("byte budget")));
}
#[test]
fn witness_decoding_requires_exact_bytes_and_bounded_nesting() {
let canister = Principal::anonymous();
let mut tree = leaf(vec![1]);
let original_digest = tree.digest();
let mut trailing = Vec::new();
ciborium::ser::into_writer(&tree, &mut trailing).unwrap();
trailing.push(0);
for _ in 0..MAX_CERTIFIED_DATA_DEPTH {
tree = label(b"nested".to_vec(), tree);
}
let mut nested = Vec::new();
ciborium::ser::into_writer(&tree, &mut nested).unwrap();
for (encoded, digest) in [(trailing, original_digest), (nested, tree.digest())] {
let certificate = Certificate {
tree: label(
b"canister".to_vec(),
label(
canister.as_slice().to_vec(),
label(b"certified_data".to_vec(), leaf(digest.to_vec())),
),
),
signature: Vec::new(),
delegation: None,
};
let error = verify_canister_hash_tree(&certificate, &canister, &encoded, "fixture")
.unwrap_err();
assert!(matches!(error, CertifiedDataError::Invalid { .. }));
}
}
}