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