1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationCert,
6 DelegationProof, DelegationProvisionResponse, DelegationProvisionTargetKind,
7 DelegationRequest, RoleAttestationRequest, SignedRoleAttestation,
8 },
9 error::{Error, ErrorCode},
10 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
11 },
12 error::InternalErrorClass,
13 log,
14 log::Topic,
15 ops::{
16 auth::DelegatedTokenOps,
17 config::ConfigOps,
18 ic::IcOps,
19 rpc::RpcOps,
20 runtime::env::EnvOps,
21 runtime::metrics::auth::{
22 record_attestation_refresh_failed, record_signer_issue_without_proof,
23 },
24 storage::auth::DelegationStateOps,
25 },
26 protocol,
27 workflow::rpc::request::handler::RootResponseWorkflow,
28};
29
30mod metadata;
31mod verify_flow;
32
33pub struct DelegationApi;
40
41impl DelegationApi {
42 const DELEGATED_TOKENS_DISABLED: &str =
43 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
44
45 fn map_delegation_error(err: crate::InternalError) -> Error {
46 match err.class() {
47 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
48 Error::internal(err.to_string())
49 }
50 _ => Error::from(err),
51 }
52 }
53
54 pub fn verify_delegation_proof(
59 proof: &DelegationProof,
60 authority_pid: Principal,
61 ) -> Result<(), Error> {
62 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
63 .map_err(Self::map_delegation_error)
64 }
65
66 async fn sign_token(
67 claims: DelegatedTokenClaims,
68 proof: DelegationProof,
69 ) -> Result<DelegatedToken, Error> {
70 DelegatedTokenOps::sign_token(claims, proof)
71 .await
72 .map_err(Self::map_delegation_error)
73 }
74
75 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
80 let proof = Self::ensure_signing_proof(&claims).await?;
81 Self::sign_token(claims, proof).await
82 }
83
84 pub fn verify_token(
89 token: &DelegatedToken,
90 authority_pid: Principal,
91 now_secs: u64,
92 ) -> Result<(), Error> {
93 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
94 .map(|_| ())
95 .map_err(Self::map_delegation_error)
96 }
97
98 pub fn verify_token_verified(
103 token: &DelegatedToken,
104 authority_pid: Principal,
105 now_secs: u64,
106 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
107 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
108 .map(|verified| (verified.claims, verified.cert))
109 .map_err(Self::map_delegation_error)
110 }
111
112 pub async fn request_delegation(
116 request: DelegationRequest,
117 ) -> Result<DelegationProvisionResponse, Error> {
118 let request = metadata::with_root_request_metadata(request);
119 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
120 .await
121 .map_err(Self::map_delegation_error)?;
122
123 match response {
124 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
125 _ => Err(Error::internal(
126 "invalid root response type for delegation request",
127 )),
128 }
129 }
130
131 pub async fn request_role_attestation(
132 request: RoleAttestationRequest,
133 ) -> Result<SignedRoleAttestation, Error> {
134 let request = metadata::with_root_attestation_request_metadata(request);
135 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
136 .await
137 .map_err(Self::map_delegation_error)?;
138
139 match response {
140 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
141 _ => Err(Error::internal(
142 "invalid root response type for role attestation request",
143 )),
144 }
145 }
146
147 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
148 DelegatedTokenOps::attestation_key_set()
149 .await
150 .map_err(Self::map_delegation_error)
151 }
152
153 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
154 DelegatedTokenOps::replace_attestation_key_set(key_set);
155 }
156
157 pub async fn verify_role_attestation(
158 attestation: &SignedRoleAttestation,
159 min_accepted_epoch: u64,
160 ) -> Result<(), Error> {
161 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
162 .map_err(Error::from)?
163 .min_accepted_epoch_by_role
164 .get(attestation.payload.role.as_str())
165 .copied();
166 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
167 min_accepted_epoch,
168 configured_min_accepted_epoch,
169 );
170
171 let caller = IcOps::msg_caller();
172 let self_pid = IcOps::canister_self();
173 let now_secs = IcOps::now_secs();
174 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
175 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
176
177 let verify = || {
178 DelegatedTokenOps::verify_role_attestation_cached(
179 attestation,
180 caller,
181 self_pid,
182 verifier_subnet,
183 now_secs,
184 min_accepted_epoch,
185 )
186 .map(|_| ())
187 };
188 let refresh = || async {
189 let key_set: AttestationKeySet =
190 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
191 DelegatedTokenOps::replace_attestation_key_set(key_set);
192 Ok(())
193 };
194
195 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
196 Ok(()) => Ok(()),
197 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
198 verify_flow::record_attestation_verifier_rejection(&err);
199 verify_flow::log_attestation_verifier_rejection(
200 &err,
201 attestation,
202 caller,
203 self_pid,
204 "cached",
205 );
206 Err(Self::map_delegation_error(err.into()))
207 }
208 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
209 verify_flow::record_attestation_verifier_rejection(&trigger);
210 verify_flow::log_attestation_verifier_rejection(
211 &trigger,
212 attestation,
213 caller,
214 self_pid,
215 "cache_miss_refresh",
216 );
217 record_attestation_refresh_failed();
218 log!(
219 Topic::Auth,
220 Warn,
221 "role attestation refresh failed local={} caller={} key_id={} error={}",
222 self_pid,
223 caller,
224 attestation.key_id,
225 source
226 );
227 Err(Self::map_delegation_error(source))
228 }
229 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
230 verify_flow::record_attestation_verifier_rejection(&err);
231 verify_flow::log_attestation_verifier_rejection(
232 &err,
233 attestation,
234 caller,
235 self_pid,
236 "post_refresh",
237 );
238 Err(Self::map_delegation_error(err.into()))
239 }
240 }
241 }
242
243 pub async fn store_proof(
244 proof: DelegationProof,
245 kind: DelegationProvisionTargetKind,
246 ) -> Result<(), Error> {
247 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
248 if !cfg.enabled {
249 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
250 }
251
252 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
253 let caller = IcOps::msg_caller();
254 if caller != root_pid {
255 return Err(Error::forbidden(
256 "delegation proof store requires root caller",
257 ));
258 }
259
260 DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
261 .await
262 .map_err(Self::map_delegation_error)?;
263 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
264 let local = IcOps::canister_self();
265 log!(
266 Topic::Auth,
267 Warn,
268 "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
269 kind,
270 local,
271 proof.cert.shard_pid,
272 proof.cert.issued_at,
273 proof.cert.expires_at,
274 err
275 );
276 return Err(Self::map_delegation_error(err));
277 }
278
279 DelegationStateOps::set_proof_from_dto(proof);
280 let local = IcOps::canister_self();
281 let stored = DelegationStateOps::proof_dto()
282 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
283 log!(
284 Topic::Auth,
285 Info,
286 "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
287 kind,
288 local,
289 stored.cert.shard_pid,
290 stored.cert.issued_at,
291 stored.cert.expires_at
292 );
293
294 Ok(())
295 }
296
297 fn require_proof() -> Result<DelegationProof, Error> {
298 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
299 if !cfg.enabled {
300 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
301 }
302
303 DelegationStateOps::proof_dto().ok_or_else(|| {
304 record_signer_issue_without_proof();
305 Error::not_found("delegation proof not set")
306 })
307 }
308
309 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
311 let now_secs = IcOps::now_secs();
312
313 match Self::require_proof() {
314 Ok(proof) if !Self::proof_is_reusable_for_claims(&proof, claims, now_secs) => {
315 Self::setup_delegation(claims).await
316 }
317 Ok(proof) => Ok(proof),
318 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
319 Err(err) => Err(err),
320 }
321 }
322
323 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
325 let request = Self::delegation_request_from_claims(claims)?;
326 let _ = Self::request_delegation(request).await?;
327 Self::require_proof()
328 }
329
330 fn delegation_request_from_claims(
332 claims: &DelegatedTokenClaims,
333 ) -> Result<DelegationRequest, Error> {
334 let ttl_secs = claims.exp.saturating_sub(claims.iat);
335 if ttl_secs == 0 {
336 return Err(Error::invalid(
337 "delegation ttl_secs must be greater than zero",
338 ));
339 }
340
341 Ok(DelegationRequest {
342 shard_pid: IcOps::canister_self(),
343 scopes: claims.scopes.clone(),
344 aud: claims.aud.clone(),
345 ttl_secs,
346 verifier_targets: Vec::new(),
347 include_root_verifier: true,
348 metadata: None,
349 })
350 }
351
352 fn proof_is_reusable_for_claims(
354 proof: &DelegationProof,
355 claims: &DelegatedTokenClaims,
356 now_secs: u64,
357 ) -> bool {
358 if now_secs > proof.cert.expires_at {
359 return false;
360 }
361
362 if claims.shard_pid != proof.cert.shard_pid {
363 return false;
364 }
365
366 if claims.iat < proof.cert.issued_at || claims.exp > proof.cert.expires_at {
367 return false;
368 }
369
370 Self::is_principal_subset(&claims.aud, &proof.cert.aud)
371 && Self::is_string_subset(&claims.scopes, &proof.cert.scopes)
372 }
373
374 fn is_principal_subset(
376 subset: &[crate::cdk::types::Principal],
377 superset: &[crate::cdk::types::Principal],
378 ) -> bool {
379 subset.iter().all(|item| superset.contains(item))
380 }
381
382 fn is_string_subset(subset: &[String], superset: &[String]) -> bool {
384 subset.iter().all(|item| superset.contains(item))
385 }
386}
387
388#[cfg(test)]
389mod tests;