#[cfg(test)]
mod tests;
use candid::Principal;
use ic_agent::{Agent, AgentError};
use ic_certification::{Certificate, HashTree, LookupResult};
use ic_host_tools::certificate::{
DecodeError, DecodeLimits, ParseError, decode_certificate, decode_hash_tree,
};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CertificateEvidenceLimits {
pub bytes: usize,
pub depth: u8,
}
#[derive(Debug, Error)]
pub enum CertificateVerificationError {
#[error("certificate evidence exceeds its byte budget")]
BudgetExceeded,
#[error("certificate evidence encoding is invalid")]
Malformed,
#[error("certificate CBOR decoding failed: {0}")]
Decode(#[source] ParseError),
#[error("certificate authentication failed")]
Invalid(#[source] AgentError),
#[error("witness is not committed by the certificate")]
WitnessMismatch,
#[error("certified ledger tip is missing, malformed or out of range")]
LedgerTipMalformed,
}
pub fn authenticate(
agent: &Agent,
canister: Principal,
bytes: &[u8],
maximum_bytes: usize,
depth: u8,
) -> Result<Certificate, CertificateVerificationError> {
let certificate = decode_certificate(
bytes,
DecodeLimits {
bytes: maximum_bytes,
depth,
},
)
.map_err(decode_error)?;
agent
.verify(&certificate, canister)
.map_err(CertificateVerificationError::Invalid)?;
Ok(certificate)
}
fn decode_error(error: DecodeError) -> CertificateVerificationError {
match error {
DecodeError::ByteLimit { .. } => CertificateVerificationError::BudgetExceeded,
DecodeError::TrailingData => CertificateVerificationError::Malformed,
DecodeError::Cbor(source) => CertificateVerificationError::Decode(source),
}
}
pub fn authenticate_hash_tree(
agent: &Agent,
canister: Principal,
certificate: &[u8],
witness: &[u8],
limits: CertificateEvidenceLimits,
) -> Result<HashTree, CertificateVerificationError> {
if certificate
.len()
.checked_add(witness.len())
.is_none_or(|size| size > limits.bytes)
{
return Err(CertificateVerificationError::BudgetExceeded);
}
let certificate = authenticate(agent, canister, certificate, limits.bytes, limits.depth)?;
let witness = decode_hash_tree(
witness,
DecodeLimits {
bytes: limits.bytes,
depth: limits.depth,
},
)
.map_err(decode_error)?;
verify_witness(&certificate, canister, &witness)?;
Ok(witness)
}
pub fn verify_witness(
certificate: &Certificate,
canister: Principal,
witness: &HashTree,
) -> Result<(), CertificateVerificationError> {
match certificate.tree.lookup_path([
b"canister".as_slice(),
canister.as_slice(),
b"certified_data".as_slice(),
]) {
LookupResult::Found(value) if value == witness.digest() => Ok(()),
_ => Err(CertificateVerificationError::WitnessMismatch),
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CertifiedLedgerTip {
pub index: u128,
pub hash: [u8; 32],
}
pub fn authenticate_ledger_tip(
agent: &Agent,
ledger: Principal,
certificate: &[u8],
witness: &[u8],
limits: CertificateEvidenceLimits,
) -> Result<CertifiedLedgerTip, CertificateVerificationError> {
let tree = authenticate_hash_tree(agent, ledger, certificate, witness, limits)?;
ledger_tip(&tree)
}
fn ledger_tip(tree: &HashTree) -> Result<CertifiedLedgerTip, CertificateVerificationError> {
let hash = required_leaf(tree, b"last_block_hash")?
.try_into()
.map_err(|_| CertificateVerificationError::LedgerTipMalformed)?;
let index = decode_index(required_leaf(tree, b"last_block_index")?)?;
Ok(CertifiedLedgerTip { index, hash })
}
fn required_leaf<'a>(
tree: &'a HashTree,
label: &[u8],
) -> Result<&'a [u8], CertificateVerificationError> {
match tree.lookup_path([label]) {
LookupResult::Found(value) => Ok(value),
_ => Err(CertificateVerificationError::LedgerTipMalformed),
}
}
fn decode_index(bytes: &[u8]) -> Result<u128, CertificateVerificationError> {
let mut value = 0_u128;
for (offset, byte) in bytes.iter().copied().enumerate() {
if offset > 18 || (offset == 18 && byte > 3) {
return Err(CertificateVerificationError::LedgerTipMalformed);
}
value |= u128::from(byte & 0x7f) << (offset * 7);
if byte < 0x80 {
if offset + 1 != bytes.len() || (offset > 0 && byte == 0) {
return Err(CertificateVerificationError::LedgerTipMalformed);
}
return Ok(value);
}
}
Err(CertificateVerificationError::LedgerTipMalformed)
}