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