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