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(context.grants, 16, 32).is_err()
107 {
108 return Err(Error::InvalidContext);
109 }
110 validate_shape(claims)?;
111 if claims.audience != *context.audience {
112 return Err(Error::AudienceMismatch);
113 }
114 if claims.issuer != context.issuer {
115 return Err(Error::IssuerMismatch);
116 }
117 if claims.mechanism != context.mechanism {
118 return Err(Error::MechanismMismatch);
119 }
120 if claims.key_binding_id != context.key_binding_id {
121 return Err(Error::KeyMismatch);
122 }
123 if browser_principal(&claims.browser_public_key) != context.caller {
124 return Err(Error::CallerMismatch);
125 }
126 if !grants_are_subset(&claims.grants, context.grants) {
127 return Err(Error::GrantsExceeded);
128 }
129 if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
130 .is_none_or(|ttl| ttl > context.max_login_ttl_ns)
131 || positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
132 .is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
133 || claims.login_started_at_ns > claims.issued_at_ns
134 || claims.admission_expires_at_ns > claims.login_deadline_ns
135 {
136 return Err(Error::InvalidWindow);
137 }
138 if claims.login_started_at_ns < context.min_login_started_at_ns
139 || claims.issued_at_ns < context.min_proof_issued_at_ns
140 {
141 return Err(Error::BeforeReset);
142 }
143 if context.now_ns < context.authority_not_before_ns
144 || context.now_ns >= context.authority_expires_at_ns
145 || context.now_ns >= context.enrollment_expires_at_ns
146 || claims.issued_at_ns < context.authority_not_before_ns
147 {
148 return Err(Error::StaleAuthority);
149 }
150 if context.now_ns < claims.issued_at_ns {
151 return Err(Error::NotYetValid);
152 }
153 if context.now_ns >= claims.admission_expires_at_ns {
154 return Err(Error::Expired);
155 }
156 Ok(())
157}