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