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("certificate validity must start at or after issuance and have positive duration")]
31 InvalidCertWindow,
32 #[error("certificate TTL {ttl_ns} exceeds host maximum {max_ttl_ns}")]
33 CertTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
34 #[error("delegated token TTL must be greater than zero")]
35 TokenTtlZero,
36 #[error("delegated token TTL {ttl_ns} exceeds host maximum {max_ttl_ns}")]
37 TokenTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
38 #[error("delegated token TTL {token_ttl_ns} exceeds certificate duration {cert_ttl_ns}")]
39 TokenTtlOutlivesCert { token_ttl_ns: u64, cert_ttl_ns: u64 },
40 #[error("certificate grants must not be empty")]
41 GrantsEmpty,
42 #[error("certificate grants {found} exceed host maximum {max}")]
43 TooManyGrants { found: usize, max: usize },
44 #[error("grant for {role:?} has no scopes")]
45 EmptyGrantScopes { role: AuthRole },
46 #[error("grant for {role:?} has {found} scopes exceeding host maximum {max}")]
47 TooManyGrantScopes {
48 role: AuthRole,
49 found: usize,
50 max: usize,
51 },
52 #[error("issuer proof binding hash mismatch")]
53 IssuerProofBindingHashMismatch,
54 #[error(transparent)]
55 Canonical(#[from] CanonicalAuthError),
56}
57
58pub fn validate_certificate_issuance(
63 cert: &DelegationCert,
64 expected_root: Principal,
65 limits: CertificateIssuanceLimits,
66) -> Result<(), CertificateIssuanceError> {
67 if cert.root_pid != expected_root {
68 return Err(CertificateIssuanceError::RootPidMismatch {
69 expected: expected_root,
70 found: cert.root_pid,
71 });
72 }
73 validate_certificate_lifetime(cert, limits.max_cert_ttl_ns, limits.max_token_ttl_ns)?;
74 validate_grants(&cert.grants, limits.max_grants, limits.max_scopes_per_grant)?;
75 if !issuer_binding_matches(cert)? {
76 return Err(CertificateIssuanceError::IssuerProofBindingHashMismatch);
77 }
78 Ok(())
79}
80
81pub(crate) enum CertificateLifetimeError {
84 IssuedAfterStart,
85 InvalidWindow,
86 CertTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
87 TokenTtlZero,
88 TokenTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
89 TokenTtlOutlivesCert { token_ttl_ns: u64, cert_ttl_ns: u64 },
90}
91
92pub(crate) fn positive_duration(start: u64, end: u64) -> Option<u64> {
93 end.checked_sub(start).filter(|duration| *duration != 0)
94}
95
96pub(crate) fn validate_certificate_lifetime(
97 cert: &DelegationCert,
98 max_cert_ttl_ns: u64,
99 max_token_ttl_ns: u64,
100) -> Result<(), CertificateLifetimeError> {
101 use CertificateLifetimeError as E;
102 if cert.issued_at_ns > cert.not_before_ns {
103 return Err(E::IssuedAfterStart);
104 }
105 let cert_ttl_ns =
106 positive_duration(cert.not_before_ns, cert.expires_at_ns).ok_or(E::InvalidWindow)?;
107 if cert_ttl_ns > max_cert_ttl_ns {
108 return Err(E::CertTtlExceeded {
109 ttl_ns: cert_ttl_ns,
110 max_ttl_ns: max_cert_ttl_ns,
111 });
112 }
113 if cert.max_token_ttl_ns == 0 {
114 return Err(E::TokenTtlZero);
115 }
116 if cert.max_token_ttl_ns > max_token_ttl_ns {
117 return Err(E::TokenTtlExceeded {
118 ttl_ns: cert.max_token_ttl_ns,
119 max_ttl_ns: max_token_ttl_ns,
120 });
121 }
122 if cert.max_token_ttl_ns > cert_ttl_ns {
123 return Err(E::TokenTtlOutlivesCert {
124 token_ttl_ns: cert.max_token_ttl_ns,
125 cert_ttl_ns,
126 });
127 }
128 Ok(())
129}
130
131impl From<CertificateLifetimeError> for CertificateIssuanceError {
132 fn from(error: CertificateLifetimeError) -> Self {
133 use CertificateLifetimeError as E;
134 match error {
135 E::IssuedAfterStart | E::InvalidWindow => Self::InvalidCertWindow,
136 E::CertTtlExceeded { ttl_ns, max_ttl_ns } => {
137 Self::CertTtlExceeded { ttl_ns, max_ttl_ns }
138 }
139 E::TokenTtlZero => Self::TokenTtlZero,
140 E::TokenTtlExceeded { ttl_ns, max_ttl_ns } => {
141 Self::TokenTtlExceeded { ttl_ns, max_ttl_ns }
142 }
143 E::TokenTtlOutlivesCert {
144 token_ttl_ns,
145 cert_ttl_ns,
146 } => Self::TokenTtlOutlivesCert {
147 token_ttl_ns,
148 cert_ttl_ns,
149 },
150 }
151 }
152}
153
154pub(crate) enum GrantValidationError {
155 Empty,
156 TooMany {
157 found: usize,
158 max: usize,
159 },
160 EmptyScopes {
161 role: AuthRole,
162 },
163 TooManyScopes {
164 role: AuthRole,
165 found: usize,
166 max: usize,
167 },
168 Canonical(CanonicalAuthError),
169}
170
171pub(crate) fn validate_grants(
172 grants: &[DelegatedRoleGrant],
173 max_grants: usize,
174 max_scopes_per_grant: usize,
175) -> Result<(), GrantValidationError> {
176 use GrantValidationError as E;
177 if grants.is_empty() {
178 return Err(E::Empty);
179 }
180 if grants.len() > max_grants {
181 return Err(E::TooMany {
182 found: grants.len(),
183 max: max_grants,
184 });
185 }
186 let mut previous = None;
187 for grant in grants {
188 let role = grant.target.as_str().as_bytes();
189 if previous.is_some_and(|previous| previous >= role) {
190 return Err(E::Canonical(CanonicalAuthError::NonCanonicalRoles));
191 }
192 previous = Some(role);
193 if grant.scopes.is_empty() {
194 return Err(E::EmptyScopes {
195 role: grant.target.clone(),
196 });
197 }
198 if grant.scopes.len() > max_scopes_per_grant {
199 return Err(E::TooManyScopes {
200 role: grant.target.clone(),
201 found: grant.scopes.len(),
202 max: max_scopes_per_grant,
203 });
204 }
205 validate_scopes(&grant.scopes).map_err(E::Canonical)?;
206 }
207 Ok(())
208}
209
210impl From<GrantValidationError> for CertificateIssuanceError {
211 fn from(error: GrantValidationError) -> Self {
212 use GrantValidationError as E;
213 match error {
214 E::Empty => Self::GrantsEmpty,
215 E::TooMany { found, max } => Self::TooManyGrants { found, max },
216 E::EmptyScopes { role } => Self::EmptyGrantScopes { role },
217 E::TooManyScopes { role, found, max } => Self::TooManyGrantScopes { role, found, max },
218 E::Canonical(error) => Self::Canonical(error),
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}