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    /// Issue a delegated token using a reusable local proof when possible.
98    ///
99    /// If the proof is missing or no longer valid for the requested claims, this
100    /// performs canonical shard-initiated setup and retries with the refreshed proof.
101    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    /// Full delegated token verification (structure + signature).
107    ///
108    /// Purely local verification; does not read certified data or require a
109    /// query context.
110    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    /// Verify a delegated token and return verified contents.
121    ///
122    /// This is intended for application-layer session construction.
123    /// It performs full verification and returns verified claims and cert.
124    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    /// Canonical shard-initiated delegation request (user_shard -> root).
135    ///
136    /// Caller must match shard_pid and be registered to the subnet.
137    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 fn replace_attestation_key_set(key_set: AttestationKeySet) {
165        DelegatedTokenOps::replace_attestation_key_set(key_set);
166    }
167
168    pub async fn verify_role_attestation(
169        attestation: &SignedRoleAttestation,
170        min_accepted_epoch: u64,
171    ) -> Result<(), Error> {
172        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
173            .map_err(Error::from)?
174            .min_accepted_epoch_by_role
175            .get(attestation.payload.role.as_str())
176            .copied();
177        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
178            min_accepted_epoch,
179            configured_min_accepted_epoch,
180        );
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_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
207            Ok(()) => Ok(()),
208            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
209                verify_flow::record_attestation_verifier_rejection(&err);
210                verify_flow::log_attestation_verifier_rejection(
211                    &err,
212                    attestation,
213                    caller,
214                    self_pid,
215                    "cached",
216                );
217                Err(Self::map_delegation_error(err.into()))
218            }
219            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
220                verify_flow::record_attestation_verifier_rejection(&trigger);
221                verify_flow::log_attestation_verifier_rejection(
222                    &trigger,
223                    attestation,
224                    caller,
225                    self_pid,
226                    "cache_miss_refresh",
227                );
228                record_attestation_refresh_failed();
229                log!(
230                    Topic::Auth,
231                    Warn,
232                    "role attestation refresh failed local={} caller={} key_id={} error={}",
233                    self_pid,
234                    caller,
235                    attestation.key_id,
236                    source
237                );
238                Err(Self::map_delegation_error(source))
239            }
240            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
241                verify_flow::record_attestation_verifier_rejection(&err);
242                verify_flow::log_attestation_verifier_rejection(
243                    &err,
244                    attestation,
245                    caller,
246                    self_pid,
247                    "post_refresh",
248                );
249                Err(Self::map_delegation_error(err.into()))
250            }
251        }
252    }
253
254    fn require_proof() -> Result<DelegationProof, Error> {
255        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
256        if !cfg.enabled {
257            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
258        }
259
260        DelegationStateOps::latest_proof_dto().ok_or_else(|| {
261            record_signer_issue_without_proof();
262            Error::not_found("delegation proof not installed")
263        })
264    }
265
266    // Resolve a proof that is currently usable for token issuance.
267    async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
268        let now_secs = IcOps::now_secs();
269
270        match Self::require_proof() {
271            Ok(proof)
272                if !DelegatedTokenOps::proof_reusable_for_claims(&proof, claims, now_secs) =>
273            {
274                Self::setup_delegation(claims).await
275            }
276            Ok(proof) => Ok(proof),
277            Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
278            Err(err) => Err(err),
279        }
280    }
281
282    // Provision a fresh delegation from root, then resolve the latest locally stored proof.
283    async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
284        let request = Self::delegation_request_from_claims(claims)?;
285        let required_verifier_targets = request.verifier_targets.clone();
286        let response = Self::request_delegation_remote(request).await?;
287        Self::ensure_required_verifier_targets_provisioned(&required_verifier_targets, &response)?;
288        Self::require_proof()
289    }
290
291    // Build a canonical delegation request from token claims.
292    fn delegation_request_from_claims(
293        claims: &DelegatedTokenClaims,
294    ) -> Result<DelegationRequest, Error> {
295        let ttl_secs = claims.exp.saturating_sub(claims.iat);
296        if ttl_secs == 0 {
297            return Err(Error::invalid(
298                "delegation ttl_secs must be greater than zero",
299            ));
300        }
301
302        let signer_pid = IcOps::canister_self();
303        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
304        let verifier_targets = DelegatedTokenOps::required_verifier_targets_from_audience(
305            &claims.aud,
306            signer_pid,
307            root_pid,
308            Self::is_registered_canister,
309        )
310        .map_err(|principal| {
311            Error::invalid(format!(
312                "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
313            ))
314        })?;
315
316        Ok(DelegationRequest {
317            shard_pid: signer_pid,
318            scopes: claims.scopes.clone(),
319            aud: claims.aud.clone(),
320            ttl_secs,
321            verifier_targets,
322            include_root_verifier: true,
323            metadata: None,
324        })
325    }
326
327    // Validate required verifier fanout and fail closed when any required target is missing/failing.
328    fn ensure_required_verifier_targets_provisioned(
329        required_targets: &[Principal],
330        response: &DelegationProvisionResponse,
331    ) -> Result<(), Error> {
332        let mut checked = Vec::new();
333        for target in required_targets {
334            if checked.contains(target) {
335                continue;
336            }
337            checked.push(*target);
338        }
339        record_delegation_verifier_target_count(checked.len());
340
341        for target in &checked {
342            let Some(result) = response.results.iter().find(|entry| {
343                entry.kind == DelegationProvisionTargetKind::Verifier && entry.target == *target
344            }) else {
345                record_delegation_verifier_target_missing();
346                return Err(Error::internal(format!(
347                    "delegation provisioning missing verifier target result for '{target}'"
348                )));
349            };
350
351            if result.status != DelegationProvisionStatus::Ok {
352                record_delegation_verifier_target_failed();
353                let detail = result
354                    .error
355                    .as_ref()
356                    .map_or_else(|| "unknown error".to_string(), ToString::to_string);
357                return Err(Error::internal(format!(
358                    "delegation provisioning failed for required verifier target '{target}': {detail}"
359                )));
360            }
361        }
362
363        record_delegation_provision_complete();
364        Ok(())
365    }
366
367    // Derive required verifier targets from audience with strict filtering/validation.
368    #[cfg(test)]
369    fn derive_required_verifier_targets_from_aud<F>(
370        audience: &[Principal],
371        signer_pid: Principal,
372        root_pid: Principal,
373        is_valid_target: F,
374    ) -> Result<Vec<Principal>, Error>
375    where
376        F: FnMut(Principal) -> bool,
377    {
378        DelegatedTokenOps::required_verifier_targets_from_audience(
379            audience,
380            signer_pid,
381            root_pid,
382            is_valid_target,
383        )
384        .map_err(|principal| {
385            Error::invalid(format!(
386                "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
387            ))
388        })
389    }
390
391    // Delegated audience invariants:
392    // 1. claims.aud must be non-empty.
393    // 2. claims.aud must be a set-subset of proof.cert.aud.
394    // 3. proof installation on target T requires T ∈ proof.cert.aud.
395    // 4. token acceptance on canister C requires C ∈ claims.aud.
396    //
397    // Keep ingress, fanout, install, and runtime checks aligned to this block.
398}
399
400impl DelegationApi {
401    // Execute one local root delegation provisioning request.
402    pub async fn request_delegation_root(
403        request: DelegationRequest,
404    ) -> Result<DelegationProvisionResponse, Error> {
405        let request = metadata::with_root_request_metadata(request);
406        let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
407            .await
408            .map_err(Self::map_delegation_error)?;
409
410        match response {
411            RootCapabilityResponse::DelegationIssued(response) => Ok(response),
412            _ => Err(Error::internal(
413                "invalid root response type for delegation request",
414            )),
415        }
416    }
417
418    // Route a canonical delegation provisioning request over RPC to root.
419    async fn request_delegation_remote(
420        request: DelegationRequest,
421    ) -> Result<DelegationProvisionResponse, Error> {
422        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
423        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
424            .await
425            .map_err(Self::map_delegation_error)
426    }
427
428    // Execute one local root role-attestation request.
429    pub async fn request_role_attestation_root(
430        request: RoleAttestationRequest,
431    ) -> Result<SignedRoleAttestation, Error> {
432        let request = metadata::with_root_attestation_request_metadata(request);
433        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
434            .await
435            .map_err(Self::map_delegation_error)?;
436
437        match response {
438            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
439            _ => Err(Error::internal(
440                "invalid root response type for role attestation request",
441            )),
442        }
443    }
444
445    // Route a canonical role-attestation request over RPC to root.
446    async fn request_role_attestation_remote(
447        request: RoleAttestationRequest,
448    ) -> Result<RootCapabilityResponse, Error> {
449        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
450        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
451            .await
452            .map_err(Self::map_delegation_error)
453    }
454}
455
456#[cfg(test)]
457mod tests;