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