Skip to main content

canic_core/api/auth/
mod.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        auth::{
5            AttestationKeySet, DelegatedToken, DelegatedTokenIssueRequest,
6            DelegatedTokenMintRequest, DelegationProof, DelegationProofIssueRequest,
7            InternalInvocationProofRequest, RoleAttestationRequest,
8            SignedInternalInvocationProofV1, SignedRoleAttestation,
9        },
10        error::Error,
11        rpc::{Request as RootRequest, Response as RootCapabilityResponse},
12    },
13    error::InternalErrorClass,
14    ids::CanisterRole,
15    log,
16    log::Topic,
17    ops::{
18        auth::{
19            AuthOps, SignDelegatedTokenInput, SignDelegationProofInput,
20            VerifyDelegatedTokenRuntimeInput,
21        },
22        config::ConfigOps,
23        ic::IcOps,
24        rpc::RpcOps,
25        runtime::env::EnvOps,
26        runtime::metrics::auth::record_attestation_refresh_failed,
27    },
28    protocol,
29    workflow::rpc::request::handler::RootResponseWorkflow,
30};
31
32// Internal auth pipeline:
33// - `session` owns delegated-session ingress and replay/session state handling.
34// - `metadata` owns root request metadata construction.
35// - `verify_flow` owns verifier-side attestation refresh behavior.
36mod metadata;
37mod session;
38mod verify_flow;
39
40///
41/// AuthApi
42///
43/// Owns delegated-token helpers and root-signed role-attestation helpers.
44///
45
46pub struct AuthApi;
47
48impl AuthApi {
49    const DELEGATED_TOKENS_DISABLED: &str =
50        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
51    const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
52    const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
53        b"canic-session-bootstrap-token-fingerprint";
54
55    // Map internal auth failures onto public endpoint errors.
56    fn map_auth_error(err: crate::InternalError) -> Error {
57        match err.class() {
58            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
59                Error::internal(err.to_string())
60            }
61            _ => Error::from(err),
62        }
63    }
64
65    // Verify delegated-token material and return the token subject.
66    //
67    // This is intentionally private: endpoint authorization must also bind the
68    // verified subject to the caller and consume update tokens once.
69    fn verify_token_material(
70        token: &DelegatedToken,
71        max_cert_ttl_secs: u64,
72        max_token_ttl_secs: u64,
73        required_scopes: &[String],
74        now_secs: u64,
75    ) -> Result<Principal, Error> {
76        AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
77            token,
78            max_cert_ttl_secs,
79            max_token_ttl_secs,
80            required_scopes,
81            now_secs,
82        })
83        .map(|verified| verified.subject)
84        .map_err(Self::map_auth_error)
85    }
86
87    /// Resolve the local shard public key in SEC1 encoding.
88    pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
89        AuthOps::local_shard_public_key_sec1(IcOps::canister_self())
90            .await
91            .map_err(Self::map_auth_error)
92    }
93
94    /// Issue a delegated token from an explicit self-contained proof.
95    pub async fn issue_token(request: DelegatedTokenIssueRequest) -> Result<DelegatedToken, Error> {
96        AuthOps::sign_token(SignDelegatedTokenInput {
97            proof: request.proof,
98            subject: request.subject,
99            audience: request.aud,
100            scopes: request.scopes,
101            ttl_secs: request.ttl_secs,
102            nonce: request.nonce,
103        })
104        .await
105        .map_err(Self::map_auth_error)
106    }
107
108    /// Request a root proof, then issue a self-contained delegated token.
109    pub async fn mint_token(request: DelegatedTokenMintRequest) -> Result<DelegatedToken, Error> {
110        let proof = Self::request_delegation(DelegationProofIssueRequest {
111            shard_pid: IcOps::canister_self(),
112            scopes: request.scopes.clone(),
113            aud: request.aud.clone(),
114            cert_ttl_secs: request.cert_ttl_secs,
115        })
116        .await?;
117
118        Self::issue_token(DelegatedTokenIssueRequest {
119            proof,
120            subject: request.subject,
121            aud: request.aud,
122            scopes: request.scopes,
123            ttl_secs: request.token_ttl_secs,
124            nonce: request.nonce,
125        })
126        .await
127    }
128
129    /// Request a self-contained delegation proof from root over RPC.
130    pub async fn request_delegation(
131        request: DelegationProofIssueRequest,
132    ) -> Result<DelegationProof, Error> {
133        Self::request_delegation_remote(request).await
134    }
135
136    /// Issue a self-contained delegation proof from the local root.
137    pub async fn issue_delegation_proof(
138        request: DelegationProofIssueRequest,
139    ) -> Result<DelegationProof, Error> {
140        EnvOps::require_root().map_err(Error::from)?;
141        let max_cert_ttl_secs = Self::delegated_token_max_ttl_secs()?;
142        let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
143        AuthOps::sign_delegation_proof(SignDelegationProofInput {
144            audience: request.aud,
145            scopes: request.scopes,
146            shard_pid: request.shard_pid,
147            cert_ttl_secs: request.cert_ttl_secs,
148            max_token_ttl_secs,
149            max_cert_ttl_secs,
150            issued_at: IcOps::now_secs(),
151        })
152        .await
153        .map_err(Self::map_auth_error)
154    }
155
156    /// Request a signed role attestation from root over RPC.
157    pub async fn request_role_attestation(
158        request: RoleAttestationRequest,
159    ) -> Result<SignedRoleAttestation, Error> {
160        let request = metadata::with_root_attestation_request_metadata(request);
161        let response = Self::request_role_attestation_remote(request).await?;
162
163        match response {
164            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
165            _ => Err(Error::internal(
166                "invalid root response type for role attestation request",
167            )),
168        }
169    }
170
171    /// Request a method-scoped internal invocation proof from root over RPC.
172    pub async fn request_internal_invocation_proof(
173        request: InternalInvocationProofRequest,
174    ) -> Result<SignedInternalInvocationProofV1, Error> {
175        let request = metadata::with_internal_invocation_proof_request_metadata(request);
176        let response = Self::request_internal_invocation_proof_remote(request).await?;
177
178        match response {
179            RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
180            _ => Err(Error::internal(
181                "invalid root response type for internal invocation proof request",
182            )),
183        }
184    }
185
186    /// Return the current root role-attestation key set.
187    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
188        AuthOps::attestation_key_set()
189            .await
190            .map_err(Self::map_auth_error)
191    }
192
193    /// Publish root auth material into subnet state and warm root-owned keys once.
194    pub async fn publish_root_auth_material() -> Result<(), Error> {
195        EnvOps::require_root().map_err(Error::from)?;
196        AuthOps::publish_root_auth_material().await.map_err(|err| {
197            log!(
198                Topic::Auth,
199                Warn,
200                "root auth material publish failed: {err}"
201            );
202            Self::map_auth_error(err)
203        })
204    }
205
206    /// Replace the verifier-local role-attestation key set.
207    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
208        AuthOps::replace_attestation_key_set(key_set);
209    }
210
211    /// Verify a role attestation, refreshing root keys once on unknown key.
212    pub async fn verify_role_attestation(
213        attestation: &SignedRoleAttestation,
214        min_accepted_epoch: u64,
215    ) -> Result<(), Error> {
216        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
217            .map_err(Error::from)?
218            .min_accepted_epoch_by_role
219            .get(attestation.payload.role.as_str())
220            .copied();
221        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
222            min_accepted_epoch,
223            configured_min_accepted_epoch,
224        );
225
226        let caller = IcOps::msg_caller();
227        let self_pid = IcOps::canister_self();
228        let now_secs = IcOps::now_secs();
229        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
230        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
231
232        let verify = || {
233            AuthOps::verify_role_attestation_cached(
234                attestation,
235                caller,
236                self_pid,
237                verifier_subnet,
238                now_secs,
239                min_accepted_epoch,
240            )
241            .map(|_| ())
242        };
243        let refresh = || async {
244            let key_set: AttestationKeySet =
245                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
246            AuthOps::replace_attestation_key_set(key_set);
247            Ok(())
248        };
249
250        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
251            Ok(()) => Ok(()),
252            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
253                verify_flow::record_attestation_verifier_rejection(&err);
254                verify_flow::log_attestation_verifier_rejection(
255                    &err,
256                    attestation,
257                    caller,
258                    self_pid,
259                    "cached",
260                );
261                Err(Self::map_auth_error(err.into()))
262            }
263            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
264                verify_flow::record_attestation_verifier_rejection(&trigger);
265                verify_flow::log_attestation_verifier_rejection(
266                    &trigger,
267                    attestation,
268                    caller,
269                    self_pid,
270                    "cache_miss_refresh",
271                );
272                record_attestation_refresh_failed();
273                log!(
274                    Topic::Auth,
275                    Warn,
276                    "role attestation refresh failed local={} caller={} key_id={} error={}",
277                    self_pid,
278                    caller,
279                    attestation.key_id,
280                    source
281                );
282                Err(Self::map_auth_error(source))
283            }
284            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
285                verify_flow::record_attestation_verifier_rejection(&err);
286                verify_flow::log_attestation_verifier_rejection(
287                    &err,
288                    attestation,
289                    caller,
290                    self_pid,
291                    "post_refresh",
292                );
293                Err(Self::map_auth_error(err.into()))
294            }
295        }
296    }
297
298    /// Verify a root-signed, method-scoped internal invocation proof for this endpoint.
299    pub async fn verify_internal_invocation_proof(
300        proof: &SignedInternalInvocationProofV1,
301        target_method: &str,
302        accepted_roles: &[CanisterRole],
303    ) -> Result<(), Error> {
304        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
305            .map_err(Error::from)?
306            .min_accepted_epoch_by_role
307            .get(proof.payload.role.as_str())
308            .copied();
309        let min_accepted_epoch =
310            verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
311
312        let caller = IcOps::msg_caller();
313        let self_pid = IcOps::canister_self();
314        let now_secs = IcOps::now_secs();
315        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
316        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
317
318        let verify = || {
319            AuthOps::verify_internal_invocation_proof_cached(
320                proof,
321                crate::ops::auth::InternalInvocationProofVerificationInput {
322                    caller,
323                    self_pid,
324                    target_method,
325                    accepted_roles,
326                    verifier_subnet,
327                    now_secs,
328                    min_accepted_epoch,
329                },
330            )
331            .map(|_| ())
332        };
333        let refresh = || async {
334            let key_set: AttestationKeySet =
335                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
336            AuthOps::replace_attestation_key_set(key_set);
337            Ok(())
338        };
339
340        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
341            Ok(()) => Ok(()),
342            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
343                verify_flow::record_attestation_verifier_rejection(&err);
344                log!(
345                    Topic::Auth,
346                    Warn,
347                    "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
348                    self_pid,
349                    caller,
350                    proof.payload.subject,
351                    proof.payload.role,
352                    proof.key_id,
353                    proof.payload.audience,
354                    proof.payload.audience_method,
355                    proof.payload.epoch,
356                    err
357                );
358                Err(Self::map_auth_error(err.into()))
359            }
360            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
361                verify_flow::record_attestation_verifier_rejection(&trigger);
362                record_attestation_refresh_failed();
363                log!(
364                    Topic::Auth,
365                    Warn,
366                    "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
367                    self_pid,
368                    caller,
369                    proof.key_id,
370                    source
371                );
372                Err(Self::map_auth_error(source))
373            }
374            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
375                verify_flow::record_attestation_verifier_rejection(&err);
376                log!(
377                    Topic::Auth,
378                    Warn,
379                    "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
380                    self_pid,
381                    caller,
382                    proof.payload.subject,
383                    proof.payload.role,
384                    proof.key_id,
385                    proof.payload.audience,
386                    proof.payload.audience_method,
387                    proof.payload.epoch,
388                    err
389                );
390                Err(Self::map_auth_error(err.into()))
391            }
392        }
393    }
394
395    // Resolve the root-owned TTL ceiling from delegated-token config.
396    fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
397        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
398        if !cfg.enabled {
399            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
400        }
401
402        Ok(cfg
403            .max_ttl_secs
404            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
405    }
406}
407
408impl AuthApi {
409    // Route a self-contained delegation proof request over RPC to root.
410    async fn request_delegation_remote(
411        request: DelegationProofIssueRequest,
412    ) -> Result<DelegationProof, Error> {
413        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
414        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
415            .await
416            .map_err(Self::map_auth_error)
417    }
418
419    // Execute one local root role-attestation request.
420    pub async fn request_role_attestation_root(
421        request: RoleAttestationRequest,
422    ) -> Result<SignedRoleAttestation, Error> {
423        let request = metadata::with_root_attestation_request_metadata(request);
424        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
425            .await
426            .map_err(Self::map_auth_error)?;
427
428        match response {
429            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
430            _ => Err(Error::internal(
431                "invalid root response type for role attestation request",
432            )),
433        }
434    }
435
436    // Execute one local root internal-invocation proof request.
437    pub async fn request_internal_invocation_proof_root(
438        request: InternalInvocationProofRequest,
439    ) -> Result<SignedInternalInvocationProofV1, Error> {
440        let request = metadata::with_internal_invocation_proof_request_metadata(request);
441        let response =
442            RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
443                .await
444                .map_err(Self::map_auth_error)?;
445
446        match response {
447            RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
448            _ => Err(Error::internal(
449                "invalid root response type for internal invocation proof request",
450            )),
451        }
452    }
453
454    // Route a canonical role-attestation request over RPC to root.
455    async fn request_role_attestation_remote(
456        request: RoleAttestationRequest,
457    ) -> Result<RootCapabilityResponse, Error> {
458        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
459        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
460            .await
461            .map_err(Self::map_auth_error)
462    }
463
464    // Route a canonical internal-invocation proof request over RPC to root.
465    async fn request_internal_invocation_proof_remote(
466        request: InternalInvocationProofRequest,
467    ) -> Result<RootCapabilityResponse, Error> {
468        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
469        RpcOps::call_rpc_result(
470            root_pid,
471            protocol::CANIC_REQUEST_INTERNAL_INVOCATION_PROOF,
472            request,
473        )
474        .await
475        .map_err(Self::map_auth_error)
476    }
477}