ic_auth/portable/
rules.rs1use super::{PortableEncodingError, browser_principal, validate_shape};
4use crate::certificate_issuance::{grants_are_subset, positive_duration, validate_grants};
5use ic_auth_protocol_types::{
6 AudienceId, DelegatedRoleGrant, PortableClaims, PortableMechanism, Principal,
7};
8use thiserror::Error;
9
10#[derive(Clone, Copy, Debug)]
15pub struct PortableAuthority<'a> {
16 pub audience: &'a AudienceId,
17 pub issuer: Principal,
18 pub mechanism: PortableMechanism,
19 pub key_binding_id: [u8; 32],
20 pub grants: &'a [DelegatedRoleGrant],
21 pub caller: Principal,
23 pub now_ns: u64,
24 pub authority_not_before_ns: u64,
25 pub authority_expires_at_ns: u64,
26 pub enrollment_expires_at_ns: u64,
27 pub min_login_started_at_ns: u64,
29}
30
31#[derive(Clone, Copy, Debug)]
34pub struct PortableClaimsContext<'a> {
35 pub authority: PortableAuthority<'a>,
36 pub min_proof_issued_at_ns: u64,
37 pub max_login_ttl_ns: u64,
39 pub max_proof_ttl_ns: u64,
40}
41
42impl PortableAuthority<'_> {
43 pub(crate) fn authority_shape_valid(&self) -> bool {
46 self.issuer != Principal::anonymous()
47 && self.authority_not_before_ns < self.authority_expires_at_ns
48 && validate_grants(
49 self.grants,
50 crate::limits::MAX_GRANTS,
51 crate::limits::MAX_SCOPES_PER_GRANT,
52 )
53 .is_ok()
54 }
55}
56
57impl PortableClaimsContext<'_> {
58 pub fn advance_reset_floors(&mut self, clock_skew_ns: u64) -> Result<(), PortableClaimsError> {
66 let proof_floor = self
67 .authority
68 .now_ns
69 .checked_add(1)
70 .ok_or(PortableClaimsError::InvalidContext)?;
71 let login_floor = proof_floor
72 .checked_add(clock_skew_ns)
73 .ok_or(PortableClaimsError::InvalidContext)?;
74 self.authority.min_login_started_at_ns =
75 self.authority.min_login_started_at_ns.max(login_floor);
76 self.min_proof_issued_at_ns = self.min_proof_issued_at_ns.max(proof_floor);
77 Ok(())
78 }
79}
80
81#[derive(Debug, Error, Eq, PartialEq)]
83pub enum PortableClaimsError {
84 #[error("invalid protected portable claims context")]
85 InvalidContext,
86 #[error("portable audience does not match protected authority")]
87 AudienceMismatch,
88 #[error("portable issuer does not match protected authority")]
89 IssuerMismatch,
90 #[error("portable mechanism does not match protected authority")]
91 MechanismMismatch,
92 #[error("portable key binding does not match protected authority")]
93 KeyMismatch,
94 #[error("browser key does not identify the authenticated caller")]
95 CallerMismatch,
96 #[error("portable grants exceed protected authority")]
97 GrantsExceeded,
98 #[error("portable login or proof window is invalid")]
99 InvalidWindow,
100 #[error("portable proof is not yet valid")]
101 NotYetValid,
102 #[error("portable proof has expired")]
103 Expired,
104 #[error("portable login or issuance predates the protected reset floor")]
105 BeforeReset,
106 #[error("portable authority or enrollment is not current")]
107 StaleAuthority,
108 #[error(transparent)]
109 Encoding(#[from] PortableEncodingError),
110}
111
112pub fn validate_claims_context(
119 claims: &PortableClaims,
120 context: &PortableClaimsContext<'_>,
121) -> Result<(), PortableClaimsError> {
122 validate_context(context)?;
123 validate_shape(claims)?;
124 validate_bindings(claims, context)
125}
126
127#[cfg(any(
130 feature = "portable-signature-verification",
131 feature = "certified-proof-verification"
132))]
133pub(super) fn validate_decoded_context(
134 claims: &PortableClaims,
135 context: &PortableClaimsContext<'_>,
136) -> Result<(), PortableClaimsError> {
137 validate_context(context)?;
138 validate_bindings(claims, context)
139}
140
141fn validate_context(context: &PortableClaimsContext<'_>) -> Result<(), PortableClaimsError> {
142 use PortableClaimsError as Error;
143 let authority = &context.authority;
144 if context.max_login_ttl_ns == 0
145 || context.max_login_ttl_ns > crate::limits::MAX_LOGIN_NS
146 || context.max_proof_ttl_ns == 0
147 || context.max_proof_ttl_ns > crate::limits::MAX_PROOF_NS
148 || !authority.authority_shape_valid()
149 || authority.enrollment_expires_at_ns == 0
150 {
151 return Err(Error::InvalidContext);
152 }
153 Ok(())
154}
155
156fn validate_bindings(
157 claims: &PortableClaims,
158 context: &PortableClaimsContext<'_>,
159) -> Result<(), PortableClaimsError> {
160 use PortableClaimsError as Error;
161 let authority = &context.authority;
162 if claims.audience != *authority.audience {
163 return Err(Error::AudienceMismatch);
164 }
165 if claims.issuer != authority.issuer {
166 return Err(Error::IssuerMismatch);
167 }
168 if claims.mechanism != authority.mechanism {
169 return Err(Error::MechanismMismatch);
170 }
171 if claims.key_binding_id != authority.key_binding_id {
172 return Err(Error::KeyMismatch);
173 }
174 if browser_principal(&claims.browser_public_key) != authority.caller {
175 return Err(Error::CallerMismatch);
176 }
177 if !grants_are_subset(&claims.grants, authority.grants) {
178 return Err(Error::GrantsExceeded);
179 }
180 if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
181 .is_none_or(|ttl| ttl > context.max_login_ttl_ns)
182 || positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
183 .is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
184 || claims.login_started_at_ns > claims.issued_at_ns
185 || claims.admission_expires_at_ns > claims.login_deadline_ns
186 {
187 return Err(Error::InvalidWindow);
188 }
189 if claims.login_started_at_ns < authority.min_login_started_at_ns
190 || claims.issued_at_ns < context.min_proof_issued_at_ns
191 {
192 return Err(Error::BeforeReset);
193 }
194 if authority.now_ns < authority.authority_not_before_ns
195 || authority.now_ns >= authority.authority_expires_at_ns
196 || authority.now_ns >= authority.enrollment_expires_at_ns
197 || claims.issued_at_ns < authority.authority_not_before_ns
198 {
199 return Err(Error::StaleAuthority);
200 }
201 if authority.now_ns < claims.issued_at_ns {
202 return Err(Error::NotYetValid);
203 }
204 if authority.now_ns >= claims.admission_expires_at_ns {
205 return Err(Error::Expired);
206 }
207 Ok(())
208}