1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationCert,
6 DelegationProof, DelegationProvisionResponse, DelegationProvisionStatus,
7 DelegationProvisionTargetKind, DelegationRequest, RoleAttestationRequest,
8 SignedRoleAttestation,
9 },
10 error::{Error, ErrorCode},
11 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
12 },
13 error::InternalErrorClass,
14 log,
15 log::Topic,
16 ops::{
17 auth::DelegatedTokenOps,
18 config::ConfigOps,
19 ic::IcOps,
20 rpc::RpcOps,
21 runtime::env::EnvOps,
22 runtime::metrics::auth::{
23 record_attestation_refresh_failed, record_delegation_provision_complete,
24 record_delegation_verifier_target_count, record_delegation_verifier_target_failed,
25 record_delegation_verifier_target_missing, record_signer_issue_without_proof,
26 },
27 storage::auth::DelegationStateOps,
28 },
29 protocol,
30 workflow::rpc::request::handler::RootResponseWorkflow,
31};
32
33mod admin;
41mod metadata;
42mod proof_store;
43mod session;
44mod verify_flow;
45
46pub struct DelegationApi;
53
54impl DelegationApi {
55 const DELEGATED_TOKENS_DISABLED: &str =
56 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
57 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
58 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
59 b"canic-session-bootstrap-token-fingerprint:v1";
60
61 fn map_delegation_error(err: crate::InternalError) -> Error {
62 match err.class() {
63 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
64 Error::internal(err.to_string())
65 }
66 _ => Error::from(err),
67 }
68 }
69
70 pub fn verify_delegation_proof(
75 proof: &DelegationProof,
76 authority_pid: Principal,
77 ) -> Result<(), Error> {
78 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
79 .map_err(Self::map_delegation_error)
80 }
81
82 #[cfg(canic_test_delegation_material)]
83 #[must_use]
84 pub fn current_signing_proof_for_test() -> Option<DelegationProof> {
85 DelegationStateOps::latest_proof_dto()
86 }
87
88 async fn sign_token(
89 claims: DelegatedTokenClaims,
90 proof: DelegationProof,
91 ) -> Result<DelegatedToken, Error> {
92 DelegatedTokenOps::sign_token(claims, proof)
93 .await
94 .map_err(Self::map_delegation_error)
95 }
96
97 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
102 let proof = Self::ensure_signing_proof(&claims).await?;
103 Self::sign_token(claims, proof).await
104 }
105
106 pub fn verify_token(
111 token: &DelegatedToken,
112 authority_pid: Principal,
113 now_secs: u64,
114 ) -> Result<(), Error> {
115 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
116 .map(|_| ())
117 .map_err(Self::map_delegation_error)
118 }
119
120 pub fn verify_token_verified(
125 token: &DelegatedToken,
126 authority_pid: Principal,
127 now_secs: u64,
128 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
129 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
130 .map(crate::ops::auth::VerifiedDelegatedToken::into_parts)
131 .map_err(Self::map_delegation_error)
132 }
133
134 pub async fn request_delegation(
138 request: DelegationRequest,
139 ) -> Result<DelegationProvisionResponse, Error> {
140 let request = metadata::with_root_request_metadata(request);
141 Self::request_delegation_remote(request).await
142 }
143
144 pub async fn request_role_attestation(
145 request: RoleAttestationRequest,
146 ) -> Result<SignedRoleAttestation, Error> {
147 let request = metadata::with_root_attestation_request_metadata(request);
148 let response = Self::request_role_attestation_remote(request).await?;
149
150 match response {
151 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
152 _ => Err(Error::internal(
153 "invalid root response type for role attestation request",
154 )),
155 }
156 }
157
158 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
159 DelegatedTokenOps::attestation_key_set()
160 .await
161 .map_err(Self::map_delegation_error)
162 }
163
164 pub async fn prewarm_root_key_material() -> Result<(), Error> {
166 EnvOps::require_root().map_err(Error::from)?;
167 DelegatedTokenOps::prewarm_root_key_material()
168 .await
169 .map_err(|err| {
170 log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
171 Self::map_delegation_error(err)
172 })
173 }
174
175 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
176 DelegatedTokenOps::replace_attestation_key_set(key_set);
177 }
178
179 pub async fn verify_role_attestation(
180 attestation: &SignedRoleAttestation,
181 min_accepted_epoch: u64,
182 ) -> Result<(), Error> {
183 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
184 .map_err(Error::from)?
185 .min_accepted_epoch_by_role
186 .get(attestation.payload.role.as_str())
187 .copied();
188 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
189 min_accepted_epoch,
190 configured_min_accepted_epoch,
191 );
192
193 let caller = IcOps::msg_caller();
194 let self_pid = IcOps::canister_self();
195 let now_secs = IcOps::now_secs();
196 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
197 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
198
199 let verify = || {
200 DelegatedTokenOps::verify_role_attestation_cached(
201 attestation,
202 caller,
203 self_pid,
204 verifier_subnet,
205 now_secs,
206 min_accepted_epoch,
207 )
208 .map(|_| ())
209 };
210 let refresh = || async {
211 let key_set: AttestationKeySet =
212 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
213 DelegatedTokenOps::replace_attestation_key_set(key_set);
214 Ok(())
215 };
216
217 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
218 Ok(()) => Ok(()),
219 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
220 verify_flow::record_attestation_verifier_rejection(&err);
221 verify_flow::log_attestation_verifier_rejection(
222 &err,
223 attestation,
224 caller,
225 self_pid,
226 "cached",
227 );
228 Err(Self::map_delegation_error(err.into()))
229 }
230 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
231 verify_flow::record_attestation_verifier_rejection(&trigger);
232 verify_flow::log_attestation_verifier_rejection(
233 &trigger,
234 attestation,
235 caller,
236 self_pid,
237 "cache_miss_refresh",
238 );
239 record_attestation_refresh_failed();
240 log!(
241 Topic::Auth,
242 Warn,
243 "role attestation refresh failed local={} caller={} key_id={} error={}",
244 self_pid,
245 caller,
246 attestation.key_id,
247 source
248 );
249 Err(Self::map_delegation_error(source))
250 }
251 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
252 verify_flow::record_attestation_verifier_rejection(&err);
253 verify_flow::log_attestation_verifier_rejection(
254 &err,
255 attestation,
256 caller,
257 self_pid,
258 "post_refresh",
259 );
260 Err(Self::map_delegation_error(err.into()))
261 }
262 }
263 }
264
265 fn require_proof() -> Result<DelegationProof, Error> {
266 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
267 if !cfg.enabled {
268 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
269 }
270
271 DelegationStateOps::latest_proof_dto().ok_or_else(|| {
272 record_signer_issue_without_proof();
273 Error::not_found("delegation proof not installed")
274 })
275 }
276
277 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
279 let now_secs = IcOps::now_secs();
280
281 match Self::require_proof() {
282 Ok(proof)
283 if !DelegatedTokenOps::proof_reusable_for_claims(&proof, claims, now_secs) =>
284 {
285 Self::setup_delegation(claims).await
286 }
287 Ok(proof) => Ok(proof),
288 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
289 Err(err) => Err(err),
290 }
291 }
292
293 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
295 let request = Self::delegation_request_from_claims(claims)?;
296 let required_verifier_targets = request.verifier_targets.clone();
297 let response = Self::request_delegation_remote(request).await?;
298 Self::ensure_required_verifier_targets_provisioned(&required_verifier_targets, &response)?;
299 let proof = response.proof;
300 Self::store_local_signer_proof(proof.clone()).await?;
301 Ok(proof)
302 }
303
304 fn delegation_request_from_claims(
306 claims: &DelegatedTokenClaims,
307 ) -> Result<DelegationRequest, Error> {
308 let ttl_secs = claims.exp.saturating_sub(claims.iat);
309 if ttl_secs == 0 {
310 return Err(Error::invalid(
311 "delegation ttl_secs must be greater than zero",
312 ));
313 }
314
315 let signer_pid = IcOps::canister_self();
316 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
317 let verifier_targets = DelegatedTokenOps::required_verifier_targets_from_audience(
318 &claims.aud,
319 signer_pid,
320 root_pid,
321 Self::is_registered_canister,
322 )
323 .map_err(|principal| {
324 Error::invalid(format!(
325 "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
326 ))
327 })?;
328
329 Ok(DelegationRequest {
330 shard_pid: signer_pid,
331 scopes: claims.scopes.clone(),
332 aud: claims.aud.clone(),
333 ttl_secs,
334 verifier_targets,
335 include_root_verifier: true,
336 metadata: None,
337 })
338 }
339
340 fn ensure_required_verifier_targets_provisioned(
342 required_targets: &[Principal],
343 response: &DelegationProvisionResponse,
344 ) -> Result<(), Error> {
345 let mut checked = Vec::new();
346 for target in required_targets {
347 if checked.contains(target) {
348 continue;
349 }
350 checked.push(*target);
351 }
352 record_delegation_verifier_target_count(checked.len());
353
354 for target in &checked {
355 let Some(result) = response.results.iter().find(|entry| {
356 entry.kind == DelegationProvisionTargetKind::Verifier && entry.target == *target
357 }) else {
358 record_delegation_verifier_target_missing();
359 return Err(Error::internal(format!(
360 "delegation provisioning missing verifier target result for '{target}'"
361 )));
362 };
363
364 if result.status != DelegationProvisionStatus::Ok {
365 record_delegation_verifier_target_failed();
366 let detail = result
367 .error
368 .as_ref()
369 .map_or_else(|| "unknown error".to_string(), ToString::to_string);
370 return Err(Error::internal(format!(
371 "delegation provisioning failed for required verifier target '{target}': {detail}"
372 )));
373 }
374 }
375
376 record_delegation_provision_complete();
377 Ok(())
378 }
379
380 #[cfg(test)]
382 fn derive_required_verifier_targets_from_aud<F>(
383 audience: &[Principal],
384 signer_pid: Principal,
385 root_pid: Principal,
386 is_valid_target: F,
387 ) -> Result<Vec<Principal>, Error>
388 where
389 F: FnMut(Principal) -> bool,
390 {
391 DelegatedTokenOps::required_verifier_targets_from_audience(
392 audience,
393 signer_pid,
394 root_pid,
395 is_valid_target,
396 )
397 .map_err(|principal| {
398 Error::invalid(format!(
399 "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
400 ))
401 })
402 }
403
404 }
412
413impl DelegationApi {
414 pub async fn request_delegation_root(
416 request: DelegationRequest,
417 ) -> Result<DelegationProvisionResponse, Error> {
418 let request = metadata::with_root_request_metadata(request);
419 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
420 .await
421 .map_err(Self::map_delegation_error)?;
422
423 match response {
424 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
425 _ => Err(Error::internal(
426 "invalid root response type for delegation request",
427 )),
428 }
429 }
430
431 async fn request_delegation_remote(
433 request: DelegationRequest,
434 ) -> Result<DelegationProvisionResponse, Error> {
435 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
436 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
437 .await
438 .map_err(Self::map_delegation_error)
439 }
440
441 pub async fn request_role_attestation_root(
443 request: RoleAttestationRequest,
444 ) -> Result<SignedRoleAttestation, Error> {
445 let request = metadata::with_root_attestation_request_metadata(request);
446 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
447 .await
448 .map_err(Self::map_delegation_error)?;
449
450 match response {
451 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
452 _ => Err(Error::internal(
453 "invalid root response type for role attestation request",
454 )),
455 }
456 }
457
458 async fn request_role_attestation_remote(
460 request: RoleAttestationRequest,
461 ) -> Result<RootCapabilityResponse, Error> {
462 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
463 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
464 .await
465 .map_err(Self::map_delegation_error)
466 }
467}
468
469#[cfg(test)]
470mod tests;