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