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