Skip to main content

canic_core/ids/
network.rs

1//! Module: ids::network
2//!
3//! Responsibility: identify one IC network by its verified trust identity.
4//! Does not own: trust-anchor enrollment, environment profiles, or network access.
5//! Boundary: derives and serializes the canonical 32-byte network identity.
6
7use 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///
20/// CanonicalNetworkId
21///
22/// Trust-derived identity for one IC network.
23///
24
25#[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    /// Derive the public IC identity from Canic's compiled, pinned DER trust anchor.
37    #[must_use]
38    pub fn public_ic() -> Self {
39        Self::from_der_bytes_unchecked(&mainnet_ic_root_public_key_der())
40    }
41
42    /// Validate and derive the identity of one explicitly enrolled DER trust anchor.
43    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///
128/// CanonicalNetworkIdParseError
129///
130/// Typed rejection for a non-canonical network identity.
131///
132
133#[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///
143/// CanonicalNetworkTrustAnchorError
144///
145/// Typed rejection for an invalid enrolled IC root trust anchor.
146///
147
148#[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}