1use crate::canonical::{CanonicalAuthError, issuer_proof_binding_hash, validate_scopes};
8use ic_auth_protocol_types::{AuthRole, DelegatedRoleGrant, DelegationCert, Principal};
9use thiserror::Error;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct CertificateIssuanceLimits {
14 pub max_cert_ttl_ns: u64,
15 pub max_token_ttl_ns: u64,
16 pub max_grants: usize,
17 pub max_scopes_per_grant: usize,
18}
19
20#[derive(Debug, Eq, Error, PartialEq)]
24pub enum CertificateIssuanceError {
25 #[error("certificate Root mismatch (expected {expected}, found {found})")]
26 RootPidMismatch {
27 expected: Principal,
28 found: Principal,
29 },
30 #[error(transparent)]
31 Lifetime(#[from] CertificateLifetimeError),
32 #[error(transparent)]
33 Grants(#[from] GrantValidationError),
34 #[error("issuer proof binding hash mismatch")]
35 IssuerProofBindingHashMismatch,
36 #[error(transparent)]
37 Canonical(#[from] CanonicalAuthError),
38}
39
40pub fn validate_certificate_issuance(
45 cert: &DelegationCert,
46 expected_root: Principal,
47 limits: CertificateIssuanceLimits,
48) -> Result<(), CertificateIssuanceError> {
49 if cert.root_pid != expected_root {
50 return Err(CertificateIssuanceError::RootPidMismatch {
51 expected: expected_root,
52 found: cert.root_pid,
53 });
54 }
55 validate_certificate_lifetime(cert, limits.max_cert_ttl_ns, limits.max_token_ttl_ns)?;
56 validate_grants(&cert.grants, limits.max_grants, limits.max_scopes_per_grant)?;
57 if !issuer_binding_matches(cert)? {
58 return Err(CertificateIssuanceError::IssuerProofBindingHashMismatch);
59 }
60 Ok(())
61}
62
63#[derive(Debug, Eq, Error, PartialEq)]
66pub enum CertificateLifetimeError {
67 #[error("certificate was issued after its validity start")]
68 IssuedAfterStart,
69 #[error("certificate validity must have positive duration")]
70 InvalidWindow,
71 #[error("certificate TTL {ttl_ns} exceeds host maximum {max_ttl_ns}")]
72 CertTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
73 #[error("delegated token TTL must be greater than zero")]
74 TokenTtlZero,
75 #[error("delegated token TTL {ttl_ns} exceeds host maximum {max_ttl_ns}")]
76 TokenTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
77 #[error("delegated token TTL {token_ttl_ns} exceeds certificate duration {cert_ttl_ns}")]
78 TokenTtlOutlivesCert { token_ttl_ns: u64, cert_ttl_ns: u64 },
79}
80
81pub(crate) fn positive_duration(start: u64, end: u64) -> Option<u64> {
82 end.checked_sub(start).filter(|duration| *duration != 0)
83}
84
85pub(crate) fn validate_certificate_lifetime(
86 cert: &DelegationCert,
87 max_cert_ttl_ns: u64,
88 max_token_ttl_ns: u64,
89) -> Result<(), CertificateLifetimeError> {
90 use CertificateLifetimeError as E;
91 if cert.issued_at_ns > cert.not_before_ns {
92 return Err(E::IssuedAfterStart);
93 }
94 let cert_ttl_ns =
95 positive_duration(cert.not_before_ns, cert.expires_at_ns).ok_or(E::InvalidWindow)?;
96 if cert_ttl_ns > max_cert_ttl_ns {
97 return Err(E::CertTtlExceeded {
98 ttl_ns: cert_ttl_ns,
99 max_ttl_ns: max_cert_ttl_ns,
100 });
101 }
102 if cert.max_token_ttl_ns == 0 {
103 return Err(E::TokenTtlZero);
104 }
105 if cert.max_token_ttl_ns > max_token_ttl_ns {
106 return Err(E::TokenTtlExceeded {
107 ttl_ns: cert.max_token_ttl_ns,
108 max_ttl_ns: max_token_ttl_ns,
109 });
110 }
111 if cert.max_token_ttl_ns > cert_ttl_ns {
112 return Err(E::TokenTtlOutlivesCert {
113 token_ttl_ns: cert.max_token_ttl_ns,
114 cert_ttl_ns,
115 });
116 }
117 Ok(())
118}
119
120#[derive(Debug, Eq, Error, PartialEq)]
122pub enum GrantValidationError {
123 #[error("grants are empty")]
124 Empty,
125 #[error("{found} grants exceed maximum {max}")]
126 TooMany { found: usize, max: usize },
127 #[error("grant for {role:?} has no scopes")]
128 EmptyScopes { role: AuthRole },
129 #[error("grant for {role:?} has {found} scopes; maximum is {max}")]
130 TooManyScopes {
131 role: AuthRole,
132 found: usize,
133 max: usize,
134 },
135 #[error(transparent)]
136 Canonical(CanonicalAuthError),
137}
138
139pub fn validate_grants(
146 grants: &[DelegatedRoleGrant],
147 max_grants: usize,
148 max_scopes_per_grant: usize,
149) -> Result<(), GrantValidationError> {
150 use GrantValidationError as E;
151 if grants.is_empty() {
152 return Err(E::Empty);
153 }
154 if grants.len() > max_grants {
155 return Err(E::TooMany {
156 found: grants.len(),
157 max: max_grants,
158 });
159 }
160 let mut previous = None;
161 for grant in grants {
162 let role = grant.target.as_str().as_bytes();
163 if previous.is_some_and(|previous| previous >= role) {
164 return Err(E::Canonical(CanonicalAuthError::NonCanonicalRoles));
165 }
166 previous = Some(role);
167 if grant.scopes.is_empty() {
168 return Err(E::EmptyScopes {
169 role: grant.target.clone(),
170 });
171 }
172 if grant.scopes.len() > max_scopes_per_grant {
173 return Err(E::TooManyScopes {
174 role: grant.target.clone(),
175 found: grant.scopes.len(),
176 max: max_scopes_per_grant,
177 });
178 }
179 validate_scopes(&grant.scopes).map_err(E::Canonical)?;
180 }
181 Ok(())
182}
183
184pub(crate) fn grants_are_subset(
187 grants: &[DelegatedRoleGrant],
188 ceiling: &[DelegatedRoleGrant],
189) -> bool {
190 grants.iter().all(|child| {
191 grant_for_role(ceiling, &child.target)
192 .is_some_and(|parent| scopes_are_subset(&child.scopes, &parent.scopes))
193 })
194}
195
196pub(crate) fn grant_for_role<'a>(
197 grants: &'a [DelegatedRoleGrant],
198 role: &AuthRole,
199) -> Option<&'a DelegatedRoleGrant> {
200 grants.iter().find(|grant| &grant.target == role)
201}
202
203pub(crate) fn scopes_are_subset(
206 requested: &[impl AsRef<str>],
207 ceiling: &[impl AsRef<str>],
208) -> bool {
209 requested.iter().all(|scope| {
210 let scope = scope.as_ref();
211 if ceiling.len() <= 2 {
214 ceiling.iter().any(|allowed| allowed.as_ref() == scope)
215 } else {
216 ceiling
217 .binary_search_by(|allowed| allowed.as_ref().cmp(scope))
218 .is_ok()
219 }
220 })
221}
222
223pub(crate) fn issuer_binding_matches(cert: &DelegationCert) -> Result<bool, CanonicalAuthError> {
224 Ok(issuer_proof_binding_hash(
225 cert.issuer_pid,
226 cert.issuer_proof_alg,
227 cert.issuer_proof_binding,
228 )? == cert.issuer_proof_binding_hash)
229}
230
231#[cfg(test)]
232mod tests;