Skip to main content

canic_core/api/auth/
mod.rs

1//! Module: api::auth
2//!
3//! Responsibility: expose auth endpoint helpers and auth boundary adapters.
4//! Does not own: stable auth records, proof verification internals, or runtime policy.
5//! Boundary: endpoint layer maps public DTOs into ops/workflow auth calls.
6
7use crate::{
8    cdk::types::Principal,
9    domain::policy::auth::{
10        RootDelegatedRoleGrantPolicy, RootDelegationAudiencePolicy, RootIssuerPolicy,
11    },
12    dto::{
13        auth::{
14            ActiveDelegationProofStatusResponse, DelegatedRoleGrant, DelegatedToken,
15            DelegatedTokenGetRequest, DelegatedTokenPrepareRequest, DelegatedTokenPrepareResponse,
16            DelegationAudience, InstallActiveDelegationProofRequest,
17            InstallActiveDelegationProofResponse, RoleAttestationGetRequest,
18            RoleAttestationPrepareResponse, RoleAttestationRequest,
19            RootDelegationProofBatchGetRequest, RootDelegationProofBatchGetResponse,
20            RootDelegationProofBatchInstallRequest, RootDelegationProofBatchInstallResponse,
21            RootDelegationProofBatchPrepareRequest, RootDelegationProofBatchPrepareResponse,
22            RootIssuerPolicyResponse, RootIssuerPolicyUpsertRequest, RootIssuerPolicyView,
23            SignedRoleAttestation,
24        },
25        error::Error,
26    },
27    error::InternalErrorClass,
28    ops::{
29        auth::{AuthOps, VerifyDelegatedTokenRuntimeInput},
30        config::ConfigOps,
31        ic::IcOps,
32        runtime::env::EnvOps,
33        storage::auth::AuthStateOps,
34    },
35    workflow::runtime::auth::RuntimeAuthWorkflow,
36};
37
38// Internal auth pipeline:
39// - `session` owns delegated-session ingress and replay/session state handling.
40mod session;
41
42///
43/// AuthApi
44///
45/// Owns delegated-token helpers and root-signed role-attestation helpers.
46/// Owned by the API layer and called by generated endpoint wrappers.
47///
48
49pub struct AuthApi;
50
51impl AuthApi {
52    const DELEGATED_TOKENS_DISABLED: &str =
53        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
54    const DELEGATED_TOKEN_ISSUER_DISABLED: &str = "delegated token issuer disabled for this canister; set subnets.<subnet>.canisters.<role>.auth.delegated_token_issuer=true in canic.toml";
55    const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
56    const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
57        b"canic-session-bootstrap-token-fingerprint";
58
59    // Map internal auth failures onto public endpoint errors.
60    fn map_auth_error(err: crate::InternalError) -> Error {
61        match err.class() {
62            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
63                Error::internal(err.to_string())
64            }
65            _ => Error::from(err),
66        }
67    }
68
69    fn require_delegated_token_issuer_enabled() -> Result<(), Error> {
70        let delegated_tokens_cfg =
71            ConfigOps::delegated_tokens_config().map_err(Self::map_auth_error)?;
72        if !delegated_tokens_cfg.enabled {
73            return Err(Error::invalid(Self::DELEGATED_TOKENS_DISABLED));
74        }
75
76        let canister_cfg = ConfigOps::current_canister().map_err(Self::map_auth_error)?;
77        if !canister_cfg.auth.delegated_token_issuer {
78            return Err(Error::forbidden(Self::DELEGATED_TOKEN_ISSUER_DISABLED));
79        }
80
81        Ok(())
82    }
83
84    // Verify delegated-token material and return the token subject.
85    //
86    // This is intentionally private: endpoint authorization must also bind the
87    // verified subject to the caller before dispatch.
88    fn verify_token_material(
89        token: &DelegatedToken,
90        max_cert_ttl_ns: u64,
91        max_token_ttl_ns: u64,
92        required_scopes: &[String],
93        now_ns: u64,
94    ) -> Result<Principal, Error> {
95        AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
96            token,
97            caller: IcOps::msg_caller(),
98            max_cert_ttl_ns,
99            max_token_ttl_ns,
100            required_scopes,
101            now_ns,
102        })
103        .map(|verified| verified.subject)
104        .map_err(Self::map_auth_error)
105    }
106
107    /// Prepare a delegated token from the issuer-local active delegation proof.
108    pub fn prepare_delegated_token(
109        request: DelegatedTokenPrepareRequest,
110    ) -> Result<DelegatedTokenPrepareResponse, Error> {
111        Self::require_delegated_token_issuer_enabled()?;
112        RuntimeAuthWorkflow::prepare_delegated_token(request).map_err(Self::map_auth_error)
113    }
114
115    /// Retrieve a prepared delegated token with its issuer canister-signature proof.
116    pub fn get_delegated_token(request: DelegatedTokenGetRequest) -> Result<DelegatedToken, Error> {
117        Self::require_delegated_token_issuer_enabled()?;
118
119        AuthOps::get_delegated_token_issuer_proof(request.claims_hash, IcOps::msg_caller())
120            .map_err(Self::map_auth_error)
121    }
122
123    /// Install validated root-certified delegation material for issuer-local token issuance.
124    pub fn install_active_delegation_proof(
125        request: InstallActiveDelegationProofRequest,
126    ) -> Result<InstallActiveDelegationProofResponse, Error> {
127        Self::require_delegated_token_issuer_enabled()?;
128
129        let active_proof =
130            AuthOps::install_active_delegation_proof(request.proof, IcOps::msg_caller())
131                .map_err(Self::map_auth_error)?;
132
133        Ok(InstallActiveDelegationProofResponse { active_proof })
134    }
135
136    /// Report non-secret issuer-local active proof lifecycle status for provisioners.
137    pub fn active_delegation_proof_status() -> Result<ActiveDelegationProofStatusResponse, Error> {
138        Self::require_delegated_token_issuer_enabled()?;
139        Ok(AuthOps::active_delegation_proof_status(IcOps::now_nanos()))
140    }
141
142    /// Upsert root issuer policy from the local root controller path.
143    pub fn upsert_root_issuer_policy_root(
144        request: RootIssuerPolicyUpsertRequest,
145    ) -> Result<RootIssuerPolicyResponse, Error> {
146        EnvOps::require_root().map_err(Error::from)?;
147        validate_root_issuer_policy_upsert_request(&request)?;
148
149        let policy = root_issuer_policy_from_request(request);
150        AuthStateOps::upsert_root_issuer_policy(policy.clone());
151
152        Ok(RootIssuerPolicyResponse {
153            issuer: root_issuer_policy_view(&policy),
154        })
155    }
156
157    /// Install root issuer policy in explicit delegation-material test builds.
158    #[cfg(canic_test_delegation_material)]
159    pub fn test_upsert_root_issuer_policy(
160        issuer_pid: Principal,
161        allowed_audiences: Vec<DelegationAudience>,
162        allowed_grants: Vec<DelegatedRoleGrant>,
163        max_cert_ttl_ns: u64,
164        refresh_after_ratio_bps: u16,
165    ) -> Result<(), Error> {
166        Self::upsert_root_issuer_policy_root(RootIssuerPolicyUpsertRequest {
167            issuer_pid,
168            enabled: true,
169            allowed_audiences,
170            allowed_grants,
171            max_cert_ttl_ns,
172            refresh_after_ratio_bps,
173        })
174        .map(|_| ())
175    }
176
177    /// Prepare root delegation proof batch metadata from the local root update path.
178    pub fn prepare_delegation_proof_batch_root(
179        request: RootDelegationProofBatchPrepareRequest,
180    ) -> Result<RootDelegationProofBatchPrepareResponse, Error> {
181        EnvOps::require_root().map_err(Error::from)?;
182        let max_cert_ttl_ns = Self::delegated_token_max_ttl_ns()?;
183        AuthOps::prepare_delegation_proof_batch(request, max_cert_ttl_ns, IcOps::now_nanos())
184            .map_err(Self::map_auth_error)
185    }
186
187    /// Retrieve root delegation proofs from the local direct root query path.
188    pub fn get_delegation_proof_batch_root(
189        request: RootDelegationProofBatchGetRequest,
190    ) -> Result<RootDelegationProofBatchGetResponse, Error> {
191        EnvOps::require_root().map_err(Error::from)?;
192        AuthOps::get_delegation_proof_batch(request).map_err(Self::map_auth_error)
193    }
194
195    /// Install retrieved root delegation proof batches from the local root update path.
196    pub async fn install_delegation_proof_batch_root(
197        request: RootDelegationProofBatchInstallRequest,
198    ) -> Result<RootDelegationProofBatchInstallResponse, Error> {
199        EnvOps::require_root().map_err(Error::from)?;
200        RuntimeAuthWorkflow::install_delegation_proof_batch_root(request)
201            .await
202            .map_err(Self::map_auth_error)
203    }
204
205    /// Prepare a root-certified role attestation from the local root update path.
206    pub fn prepare_role_attestation_root(
207        request: RoleAttestationRequest,
208    ) -> Result<RoleAttestationPrepareResponse, Error> {
209        RuntimeAuthWorkflow::prepare_role_attestation_root(request).map_err(Self::map_auth_error)
210    }
211
212    /// Retrieve a prepared role attestation with its root canister-signature proof.
213    pub fn get_role_attestation_root(
214        request: RoleAttestationGetRequest,
215    ) -> Result<SignedRoleAttestation, Error> {
216        EnvOps::require_root().map_err(Error::from)?;
217        AuthOps::get_role_attestation(IcOps::msg_caller(), request.payload_hash)
218            .map_err(Self::map_auth_error)
219    }
220
221    /// Verify a role attestation locally from its embedded root proof.
222    pub async fn verify_role_attestation(
223        attestation: &SignedRoleAttestation,
224        min_accepted_epoch: u64,
225    ) -> Result<(), Error> {
226        crate::workflow::runtime::auth::RuntimeAuthWorkflow::verify_role_attestation(
227            attestation,
228            min_accepted_epoch,
229        )
230        .await
231        .map_err(Self::map_auth_error)
232    }
233
234    // Resolve the delegated-token TTL ceiling for endpoint auth/session callers.
235    fn delegated_token_max_ttl_ns() -> Result<u64, Error> {
236        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
237        if !cfg.enabled {
238            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
239        }
240
241        let max_ttl_secs = cfg
242            .max_ttl_secs
243            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
244        max_ttl_secs.checked_mul(1_000_000_000).ok_or_else(|| {
245            Error::invalid("auth.delegated_tokens.max_ttl_secs overflows nanoseconds")
246        })
247    }
248}
249
250fn validate_root_issuer_policy_upsert_request(
251    request: &RootIssuerPolicyUpsertRequest,
252) -> Result<(), Error> {
253    if request.max_cert_ttl_ns == 0 {
254        return Err(Error::invalid(
255            "root issuer max certificate TTL must be greater than zero",
256        ));
257    }
258    if request.refresh_after_ratio_bps == 0 || request.refresh_after_ratio_bps >= 10_000 {
259        return Err(Error::invalid(
260            "root issuer refresh ratio must be between 1 and 9999 basis points",
261        ));
262    }
263    if request.enabled && request.allowed_audiences.is_empty() {
264        return Err(Error::invalid(
265            "enabled root issuer policy must allow at least one audience",
266        ));
267    }
268    if request.enabled && request.allowed_grants.is_empty() {
269        return Err(Error::invalid(
270            "enabled root issuer policy must allow at least one grant",
271        ));
272    }
273    Ok(())
274}
275
276fn root_issuer_policy_from_request(request: RootIssuerPolicyUpsertRequest) -> RootIssuerPolicy {
277    RootIssuerPolicy {
278        issuer_pid: request.issuer_pid,
279        enabled: request.enabled,
280        allowed_audiences: request
281            .allowed_audiences
282            .iter()
283            .map(root_delegation_audience_policy)
284            .collect(),
285        allowed_grants: request
286            .allowed_grants
287            .iter()
288            .map(root_delegated_role_grant_policy)
289            .collect(),
290        max_cert_ttl_ns: request.max_cert_ttl_ns,
291        refresh_after_ratio_bps: request.refresh_after_ratio_bps,
292    }
293}
294
295fn root_issuer_policy_view(policy: &RootIssuerPolicy) -> RootIssuerPolicyView {
296    RootIssuerPolicyView {
297        issuer_pid: policy.issuer_pid,
298        enabled: policy.enabled,
299        allowed_audiences: policy
300            .allowed_audiences
301            .iter()
302            .map(root_delegation_audience_view)
303            .collect(),
304        allowed_grants: policy
305            .allowed_grants
306            .iter()
307            .map(root_delegated_role_grant_view)
308            .collect(),
309        max_cert_ttl_ns: policy.max_cert_ttl_ns,
310        refresh_after_ratio_bps: policy.refresh_after_ratio_bps,
311    }
312}
313
314fn root_delegation_audience_policy(audience: &DelegationAudience) -> RootDelegationAudiencePolicy {
315    match audience {
316        DelegationAudience::Canister(canister) => RootDelegationAudiencePolicy::Canister(*canister),
317        DelegationAudience::CanicSubnet(subnet) => {
318            RootDelegationAudiencePolicy::CanicSubnet(*subnet)
319        }
320        DelegationAudience::Project(project) => {
321            RootDelegationAudiencePolicy::Project(project.clone())
322        }
323    }
324}
325
326fn root_delegated_role_grant_policy(grant: &DelegatedRoleGrant) -> RootDelegatedRoleGrantPolicy {
327    RootDelegatedRoleGrantPolicy {
328        target: grant.target.clone(),
329        scopes: grant.scopes.clone(),
330    }
331}
332
333fn root_delegation_audience_view(policy: &RootDelegationAudiencePolicy) -> DelegationAudience {
334    match policy {
335        RootDelegationAudiencePolicy::Canister(canister) => DelegationAudience::Canister(*canister),
336        RootDelegationAudiencePolicy::CanicSubnet(subnet) => {
337            DelegationAudience::CanicSubnet(*subnet)
338        }
339        RootDelegationAudiencePolicy::Project(project) => {
340            DelegationAudience::Project(project.clone())
341        }
342    }
343}
344
345fn root_delegated_role_grant_view(policy: &RootDelegatedRoleGrantPolicy) -> DelegatedRoleGrant {
346    DelegatedRoleGrant {
347        target: policy.target.clone(),
348        scopes: policy.scopes.clone(),
349    }
350}