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
50#[derive(Debug, Error, Eq, PartialEq)]
52pub enum PortableClaimsError {
53 #[error("invalid protected portable claims context")]
54 InvalidContext,
55 #[error("portable audience does not match protected authority")]
56 AudienceMismatch,
57 #[error("portable issuer does not match protected authority")]
58 IssuerMismatch,
59 #[error("portable mechanism does not match protected authority")]
60 MechanismMismatch,
61 #[error("portable key binding does not match protected authority")]
62 KeyMismatch,
63 #[error("browser key does not identify the authenticated caller")]
64 CallerMismatch,
65 #[error("portable grants exceed protected authority")]
66 GrantsExceeded,
67 #[error("portable login or proof window is invalid")]
68 InvalidWindow,
69 #[error("portable proof is not yet valid")]
70 NotYetValid,
71 #[error("portable proof has expired")]
72 Expired,
73 #[error("portable login or issuance predates the protected reset floor")]
74 BeforeReset,
75 #[error("portable authority or enrollment is not current")]
76 StaleAuthority,
77 #[error(transparent)]
78 Encoding(#[from] PortableEncodingError),
79}
80
81pub fn validate_claims_context(
88 claims: &PortableClaims,
89 context: &PortableClaimsContext<'_>,
90) -> Result<(), PortableClaimsError> {
91 validate_context(context)?;
92 validate_shape(claims)?;
93 validate_bindings(
94 claims,
95 browser_principal(&claims.browser_public_key),
96 context,
97 )
98}
99
100#[cfg(any(
103 feature = "portable-signature-verification",
104 feature = "certified-proof-verification"
105))]
106pub(super) fn validate_decoded_context(
107 claims: &PortableClaims,
108 browser_caller: Principal,
109 context: &PortableClaimsContext<'_>,
110) -> Result<(), PortableClaimsError> {
111 validate_context(context)?;
112 validate_bindings(claims, browser_caller, context)
113}
114
115fn validate_context(context: &PortableClaimsContext<'_>) -> Result<(), PortableClaimsError> {
116 use PortableClaimsError as Error;
117 let authority = &context.authority;
118 if context.max_login_ttl_ns == 0
119 || context.max_login_ttl_ns > crate::limits::MAX_LOGIN_NS
120 || context.max_proof_ttl_ns == 0
121 || context.max_proof_ttl_ns > crate::limits::MAX_PROOF_NS
122 || !authority.authority_shape_valid()
123 || authority.enrollment_expires_at_ns == 0
124 {
125 return Err(Error::InvalidContext);
126 }
127 Ok(())
128}
129
130fn validate_bindings(
131 claims: &PortableClaims,
132 browser_caller: Principal,
133 context: &PortableClaimsContext<'_>,
134) -> Result<(), PortableClaimsError> {
135 use PortableClaimsError as Error;
136 let authority = &context.authority;
137 if claims.audience != *authority.audience {
138 return Err(Error::AudienceMismatch);
139 }
140 if claims.issuer != authority.issuer {
141 return Err(Error::IssuerMismatch);
142 }
143 if claims.mechanism != authority.mechanism {
144 return Err(Error::MechanismMismatch);
145 }
146 if claims.key_binding_id != authority.key_binding_id {
147 return Err(Error::KeyMismatch);
148 }
149 if browser_caller != authority.caller {
150 return Err(Error::CallerMismatch);
151 }
152 if !grants_are_subset(&claims.grants, authority.grants.as_slice()) {
153 return Err(Error::GrantsExceeded);
154 }
155 if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
156 .is_none_or(|ttl| ttl > context.max_login_ttl_ns)
157 || positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
158 .is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
159 || claims.login_started_at_ns > claims.issued_at_ns
160 || claims.admission_expires_at_ns > claims.login_deadline_ns
161 {
162 return Err(Error::InvalidWindow);
163 }
164 if claims.login_started_at_ns < authority.min_login_started_at_ns
165 || claims.issued_at_ns < context.min_proof_issued_at_ns
166 {
167 return Err(Error::BeforeReset);
168 }
169 if authority.now_ns < authority.authority_not_before_ns
170 || authority.now_ns >= authority.authority_expires_at_ns
171 || authority.now_ns >= authority.enrollment_expires_at_ns
172 || claims.issued_at_ns < authority.authority_not_before_ns
173 {
174 return Err(Error::StaleAuthority);
175 }
176 if authority.now_ns < claims.issued_at_ns {
177 return Err(Error::NotYetValid);
178 }
179 if authority.now_ns >= claims.admission_expires_at_ns {
180 return Err(Error::Expired);
181 }
182 Ok(())
183}