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