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