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        runtime::env::EnvOps,
25        runtime::metrics::auth::record_attestation_refresh_failed,
26    },
27    workflow::rpc::request::handler::RootResponseWorkflow,
28};
29use root_client::RootAuthMaterialClient;
30
31// Internal auth pipeline:
32// - `session` owns delegated-session ingress and replay/session state handling.
33// - `metadata` owns root request metadata construction.
34// - `verify_flow` owns verifier-side attestation refresh behavior.
35mod metadata;
36mod root_client;
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 = RootAuthMaterialClient::new(root_pid)
246                .attestation_key_set()
247                .await?;
248            AuthOps::replace_attestation_key_set(key_set);
249            Ok(())
250        };
251
252        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
253            Ok(()) => Ok(()),
254            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
255                verify_flow::record_attestation_verifier_rejection(&err);
256                verify_flow::log_attestation_verifier_rejection(
257                    &err,
258                    attestation,
259                    caller,
260                    self_pid,
261                    "cached",
262                );
263                Err(Self::map_auth_error(err.into()))
264            }
265            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
266                verify_flow::record_attestation_verifier_rejection(&trigger);
267                verify_flow::log_attestation_verifier_rejection(
268                    &trigger,
269                    attestation,
270                    caller,
271                    self_pid,
272                    "cache_miss_refresh",
273                );
274                record_attestation_refresh_failed();
275                log!(
276                    Topic::Auth,
277                    Warn,
278                    "role attestation refresh failed local={} caller={} key_id={} error={}",
279                    self_pid,
280                    caller,
281                    attestation.key_id,
282                    source
283                );
284                Err(Self::map_auth_error(source))
285            }
286            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
287                verify_flow::record_attestation_verifier_rejection(&err);
288                verify_flow::log_attestation_verifier_rejection(
289                    &err,
290                    attestation,
291                    caller,
292                    self_pid,
293                    "post_refresh",
294                );
295                Err(Self::map_auth_error(err.into()))
296            }
297        }
298    }
299
300    /// Verify a root-signed, method-scoped internal invocation proof for this endpoint.
301    pub async fn verify_internal_invocation_proof(
302        proof: &SignedInternalInvocationProofV1,
303        target_method: &str,
304        accepted_roles: &[CanisterRole],
305    ) -> Result<(), Error> {
306        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
307            .map_err(Error::from)?
308            .min_accepted_epoch_by_role
309            .get(proof.payload.role.as_str())
310            .copied();
311        let min_accepted_epoch =
312            verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
313
314        let caller = IcOps::msg_caller();
315        let self_pid = IcOps::canister_self();
316        let now_secs = IcOps::now_secs();
317        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
318        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
319
320        let verify = || {
321            AuthOps::verify_internal_invocation_proof_cached(
322                proof,
323                crate::ops::auth::InternalInvocationProofVerificationInput {
324                    caller,
325                    self_pid,
326                    target_method,
327                    accepted_roles,
328                    verifier_subnet,
329                    now_secs,
330                    min_accepted_epoch,
331                },
332            )
333            .map(|_| ())
334        };
335        let refresh = || async {
336            let key_set = RootAuthMaterialClient::new(root_pid)
337                .attestation_key_set()
338                .await?;
339            AuthOps::replace_attestation_key_set(key_set);
340            Ok(())
341        };
342
343        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
344            Ok(()) => Ok(()),
345            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
346                verify_flow::record_attestation_verifier_rejection(&err);
347                log!(
348                    Topic::Auth,
349                    Warn,
350                    "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
351                    self_pid,
352                    caller,
353                    proof.payload.subject,
354                    proof.payload.role,
355                    proof.key_id,
356                    proof.payload.audience,
357                    proof.payload.audience_method,
358                    proof.payload.epoch,
359                    err
360                );
361                Err(Self::map_internal_invocation_verify_error(err))
362            }
363            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
364                verify_flow::record_attestation_verifier_rejection(&trigger);
365                record_attestation_refresh_failed();
366                log!(
367                    Topic::Auth,
368                    Warn,
369                    "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
370                    self_pid,
371                    caller,
372                    proof.key_id,
373                    source
374                );
375                Err(Self::map_auth_error(source))
376            }
377            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
378                verify_flow::record_attestation_verifier_rejection(&err);
379                log!(
380                    Topic::Auth,
381                    Warn,
382                    "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
383                    self_pid,
384                    caller,
385                    proof.payload.subject,
386                    proof.payload.role,
387                    proof.key_id,
388                    proof.payload.audience,
389                    proof.payload.audience_method,
390                    proof.payload.epoch,
391                    err
392                );
393                Err(Self::map_internal_invocation_verify_error(err))
394            }
395        }
396    }
397
398    // Resolve the root-owned TTL ceiling from delegated-token config.
399    fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
400        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
401        if !cfg.enabled {
402            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
403        }
404
405        Ok(cfg
406            .max_ttl_secs
407            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
408    }
409}
410
411impl AuthApi {
412    // Route a self-contained delegation proof request over RPC to root.
413    async fn request_delegation_remote(
414        request: DelegationProofIssueRequest,
415    ) -> Result<DelegationProof, Error> {
416        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
417        RootAuthMaterialClient::new(root_pid)
418            .request_delegation(request)
419            .await
420            .map_err(Self::map_auth_error)
421    }
422
423    // Execute one local root role-attestation request.
424    pub async fn request_role_attestation_root(
425        request: RoleAttestationRequest,
426    ) -> Result<SignedRoleAttestation, Error> {
427        let request = metadata::with_root_attestation_request_metadata(request);
428        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
429            .await
430            .map_err(Self::map_auth_error)?;
431
432        match response {
433            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
434            _ => Err(Error::internal(
435                "invalid root response type for role attestation request",
436            )),
437        }
438    }
439
440    // Execute one local root internal-invocation proof request.
441    pub async fn request_internal_invocation_proof_root(
442        request: InternalInvocationProofRequest,
443    ) -> Result<SignedInternalInvocationProofV1, Error> {
444        let request = metadata::with_internal_invocation_proof_request_metadata(request);
445        let response =
446            RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
447                .await
448                .map_err(Self::map_auth_error)?;
449
450        match response {
451            RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
452            _ => Err(Error::internal(
453                "invalid root response type for internal invocation proof request",
454            )),
455        }
456    }
457
458    // Route a canonical role-attestation request over RPC to root.
459    async fn request_role_attestation_remote(
460        request: RoleAttestationRequest,
461    ) -> Result<SignedRoleAttestation, Error> {
462        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
463        RootAuthMaterialClient::new(root_pid)
464            .request_role_attestation(request)
465            .await
466            .map_err(Self::map_auth_error)
467    }
468
469    // Route a canonical internal-invocation proof request over RPC to root.
470    async fn request_internal_invocation_proof_remote(
471        request: InternalInvocationProofRequest,
472    ) -> Result<SignedInternalInvocationProofV1, Error> {
473        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
474        RootAuthMaterialClient::new(root_pid)
475            .request_internal_invocation_proof(request)
476            .await
477            .map_err(Self::map_auth_error)
478    }
479}