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, Wire};
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 transport field, its explicit ID and both typed
73// projections. Rust checks the domain field names/types; no intermediate codec.
74macro_rules! record {
75    ($name:ident { $($id:literal: $field:ident: $ty:ty),* $(,)? }) => {
76        #[derive(Clone, Debug, Eq, PartialEq, Wire)]
77        pub struct $name { $(#[wire(id = $id)] pub $field: $ty),* }
78        impl TryFrom<domain::$name> for $name {
79            type Error = Error;
80            fn try_from(value: domain::$name) -> Result<Self, Error> {
81                Ok(Self { $($field: Field::from_domain(value.$field)?),* })
82            }
83        }
84        impl TryFrom<$name> for domain::$name {
85            type Error = Error;
86            fn try_from(value: $name) -> Result<Self, Error> {
87                Ok(Self { $($field: Field::into_domain(value.$field)?),* })
88            }
89        }
90        impl Field<domain::$name> for $name {
91            fn from_domain(value: domain::$name) -> Result<Self, Error> { value.try_into() }
92            fn into_domain(self) -> Result<domain::$name, Error> { self.try_into() }
93        }
94    };
95}
96macro_rules! unit_enum {
97    ($name:ident { $($id:literal: $variant:ident),* $(,)? }) => {
98        #[derive(Clone, Debug, Eq, PartialEq, Wire)]
99        pub enum $name { $(#[wire(id = $id)] $variant),* }
100        impl Field<domain::$name> for $name {
101            fn from_domain(value: domain::$name) -> Result<Self, Error> {
102                Ok(match value { $(domain::$name::$variant => Self::$variant),* })
103            }
104            fn into_domain(self) -> Result<domain::$name, Error> {
105                Ok(match self { $(Self::$variant => domain::$name::$variant),* })
106            }
107        }
108    };
109}
110macro_rules! tuple_enum {
111    ($name:ident { $($id:literal: $variant:ident($ty:ty)),* $(,)? }) => {
112        #[derive(Clone, Debug, Eq, PartialEq, Wire)]
113        pub enum $name { $(#[wire(id = $id)] $variant($ty)),* }
114        impl Field<domain::$name> for $name {
115            fn from_domain(value: domain::$name) -> Result<Self, Error> {
116                Ok(match value { $(domain::$name::$variant(v) => Self::$variant(Field::from_domain(v)?)),* })
117            }
118            fn into_domain(self) -> Result<domain::$name, Error> {
119                Ok(match self { $(Self::$variant(v) => domain::$name::$variant(Field::into_domain(v)?)),* })
120            }
121        }
122    };
123}
124record!(AudienceId { 1: canonical_network_id: [u8; 32], 2: fleet_id: [u8; 32] });
125record!(DelegatedRoleGrant { 1: target: Text<64>, 2: scopes: List<Text<64>, 32> });
126tuple_enum!(DelegationAudience {
127    0: Fleet(AudienceId)
128});
129unit_enum!(ChainKeyAlgorithm { 0: EcdsaSecp256k1 });
130record!(ChainKeyKeyId { 1: name: Text<128> });
131unit_enum!(IssuerProofAlgorithm {
132    0: IcCanisterSignatureV1
133});
134#[derive(Clone, Debug, Eq, PartialEq, Wire)]
135pub enum IssuerProofBinding {
136    #[wire(id = 0)]
137    IcCanisterSignatureV1([u8; 32]),
138}
139impl Field<domain::IssuerProofBinding> for IssuerProofBinding {
140    fn from_domain(value: domain::IssuerProofBinding) -> Result<Self, Error> {
141        let domain::IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } = value;
142        Ok(Self::IcCanisterSignatureV1(seed_hash))
143    }
144    fn into_domain(self) -> Result<domain::IssuerProofBinding, Error> {
145        let Self::IcCanisterSignatureV1(seed_hash) = self;
146        Ok(domain::IssuerProofBinding::IcCanisterSignatureV1 { seed_hash })
147    }
148}
149record!(IcCanisterSignatureProofV1 { 1: signature_cbor: Bytes<131072>, 2: public_key_der: Bytes<256> });
150record!(ChainKeyBatchHeaderV1 {
151    1: schema_version: u16, 2: root_canister_id: Bytes<29>, 3: batch_id: [u8; 32],
152    4: proof_epoch: u64, 5: registry_epoch: u64, 6: registry_hash: [u8; 32],
153    7: tree_root: [u8; 32], 8: not_before_ns: u64, 9: expires_at_ns: u64,
154    10: algorithm: ChainKeyAlgorithm, 11: key_id: ChainKeyKeyId,
155    12: derivation_path_hash: [u8; 32], 13: key_version: u64,
156});
157record!(ChainKeyDelegationCertV1 {
158    1: root_canister_id: Bytes<29>, 2: issuer_canister_id: Bytes<29>, 3: proof_epoch: u64,
159    4: issuer_proof_algorithm: IssuerProofAlgorithm, 5: issuer_proof_binding_hash: [u8; 32],
160    6: issuer_proof_binding: IssuerProofBinding, 7: max_token_ttl_ns: u64,
161    8: audience: DelegationAudience, 9: grants: List<DelegatedRoleGrant, 16>,
162    10: not_before_ns: u64, 11: expires_at_ns: u64, 12: registry_epoch: u64, 13: registry_hash: [u8; 32],
163});
164record!(ChainKeyRootSignatureV1 {
165    1: algorithm: ChainKeyAlgorithm, 2: key_id: ChainKeyKeyId,
166    3: derivation_path: List<Bytes<256>, 32>, 4: public_key: Bytes<65>, 5: signature: Bytes<64>,
167});
168tuple_enum!(ChainKeyBatchWitnessStepV1 {
169    0: LeftSibling([u8; 32]),
170    1: RightSibling([u8; 32])
171});
172record!(ChainKeyBatchWitnessV1 { 1: steps: List<ChainKeyBatchWitnessStepV1, 64> });
173record!(IcChainKeyBatchSignatureProofV1 {
174    1: header: ChainKeyBatchHeaderV1, 2: delegation_cert: ChainKeyDelegationCertV1,
175    3: issuer_witness: ChainKeyBatchWitnessV1, 4: signature: ChainKeyRootSignatureV1,
176});
177tuple_enum!(RootProof {
178    0: IcChainKeyBatchSignatureV1(IcChainKeyBatchSignatureProofV1)
179});
180record!(DelegationCert {
181    1: root_pid: Bytes<29>, 2: issuer_pid: Bytes<29>, 3: issuer_proof_alg: IssuerProofAlgorithm,
182    4: issuer_proof_binding_hash: [u8; 32], 5: issuer_proof_binding: IssuerProofBinding,
183    6: issued_at_ns: u64, 7: not_before_ns: u64, 8: expires_at_ns: u64,
184    9: max_token_ttl_ns: u64, 10: aud: DelegationAudience, 11: grants: List<DelegatedRoleGrant, 16>,
185});
186record!(DelegationProof { 1: cert: DelegationCert, 2: root_proof: RootProof });
187#[derive(Clone, Debug, Eq, PartialEq, Wire)]
188pub struct CertifiedProof {
189    #[wire(id = 1)]
190    pub delegation: DelegationProof,
191    #[wire(id = 2)]
192    pub issuer_signature: IcCanisterSignatureProofV1,
193}
194#[derive(Clone, Debug, Eq, PartialEq, Wire)]
195#[allow(
196    clippy::large_enum_variant,
197    reason = "The upstream derive has no Box shape; retain its bounded canonical schema instead of adding a private codec"
198)]
199pub enum PortableProof {
200    #[wire(id = 0)]
201    Certified(CertifiedProof),
202    #[wire(id = 1)]
203    Ed25519(Bytes<64>),
204}
205impl Field<domain::PortableProof> for PortableProof {
206    fn from_domain(value: domain::PortableProof) -> Result<Self, Error> {
207        Ok(match value {
208            domain::PortableProof::Certified {
209                delegation,
210                issuer_signature,
211            } => Self::Certified(CertifiedProof {
212                delegation: (*delegation).try_into()?,
213                issuer_signature: issuer_signature.try_into()?,
214            }),
215            domain::PortableProof::Ed25519 { signature } => Self::Ed25519(Bytes::new(signature)?),
216        })
217    }
218    fn into_domain(self) -> Result<domain::PortableProof, Error> {
219        Ok(match self {
220            Self::Certified(value) => domain::PortableProof::Certified {
221                delegation: Box::new(value.delegation.try_into()?),
222                issuer_signature: value.issuer_signature.try_into()?,
223            },
224            Self::Ed25519(signature) => domain::PortableProof::Ed25519 {
225                signature: signature.into_vec(),
226            },
227        })
228    }
229}
230record!(PortableCredential { 1: claims: Bytes<65536>, 2: proof: PortableProof });
231unit_enum!(LogoutUnavailable {
232    0: Capacity,
233    1: Storage,
234    2: Clock,
235    3: ResetPending
236});
237#[derive(Clone, Debug, Eq, PartialEq, Wire)]
238pub enum LogoutResult {
239    #[wire(id = 0)]
240    Revoked(u64),
241    #[wire(id = 1)]
242    InvalidCaller,
243    #[wire(id = 2)]
244    Unavailable(LogoutUnavailable),
245}
246impl From<domain::LogoutResult> for LogoutResult {
247    fn from(value: domain::LogoutResult) -> Self {
248        match value {
249            domain::LogoutResult::Revoked { deny_until_ns } => Self::Revoked(deny_until_ns),
250            domain::LogoutResult::InvalidCaller => Self::InvalidCaller,
251            domain::LogoutResult::Unavailable(reason) => Self::Unavailable(match reason {
252                domain::LogoutUnavailable::Capacity => LogoutUnavailable::Capacity,
253                domain::LogoutUnavailable::Storage => LogoutUnavailable::Storage,
254                domain::LogoutUnavailable::Clock => LogoutUnavailable::Clock,
255                domain::LogoutUnavailable::ResetPending => LogoutUnavailable::ResetPending,
256            }),
257        }
258    }
259}