Skip to main content

ic_auth_protocol_types/
identifiers.rs

1//! Validated protocol identifiers; deployment identity and trust enrollment stay
2//! with the host. Existing text wire representations are deliberately preserved.
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
5use std::{fmt, str::FromStr};
6use thiserror::Error;
7
8/// Invalid protocol identifier syntax. This says nothing about authorization.
9#[derive(Clone, Debug, Eq, Error, PartialEq)]
10pub enum IdentifierError {
11    #[error("identifier must contain exactly 64 lowercase hexadecimal characters")]
12    NonCanonicalId,
13    #[error("role must be nonempty and contain only a-z, 0-9, underscore, colon or hyphen")]
14    InvalidRole,
15}
16
17/// Exactly 32 identity bytes, represented as lowercase hexadecimal on the wire.
18/// No sentinel bytes are reserved by this protocol. A host must compare this
19/// value with its protected identity, never enroll trust from a client claim.
20#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
21pub struct CanonicalId([u8; 32]);
22
23impl CanonicalId {
24    /// Project an already resolved host identity without changing its bytes.
25    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
26        Self(bytes)
27    }
28
29    /// Exact bytes used in canonical signed encoding.
30    pub const fn as_bytes(&self) -> &[u8; 32] {
31        &self.0
32    }
33}
34
35impl fmt::Display for CanonicalId {
36    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37        for byte in self.0 {
38            write!(formatter, "{byte:02x}")?;
39        }
40        Ok(())
41    }
42}
43
44impl FromStr for CanonicalId {
45    type Err = IdentifierError;
46
47    fn from_str(value: &str) -> Result<Self, Self::Err> {
48        if value.len() != 64
49            || !value
50                .bytes()
51                .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
52        {
53            return Err(IdentifierError::NonCanonicalId);
54        }
55        let mut bytes = [0; 32];
56        for (byte, pair) in bytes.iter_mut().zip(value.as_bytes().as_chunks::<2>().0) {
57            let nibble = |b| if b <= b'9' { b - b'0' } else { b - b'a' + 10 };
58            *byte = (nibble(pair[0]) << 4) | nibble(pair[1]);
59        }
60        Ok(Self(bytes))
61    }
62}
63
64impl Serialize for CanonicalId {
65    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
66        serializer.collect_str(self)
67    }
68}
69
70impl<'de> Deserialize<'de> for CanonicalId {
71    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
72        String::deserialize(deserializer)?
73            .parse()
74            .map_err(de::Error::custom)
75    }
76}
77
78/// Exact network-qualified audience, with existing protocol field labels.
79/// The host owns the meaning and trusted source of both identifiers; this type
80/// contains no fleet topology, membership, derivation or installation policy.
81#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
82#[serde(deny_unknown_fields)]
83pub struct AudienceId {
84    pub canonical_network_id: CanonicalId,
85    pub fleet_id: CanonicalId,
86}
87
88/// Canonical role label, checked at construction and deserialization.
89/// The grammar matches the existing signed protocol; it adds no case folding,
90/// normalization, built-in roles or implicit role authority.
91#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
92#[serde(transparent)]
93pub struct AuthRole(String);
94
95impl AuthRole {
96    pub fn as_str(&self) -> &str {
97        &self.0
98    }
99}
100
101impl FromStr for AuthRole {
102    type Err = IdentifierError;
103
104    fn from_str(value: &str) -> Result<Self, Self::Err> {
105        if value.is_empty()
106            || !value.bytes().all(|b| {
107                b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b':' | b'-')
108            })
109        {
110            return Err(IdentifierError::InvalidRole);
111        }
112        Ok(Self(value.to_owned()))
113    }
114}
115
116impl<'de> Deserialize<'de> for AuthRole {
117    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118        String::deserialize(deserializer)?
119            .parse()
120            .map_err(de::Error::custom)
121    }
122}