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    /// Full delegated token verification (structure + signature).
86    ///
87    /// Purely local verification; does not read certified data or require a
88    /// query context.
89    pub fn verify_token(
90        token: &DelegatedToken,
91        authority_pid: Principal,
92        now_secs: u64,
93    ) -> Result<(), Error> {
94        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
95            .map(|_| ())
96            .map_err(Self::map_delegation_error)
97    }
98
99    /// Verify a delegated token and return verified contents.
100    ///
101    /// This is intended for application-layer session construction.
102    /// It performs full verification and returns verified claims and cert.
103    pub fn verify_token_verified(
104        token: &DelegatedToken,
105        authority_pid: Principal,
106        now_secs: u64,
107    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
108        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
109            .map(|verified| (verified.claims, verified.cert))
110            .map_err(Self::map_delegation_error)
111    }
112
113    /// admin-only delegation provisioning (root-only escape hatch).
114    ///
115    /// Not part of canonical delegation flow.
116    /// Used for tests / tooling due to PocketIC limitations.
117    ///
118    /// Root does not infer targets; callers must supply them.
119    pub fn provision_prepare(request: DelegationProvisionRequest) -> Result<(), Error> {
120        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
121        if !cfg.enabled {
122            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
123        }
124
125        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
126        let caller = IcOps::msg_caller();
127        if caller != root_pid {
128            return Err(Error::forbidden(
129                "delegation provision requires root caller",
130            ));
131        }
132
133        validate_issuance_policy(&request.cert)?;
134        log!(
135            Topic::Auth,
136            Info,
137            "delegation provision prepare signer={} signer_targets={:?} verifier_targets={:?}",
138            request.cert.signer_pid,
139            request.signer_targets,
140            request.verifier_targets
141        );
142
143        DelegationWorkflow::provision_prepare(request, false).map_err(Self::map_delegation_error)
144    }
145
146    pub fn provision_get() -> Result<DelegationProof, Error> {
147        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
148        if !cfg.enabled {
149            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
150        }
151
152        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
153        let caller = IcOps::msg_caller();
154        if caller != root_pid {
155            return Err(Error::forbidden(
156                "delegation provision requires root caller",
157            ));
158        }
159
160        DelegationWorkflow::provision_get().map_err(Self::map_delegation_error)
161    }
162
163    pub async fn provision_finalize(
164        proof: DelegationProof,
165    ) -> Result<DelegationProvisionResponse, Error> {
166        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
167        if !cfg.enabled {
168            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
169        }
170
171        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
172        let caller = IcOps::msg_caller();
173        if caller != root_pid {
174            return Err(Error::forbidden(
175                "delegation provision requires root caller",
176            ));
177        }
178
179        DelegationWorkflow::provision_finalize(proof)
180            .await
181            .map_err(Self::map_delegation_error)
182    }
183
184    /// Canonical signer-initiated delegation request (user_shard -> root).
185    ///
186    /// Caller must match signer_pid and be registered to the subnet.
187    pub fn request_delegation_prepare(request: DelegationRequest) -> Result<(), Error> {
188        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
189        if !cfg.enabled {
190            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
191        }
192
193        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
194        if root_pid != IcOps::canister_self() {
195            return Err(Error::forbidden("delegation request must target root"));
196        }
197
198        let caller = IcOps::msg_caller();
199        if caller != request.signer_pid {
200            return Err(Error::forbidden(
201                "delegation request signer must match caller",
202            ));
203        }
204
205        if request.ttl_secs == 0 {
206            return Err(Error::invalid(
207                "delegation ttl_secs must be greater than zero",
208            ));
209        }
210
211        let now_secs = IcOps::now_secs();
212        let cert = DelegationCert {
213            v: 1,
214            signer_pid: request.signer_pid,
215            audiences: request.audiences,
216            scopes: request.scopes,
217            issued_at: now_secs,
218            expires_at: now_secs.saturating_add(request.ttl_secs),
219        };
220
221        validate_issuance_policy(&cert)?;
222
223        DelegationWorkflow::provision_prepare(
224            DelegationProvisionRequest {
225                cert,
226                signer_targets: vec![caller],
227                verifier_targets: request.verifier_targets,
228            },
229            request.include_root_verifier,
230        )
231        .map_err(Self::map_delegation_error)
232    }
233
234    pub fn request_delegation_get() -> Result<DelegationProof, Error> {
235        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
236        if !cfg.enabled {
237            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
238        }
239
240        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
241        if root_pid != IcOps::canister_self() {
242            return Err(Error::forbidden("delegation request must target root"));
243        }
244
245        DelegationWorkflow::provision_get().map_err(Self::map_delegation_error)
246    }
247
248    pub async fn request_delegation_finalize(
249        proof: DelegationProof,
250    ) -> Result<DelegationProvisionResponse, Error> {
251        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
252        if !cfg.enabled {
253            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
254        }
255
256        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
257        if root_pid != IcOps::canister_self() {
258            return Err(Error::forbidden("delegation request must target root"));
259        }
260
261        DelegationWorkflow::provision_finalize(proof)
262            .await
263            .map_err(Self::map_delegation_error)
264    }
265
266    pub fn store_proof(
267        proof: DelegationProof,
268        kind: DelegationProvisionTargetKind,
269    ) -> Result<(), Error> {
270        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
271        if !cfg.enabled {
272            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
273        }
274
275        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
276        let caller = IcOps::msg_caller();
277        if caller != root_pid {
278            return Err(Error::forbidden(
279                "delegation proof store requires root caller",
280            ));
281        }
282
283        if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
284            let local = IcOps::canister_self();
285            log!(
286                Topic::Auth,
287                Warn,
288                "delegation proof rejected kind={:?} local={} signer={} issued_at={} expires_at={} error={}",
289                kind,
290                local,
291                proof.cert.signer_pid,
292                proof.cert.issued_at,
293                proof.cert.expires_at,
294                err
295            );
296            return Err(Self::map_delegation_error(err));
297        }
298
299        DelegationStateOps::set_proof_from_dto(proof);
300        let local = IcOps::canister_self();
301        let stored = DelegationStateOps::proof_dto()
302            .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
303        log!(
304            Topic::Auth,
305            Info,
306            "delegation proof stored kind={:?} local={} signer={} issued_at={} expires_at={}",
307            kind,
308            local,
309            stored.cert.signer_pid,
310            stored.cert.issued_at,
311            stored.cert.expires_at
312        );
313
314        Ok(())
315    }
316
317    pub fn require_proof() -> Result<DelegationProof, Error> {
318        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
319        if !cfg.enabled {
320            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
321        }
322
323        DelegationStateOps::proof_dto().ok_or_else(|| {
324            record_signer_mint_without_proof();
325            Error::not_found("delegation proof not set")
326        })
327    }
328}
329
330fn validate_issuance_policy(cert: &DelegationCert) -> Result<(), Error> {
331    if cert.expires_at <= cert.issued_at {
332        return Err(Error::invalid(
333            "delegation expires_at must be greater than issued_at",
334        ));
335    }
336
337    if cert.audiences.is_empty() {
338        return Err(Error::invalid("delegation audiences must not be empty"));
339    }
340
341    if cert.scopes.is_empty() {
342        return Err(Error::invalid("delegation scopes must not be empty"));
343    }
344
345    if cert.audiences.iter().any(String::is_empty) {
346        return Err(Error::invalid("delegation audience must not be empty"));
347    }
348
349    if cert.scopes.iter().any(String::is_empty) {
350        return Err(Error::invalid("delegation scope must not be empty"));
351    }
352
353    let root_pid = EnvOps::root_pid().map_err(Error::from)?;
354    if cert.signer_pid == root_pid {
355        return Err(Error::invalid("delegation signer must not be root"));
356    }
357
358    let record = SubnetRegistryOps::get(cert.signer_pid)
359        .ok_or_else(|| Error::invalid("delegation signer must be registered to subnet"))?;
360    if record.role.is_root() {
361        return Err(Error::invalid("delegation signer role must not be root"));
362    }
363
364    Ok(())
365}