Skip to main content

canic_core/api/auth/
mod.rs

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
33// Internal auth pipeline:
34// - `session` owns delegated-session ingress and replay/session state handling.
35// - `admin` owns explicit root-driven fanout preparation and routing.
36// - `proof_store` owns proof-install validation and storage/cache side effects.
37//
38// Keep these modules free of lateral calls to each other. Coordination stays here,
39// and shared invariants should live in dedicated seams like `ops::auth::audience`.
40mod admin;
41mod metadata;
42mod proof_store;
43mod session;
44mod verify_flow;
45
46///
47/// DelegationApi
48///
49/// Requires auth.delegated_tokens.enabled = true in config.
50///
51
52pub 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    /// Full delegation proof verification (structure + signature).
71    ///
72    /// Purely local verification; does not read certified data or require a
73    /// query context.
74    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    /// Resolve the local shard public key in SEC1 encoding.
98    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    /// Issue a delegated token using a reusable local proof when possible.
105    ///
106    /// If the proof is missing or no longer valid for the requested claims, this
107    /// performs canonical shard-initiated setup and retries with the refreshed proof.
108    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    /// Full delegated token verification (structure + signature).
115    ///
116    /// Purely local verification; does not read certified data or require a
117    /// query context.
118    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    /// Verify a delegated token and return verified contents.
129    ///
130    /// This is intended for application-layer session construction.
131    /// It performs full verification and returns verified claims and cert.
132    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    /// Canonical shard-initiated delegation request (user_shard -> root).
143    ///
144    /// Caller must match shard_pid and be registered to the subnet.
145    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    /// Warm the root delegation and attestation key caches once.
173    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    // Resolve a proof that is currently usable for token issuance.
286    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    // Provision a fresh delegation from root, then resolve the latest locally stored proof.
302    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    // Rebase claims onto a freshly issued proof window when delegation setup
318    // completed after the original token timestamps were chosen.
319    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    // Build a canonical delegation request from token claims.
335    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    // Validate required verifier fanout and fail closed when any required target is missing/failing.
372    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    // Derive required verifier targets from audience with strict filtering/validation.
412    #[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    // Delegated audience invariants:
436    // 1. claims.aud must be non-empty.
437    // 2. claims.aud must be a set-subset of proof.cert.aud.
438    // 3. proof installation on target T requires T ∈ proof.cert.aud.
439    // 4. token acceptance on canister C requires C ∈ claims.aud.
440    //
441    // Keep ingress, fanout, install, and runtime checks aligned to this block.
442}
443
444impl DelegationApi {
445    // Execute one local root delegation provisioning request.
446    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    // Route a canonical delegation provisioning request over RPC to root.
463    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    // Execute one local root role-attestation request.
473    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    // Route a canonical role-attestation request over RPC to root.
490    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;