use super::RoleCapabilityKey;
use crate::ids::CanisterRole;
use candid::CandidType;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::str::FromStr;
use std::{collections::BTreeSet, fmt};
use thiserror::Error as ThisError;
pub const PROTOCOL_PROFILE_DIGEST_ENV: &str = "CANIC_PROTOCOL_PROFILE_DIGEST";
const PROTOCOL_PROFILE_DOMAIN: &[u8] = b"canic.protocol-profile.v1";
#[derive(
CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(transparent)]
pub struct ProtocolProfileDigest([u8; 32]);
impl ProtocolProfileDigest {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub const fn into_bytes(self) -> [u8; 32] {
self.0
}
pub fn from_hex(value: &str) -> Result<Self, ProtocolProfileDigestParseError> {
if value.len() != 64 {
return Err(ProtocolProfileDigestParseError::Length(value.len()));
}
let mut bytes = [0_u8; 32];
for (index, byte) in bytes.iter_mut().enumerate() {
let offset = index * 2;
let high = decode_nibble(value.as_bytes()[offset], offset)?;
let low = decode_nibble(value.as_bytes()[offset + 1], offset + 1)?;
*byte = (high << 4) | low;
}
Ok(Self(bytes))
}
}
impl fmt::Display for ProtocolProfileDigest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
write!(formatter, "{byte:02x}")?;
}
Ok(())
}
}
impl FromStr for ProtocolProfileDigest {
type Err = ProtocolProfileDigestParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::from_hex(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProtocolProfileHashes {
pub candid_sha256: [u8; 32],
pub protocol_profile_digest: ProtocolProfileDigest,
}
#[must_use]
pub fn derive_protocol_profile_hashes(
release_identity: &str,
role: &CanisterRole,
capabilities: &BTreeSet<RoleCapabilityKey>,
canonical_candid: &[u8],
) -> ProtocolProfileHashes {
let candid_sha256 = sha256_array(canonical_candid);
let mut hasher = Sha256::new();
hasher.update(PROTOCOL_PROFILE_DOMAIN);
update_length_prefixed(&mut hasher, release_identity.as_bytes());
update_length_prefixed(&mut hasher, role.as_str().as_bytes());
hasher.update(
u32::try_from(capabilities.len())
.expect("compiled capability count must fit u32")
.to_be_bytes(),
);
for capability in capabilities {
update_length_prefixed(&mut hasher, capability.manifest_name().as_bytes());
}
hasher.update(candid_sha256);
ProtocolProfileHashes {
candid_sha256,
protocol_profile_digest: ProtocolProfileDigest::from_bytes(hasher.finalize().into()),
}
}
fn update_length_prefixed(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update(
u32::try_from(bytes.len())
.expect("protocol-profile field length must fit u32")
.to_be_bytes(),
);
hasher.update(bytes);
}
fn sha256_array(bytes: &[u8]) -> [u8; 32] {
Sha256::digest(bytes).into()
}
fn decode_nibble(byte: u8, index: usize) -> Result<u8, ProtocolProfileDigestParseError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
_ => Err(ProtocolProfileDigestParseError::Digit {
index,
byte: char::from(byte),
}),
}
}
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum ProtocolProfileDigestParseError {
#[error("protocol-profile digest must contain 64 lowercase hexadecimal bytes, got {0}")]
Length(usize),
#[error("invalid lowercase hexadecimal digit {byte:?} at index {index}")]
Digit { index: usize, byte: char },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_encoding_is_ordered_stable_and_binds_every_input() {
let capabilities = BTreeSet::from([
RoleCapabilityKey::Runtime,
RoleCapabilityKey::AutomaticTopup,
]);
let first = derive_protocol_profile_hashes(
"0.103.0",
&CanisterRole::new("app"),
&capabilities,
b"service : {}\n",
);
let reordered = BTreeSet::from([
RoleCapabilityKey::AutomaticTopup,
RoleCapabilityKey::Runtime,
]);
assert_eq!(
first,
derive_protocol_profile_hashes(
"0.103.0",
&CanisterRole::new("app"),
&reordered,
b"service : {}\n",
)
);
assert_ne!(
first,
derive_protocol_profile_hashes(
"0.103.1",
&CanisterRole::new("app"),
&capabilities,
b"service : {}\n",
)
);
assert_eq!(
ProtocolProfileDigest::from_hex(&first.protocol_profile_digest.to_string())
.expect("parse rendered digest"),
first.protocol_profile_digest
);
}
#[test]
fn capability_manifest_names_are_lexicographically_ordered() {
let names = [
RoleCapabilityKey::AutomaticTopup,
RoleCapabilityKey::ChildProvisioning,
RoleCapabilityKey::DelegatedTokenIssuer,
RoleCapabilityKey::DelegatedTokenVerifier,
RoleCapabilityKey::FleetAdmissionProjection,
RoleCapabilityKey::FleetCoordinator,
RoleCapabilityKey::Icrc21,
RoleCapabilityKey::Index,
RoleCapabilityKey::LocalApplicationAuthorization,
RoleCapabilityKey::RoleAttestationSigner,
RoleCapabilityKey::RoleAttestationVerifier,
RoleCapabilityKey::Root,
RoleCapabilityKey::RootControlPlane,
RoleCapabilityKey::Runtime,
RoleCapabilityKey::Scaling,
RoleCapabilityKey::Sharding,
RoleCapabilityKey::WasmStore,
]
.map(RoleCapabilityKey::manifest_name);
assert!(names.windows(2).all(|pair| pair[0] < pair[1]));
}
}