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