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