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(claims, context)
118}
119
120#[cfg(any(
123 feature = "portable-signature-verification",
124 feature = "certified-proof-verification"
125))]
126pub(super) fn validate_decoded_context(
127 claims: &PortableClaims,
128 context: &PortableClaimsContext<'_>,
129) -> Result<(), PortableClaimsError> {
130 validate_context(context)?;
131 validate_bindings(claims, context)
132}
133
134fn validate_context(context: &PortableClaimsContext<'_>) -> Result<(), PortableClaimsError> {
135 use PortableClaimsError as Error;
136 let authority = &context.authority;
137 if context.max_login_ttl_ns == 0
138 || context.max_login_ttl_ns > crate::limits::MAX_LOGIN_NS
139 || context.max_proof_ttl_ns == 0
140 || context.max_proof_ttl_ns > crate::limits::MAX_PROOF_NS
141 || !authority.authority_shape_valid()
142 || authority.enrollment_expires_at_ns == 0
143 {
144 return Err(Error::InvalidContext);
145 }
146 Ok(())
147}
148
149fn validate_bindings(
150 claims: &PortableClaims,
151 context: &PortableClaimsContext<'_>,
152) -> Result<(), PortableClaimsError> {
153 use PortableClaimsError as Error;
154 let authority = &context.authority;
155 if claims.audience != *authority.audience {
156 return Err(Error::AudienceMismatch);
157 }
158 if claims.issuer != authority.issuer {
159 return Err(Error::IssuerMismatch);
160 }
161 if claims.mechanism != authority.mechanism {
162 return Err(Error::MechanismMismatch);
163 }
164 if claims.key_binding_id != authority.key_binding_id {
165 return Err(Error::KeyMismatch);
166 }
167 if browser_principal(&claims.browser_public_key) != authority.caller {
168 return Err(Error::CallerMismatch);
169 }
170 if !grants_are_subset(&claims.grants, authority.grants.as_slice()) {
171 return Err(Error::GrantsExceeded);
172 }
173 if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
174 .is_none_or(|ttl| ttl > context.max_login_ttl_ns)
175 || positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
176 .is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
177 || claims.login_started_at_ns > claims.issued_at_ns
178 || claims.admission_expires_at_ns > claims.login_deadline_ns
179 {
180 return Err(Error::InvalidWindow);
181 }
182 if claims.login_started_at_ns < authority.min_login_started_at_ns
183 || claims.issued_at_ns < context.min_proof_issued_at_ns
184 {
185 return Err(Error::BeforeReset);
186 }
187 if authority.now_ns < authority.authority_not_before_ns
188 || authority.now_ns >= authority.authority_expires_at_ns
189 || authority.now_ns >= authority.enrollment_expires_at_ns
190 || claims.issued_at_ns < authority.authority_not_before_ns
191 {
192 return Err(Error::StaleAuthority);
193 }
194 if authority.now_ns < claims.issued_at_ns {
195 return Err(Error::NotYetValid);
196 }
197 if authority.now_ns >= claims.admission_expires_at_ns {
198 return Err(Error::Expired);
199 }
200 Ok(())
201}