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 local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
99 DelegatedTokenOps::local_shard_public_key_sec1(IcOps::canister_self())
100 .await
101 .map_err(Self::map_delegation_error)
102 }
103
104 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
109 let proof = Self::ensure_signing_proof(&claims).await?;
110 let claims = Self::canonicalize_claims_for_proof(claims, &proof);
111 Self::sign_token(claims, proof).await
112 }
113
114 pub fn verify_token(
119 token: &DelegatedToken,
120 authority_pid: Principal,
121 now_secs: u64,
122 ) -> Result<(), Error> {
123 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
124 .map(|_| ())
125 .map_err(Self::map_delegation_error)
126 }
127
128 pub fn verify_token_verified(
133 token: &DelegatedToken,
134 authority_pid: Principal,
135 now_secs: u64,
136 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
137 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
138 .map(crate::ops::auth::VerifiedDelegatedToken::into_parts)
139 .map_err(Self::map_delegation_error)
140 }
141
142 pub async fn request_delegation(
146 request: DelegationRequest,
147 ) -> Result<DelegationProvisionResponse, Error> {
148 let request = metadata::with_root_request_metadata(request);
149 Self::request_delegation_remote(request).await
150 }
151
152 pub async fn request_role_attestation(
153 request: RoleAttestationRequest,
154 ) -> Result<SignedRoleAttestation, Error> {
155 let request = metadata::with_root_attestation_request_metadata(request);
156 let response = Self::request_role_attestation_remote(request).await?;
157
158 match response {
159 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
160 _ => Err(Error::internal(
161 "invalid root response type for role attestation request",
162 )),
163 }
164 }
165
166 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
167 DelegatedTokenOps::attestation_key_set()
168 .await
169 .map_err(Self::map_delegation_error)
170 }
171
172 pub async fn prewarm_root_key_material() -> Result<(), Error> {
174 EnvOps::require_root().map_err(Error::from)?;
175 DelegatedTokenOps::prewarm_root_key_material()
176 .await
177 .map_err(|err| {
178 log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
179 Self::map_delegation_error(err)
180 })
181 }
182
183 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
184 DelegatedTokenOps::replace_attestation_key_set(key_set);
185 }
186
187 pub async fn verify_role_attestation(
188 attestation: &SignedRoleAttestation,
189 min_accepted_epoch: u64,
190 ) -> Result<(), Error> {
191 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
192 .map_err(Error::from)?
193 .min_accepted_epoch_by_role
194 .get(attestation.payload.role.as_str())
195 .copied();
196 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
197 min_accepted_epoch,
198 configured_min_accepted_epoch,
199 );
200
201 let caller = IcOps::msg_caller();
202 let self_pid = IcOps::canister_self();
203 let now_secs = IcOps::now_secs();
204 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
205 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
206
207 let verify = || {
208 DelegatedTokenOps::verify_role_attestation_cached(
209 attestation,
210 caller,
211 self_pid,
212 verifier_subnet,
213 now_secs,
214 min_accepted_epoch,
215 )
216 .map(|_| ())
217 };
218 let refresh = || async {
219 let key_set: AttestationKeySet =
220 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
221 DelegatedTokenOps::replace_attestation_key_set(key_set);
222 Ok(())
223 };
224
225 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
226 Ok(()) => Ok(()),
227 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
228 verify_flow::record_attestation_verifier_rejection(&err);
229 verify_flow::log_attestation_verifier_rejection(
230 &err,
231 attestation,
232 caller,
233 self_pid,
234 "cached",
235 );
236 Err(Self::map_delegation_error(err.into()))
237 }
238 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
239 verify_flow::record_attestation_verifier_rejection(&trigger);
240 verify_flow::log_attestation_verifier_rejection(
241 &trigger,
242 attestation,
243 caller,
244 self_pid,
245 "cache_miss_refresh",
246 );
247 record_attestation_refresh_failed();
248 log!(
249 Topic::Auth,
250 Warn,
251 "role attestation refresh failed local={} caller={} key_id={} error={}",
252 self_pid,
253 caller,
254 attestation.key_id,
255 source
256 );
257 Err(Self::map_delegation_error(source))
258 }
259 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
260 verify_flow::record_attestation_verifier_rejection(&err);
261 verify_flow::log_attestation_verifier_rejection(
262 &err,
263 attestation,
264 caller,
265 self_pid,
266 "post_refresh",
267 );
268 Err(Self::map_delegation_error(err.into()))
269 }
270 }
271 }
272
273 fn require_proof() -> Result<DelegationProof, Error> {
274 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
275 if !cfg.enabled {
276 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
277 }
278
279 DelegationStateOps::latest_proof_dto().ok_or_else(|| {
280 record_signer_issue_without_proof();
281 Error::not_found("delegation proof not installed")
282 })
283 }
284
285 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
287 let now_secs = IcOps::now_secs();
288
289 match Self::require_proof() {
290 Ok(proof)
291 if !DelegatedTokenOps::proof_reusable_for_claims(&proof, claims, now_secs) =>
292 {
293 Self::setup_delegation(claims).await
294 }
295 Ok(proof) => Ok(proof),
296 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
297 Err(err) => Err(err),
298 }
299 }
300
301 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
303 let mut request = Self::delegation_request_from_claims(claims)?;
304 request.shard_public_key_sec1 = Some(
305 DelegatedTokenOps::local_shard_public_key_sec1(request.shard_pid)
306 .await
307 .map_err(Self::map_delegation_error)?,
308 );
309 let required_verifier_targets = request.verifier_targets.clone();
310 let response = Self::request_delegation_remote(request).await?;
311 Self::ensure_required_verifier_targets_provisioned(&required_verifier_targets, &response)?;
312 let proof = response.proof;
313 Self::store_local_signer_proof(proof.clone()).await?;
314 Ok(proof)
315 }
316
317 fn canonicalize_claims_for_proof(
320 claims: DelegatedTokenClaims,
321 proof: &DelegationProof,
322 ) -> DelegatedTokenClaims {
323 if claims.iat >= proof.cert.issued_at && claims.exp <= proof.cert.expires_at {
324 return claims;
325 }
326
327 DelegatedTokenClaims {
328 iat: proof.cert.issued_at,
329 exp: proof.cert.expires_at,
330 ..claims
331 }
332 }
333
334 fn delegation_request_from_claims(
336 claims: &DelegatedTokenClaims,
337 ) -> Result<DelegationRequest, Error> {
338 let ttl_secs = claims.exp.saturating_sub(claims.iat);
339 if ttl_secs == 0 {
340 return Err(Error::invalid(
341 "delegation ttl_secs must be greater than zero",
342 ));
343 }
344
345 let signer_pid = IcOps::canister_self();
346 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
347 let verifier_targets = DelegatedTokenOps::required_verifier_targets_from_audience(
348 &claims.aud,
349 signer_pid,
350 root_pid,
351 Self::is_registered_canister,
352 )
353 .map_err(|principal| {
354 Error::invalid(format!(
355 "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
356 ))
357 })?;
358
359 Ok(DelegationRequest {
360 shard_pid: signer_pid,
361 scopes: claims.scopes.clone(),
362 aud: claims.aud.clone(),
363 ttl_secs,
364 verifier_targets,
365 include_root_verifier: true,
366 shard_public_key_sec1: None,
367 metadata: None,
368 })
369 }
370
371 fn ensure_required_verifier_targets_provisioned(
373 required_targets: &[Principal],
374 response: &DelegationProvisionResponse,
375 ) -> Result<(), Error> {
376 let mut checked = Vec::new();
377 for target in required_targets {
378 if checked.contains(target) {
379 continue;
380 }
381 checked.push(*target);
382 }
383 record_delegation_verifier_target_count(checked.len());
384
385 for target in &checked {
386 let Some(result) = response.results.iter().find(|entry| {
387 entry.kind == DelegationProvisionTargetKind::Verifier && entry.target == *target
388 }) else {
389 record_delegation_verifier_target_missing();
390 return Err(Error::internal(format!(
391 "delegation provisioning missing verifier target result for '{target}'"
392 )));
393 };
394
395 if result.status != DelegationProvisionStatus::Ok {
396 record_delegation_verifier_target_failed();
397 let detail = result
398 .error
399 .as_ref()
400 .map_or_else(|| "unknown error".to_string(), ToString::to_string);
401 return Err(Error::internal(format!(
402 "delegation provisioning failed for required verifier target '{target}': {detail}"
403 )));
404 }
405 }
406
407 record_delegation_provision_complete();
408 Ok(())
409 }
410
411 #[cfg(test)]
413 fn derive_required_verifier_targets_from_aud<F>(
414 audience: &[Principal],
415 signer_pid: Principal,
416 root_pid: Principal,
417 is_valid_target: F,
418 ) -> Result<Vec<Principal>, Error>
419 where
420 F: FnMut(Principal) -> bool,
421 {
422 DelegatedTokenOps::required_verifier_targets_from_audience(
423 audience,
424 signer_pid,
425 root_pid,
426 is_valid_target,
427 )
428 .map_err(|principal| {
429 Error::invalid(format!(
430 "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
431 ))
432 })
433 }
434
435 }
443
444impl DelegationApi {
445 pub async fn request_delegation_root(
447 request: DelegationRequest,
448 ) -> Result<DelegationProvisionResponse, Error> {
449 let request = metadata::with_root_request_metadata(request);
450 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
451 .await
452 .map_err(Self::map_delegation_error)?;
453
454 match response {
455 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
456 _ => Err(Error::internal(
457 "invalid root response type for delegation request",
458 )),
459 }
460 }
461
462 async fn request_delegation_remote(
464 request: DelegationRequest,
465 ) -> Result<DelegationProvisionResponse, Error> {
466 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
467 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
468 .await
469 .map_err(Self::map_delegation_error)
470 }
471
472 pub async fn request_role_attestation_root(
474 request: RoleAttestationRequest,
475 ) -> Result<SignedRoleAttestation, Error> {
476 let request = metadata::with_root_attestation_request_metadata(request);
477 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
478 .await
479 .map_err(Self::map_delegation_error)?;
480
481 match response {
482 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
483 _ => Err(Error::internal(
484 "invalid root response type for role attestation request",
485 )),
486 }
487 }
488
489 async fn request_role_attestation_remote(
491 request: RoleAttestationRequest,
492 ) -> Result<RootCapabilityResponse, Error> {
493 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
494 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
495 .await
496 .map_err(Self::map_delegation_error)
497 }
498}
499
500#[cfg(test)]
501mod tests;