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