Skip to main content

canic_core/api/auth/
mod.rs

1use crate::{
2    dto::{
3        auth::{
4            AttestationKeySet, DelegatedTokenIssueRequestV2, DelegatedTokenMintRequestV2,
5            DelegatedTokenV2, DelegationProofIssueRequestV2, DelegationProofV2,
6            RoleAttestationRequest, SignedRoleAttestation,
7        },
8        error::Error,
9        rpc::{Request as RootRequest, Response as RootCapabilityResponse},
10    },
11    error::InternalErrorClass,
12    log,
13    log::Topic,
14    ops::{
15        auth::{
16            DelegatedTokenOps, SignDelegatedTokenV2Input, SignDelegationProofV2Input,
17            VerifyDelegatedTokenV2RuntimeInput,
18        },
19        config::ConfigOps,
20        ic::IcOps,
21        rpc::RpcOps,
22        runtime::env::EnvOps,
23        runtime::metrics::auth::record_attestation_refresh_failed,
24    },
25    protocol,
26    workflow::rpc::request::handler::RootResponseWorkflow,
27};
28
29// Internal auth pipeline:
30// - `session` owns delegated-session ingress and replay/session state handling.
31// - `metadata` owns root request metadata construction.
32// - `verify_flow` owns verifier-side attestation refresh behavior.
33mod metadata;
34mod session;
35mod verify_flow;
36
37///
38/// DelegationApi
39///
40/// Requires auth.delegated_tokens.enabled = true in config.
41///
42
43pub struct DelegationApi;
44
45impl DelegationApi {
46    const DELEGATED_TOKENS_DISABLED: &str =
47        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
48    const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
49    const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
50        b"canic-session-bootstrap-token-fingerprint:v2";
51
52    // Map internal auth failures onto public endpoint errors.
53    fn map_delegation_error(err: crate::InternalError) -> Error {
54        match err.class() {
55            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
56                Error::internal(err.to_string())
57            }
58            _ => Error::from(err),
59        }
60    }
61
62    /// Resolve the local shard public key in SEC1 encoding.
63    pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
64        DelegatedTokenOps::local_shard_public_key_sec1(IcOps::canister_self())
65            .await
66            .map_err(Self::map_delegation_error)
67    }
68
69    /// Issue a delegated token from an explicit self-contained proof.
70    pub async fn issue_token(
71        request: DelegatedTokenIssueRequestV2,
72    ) -> Result<DelegatedTokenV2, Error> {
73        DelegatedTokenOps::sign_token_v2(SignDelegatedTokenV2Input {
74            proof: request.proof,
75            subject: request.subject,
76            audience: request.aud,
77            scopes: request.scopes,
78            ttl_secs: request.ttl_secs,
79            nonce: request.nonce,
80        })
81        .await
82        .map_err(Self::map_delegation_error)
83    }
84
85    /// Request a root proof, then issue a self-contained delegated token.
86    pub async fn mint_token(
87        request: DelegatedTokenMintRequestV2,
88    ) -> Result<DelegatedTokenV2, Error> {
89        let proof = Self::request_delegation(DelegationProofIssueRequestV2 {
90            shard_pid: IcOps::canister_self(),
91            scopes: request.scopes.clone(),
92            aud: request.aud.clone(),
93            cert_ttl_secs: request.cert_ttl_secs,
94            root_key_cert: request.root_key_cert,
95        })
96        .await?;
97
98        Self::issue_token(DelegatedTokenIssueRequestV2 {
99            proof,
100            subject: request.subject,
101            aud: request.aud,
102            scopes: request.scopes,
103            ttl_secs: request.token_ttl_secs,
104            nonce: request.nonce,
105        })
106        .await
107    }
108
109    /// Backwards-compatible Rust helper name for callers already moved to V2 DTOs.
110    pub async fn mint_token_v2(
111        request: DelegatedTokenMintRequestV2,
112    ) -> Result<DelegatedTokenV2, Error> {
113        Self::mint_token(request).await
114    }
115
116    /// Full delegated token verification without verifier-local proof lookup.
117    pub fn verify_token(
118        token: &DelegatedTokenV2,
119        max_cert_ttl_secs: u64,
120        max_token_ttl_secs: u64,
121        required_scopes: &[String],
122        now_secs: u64,
123    ) -> Result<(), Error> {
124        DelegatedTokenOps::verify_token_v2(VerifyDelegatedTokenV2RuntimeInput {
125            token,
126            max_cert_ttl_secs,
127            max_token_ttl_secs,
128            required_scopes,
129            now_secs,
130        })
131        .map(|_| ())
132        .map_err(Self::map_delegation_error)
133    }
134
135    /// Request a self-contained delegation proof from root over RPC.
136    pub async fn request_delegation(
137        request: DelegationProofIssueRequestV2,
138    ) -> Result<DelegationProofV2, Error> {
139        Self::request_delegation_remote(request).await
140    }
141
142    /// Backwards-compatible Rust helper name for callers already moved to V2 DTOs.
143    pub async fn request_delegation_v2(
144        request: DelegationProofIssueRequestV2,
145    ) -> Result<DelegationProofV2, Error> {
146        Self::request_delegation(request).await
147    }
148
149    /// Issue a self-contained delegation proof from the local root.
150    pub async fn issue_delegation_proof(
151        request: DelegationProofIssueRequestV2,
152    ) -> Result<DelegationProofV2, Error> {
153        EnvOps::require_root().map_err(Error::from)?;
154        let max_cert_ttl_secs = Self::delegated_auth_max_ttl_secs()?;
155        let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
156        DelegatedTokenOps::sign_delegation_proof_v2(SignDelegationProofV2Input {
157            audience: request.aud,
158            scopes: request.scopes,
159            shard_pid: request.shard_pid,
160            cert_ttl_secs: request.cert_ttl_secs,
161            max_token_ttl_secs,
162            max_cert_ttl_secs,
163            issued_at: IcOps::now_secs(),
164            root_key_cert: request.root_key_cert,
165        })
166        .await
167        .map_err(Self::map_delegation_error)
168    }
169
170    /// Backwards-compatible Rust helper name for callers already moved to V2 DTOs.
171    pub async fn issue_delegation_proof_v2(
172        request: DelegationProofIssueRequestV2,
173    ) -> Result<DelegationProofV2, Error> {
174        Self::issue_delegation_proof(request).await
175    }
176
177    /// Request a signed role attestation from root over RPC.
178    pub async fn request_role_attestation(
179        request: RoleAttestationRequest,
180    ) -> Result<SignedRoleAttestation, Error> {
181        let request = metadata::with_root_attestation_request_metadata(request);
182        let response = Self::request_role_attestation_remote(request).await?;
183
184        match response {
185            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
186            _ => Err(Error::internal(
187                "invalid root response type for role attestation request",
188            )),
189        }
190    }
191
192    /// Return the current root role-attestation key set.
193    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
194        DelegatedTokenOps::attestation_key_set()
195            .await
196            .map_err(Self::map_delegation_error)
197    }
198
199    /// Warm the root delegation and attestation key caches once.
200    pub async fn prewarm_root_key_material() -> Result<(), Error> {
201        EnvOps::require_root().map_err(Error::from)?;
202        DelegatedTokenOps::prewarm_root_key_material()
203            .await
204            .map_err(|err| {
205                log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
206                Self::map_delegation_error(err)
207            })
208    }
209
210    /// Replace the verifier-local role-attestation key set.
211    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
212        DelegatedTokenOps::replace_attestation_key_set(key_set);
213    }
214
215    /// Verify a role attestation, refreshing root keys once on unknown key.
216    pub async fn verify_role_attestation(
217        attestation: &SignedRoleAttestation,
218        min_accepted_epoch: u64,
219    ) -> Result<(), Error> {
220        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
221            .map_err(Error::from)?
222            .min_accepted_epoch_by_role
223            .get(attestation.payload.role.as_str())
224            .copied();
225        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
226            min_accepted_epoch,
227            configured_min_accepted_epoch,
228        );
229
230        let caller = IcOps::msg_caller();
231        let self_pid = IcOps::canister_self();
232        let now_secs = IcOps::now_secs();
233        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
234        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
235
236        let verify = || {
237            DelegatedTokenOps::verify_role_attestation_cached(
238                attestation,
239                caller,
240                self_pid,
241                verifier_subnet,
242                now_secs,
243                min_accepted_epoch,
244            )
245            .map(|_| ())
246        };
247        let refresh = || async {
248            let key_set: AttestationKeySet =
249                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
250            DelegatedTokenOps::replace_attestation_key_set(key_set);
251            Ok(())
252        };
253
254        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
255            Ok(()) => Ok(()),
256            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
257                verify_flow::record_attestation_verifier_rejection(&err);
258                verify_flow::log_attestation_verifier_rejection(
259                    &err,
260                    attestation,
261                    caller,
262                    self_pid,
263                    "cached",
264                );
265                Err(Self::map_delegation_error(err.into()))
266            }
267            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
268                verify_flow::record_attestation_verifier_rejection(&trigger);
269                verify_flow::log_attestation_verifier_rejection(
270                    &trigger,
271                    attestation,
272                    caller,
273                    self_pid,
274                    "cache_miss_refresh",
275                );
276                record_attestation_refresh_failed();
277                log!(
278                    Topic::Auth,
279                    Warn,
280                    "role attestation refresh failed local={} caller={} key_id={} error={}",
281                    self_pid,
282                    caller,
283                    attestation.key_id,
284                    source
285                );
286                Err(Self::map_delegation_error(source))
287            }
288            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
289                verify_flow::record_attestation_verifier_rejection(&err);
290                verify_flow::log_attestation_verifier_rejection(
291                    &err,
292                    attestation,
293                    caller,
294                    self_pid,
295                    "post_refresh",
296                );
297                Err(Self::map_delegation_error(err.into()))
298            }
299        }
300    }
301
302    // Resolve the root-owned TTL ceiling from delegated-token config.
303    fn delegated_auth_max_ttl_secs() -> Result<u64, Error> {
304        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
305        if !cfg.enabled {
306            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
307        }
308
309        Ok(cfg
310            .max_ttl_secs
311            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
312    }
313}
314
315impl DelegationApi {
316    // Route a self-contained delegation proof request over RPC to root.
317    async fn request_delegation_remote(
318        request: DelegationProofIssueRequestV2,
319    ) -> Result<DelegationProofV2, Error> {
320        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
321        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION_V2, request)
322            .await
323            .map_err(Self::map_delegation_error)
324    }
325
326    // Execute one local root role-attestation request.
327    pub async fn request_role_attestation_root(
328        request: RoleAttestationRequest,
329    ) -> Result<SignedRoleAttestation, Error> {
330        let request = metadata::with_root_attestation_request_metadata(request);
331        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
332            .await
333            .map_err(Self::map_delegation_error)?;
334
335        match response {
336            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
337            _ => Err(Error::internal(
338                "invalid root response type for role attestation request",
339            )),
340        }
341    }
342
343    // Route a canonical role-attestation request over RPC to root.
344    async fn request_role_attestation_remote(
345        request: RoleAttestationRequest,
346    ) -> Result<RootCapabilityResponse, Error> {
347        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
348        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
349            .await
350            .map_err(Self::map_delegation_error)
351    }
352}