Skip to main content

ic_auth_wire/
model.rs

1//! Bounded transport projections. Conversion is structural, never verification.
2use ic_auth_protocol_types as domain;
3use ic_auth_protocol_types::limits::{
4    MAX_CLAIMS_BYTES, MAX_GRANTS, MAX_LABEL_BYTES, MAX_SCOPES_PER_GRANT,
5};
6use ic_wire::{Bytes, Error, List, Text};
7
8trait Field<D>: Sized {
9    fn from_domain(value: D) -> Result<Self, Error>;
10    fn into_domain(self) -> Result<D, Error>;
11}
12macro_rules! scalar {
13    ($($ty:ty),*) => {$(impl Field<$ty> for $ty {
14        fn from_domain(value: $ty) -> Result<Self, Error> { Ok(value) }
15        fn into_domain(self) -> Result<$ty, Error> { Ok(self) }
16    })*};
17}
18scalar!(u16, u64, [u8; 16], [u8; 32]);
19impl<const N: usize> Field<Vec<u8>> for Bytes<N> {
20    fn from_domain(value: Vec<u8>) -> Result<Self, Error> {
21        Self::new(value)
22    }
23    fn into_domain(self) -> Result<Vec<u8>, Error> {
24        Ok(self.into_vec())
25    }
26}
27impl<const N: usize> Field<String> for Text<N> {
28    fn from_domain(value: String) -> Result<Self, Error> {
29        Self::new(value)
30    }
31    fn into_domain(self) -> Result<String, Error> {
32        Ok(self.into_string())
33    }
34}
35impl<D, W: Field<D>, const N: usize> Field<Vec<D>> for List<W, N> {
36    fn from_domain(value: Vec<D>) -> Result<Self, Error> {
37        if value.len() > N {
38            return Err(Error::Limit);
39        }
40        Self::new(
41            value
42                .into_iter()
43                .map(W::from_domain)
44                .collect::<Result<_, _>>()?,
45        )
46    }
47    fn into_domain(self) -> Result<Vec<D>, Error> {
48        self.into_vec().into_iter().map(W::into_domain).collect()
49    }
50}
51impl Field<domain::Principal> for Bytes<29> {
52    fn from_domain(value: domain::Principal) -> Result<Self, Error> {
53        Self::new(value.as_slice().to_vec())
54    }
55    fn into_domain(self) -> Result<domain::Principal, Error> {
56        domain::Principal::try_from_slice(self.as_slice()).map_err(|_| Error::Malformed)
57    }
58}
59impl Field<domain::CanonicalId> for [u8; 32] {
60    fn from_domain(value: domain::CanonicalId) -> Result<Self, Error> {
61        Ok(*value.as_bytes())
62    }
63    fn into_domain(self) -> Result<domain::CanonicalId, Error> {
64        Ok(domain::CanonicalId::from_bytes(self))
65    }
66}
67impl Field<domain::AuthRole> for Text<MAX_LABEL_BYTES> {
68    fn from_domain(value: domain::AuthRole) -> Result<Self, Error> {
69        Self::new(value.as_str().to_owned())
70    }
71    fn into_domain(self) -> Result<domain::AuthRole, Error> {
72        self.as_str().parse().map_err(|_| Error::Malformed)
73    }
74}
75// One declaration owns each Serde transport field and both typed
76// projections. Rust checks the domain field names/types; no intermediate codec.
77macro_rules! record {
78    ($name:ident { $($field:ident: $ty:ty),* $(,)? }) => {
79        #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
80#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
81        pub struct $name { $(pub $field: $ty),* }
82        impl TryFrom<domain::$name> for $name {
83            type Error = Error;
84            fn try_from(value: domain::$name) -> Result<Self, Error> {
85                Ok(Self { $($field: Field::from_domain(value.$field)?),* })
86            }
87        }
88        impl TryFrom<$name> for domain::$name {
89            type Error = Error;
90            fn try_from(value: $name) -> Result<Self, Error> {
91                Ok(Self { $($field: Field::into_domain(value.$field)?),* })
92            }
93        }
94        impl Field<domain::$name> for $name {
95            fn from_domain(value: domain::$name) -> Result<Self, Error> { value.try_into() }
96            fn into_domain(self) -> Result<domain::$name, Error> { self.try_into() }
97        }
98    };
99}
100macro_rules! unit_enum {
101    ($name:ident { $($variant:ident),* $(,)? }) => {
102        #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
103#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
104        pub enum $name { $($variant),* }
105        impl Field<domain::$name> for $name {
106            fn from_domain(value: domain::$name) -> Result<Self, Error> {
107                Ok(match value { $(domain::$name::$variant => Self::$variant),* })
108            }
109            fn into_domain(self) -> Result<domain::$name, Error> {
110                Ok(match self { $(Self::$variant => domain::$name::$variant),* })
111            }
112        }
113    };
114}
115macro_rules! tuple_enum {
116    ($name:ident { $($variant:ident($ty:ty)),* $(,)? }) => {
117        #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
118#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
119        pub enum $name { $($variant($ty)),* }
120        impl Field<domain::$name> for $name {
121            fn from_domain(value: domain::$name) -> Result<Self, Error> {
122                Ok(match value { $(domain::$name::$variant(v) => Self::$variant(Field::from_domain(v)?)),* })
123            }
124            fn into_domain(self) -> Result<domain::$name, Error> {
125                Ok(match self { $(Self::$variant(v) => domain::$name::$variant(Field::into_domain(v)?)),* })
126            }
127        }
128    };
129}
130record!(AudienceId {
131    canonical_network_id: [u8; 32],
132    fleet_id: [u8; 32]
133});
134record!(DelegatedRoleGrant { target: Text<MAX_LABEL_BYTES>, scopes: List<Text<MAX_LABEL_BYTES>, MAX_SCOPES_PER_GRANT> });
135tuple_enum!(DelegationAudience {
136    Fleet(AudienceId)
137});
138unit_enum!(ChainKeyAlgorithm { EcdsaSecp256k1 });
139record!(ChainKeyKeyId { name: Text<128> });
140unit_enum!(IssuerProofAlgorithm {
141    IcCanisterSignatureV1
142});
143#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
144#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
145pub enum IssuerProofBinding {
146    IcCanisterSignatureV1([u8; 32]),
147}
148impl Field<domain::IssuerProofBinding> for IssuerProofBinding {
149    fn from_domain(value: domain::IssuerProofBinding) -> Result<Self, Error> {
150        let domain::IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } = value;
151        Ok(Self::IcCanisterSignatureV1(seed_hash))
152    }
153    fn into_domain(self) -> Result<domain::IssuerProofBinding, Error> {
154        let Self::IcCanisterSignatureV1(seed_hash) = self;
155        Ok(domain::IssuerProofBinding::IcCanisterSignatureV1 { seed_hash })
156    }
157}
158record!(IcCanisterSignatureProofV1 { signature_cbor: Bytes<131072>, public_key_der: Bytes<256> });
159record!(ChainKeyBatchHeaderV1 {
160    schema_version: u16, root_canister_id: Bytes<29>, batch_id: [u8; 32],
161    proof_epoch: u64, registry_epoch: u64, registry_hash: [u8; 32],
162    tree_root: [u8; 32], not_before_ns: u64, expires_at_ns: u64,
163    algorithm: ChainKeyAlgorithm, key_id: ChainKeyKeyId,
164    derivation_path_hash: [u8; 32], key_version: u64,
165});
166record!(ChainKeyDelegationCertV1 {
167    root_canister_id: Bytes<29>, issuer_canister_id: Bytes<29>, proof_epoch: u64,
168    issuer_proof_algorithm: IssuerProofAlgorithm, issuer_proof_binding_hash: [u8; 32],
169    issuer_proof_binding: IssuerProofBinding, max_token_ttl_ns: u64,
170    audience: DelegationAudience, grants: List<DelegatedRoleGrant, MAX_GRANTS>,
171    not_before_ns: u64, expires_at_ns: u64, registry_epoch: u64, registry_hash: [u8; 32],
172});
173record!(ChainKeyRootSignatureV1 {
174    algorithm: ChainKeyAlgorithm, key_id: ChainKeyKeyId,
175    derivation_path: List<Bytes<256>, 32>, public_key: Bytes<65>, signature: Bytes<64>,
176});
177tuple_enum!(ChainKeyBatchWitnessStepV1 {
178    LeftSibling([u8; 32]),
179    RightSibling([u8; 32])
180});
181record!(ChainKeyBatchWitnessV1 { steps: List<ChainKeyBatchWitnessStepV1, 64> });
182record!(IcChainKeyBatchSignatureProofV1 {
183    header: ChainKeyBatchHeaderV1,
184    delegation_cert: ChainKeyDelegationCertV1,
185    issuer_witness: ChainKeyBatchWitnessV1,
186    signature: ChainKeyRootSignatureV1,
187});
188tuple_enum!(RootProof {
189    IcChainKeyBatchSignatureV1(IcChainKeyBatchSignatureProofV1)
190});
191record!(DelegationCert {
192    root_pid: Bytes<29>, issuer_pid: Bytes<29>, issuer_proof_alg: IssuerProofAlgorithm,
193    issuer_proof_binding_hash: [u8; 32], issuer_proof_binding: IssuerProofBinding,
194    issued_at_ns: u64, not_before_ns: u64, expires_at_ns: u64,
195    max_token_ttl_ns: u64, aud: DelegationAudience, grants: List<DelegatedRoleGrant, MAX_GRANTS>,
196});
197record!(DelegationProof {
198    cert: DelegationCert,
199    root_proof: RootProof
200});
201#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
202#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
203pub struct CertifiedProof {
204    pub delegation: DelegationProof,
205    pub issuer_signature: IcCanisterSignatureProofV1,
206}
207#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
208#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
209#[allow(
210    clippy::large_enum_variant,
211    reason = "Keep the bounded transport projection and explicit domain conversion"
212)]
213pub enum PortableProof {
214    Certified(CertifiedProof),
215    Ed25519(Bytes<64>),
216}
217impl Field<domain::PortableProof> for PortableProof {
218    fn from_domain(value: domain::PortableProof) -> Result<Self, Error> {
219        Ok(match value {
220            domain::PortableProof::Certified {
221                delegation,
222                issuer_signature,
223            } => Self::Certified(CertifiedProof {
224                delegation: (*delegation).try_into()?,
225                issuer_signature: issuer_signature.try_into()?,
226            }),
227            domain::PortableProof::Ed25519 { signature } => Self::Ed25519(Bytes::new(signature)?),
228        })
229    }
230    fn into_domain(self) -> Result<domain::PortableProof, Error> {
231        Ok(match self {
232            Self::Certified(value) => domain::PortableProof::Certified {
233                delegation: Box::new(value.delegation.try_into()?),
234                issuer_signature: value.issuer_signature.try_into()?,
235            },
236            Self::Ed25519(signature) => domain::PortableProof::Ed25519 {
237                signature: signature.into_vec(),
238            },
239        })
240    }
241}
242record!(PortableCredential { claims: Bytes<MAX_CLAIMS_BYTES>, proof: PortableProof });
243// Admission diagnostics are independent of proof validity and retry policy.
244unit_enum!(AdmissionRefusal {
245    NotYetValid,
246    Expired,
247    InvalidCredential,
248    AuthorityUnavailable,
249    BeforeReset,
250    ScopeRejected,
251    ReplayConflict,
252    Revoked,
253    Capacity,
254    Throttled,
255    Unavailable
256});
257impl From<domain::AdmissionRefusal> for AdmissionRefusal {
258    fn from(value: domain::AdmissionRefusal) -> Self {
259        use domain::AdmissionRefusal as R;
260        match value {
261            R::NotYetValid => Self::NotYetValid,
262            R::Expired => Self::Expired,
263            R::InvalidCredential => Self::InvalidCredential,
264            R::AuthorityUnavailable => Self::AuthorityUnavailable,
265            R::BeforeReset => Self::BeforeReset,
266            R::ScopeRejected => Self::ScopeRejected,
267            R::ReplayConflict => Self::ReplayConflict,
268            R::Revoked => Self::Revoked,
269            R::Capacity => Self::Capacity,
270            R::Throttled => Self::Throttled,
271            R::Unavailable => Self::Unavailable,
272        }
273    }
274}
275unit_enum!(LogoutUnavailable {
276    Capacity,
277    Storage,
278    Clock,
279    ResetPending
280});
281#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
282#[cfg_attr(feature = "typescript", derive(ic_wire::Wire))]
283pub enum LogoutResult {
284    Revoked(u64),
285    InvalidCaller,
286    Unavailable(LogoutUnavailable),
287}
288impl From<domain::LogoutResult> for LogoutResult {
289    fn from(value: domain::LogoutResult) -> Self {
290        match value {
291            domain::LogoutResult::Revoked { deny_until_ns } => Self::Revoked(deny_until_ns),
292            domain::LogoutResult::InvalidCaller => Self::InvalidCaller,
293            domain::LogoutResult::Unavailable(reason) => Self::Unavailable(match reason {
294                domain::LogoutUnavailable::Capacity => LogoutUnavailable::Capacity,
295                domain::LogoutUnavailable::Storage => LogoutUnavailable::Storage,
296                domain::LogoutUnavailable::Clock => LogoutUnavailable::Clock,
297                domain::LogoutUnavailable::ResetPending => LogoutUnavailable::ResetPending,
298            }),
299        }
300    }
301}