Skip to main content

canic_core/api/
auth.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        auth::{
5            DelegatedToken, DelegatedTokenClaims, DelegationCert, DelegationProof,
6            DelegationProvisionRequest, DelegationProvisionResponse, DelegationProvisionTargetKind,
7            DelegationRequest,
8        },
9        error::Error,
10    },
11    error::InternalErrorClass,
12    log,
13    log::Topic,
14    ops::{
15        auth::DelegatedTokenOps,
16        config::ConfigOps,
17        ic::IcOps,
18        runtime::env::EnvOps,
19        runtime::metrics::auth::record_signer_mint_without_proof,
20        storage::{auth::DelegationStateOps, registry::subnet::SubnetRegistryOps},
21    },
22    workflow::auth::DelegationWorkflow,
23};
24
25///
26/// DelegationApi
27///
28/// Requires auth.delegated_tokens.enabled = true in config.
29///
30
31pub struct DelegationApi;
32
33impl DelegationApi {
34    const DELEGATED_TOKENS_DISABLED: &str =
35        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
36
37    fn map_delegation_error(err: crate::InternalError) -> Error {
38        match err.class() {
39            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
40                Error::internal(err.to_string())
41            }
42            _ => Error::from(err),
43        }
44    }
45
46    /// Full delegation proof verification (structure + signature).
47    ///
48    /// Purely local verification; does not read certified data or require a
49    /// query context.
50    pub fn verify_delegation_proof(
51        proof: &DelegationProof,
52        authority_pid: Principal,
53    ) -> Result<(), Error> {
54        DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
55            .map_err(Self::map_delegation_error)
56    }
57
58    pub fn prepare_delegation_cert_signature(cert: &DelegationCert) -> Result<(), Error> {
59        DelegatedTokenOps::prepare_delegation_cert_signature(cert)
60            .map_err(Self::map_delegation_error)
61    }
62
63    pub fn get_delegation_cert_signature(cert: DelegationCert) -> Result<DelegationProof, Error> {
64        DelegatedTokenOps::get_delegation_cert_signature(cert).map_err(Self::map_delegation_error)
65    }
66
67    pub fn prepare_token_signature(
68        token_version: u16,
69        claims: &DelegatedTokenClaims,
70        proof: &DelegationProof,
71    ) -> Result<(), Error> {
72        DelegatedTokenOps::prepare_token_signature(token_version, claims, proof)
73            .map_err(Self::map_delegation_error)
74    }
75
76    pub fn get_token_signature(
77        token_version: u16,
78        claims: DelegatedTokenClaims,
79        proof: DelegationProof,
80    ) -> Result<DelegatedToken, Error> {
81        DelegatedTokenOps::get_token_signature(token_version, claims, proof)
82            .map_err(Self::map_delegation_error)
83    }
84
85    pub fn issue_token_prepare(
86        token_version: u16,
87        claims: DelegatedTokenClaims,
88    ) -> Result<(), Error> {
89        let proof = Self::require_proof()?;
90        DelegationWorkflow::issue_token_prepare(token_version, claims, proof)
91            .map_err(Self::map_delegation_error)
92    }
93
94    pub fn issue_token_get() -> Result<DelegatedToken, Error> {
95        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
96        if !cfg.enabled {
97            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
98        }
99
100        DelegationWorkflow::issue_token_get().map_err(Self::map_delegation_error)
101    }
102
103    /// Full delegated token verification (structure + signature).
104    ///
105    /// Purely local verification; does not read certified data or require a
106    /// query context.
107    pub fn verify_token(
108        token: &DelegatedToken,
109        authority_pid: Principal,
110        now_secs: u64,
111    ) -> Result<(), Error> {
112        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
113            .map(|_| ())
114            .map_err(Self::map_delegation_error)
115    }
116
117    /// Verify a delegated token and return verified contents.
118    ///
119    /// This is intended for application-layer session construction.
120    /// It performs full verification and returns verified claims and cert.
121    pub fn verify_token_verified(
122        token: &DelegatedToken,
123        authority_pid: Principal,
124        now_secs: u64,
125    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
126        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
127            .map(|verified| (verified.claims, verified.cert))
128            .map_err(Self::map_delegation_error)
129    }
130
131    /// admin-only delegation provisioning (root-only escape hatch).
132    ///
133    /// Not part of canonical delegation flow.
134    /// Used for tests / tooling due to PocketIC limitations.
135    ///
136    /// Root does not infer targets; callers must supply them.
137    pub fn provision_prepare(request: DelegationProvisionRequest) -> Result<(), Error> {
138        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
139        if !cfg.enabled {
140            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
141        }
142
143        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
144        let caller = IcOps::msg_caller();
145        if caller != root_pid {
146            return Err(Error::forbidden(
147                "delegation provision requires root caller",
148            ));
149        }
150
151        validate_issuance_policy(&request.cert)?;
152        log!(
153            Topic::Auth,
154            Info,
155            "delegation provision prepare signer={} signer_targets={:?} verifier_targets={:?}",
156            request.cert.signer_pid,
157            request.signer_targets,
158            request.verifier_targets
159        );
160
161        DelegationWorkflow::provision_prepare(request, false).map_err(Self::map_delegation_error)
162    }
163
164    pub fn provision_get() -> Result<DelegationProof, Error> {
165        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
166        if !cfg.enabled {
167            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
168        }
169
170        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
171        let caller = IcOps::msg_caller();
172        if caller != root_pid {
173            return Err(Error::forbidden(
174                "delegation provision requires root caller",
175            ));
176        }
177
178        DelegationWorkflow::provision_get().map_err(Self::map_delegation_error)
179    }
180
181    pub async fn provision_finalize(
182        proof: DelegationProof,
183    ) -> Result<DelegationProvisionResponse, Error> {
184        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
185        if !cfg.enabled {
186            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
187        }
188
189        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
190        let caller = IcOps::msg_caller();
191        if caller != root_pid {
192            return Err(Error::forbidden(
193                "delegation provision requires root caller",
194            ));
195        }
196
197        DelegationWorkflow::provision_finalize(proof)
198            .await
199            .map_err(Self::map_delegation_error)
200    }
201
202    /// Canonical signer-initiated delegation request (user_shard -> root).
203    ///
204    /// Caller must match signer_pid and be registered to the subnet.
205    pub fn request_delegation_prepare(request: DelegationRequest) -> Result<(), Error> {
206        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
207        if !cfg.enabled {
208            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
209        }
210
211        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
212        if root_pid != IcOps::canister_self() {
213            return Err(Error::forbidden("delegation request must target root"));
214        }
215
216        let caller = IcOps::msg_caller();
217        if caller != request.signer_pid {
218            return Err(Error::forbidden(
219                "delegation request signer must match caller",
220            ));
221        }
222
223        if request.ttl_secs == 0 {
224            return Err(Error::invalid(
225                "delegation ttl_secs must be greater than zero",
226            ));
227        }
228
229        let now_secs = IcOps::now_secs();
230        let cert = DelegationCert {
231            v: 1,
232            signer_pid: request.signer_pid,
233            audiences: request.audiences,
234            scopes: request.scopes,
235            issued_at: now_secs,
236            expires_at: now_secs.saturating_add(request.ttl_secs),
237        };
238
239        validate_issuance_policy(&cert)?;
240
241        DelegationWorkflow::provision_prepare(
242            DelegationProvisionRequest {
243                cert,
244                signer_targets: vec![caller],
245                verifier_targets: request.verifier_targets,
246            },
247            request.include_root_verifier,
248        )
249        .map_err(Self::map_delegation_error)
250    }
251
252    pub fn request_delegation_get() -> Result<DelegationProof, Error> {
253        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
254        if !cfg.enabled {
255            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
256        }
257
258        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
259        if root_pid != IcOps::canister_self() {
260            return Err(Error::forbidden("delegation request must target root"));
261        }
262
263        DelegationWorkflow::provision_get().map_err(Self::map_delegation_error)
264    }
265
266    pub async fn request_delegation_finalize(
267        proof: DelegationProof,
268    ) -> Result<DelegationProvisionResponse, Error> {
269        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
270        if !cfg.enabled {
271            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
272        }
273
274        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
275        if root_pid != IcOps::canister_self() {
276            return Err(Error::forbidden("delegation request must target root"));
277        }
278
279        DelegationWorkflow::provision_finalize(proof)
280            .await
281            .map_err(Self::map_delegation_error)
282    }
283
284    pub fn store_proof(
285        proof: DelegationProof,
286        kind: DelegationProvisionTargetKind,
287    ) -> Result<(), Error> {
288        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
289        if !cfg.enabled {
290            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
291        }
292
293        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
294        let caller = IcOps::msg_caller();
295        if caller != root_pid {
296            return Err(Error::forbidden(
297                "delegation proof store requires root caller",
298            ));
299        }
300
301        if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
302            let local = IcOps::canister_self();
303            log!(
304                Topic::Auth,
305                Warn,
306                "delegation proof rejected kind={:?} local={} signer={} issued_at={} expires_at={} error={}",
307                kind,
308                local,
309                proof.cert.signer_pid,
310                proof.cert.issued_at,
311                proof.cert.expires_at,
312                err
313            );
314            return Err(Self::map_delegation_error(err));
315        }
316
317        DelegationStateOps::set_proof_from_dto(proof);
318        let local = IcOps::canister_self();
319        let stored = DelegationStateOps::proof_dto()
320            .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
321        log!(
322            Topic::Auth,
323            Info,
324            "delegation proof stored kind={:?} local={} signer={} issued_at={} expires_at={}",
325            kind,
326            local,
327            stored.cert.signer_pid,
328            stored.cert.issued_at,
329            stored.cert.expires_at
330        );
331
332        Ok(())
333    }
334
335    pub fn require_proof() -> Result<DelegationProof, Error> {
336        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
337        if !cfg.enabled {
338            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
339        }
340
341        DelegationStateOps::proof_dto().ok_or_else(|| {
342            record_signer_mint_without_proof();
343            Error::not_found("delegation proof not set")
344        })
345    }
346}
347
348fn validate_issuance_policy(cert: &DelegationCert) -> Result<(), Error> {
349    if cert.expires_at <= cert.issued_at {
350        return Err(Error::invalid(
351            "delegation expires_at must be greater than issued_at",
352        ));
353    }
354
355    if cert.audiences.is_empty() {
356        return Err(Error::invalid("delegation audiences must not be empty"));
357    }
358
359    if cert.scopes.is_empty() {
360        return Err(Error::invalid("delegation scopes must not be empty"));
361    }
362
363    if cert.audiences.iter().any(String::is_empty) {
364        return Err(Error::invalid("delegation audience must not be empty"));
365    }
366
367    if cert.scopes.iter().any(String::is_empty) {
368        return Err(Error::invalid("delegation scope must not be empty"));
369    }
370
371    let root_pid = EnvOps::root_pid().map_err(Error::from)?;
372    if cert.signer_pid == root_pid {
373        return Err(Error::invalid("delegation signer must not be root"));
374    }
375
376    let record = SubnetRegistryOps::get(cert.signer_pid)
377        .ok_or_else(|| Error::invalid("delegation signer must be registered to subnet"))?;
378    if record.role.is_root() {
379        return Err(Error::invalid("delegation signer role must not be root"));
380    }
381
382    Ok(())
383}