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 PortableClaimsContext<'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 pub min_proof_issued_at_ns: u64,
30 pub max_login_ttl_ns: u64,
32 pub max_proof_ttl_ns: u64,
33}
34
35impl PortableClaimsContext<'_> {
36 pub(crate) fn authority_shape_valid(&self) -> bool {
39 self.issuer != Principal::anonymous()
40 && self.authority_not_before_ns < self.authority_expires_at_ns
41 && validate_grants(
42 self.grants,
43 crate::limits::MAX_GRANTS,
44 crate::limits::MAX_SCOPES_PER_GRANT,
45 )
46 .is_ok()
47 }
48
49 pub fn advance_reset_floors(&mut self, clock_skew_ns: u64) -> Result<(), PortableClaimsError> {
57 let proof_floor = self
58 .now_ns
59 .checked_add(1)
60 .ok_or(PortableClaimsError::InvalidContext)?;
61 let login_floor = proof_floor
62 .checked_add(clock_skew_ns)
63 .ok_or(PortableClaimsError::InvalidContext)?;
64 self.min_login_started_at_ns = self.min_login_started_at_ns.max(login_floor);
65 self.min_proof_issued_at_ns = self.min_proof_issued_at_ns.max(proof_floor);
66 Ok(())
67 }
68}
69
70#[derive(Debug, Error, Eq, PartialEq)]
72pub enum PortableClaimsError {
73 #[error("invalid protected portable claims context")]
74 InvalidContext,
75 #[error("portable audience does not match protected authority")]
76 AudienceMismatch,
77 #[error("portable issuer does not match protected authority")]
78 IssuerMismatch,
79 #[error("portable mechanism does not match protected authority")]
80 MechanismMismatch,
81 #[error("portable key binding does not match protected authority")]
82 KeyMismatch,
83 #[error("browser key does not identify the authenticated caller")]
84 CallerMismatch,
85 #[error("portable grants exceed protected authority")]
86 GrantsExceeded,
87 #[error("portable login or proof window is invalid")]
88 InvalidWindow,
89 #[error("portable proof is not yet valid")]
90 NotYetValid,
91 #[error("portable proof has expired")]
92 Expired,
93 #[error("portable login or issuance predates the protected reset floor")]
94 BeforeReset,
95 #[error("portable authority or enrollment is not current")]
96 StaleAuthority,
97 #[error(transparent)]
98 Encoding(#[from] PortableEncodingError),
99}
100
101pub fn validate_claims_context(
108 claims: &PortableClaims,
109 context: &PortableClaimsContext<'_>,
110) -> Result<(), PortableClaimsError> {
111 use PortableClaimsError as Error;
112 if context.max_login_ttl_ns == 0
113 || context.max_login_ttl_ns > crate::limits::MAX_LOGIN_NS
114 || context.max_proof_ttl_ns == 0
115 || context.max_proof_ttl_ns > crate::limits::MAX_PROOF_NS
116 || !context.authority_shape_valid()
117 || context.enrollment_expires_at_ns == 0
118 {
119 return Err(Error::InvalidContext);
120 }
121 validate_shape(claims)?;
122 if claims.audience != *context.audience {
123 return Err(Error::AudienceMismatch);
124 }
125 if claims.issuer != context.issuer {
126 return Err(Error::IssuerMismatch);
127 }
128 if claims.mechanism != context.mechanism {
129 return Err(Error::MechanismMismatch);
130 }
131 if claims.key_binding_id != context.key_binding_id {
132 return Err(Error::KeyMismatch);
133 }
134 if browser_principal(&claims.browser_public_key) != context.caller {
135 return Err(Error::CallerMismatch);
136 }
137 if !grants_are_subset(&claims.grants, context.grants) {
138 return Err(Error::GrantsExceeded);
139 }
140 if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
141 .is_none_or(|ttl| ttl > context.max_login_ttl_ns)
142 || positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
143 .is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
144 || claims.login_started_at_ns > claims.issued_at_ns
145 || claims.admission_expires_at_ns > claims.login_deadline_ns
146 {
147 return Err(Error::InvalidWindow);
148 }
149 if claims.login_started_at_ns < context.min_login_started_at_ns
150 || claims.issued_at_ns < context.min_proof_issued_at_ns
151 {
152 return Err(Error::BeforeReset);
153 }
154 if context.now_ns < context.authority_not_before_ns
155 || context.now_ns >= context.authority_expires_at_ns
156 || context.now_ns >= context.enrollment_expires_at_ns
157 || claims.issued_at_ns < context.authority_not_before_ns
158 {
159 return Err(Error::StaleAuthority);
160 }
161 if context.now_ns < claims.issued_at_ns {
162 return Err(Error::NotYetValid);
163 }
164 if context.now_ns >= claims.admission_expires_at_ns {
165 return Err(Error::Expired);
166 }
167 Ok(())
168}