1#[cfg(any(
7 feature = "portable-signature-verification",
8 feature = "certified-proof-verification"
9))]
10mod decoded;
11mod revocation;
12mod rules;
13mod verified;
14#[cfg(any(
15 feature = "portable-signature-verification",
16 feature = "certified-proof-verification"
17))]
18pub(crate) use decoded::DecodedPortableClaims;
19pub use revocation::{
20 LoginBinding, LoginRecordState, LoginRevocationChange, LoginRevocationError,
21 MemoryLoginRevocations,
22};
23pub use rules::{
24 PortableAuthority, PortableClaimsContext, PortableClaimsError, validate_claims_context,
25};
26pub use verified::VerifiedPortableClaims;
27
28use crate::canonical::{CanonicalAuthError, encode_principal, encode_role_grants};
29use crate::certificate_issuance::{GrantValidationError, validate_grants};
30use crate::limits::{MAX_GRANTS, MAX_LABEL_BYTES, MAX_SCOPES_PER_GRANT};
31use ic_auth_protocol_types::{
32 AudienceId, CanonicalId, DelegatedRoleGrant, PortableClaims, PortableMechanism, Principal,
33};
34use sha2::{Digest, Sha256};
35use thiserror::Error;
36
37const DOMAIN: &[u8] = b"IC-AUTH-PORTABLE-CREDENTIAL\0";
38const SCHEMA: u16 = 1;
39
40pub use ic_auth_protocol_types::limits::MAX_CLAIMS_BYTES;
42
43#[derive(Debug, Error, PartialEq, Eq)]
45pub enum PortableEncodingError {
46 #[error("portable claims exceed their byte bound")]
47 TooLarge,
48 #[error("truncated portable claims")]
49 Truncated,
50 #[error("wrong portable domain, schema or mechanism")]
51 WrongDomain,
52 #[error("portable claims contain trailing bytes")]
53 TrailingBytes,
54 #[error("invalid portable principal, label or count")]
55 InvalidShape,
56 #[error("anonymous account subject")]
57 AnonymousSubject,
58 #[error(transparent)]
59 Grants(#[from] GrantValidationError),
60 #[error(transparent)]
61 Canonical(#[from] CanonicalAuthError),
62}
63
64pub fn encode_claims(claims: &PortableClaims) -> Result<Vec<u8>, PortableEncodingError> {
67 validate_shape(claims)?;
68 let mut out = Vec::new();
71 out.extend_from_slice(DOMAIN);
72 out.extend_from_slice(&SCHEMA.to_be_bytes());
73 out.push(match claims.mechanism {
74 PortableMechanism::Certified => 0,
75 PortableMechanism::Ed25519 => 1,
76 });
77 out.extend_from_slice(claims.audience.canonical_network_id.as_bytes());
78 out.extend_from_slice(claims.audience.fleet_id.as_bytes());
79 encode_principal(&mut out, claims.issuer)?;
80 out.extend_from_slice(&claims.key_binding_id);
81 out.extend_from_slice(&claims.browser_public_key);
82 encode_principal(&mut out, claims.account_subject)?;
83 for time in [
84 claims.login_started_at_ns,
85 claims.login_deadline_ns,
86 claims.issued_at_ns,
87 claims.admission_expires_at_ns,
88 ] {
89 out.extend_from_slice(&time.to_be_bytes());
90 }
91 out.extend_from_slice(&claims.nonce);
92 encode_role_grants(&mut out, &claims.grants)?;
93 if out.len() > MAX_CLAIMS_BYTES {
94 return Err(PortableEncodingError::TooLarge);
95 }
96 Ok(out)
97}
98
99pub fn decode_claims(bytes: &[u8]) -> Result<PortableClaims, PortableEncodingError> {
103 if bytes.len() > MAX_CLAIMS_BYTES {
104 return Err(PortableEncodingError::TooLarge);
105 }
106 let mut input = Reader(bytes);
107 if input.take(DOMAIN.len())? != DOMAIN || input.array::<2>()? != SCHEMA.to_be_bytes() {
108 return Err(PortableEncodingError::WrongDomain);
109 }
110 let mechanism = match input.array::<1>()? {
111 [0] => PortableMechanism::Certified,
112 [1] => PortableMechanism::Ed25519,
113 _ => return Err(PortableEncodingError::WrongDomain),
114 };
115 let audience = AudienceId {
116 canonical_network_id: CanonicalId::from_bytes(input.array()?),
117 fleet_id: CanonicalId::from_bytes(input.array()?),
118 };
119 let issuer = input.principal()?;
120 let key_binding_id = input.array()?;
121 let browser_public_key = input.array()?;
122 let account_subject = input.principal()?;
123 let login_started_at_ns = u64::from_be_bytes(input.array()?);
124 let login_deadline_ns = u64::from_be_bytes(input.array()?);
125 let issued_at_ns = u64::from_be_bytes(input.array()?);
126 let admission_expires_at_ns = u64::from_be_bytes(input.array()?);
127 let nonce = input.array()?;
128 let count = input.count(MAX_GRANTS)?;
129 let mut grants = Vec::with_capacity(count);
130 for _ in 0..count {
131 let target = input
132 .label()?
133 .parse()
134 .map_err(|_| PortableEncodingError::InvalidShape)?;
135 let count = input.count(MAX_SCOPES_PER_GRANT)?;
136 let mut scopes = Vec::with_capacity(count);
137 for _ in 0..count {
138 scopes.push(input.label()?.to_owned());
139 }
140 grants.push(DelegatedRoleGrant { target, scopes });
141 }
142 if !input.0.is_empty() {
143 return Err(PortableEncodingError::TrailingBytes);
144 }
145 let claims = PortableClaims {
146 mechanism,
147 audience,
148 issuer,
149 key_binding_id,
150 browser_public_key,
151 account_subject,
152 login_started_at_ns,
153 login_deadline_ns,
154 issued_at_ns,
155 admission_expires_at_ns,
156 nonce,
157 grants,
158 };
159 validate_shape(&claims)?;
160 Ok(claims)
161}
162
163pub fn claims_hash(claims: &PortableClaims) -> Result<[u8; 32], PortableEncodingError> {
166 Ok(Sha256::digest(encode_claims(claims)?).into())
167}
168
169pub fn browser_principal(public_key: &[u8; 32]) -> Principal {
173 let mut der = [0; 44];
175 der[..12].copy_from_slice(&[
176 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0,
177 ]);
178 der[12..].copy_from_slice(public_key);
179 Principal::self_authenticating(der)
180}
181
182fn validate_shape(claims: &PortableClaims) -> Result<(), PortableEncodingError> {
183 if claims.account_subject == Principal::anonymous() {
184 return Err(PortableEncodingError::AnonymousSubject);
185 }
186 validate_grants(&claims.grants, MAX_GRANTS, MAX_SCOPES_PER_GRANT)?;
187 if claims
188 .grants
189 .iter()
190 .any(|grant| grant.target.as_str().len() > MAX_LABEL_BYTES)
191 {
192 return Err(PortableEncodingError::InvalidShape);
193 }
194 Ok(())
195}
196
197struct Reader<'a>(&'a [u8]);
198
199impl<'a> Reader<'a> {
200 fn take(&mut self, len: usize) -> Result<&'a [u8], PortableEncodingError> {
201 let (head, tail) = self
202 .0
203 .split_at_checked(len)
204 .ok_or(PortableEncodingError::Truncated)?;
205 self.0 = tail;
206 Ok(head)
207 }
208
209 fn array<const N: usize>(&mut self) -> Result<[u8; N], PortableEncodingError> {
210 self.take(N)?
211 .try_into()
212 .map_err(|_| PortableEncodingError::Truncated)
213 }
214
215 fn length(&mut self, max: usize) -> Result<usize, PortableEncodingError> {
216 let len = u32::from_be_bytes(self.array()?);
217 let len = usize::try_from(len).map_err(|_| PortableEncodingError::InvalidShape)?;
218 if len > max {
219 return Err(PortableEncodingError::InvalidShape);
220 }
221 Ok(len)
222 }
223
224 fn count(&mut self, max: usize) -> Result<usize, PortableEncodingError> {
225 let count = self.length(max)?;
226 if count == 0 {
227 return Err(PortableEncodingError::InvalidShape);
228 }
229 Ok(count)
230 }
231
232 fn principal(&mut self) -> Result<Principal, PortableEncodingError> {
233 let len = self.length(29)?;
234 Ok(Principal::from_slice(self.take(len)?))
235 }
236
237 fn label(&mut self) -> Result<&'a str, PortableEncodingError> {
238 let len = self.count(MAX_LABEL_BYTES)?;
239 std::str::from_utf8(self.take(len)?).map_err(|_| PortableEncodingError::InvalidShape)
240 }
241}