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        let response = Self::request_role_attestation_remote(request).await?;
177
178        match response {
179            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
180            _ => Err(Error::internal(
181                "invalid root response type for role attestation request",
182            )),
183        }
184    }
185
186    /// Request a method-scoped internal invocation proof from root over RPC.
187    pub async fn request_internal_invocation_proof(
188        request: InternalInvocationProofRequest,
189    ) -> Result<SignedInternalInvocationProofV1, Error> {
190        let request = metadata::with_internal_invocation_proof_request_metadata(request);
191        let response = Self::request_internal_invocation_proof_remote(request).await?;
192
193        match response {
194            RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
195            _ => Err(Error::internal(
196                "invalid root response type for internal invocation proof request",
197            )),
198        }
199    }
200
201    /// Return the current root role-attestation key set.
202    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
203        AuthOps::attestation_key_set()
204            .await
205            .map_err(Self::map_auth_error)
206    }
207
208    /// Publish root auth material into subnet state and warm root-owned keys once.
209    pub async fn publish_root_auth_material() -> Result<(), Error> {
210        EnvOps::require_root().map_err(Error::from)?;
211        AuthOps::publish_root_auth_material().await.map_err(|err| {
212            log!(
213                Topic::Auth,
214                Warn,
215                "root auth material publish failed: {err}"
216            );
217            Self::map_auth_error(err)
218        })
219    }
220
221    /// Replace the verifier-local role-attestation key set.
222    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
223        AuthOps::replace_attestation_key_set(key_set);
224    }
225
226    /// Verify a role attestation, refreshing root keys once on unknown key.
227    pub async fn verify_role_attestation(
228        attestation: &SignedRoleAttestation,
229        min_accepted_epoch: u64,
230    ) -> Result<(), Error> {
231        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
232            .map_err(Error::from)?
233            .min_accepted_epoch_by_role
234            .get(attestation.payload.role.as_str())
235            .copied();
236        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
237            min_accepted_epoch,
238            configured_min_accepted_epoch,
239        );
240
241        let caller = IcOps::msg_caller();
242        let self_pid = IcOps::canister_self();
243        let now_secs = IcOps::now_secs();
244        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
245        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
246
247        let verify = || {
248            AuthOps::verify_role_attestation_cached(
249                attestation,
250                caller,
251                self_pid,
252                verifier_subnet,
253                now_secs,
254                min_accepted_epoch,
255            )
256            .map(|_| ())
257        };
258        let refresh = || async {
259            let key_set: AttestationKeySet =
260                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
261            AuthOps::replace_attestation_key_set(key_set);
262            Ok(())
263        };
264
265        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
266            Ok(()) => Ok(()),
267            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
268                verify_flow::record_attestation_verifier_rejection(&err);
269                verify_flow::log_attestation_verifier_rejection(
270                    &err,
271                    attestation,
272                    caller,
273                    self_pid,
274                    "cached",
275                );
276                Err(Self::map_auth_error(err.into()))
277            }
278            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
279                verify_flow::record_attestation_verifier_rejection(&trigger);
280                verify_flow::log_attestation_verifier_rejection(
281                    &trigger,
282                    attestation,
283                    caller,
284                    self_pid,
285                    "cache_miss_refresh",
286                );
287                record_attestation_refresh_failed();
288                log!(
289                    Topic::Auth,
290                    Warn,
291                    "role attestation refresh failed local={} caller={} key_id={} error={}",
292                    self_pid,
293                    caller,
294                    attestation.key_id,
295                    source
296                );
297                Err(Self::map_auth_error(source))
298            }
299            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
300                verify_flow::record_attestation_verifier_rejection(&err);
301                verify_flow::log_attestation_verifier_rejection(
302                    &err,
303                    attestation,
304                    caller,
305                    self_pid,
306                    "post_refresh",
307                );
308                Err(Self::map_auth_error(err.into()))
309            }
310        }
311    }
312
313    /// Verify a root-signed, method-scoped internal invocation proof for this endpoint.
314    pub async fn verify_internal_invocation_proof(
315        proof: &SignedInternalInvocationProofV1,
316        target_method: &str,
317        accepted_roles: &[CanisterRole],
318    ) -> Result<(), Error> {
319        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
320            .map_err(Error::from)?
321            .min_accepted_epoch_by_role
322            .get(proof.payload.role.as_str())
323            .copied();
324        let min_accepted_epoch =
325            verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
326
327        let caller = IcOps::msg_caller();
328        let self_pid = IcOps::canister_self();
329        let now_secs = IcOps::now_secs();
330        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
331        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
332
333        let verify = || {
334            AuthOps::verify_internal_invocation_proof_cached(
335                proof,
336                crate::ops::auth::InternalInvocationProofVerificationInput {
337                    caller,
338                    self_pid,
339                    target_method,
340                    accepted_roles,
341                    verifier_subnet,
342                    now_secs,
343                    min_accepted_epoch,
344                },
345            )
346            .map(|_| ())
347        };
348        let refresh = || async {
349            let key_set: AttestationKeySet =
350                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
351            AuthOps::replace_attestation_key_set(key_set);
352            Ok(())
353        };
354
355        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
356            Ok(()) => Ok(()),
357            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
358                verify_flow::record_attestation_verifier_rejection(&err);
359                log!(
360                    Topic::Auth,
361                    Warn,
362                    "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
363                    self_pid,
364                    caller,
365                    proof.payload.subject,
366                    proof.payload.role,
367                    proof.key_id,
368                    proof.payload.audience,
369                    proof.payload.audience_method,
370                    proof.payload.epoch,
371                    err
372                );
373                Err(Self::map_internal_invocation_verify_error(err))
374            }
375            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
376                verify_flow::record_attestation_verifier_rejection(&trigger);
377                record_attestation_refresh_failed();
378                log!(
379                    Topic::Auth,
380                    Warn,
381                    "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
382                    self_pid,
383                    caller,
384                    proof.key_id,
385                    source
386                );
387                Err(Self::map_auth_error(source))
388            }
389            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
390                verify_flow::record_attestation_verifier_rejection(&err);
391                log!(
392                    Topic::Auth,
393                    Warn,
394                    "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
395                    self_pid,
396                    caller,
397                    proof.payload.subject,
398                    proof.payload.role,
399                    proof.key_id,
400                    proof.payload.audience,
401                    proof.payload.audience_method,
402                    proof.payload.epoch,
403                    err
404                );
405                Err(Self::map_internal_invocation_verify_error(err))
406            }
407        }
408    }
409
410    // Resolve the root-owned TTL ceiling from delegated-token config.
411    fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
412        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
413        if !cfg.enabled {
414            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
415        }
416
417        Ok(cfg
418            .max_ttl_secs
419            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
420    }
421}
422
423impl AuthApi {
424    // Route a self-contained delegation proof request over RPC to root.
425    async fn request_delegation_remote(
426        request: DelegationProofIssueRequest,
427    ) -> Result<DelegationProof, Error> {
428        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
429        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
430            .await
431            .map_err(Self::map_auth_error)
432    }
433
434    // Execute one local root role-attestation request.
435    pub async fn request_role_attestation_root(
436        request: RoleAttestationRequest,
437    ) -> Result<SignedRoleAttestation, Error> {
438        let request = metadata::with_root_attestation_request_metadata(request);
439        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
440            .await
441            .map_err(Self::map_auth_error)?;
442
443        match response {
444            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
445            _ => Err(Error::internal(
446                "invalid root response type for role attestation request",
447            )),
448        }
449    }
450
451    // Execute one local root internal-invocation proof request.
452    pub async fn request_internal_invocation_proof_root(
453        request: InternalInvocationProofRequest,
454    ) -> Result<SignedInternalInvocationProofV1, Error> {
455        let request = metadata::with_internal_invocation_proof_request_metadata(request);
456        let response =
457            RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
458                .await
459                .map_err(Self::map_auth_error)?;
460
461        match response {
462            RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
463            _ => Err(Error::internal(
464                "invalid root response type for internal invocation proof request",
465            )),
466        }
467    }
468
469    // Route a canonical role-attestation request over RPC to root.
470    async fn request_role_attestation_remote(
471        request: RoleAttestationRequest,
472    ) -> Result<RootCapabilityResponse, Error> {
473        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
474        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
475            .await
476            .map_err(Self::map_auth_error)
477    }
478
479    // Route a canonical internal-invocation proof request over RPC to root.
480    async fn request_internal_invocation_proof_remote(
481        request: InternalInvocationProofRequest,
482    ) -> Result<RootCapabilityResponse, Error> {
483        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
484        RpcOps::call_rpc_result(
485            root_pid,
486            protocol::CANIC_REQUEST_INTERNAL_INVOCATION_PROOF,
487            request,
488        )
489        .await
490        .map_err(Self::map_auth_error)
491    }
492}