canic_core/ids/
network.rs1use crate::domain::auth::{
8 IC_ROOT_PUBLIC_KEY_RAW_LENGTH, ic_root_public_key_raw_from_der_or_raw,
9 mainnet_ic_root_public_key_der,
10};
11use candid::CandidType;
12use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
13use sha2::{Digest, Sha256};
14use std::{fmt, str::FromStr};
15use thiserror::Error as ThisError;
16
17const CANONICAL_NETWORK_ID_DOMAIN: &[u8] = b"canic:canonical-network-id\0";
18
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub struct CanonicalNetworkId([u8; 32]);
27
28impl CanonicalNetworkId {
29 fn from_network_trust_identity(network_trust_identity: [u8; 32]) -> Self {
30 let mut hasher = Sha256::new();
31 hasher.update(CANONICAL_NETWORK_ID_DOMAIN);
32 hasher.update(network_trust_identity);
33 Self(hasher.finalize().into())
34 }
35
36 #[must_use]
38 pub fn public_ic() -> Self {
39 Self::from_der_bytes_unchecked(&mainnet_ic_root_public_key_der())
40 }
41
42 pub fn from_der_root_trust_anchor(
44 root_key: &[u8],
45 ) -> Result<Self, CanonicalNetworkTrustAnchorError> {
46 if root_key.len() == IC_ROOT_PUBLIC_KEY_RAW_LENGTH {
47 return Err(CanonicalNetworkTrustAnchorError::RawKey);
48 }
49 ic_root_public_key_raw_from_der_or_raw(root_key)
50 .map_err(CanonicalNetworkTrustAnchorError::InvalidDer)?;
51 Ok(Self::from_der_bytes_unchecked(root_key))
52 }
53
54 #[must_use]
55 pub const fn as_bytes(&self) -> &[u8; 32] {
56 &self.0
57 }
58
59 fn from_der_bytes_unchecked(root_key: &[u8]) -> Self {
60 Self::from_network_trust_identity(Sha256::digest(root_key).into())
61 }
62}
63
64impl fmt::Display for CanonicalNetworkId {
65 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66 for byte in self.0 {
67 write!(formatter, "{byte:02x}")?;
68 }
69 Ok(())
70 }
71}
72
73impl CandidType for CanonicalNetworkId {
74 fn _ty() -> candid::types::Type {
75 candid::types::TypeInner::Text.into()
76 }
77
78 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
79 where
80 S: candid::types::Serializer,
81 {
82 serializer.serialize_text(&self.to_string())
83 }
84}
85
86impl FromStr for CanonicalNetworkId {
87 type Err = CanonicalNetworkIdParseError;
88
89 fn from_str(value: &str) -> Result<Self, Self::Err> {
90 if value.len() != 64 {
91 return Err(CanonicalNetworkIdParseError::Length(value.len()));
92 }
93 if !value
94 .bytes()
95 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
96 {
97 return Err(CanonicalNetworkIdParseError::CanonicalHex);
98 }
99
100 let mut bytes = [0; 32];
101 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
102 bytes[index] = (decode_nibble(pair[0]) << 4) | decode_nibble(pair[1]);
103 }
104 Ok(Self(bytes))
105 }
106}
107
108impl Serialize for CanonicalNetworkId {
109 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
110 where
111 S: Serializer,
112 {
113 serializer.collect_str(self)
114 }
115}
116
117impl<'de> Deserialize<'de> for CanonicalNetworkId {
118 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
119 where
120 D: Deserializer<'de>,
121 {
122 let value = String::deserialize(deserializer)?;
123 value.parse().map_err(de::Error::custom)
124 }
125}
126
127#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
134pub enum CanonicalNetworkIdParseError {
135 #[error("canonical network ID must contain exactly 64 characters, got {0}")]
136 Length(usize),
137
138 #[error("canonical network ID must contain only lowercase hexadecimal characters")]
139 CanonicalHex,
140}
141
142#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
149pub enum CanonicalNetworkTrustAnchorError {
150 #[error("raw root public key bytes are not an enrolled DER trust anchor")]
151 RawKey,
152
153 #[error("{0}")]
154 InvalidDer(String),
155}
156
157fn decode_nibble(byte: u8) -> u8 {
158 match byte {
159 b'0'..=b'9' => byte - b'0',
160 b'a'..=b'f' => byte - b'a' + 10,
161 _ => unreachable!("canonical hex was validated before decoding"),
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn derives_domain_separated_network_identity() {
171 let identity = CanonicalNetworkId::from_network_trust_identity([7; 32]);
172
173 let mut expected = Sha256::new();
174 expected.update(CANONICAL_NETWORK_ID_DOMAIN);
175 expected.update([7; 32]);
176 assert_eq!(identity.as_bytes(), &<[u8; 32]>::from(expected.finalize()));
177 }
178
179 #[test]
180 fn text_uses_exact_lowercase_hex() {
181 let identity = CanonicalNetworkId::from_network_trust_identity([11; 32]);
182 let text = identity.to_string();
183
184 assert_eq!(text.len(), 64);
185 assert!(
186 text.bytes()
187 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
188 );
189 assert_eq!(text.parse::<CanonicalNetworkId>(), Ok(identity));
190 }
191
192 #[test]
193 fn parsing_rejects_noncanonical_text() {
194 std::assert_matches!(
195 "ab".parse::<CanonicalNetworkId>(),
196 Err(CanonicalNetworkIdParseError::Length(2))
197 );
198 std::assert_matches!(
199 "A000000000000000000000000000000000000000000000000000000000000000"
200 .parse::<CanonicalNetworkId>(),
201 Err(CanonicalNetworkIdParseError::CanonicalHex)
202 );
203 std::assert_matches!(
204 "g000000000000000000000000000000000000000000000000000000000000000"
205 .parse::<CanonicalNetworkId>(),
206 Err(CanonicalNetworkIdParseError::CanonicalHex)
207 );
208 }
209
210 #[test]
211 fn public_and_enrolled_der_paths_share_the_canonical_der_derivation() {
212 let der = mainnet_ic_root_public_key_der();
213
214 assert_eq!(
215 CanonicalNetworkId::from_der_root_trust_anchor(&der).expect("valid DER"),
216 CanonicalNetworkId::public_ic()
217 );
218 std::assert_matches!(
219 CanonicalNetworkId::from_der_root_trust_anchor(&[0; IC_ROOT_PUBLIC_KEY_RAW_LENGTH]),
220 Err(CanonicalNetworkTrustAnchorError::RawKey)
221 );
222 }
223}