Skip to main content

canic_core/api/
auth.rs

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,
10        rpc::{
11            Request as RootCapabilityRequest, Response as RootCapabilityResponse,
12            RootRequestMetadata,
13        },
14    },
15    error::InternalErrorClass,
16    log,
17    log::Topic,
18    ops::{
19        auth::{DelegatedTokenOps, DelegatedTokenOpsError},
20        config::ConfigOps,
21        ic::IcOps,
22        rpc::RpcOps,
23        runtime::env::EnvOps,
24        runtime::metrics::auth::{
25            record_attestation_epoch_rejected, record_attestation_refresh_failed,
26            record_attestation_unknown_key_id, record_attestation_verify_failed,
27            record_signer_mint_without_proof,
28        },
29        storage::auth::DelegationStateOps,
30    },
31    protocol,
32    workflow::rpc::request::handler::RootResponseWorkflow,
33};
34use sha2::{Digest, Sha256};
35use std::{
36    future::Future,
37    sync::atomic::{AtomicU64, Ordering},
38};
39
40///
41/// DelegationApi
42///
43/// Requires auth.delegated_tokens.enabled = true in config.
44///
45
46pub struct DelegationApi;
47
48const DEFAULT_ROOT_REQUEST_TTL_SECONDS: u64 = 300;
49static ROOT_REQUEST_NONCE: AtomicU64 = AtomicU64::new(1);
50
51#[derive(Debug)]
52enum RoleAttestationVerifyFlowError {
53    Initial(DelegatedTokenOpsError),
54    Refresh {
55        trigger: DelegatedTokenOpsError,
56        source: crate::InternalError,
57    },
58    PostRefresh(DelegatedTokenOpsError),
59}
60
61impl DelegationApi {
62    const DELEGATED_TOKENS_DISABLED: &str =
63        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
64
65    fn map_delegation_error(err: crate::InternalError) -> Error {
66        match err.class() {
67            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
68                Error::internal(err.to_string())
69            }
70            _ => Error::from(err),
71        }
72    }
73
74    /// Full delegation proof verification (structure + signature).
75    ///
76    /// Purely local verification; does not read certified data or require a
77    /// query context.
78    pub fn verify_delegation_proof(
79        proof: &DelegationProof,
80        authority_pid: Principal,
81    ) -> Result<(), Error> {
82        DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
83            .map_err(Self::map_delegation_error)
84    }
85
86    pub async fn sign_token(
87        claims: DelegatedTokenClaims,
88        proof: DelegationProof,
89    ) -> Result<DelegatedToken, Error> {
90        DelegatedTokenOps::sign_token(claims, proof)
91            .await
92            .map_err(Self::map_delegation_error)
93    }
94
95    /// Full delegated token verification (structure + signature).
96    ///
97    /// Purely local verification; does not read certified data or require a
98    /// query context.
99    pub fn verify_token(
100        token: &DelegatedToken,
101        authority_pid: Principal,
102        now_secs: u64,
103    ) -> Result<(), Error> {
104        DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
105            .map(|_| ())
106            .map_err(Self::map_delegation_error)
107    }
108
109    /// Verify a delegated token and return verified contents.
110    ///
111    /// This is intended for application-layer session construction.
112    /// It performs full verification and returns verified claims and cert.
113    pub fn verify_token_verified(
114        token: &DelegatedToken,
115        authority_pid: Principal,
116        now_secs: u64,
117    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
118        DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
119            .map(|verified| (verified.claims, verified.cert))
120            .map_err(Self::map_delegation_error)
121    }
122
123    /// Canonical shard-initiated delegation request (user_shard -> root).
124    ///
125    /// Caller must match shard_pid and be registered to the subnet.
126    pub async fn request_delegation(
127        request: DelegationRequest,
128    ) -> Result<DelegationProvisionResponse, Error> {
129        let request = with_root_request_metadata(request);
130        let response =
131            RootResponseWorkflow::response(RootCapabilityRequest::IssueDelegation(request))
132                .await
133                .map_err(Self::map_delegation_error)?;
134
135        match response {
136            RootCapabilityResponse::DelegationIssued(response) => Ok(response),
137            _ => Err(Error::internal(
138                "invalid root response type for delegation request",
139            )),
140        }
141    }
142
143    pub async fn request_role_attestation(
144        request: RoleAttestationRequest,
145    ) -> Result<SignedRoleAttestation, Error> {
146        let request = with_root_attestation_request_metadata(request);
147        let response =
148            RootResponseWorkflow::response(RootCapabilityRequest::IssueRoleAttestation(request))
149                .await
150                .map_err(Self::map_delegation_error)?;
151
152        match response {
153            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
154            _ => Err(Error::internal(
155                "invalid root response type for role attestation request",
156            )),
157        }
158    }
159
160    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
161        DelegatedTokenOps::attestation_key_set()
162            .await
163            .map_err(Self::map_delegation_error)
164    }
165
166    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
167        DelegatedTokenOps::replace_attestation_key_set(key_set);
168    }
169
170    pub async fn verify_role_attestation(
171        attestation: &SignedRoleAttestation,
172        min_accepted_epoch: u64,
173    ) -> Result<(), Error> {
174        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
175            .map_err(Error::from)?
176            .min_accepted_epoch_by_role
177            .get(attestation.payload.role.as_str())
178            .copied();
179        let min_accepted_epoch =
180            resolve_min_accepted_epoch(min_accepted_epoch, configured_min_accepted_epoch);
181
182        let caller = IcOps::msg_caller();
183        let self_pid = IcOps::canister_self();
184        let now_secs = IcOps::now_secs();
185        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
186        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
187
188        let verify = || {
189            DelegatedTokenOps::verify_role_attestation_cached(
190                attestation,
191                caller,
192                self_pid,
193                verifier_subnet,
194                now_secs,
195                min_accepted_epoch,
196            )
197            .map(|_| ())
198        };
199        let refresh = || async {
200            let key_set: AttestationKeySet =
201                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
202            DelegatedTokenOps::replace_attestation_key_set(key_set);
203            Ok(())
204        };
205
206        match verify_role_attestation_with_single_refresh(verify, refresh).await {
207            Ok(()) => Ok(()),
208            Err(RoleAttestationVerifyFlowError::Initial(err)) => {
209                record_attestation_verifier_rejection(&err);
210                log_attestation_verifier_rejection(&err, attestation, caller, self_pid, "cached");
211                Err(Self::map_delegation_error(err.into()))
212            }
213            Err(RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
214                record_attestation_verifier_rejection(&trigger);
215                log_attestation_verifier_rejection(
216                    &trigger,
217                    attestation,
218                    caller,
219                    self_pid,
220                    "cache_miss_refresh",
221                );
222                record_attestation_refresh_failed();
223                log!(
224                    Topic::Auth,
225                    Warn,
226                    "role attestation refresh failed local={} caller={} key_id={} error={}",
227                    self_pid,
228                    caller,
229                    attestation.key_id,
230                    source
231                );
232                Err(Self::map_delegation_error(source))
233            }
234            Err(RoleAttestationVerifyFlowError::PostRefresh(err)) => {
235                record_attestation_verifier_rejection(&err);
236                log_attestation_verifier_rejection(
237                    &err,
238                    attestation,
239                    caller,
240                    self_pid,
241                    "post_refresh",
242                );
243                Err(Self::map_delegation_error(err.into()))
244            }
245        }
246    }
247
248    pub async fn store_proof(
249        proof: DelegationProof,
250        kind: DelegationProvisionTargetKind,
251    ) -> Result<(), Error> {
252        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
253        if !cfg.enabled {
254            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
255        }
256
257        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
258        let caller = IcOps::msg_caller();
259        if caller != root_pid {
260            return Err(Error::forbidden(
261                "delegation proof store requires root caller",
262            ));
263        }
264
265        DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
266            .await
267            .map_err(Self::map_delegation_error)?;
268        if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
269            let local = IcOps::canister_self();
270            log!(
271                Topic::Auth,
272                Warn,
273                "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
274                kind,
275                local,
276                proof.cert.shard_pid,
277                proof.cert.issued_at,
278                proof.cert.expires_at,
279                err
280            );
281            return Err(Self::map_delegation_error(err));
282        }
283
284        DelegationStateOps::set_proof_from_dto(proof);
285        let local = IcOps::canister_self();
286        let stored = DelegationStateOps::proof_dto()
287            .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
288        log!(
289            Topic::Auth,
290            Info,
291            "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
292            kind,
293            local,
294            stored.cert.shard_pid,
295            stored.cert.issued_at,
296            stored.cert.expires_at
297        );
298
299        Ok(())
300    }
301
302    pub fn require_proof() -> Result<DelegationProof, Error> {
303        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
304        if !cfg.enabled {
305            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
306        }
307
308        DelegationStateOps::proof_dto().ok_or_else(|| {
309            record_signer_mint_without_proof();
310            Error::not_found("delegation proof not set")
311        })
312    }
313}
314
315fn with_root_request_metadata(mut request: DelegationRequest) -> DelegationRequest {
316    if request.metadata.is_none() {
317        request.metadata = Some(new_request_metadata());
318    }
319    request
320}
321
322fn with_root_attestation_request_metadata(
323    mut request: RoleAttestationRequest,
324) -> RoleAttestationRequest {
325    if request.metadata.is_none() {
326        request.metadata = Some(new_request_metadata());
327    }
328    request
329}
330
331fn new_request_metadata() -> RootRequestMetadata {
332    RootRequestMetadata {
333        request_id: generate_request_id(),
334        ttl_seconds: DEFAULT_ROOT_REQUEST_TTL_SECONDS,
335    }
336}
337
338fn generate_request_id() -> [u8; 32] {
339    if let Ok(bytes) = crate::utils::rand::random_bytes(32)
340        && bytes.len() == 32
341    {
342        let mut out = [0u8; 32];
343        out.copy_from_slice(&bytes);
344        return out;
345    }
346
347    let nonce = ROOT_REQUEST_NONCE.fetch_add(1, Ordering::Relaxed);
348    let now = IcOps::now_secs();
349    let caller = IcOps::msg_caller();
350    let canister = IcOps::canister_self();
351
352    let mut hasher = Sha256::new();
353    hasher.update(now.to_be_bytes());
354    hasher.update(nonce.to_be_bytes());
355    hasher.update(caller.as_slice());
356    hasher.update(canister.as_slice());
357    hasher.finalize().into()
358}
359
360async fn verify_role_attestation_with_single_refresh<Verify, Refresh, RefreshFuture>(
361    mut verify: Verify,
362    mut refresh: Refresh,
363) -> Result<(), RoleAttestationVerifyFlowError>
364where
365    Verify: FnMut() -> Result<(), DelegatedTokenOpsError>,
366    Refresh: FnMut() -> RefreshFuture,
367    RefreshFuture: Future<Output = Result<(), crate::InternalError>>,
368{
369    match verify() {
370        Ok(()) => Ok(()),
371        Err(err @ DelegatedTokenOpsError::AttestationUnknownKeyId { .. }) => {
372            refresh()
373                .await
374                .map_err(|source| RoleAttestationVerifyFlowError::Refresh {
375                    trigger: err,
376                    source,
377                })?;
378            verify().map_err(RoleAttestationVerifyFlowError::PostRefresh)
379        }
380        Err(err) => Err(RoleAttestationVerifyFlowError::Initial(err)),
381    }
382}
383
384fn resolve_min_accepted_epoch(explicit: u64, configured: Option<u64>) -> u64 {
385    if explicit > 0 {
386        explicit
387    } else {
388        configured.unwrap_or(0)
389    }
390}
391
392fn record_attestation_verifier_rejection(err: &DelegatedTokenOpsError) {
393    record_attestation_verify_failed();
394    match err {
395        DelegatedTokenOpsError::AttestationUnknownKeyId { .. } => {
396            record_attestation_unknown_key_id();
397        }
398        DelegatedTokenOpsError::AttestationEpochRejected { .. } => {
399            record_attestation_epoch_rejected();
400        }
401        _ => {}
402    }
403}
404
405fn log_attestation_verifier_rejection(
406    err: &DelegatedTokenOpsError,
407    attestation: &SignedRoleAttestation,
408    caller: Principal,
409    self_pid: Principal,
410    phase: &str,
411) {
412    log!(
413        Topic::Auth,
414        Warn,
415        "role attestation rejected phase={} local={} caller={} subject={} role={} key_id={} audience={:?} subnet={:?} issued_at={} expires_at={} epoch={} error={}",
416        phase,
417        self_pid,
418        caller,
419        attestation.payload.subject,
420        attestation.payload.role,
421        attestation.key_id,
422        attestation.payload.audience,
423        attestation.payload.subnet_id,
424        attestation.payload.issued_at,
425        attestation.payload.expires_at,
426        attestation.payload.epoch,
427        err
428    );
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use crate::InternalErrorOrigin;
435    use futures::executor::block_on;
436    use std::cell::Cell;
437
438    #[test]
439    fn verify_role_attestation_with_single_refresh_accepts_without_refresh() {
440        let verify_calls = Cell::new(0usize);
441        let refresh_calls = Cell::new(0usize);
442
443        let result = block_on(verify_role_attestation_with_single_refresh(
444            || {
445                verify_calls.set(verify_calls.get() + 1);
446                Ok(())
447            },
448            || {
449                refresh_calls.set(refresh_calls.get() + 1);
450                std::future::ready(Ok(()))
451            },
452        ));
453
454        assert!(result.is_ok());
455        assert_eq!(verify_calls.get(), 1, "verify must run exactly once");
456        assert_eq!(refresh_calls.get(), 0, "refresh must not run");
457    }
458
459    #[test]
460    fn verify_role_attestation_with_single_refresh_retries_once_on_unknown_key() {
461        let verify_calls = Cell::new(0usize);
462        let refresh_calls = Cell::new(0usize);
463
464        let result = block_on(verify_role_attestation_with_single_refresh(
465            || {
466                let attempt = verify_calls.get();
467                verify_calls.set(attempt + 1);
468                if attempt == 0 {
469                    Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 7 })
470                } else {
471                    Ok(())
472                }
473            },
474            || {
475                refresh_calls.set(refresh_calls.get() + 1);
476                std::future::ready(Ok(()))
477            },
478        ));
479
480        assert!(result.is_ok());
481        assert_eq!(verify_calls.get(), 2, "verify must run exactly twice");
482        assert_eq!(refresh_calls.get(), 1, "refresh must run exactly once");
483    }
484
485    #[test]
486    fn verify_role_attestation_with_single_refresh_fails_closed_on_refresh_error() {
487        let verify_calls = Cell::new(0usize);
488        let refresh_calls = Cell::new(0usize);
489
490        let result = block_on(verify_role_attestation_with_single_refresh(
491            || {
492                verify_calls.set(verify_calls.get() + 1);
493                Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 9 })
494            },
495            || {
496                refresh_calls.set(refresh_calls.get() + 1);
497                std::future::ready(Err(crate::InternalError::infra(
498                    InternalErrorOrigin::Infra,
499                    "refresh failed",
500                )))
501            },
502        ));
503
504        match result {
505            Err(RoleAttestationVerifyFlowError::Refresh {
506                trigger: DelegatedTokenOpsError::AttestationUnknownKeyId { key_id },
507                ..
508            }) => assert_eq!(key_id, 9),
509            other => panic!("expected refresh failure for unknown key, got: {other:?}"),
510        }
511
512        assert_eq!(
513            verify_calls.get(),
514            1,
515            "verify must not retry after refresh failure"
516        );
517        assert_eq!(refresh_calls.get(), 1, "refresh must run once");
518    }
519
520    #[test]
521    fn verify_role_attestation_with_single_refresh_does_not_refresh_on_non_unknown_error() {
522        let verify_calls = Cell::new(0usize);
523        let refresh_calls = Cell::new(0usize);
524
525        let result = block_on(verify_role_attestation_with_single_refresh(
526            || {
527                verify_calls.set(verify_calls.get() + 1);
528                Err(DelegatedTokenOpsError::AttestationEpochRejected {
529                    epoch: 1,
530                    min_accepted_epoch: 2,
531                })
532            },
533            || {
534                refresh_calls.set(refresh_calls.get() + 1);
535                std::future::ready(Ok(()))
536            },
537        ));
538
539        match result {
540            Err(RoleAttestationVerifyFlowError::Initial(
541                DelegatedTokenOpsError::AttestationEpochRejected {
542                    epoch,
543                    min_accepted_epoch,
544                },
545            )) => {
546                assert_eq!(epoch, 1);
547                assert_eq!(min_accepted_epoch, 2);
548            }
549            other => panic!("expected initial epoch rejection, got: {other:?}"),
550        }
551
552        assert_eq!(verify_calls.get(), 1, "verify must run once");
553        assert_eq!(refresh_calls.get(), 0, "refresh must not run");
554    }
555
556    #[test]
557    fn verify_role_attestation_with_single_refresh_only_attempts_one_refresh() {
558        let verify_calls = Cell::new(0usize);
559        let refresh_calls = Cell::new(0usize);
560
561        let result = block_on(verify_role_attestation_with_single_refresh(
562            || {
563                let attempt = verify_calls.get();
564                verify_calls.set(attempt + 1);
565                if attempt == 0 {
566                    Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 5 })
567                } else {
568                    Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 6 })
569                }
570            },
571            || {
572                refresh_calls.set(refresh_calls.get() + 1);
573                std::future::ready(Ok(()))
574            },
575        ));
576
577        match result {
578            Err(RoleAttestationVerifyFlowError::PostRefresh(
579                DelegatedTokenOpsError::AttestationUnknownKeyId { key_id },
580            )) => assert_eq!(key_id, 6),
581            other => panic!("expected post-refresh unknown-key rejection, got: {other:?}"),
582        }
583
584        assert_eq!(verify_calls.get(), 2, "verify must run exactly twice");
585        assert_eq!(refresh_calls.get(), 1, "refresh must run exactly once");
586    }
587
588    #[test]
589    fn resolve_min_accepted_epoch_prefers_explicit_argument() {
590        assert_eq!(resolve_min_accepted_epoch(7, Some(3)), 7);
591        assert_eq!(resolve_min_accepted_epoch(5, None), 5);
592    }
593
594    #[test]
595    fn resolve_min_accepted_epoch_falls_back_to_config_or_zero() {
596        assert_eq!(resolve_min_accepted_epoch(0, Some(4)), 4);
597        assert_eq!(resolve_min_accepted_epoch(0, None), 0);
598    }
599}