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 use PortableClaimsError as Error;
123 let authority = &context.authority;
124 if context.max_login_ttl_ns == 0
125 || context.max_login_ttl_ns > crate::limits::MAX_LOGIN_NS
126 || context.max_proof_ttl_ns == 0
127 || context.max_proof_ttl_ns > crate::limits::MAX_PROOF_NS
128 || !authority.authority_shape_valid()
129 || authority.enrollment_expires_at_ns == 0
130 {
131 return Err(Error::InvalidContext);
132 }
133 validate_shape(claims)?;
134 if claims.audience != *authority.audience {
135 return Err(Error::AudienceMismatch);
136 }
137 if claims.issuer != authority.issuer {
138 return Err(Error::IssuerMismatch);
139 }
140 if claims.mechanism != authority.mechanism {
141 return Err(Error::MechanismMismatch);
142 }
143 if claims.key_binding_id != authority.key_binding_id {
144 return Err(Error::KeyMismatch);
145 }
146 if browser_principal(&claims.browser_public_key) != authority.caller {
147 return Err(Error::CallerMismatch);
148 }
149 if !grants_are_subset(&claims.grants, authority.grants) {
150 return Err(Error::GrantsExceeded);
151 }
152 if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
153 .is_none_or(|ttl| ttl > context.max_login_ttl_ns)
154 || positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
155 .is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
156 || claims.login_started_at_ns > claims.issued_at_ns
157 || claims.admission_expires_at_ns > claims.login_deadline_ns
158 {
159 return Err(Error::InvalidWindow);
160 }
161 if claims.login_started_at_ns < authority.min_login_started_at_ns
162 || claims.issued_at_ns < context.min_proof_issued_at_ns
163 {
164 return Err(Error::BeforeReset);
165 }
166 if authority.now_ns < authority.authority_not_before_ns
167 || authority.now_ns >= authority.authority_expires_at_ns
168 || authority.now_ns >= authority.enrollment_expires_at_ns
169 || claims.issued_at_ns < authority.authority_not_before_ns
170 {
171 return Err(Error::StaleAuthority);
172 }
173 if authority.now_ns < claims.issued_at_ns {
174 return Err(Error::NotYetValid);
175 }
176 if authority.now_ns >= claims.admission_expires_at_ns {
177 return Err(Error::Expired);
178 }
179 Ok(())
180}