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 sign_token(
59        token_version: u16,
60        claims: DelegatedTokenClaims,
61        proof: DelegationProof,
62    ) -> Result<DelegatedToken, Error> {
63        DelegatedTokenOps::sign_token(token_version, claims, proof)
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)
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)
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    /// Used for tests / tooling due to PocketIC limitations.
99    ///
100    /// Root does not infer targets; callers must supply them.
101    pub async fn provision(
102        request: DelegationProvisionRequest,
103    ) -> Result<DelegationProvisionResponse, Error> {
104        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
105        if !cfg.enabled {
106            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
107        }
108
109        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
110        let caller = IcOps::msg_caller();
111        if caller != root_pid {
112            return Err(Error::forbidden(
113                "delegation provision requires root caller",
114            ));
115        }
116
117        validate_issuance_policy(&request.cert)?;
118        log!(
119            Topic::Auth,
120            Info,
121            "delegation provision start signer={} signer_targets={:?} verifier_targets={:?}",
122            request.cert.signer_pid,
123            request.signer_targets,
124            request.verifier_targets
125        );
126        DelegationWorkflow::provision(request)
127            .await
128            .map_err(Self::map_delegation_error)
129    }
130
131    /// Canonical signer-initiated delegation request (user_shard -> root).
132    ///
133    /// Caller must match signer_pid and be registered to the subnet.
134    pub async fn request_delegation(
135        request: DelegationRequest,
136    ) -> Result<DelegationProvisionResponse, Error> {
137        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
138        if !cfg.enabled {
139            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
140        }
141
142        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
143        if root_pid != IcOps::canister_self() {
144            return Err(Error::forbidden("delegation request must target root"));
145        }
146
147        let caller = IcOps::msg_caller();
148        if caller != request.signer_pid {
149            return Err(Error::forbidden(
150                "delegation request signer must match caller",
151            ));
152        }
153
154        if request.ttl_secs == 0 {
155            return Err(Error::invalid(
156                "delegation ttl_secs must be greater than zero",
157            ));
158        }
159
160        let now_secs = IcOps::now_secs();
161        let cert = DelegationCert {
162            v: 1,
163            signer_pid: request.signer_pid,
164            audiences: request.audiences,
165            scopes: request.scopes,
166            issued_at: now_secs,
167            expires_at: now_secs.saturating_add(request.ttl_secs),
168        };
169
170        validate_issuance_policy(&cert)?;
171
172        let response = DelegationWorkflow::provision(DelegationProvisionRequest {
173            cert,
174            signer_targets: vec![caller],
175            verifier_targets: request.verifier_targets,
176        })
177        .await
178        .map_err(Self::map_delegation_error)?;
179
180        if request.include_root_verifier {
181            DelegationStateOps::set_proof_from_dto(response.proof.clone());
182        }
183
184        Ok(response)
185    }
186
187    pub fn store_proof(
188        proof: DelegationProof,
189        kind: DelegationProvisionTargetKind,
190    ) -> Result<(), Error> {
191        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
192        if !cfg.enabled {
193            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
194        }
195
196        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
197        let caller = IcOps::msg_caller();
198        if caller != root_pid {
199            return Err(Error::forbidden(
200                "delegation proof store requires root caller",
201            ));
202        }
203
204        if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
205            let local = IcOps::canister_self();
206            log!(
207                Topic::Auth,
208                Warn,
209                "delegation proof rejected kind={:?} local={} signer={} issued_at={} expires_at={} error={}",
210                kind,
211                local,
212                proof.cert.signer_pid,
213                proof.cert.issued_at,
214                proof.cert.expires_at,
215                err
216            );
217            return Err(Self::map_delegation_error(err));
218        }
219
220        DelegationStateOps::set_proof_from_dto(proof);
221        let local = IcOps::canister_self();
222        let stored = DelegationStateOps::proof_dto()
223            .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
224        log!(
225            Topic::Auth,
226            Info,
227            "delegation proof stored kind={:?} local={} signer={} issued_at={} expires_at={}",
228            kind,
229            local,
230            stored.cert.signer_pid,
231            stored.cert.issued_at,
232            stored.cert.expires_at
233        );
234
235        Ok(())
236    }
237
238    pub fn require_proof() -> Result<DelegationProof, Error> {
239        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
240        if !cfg.enabled {
241            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
242        }
243
244        DelegationStateOps::proof_dto().ok_or_else(|| {
245            record_signer_mint_without_proof();
246            Error::not_found("delegation proof not set")
247        })
248    }
249}
250
251fn validate_issuance_policy(cert: &DelegationCert) -> Result<(), Error> {
252    if cert.expires_at <= cert.issued_at {
253        return Err(Error::invalid(
254            "delegation expires_at must be greater than issued_at",
255        ));
256    }
257
258    if cert.audiences.is_empty() {
259        return Err(Error::invalid("delegation audiences must not be empty"));
260    }
261
262    if cert.scopes.is_empty() {
263        return Err(Error::invalid("delegation scopes must not be empty"));
264    }
265
266    if cert.audiences.iter().any(String::is_empty) {
267        return Err(Error::invalid("delegation audience must not be empty"));
268    }
269
270    if cert.scopes.iter().any(String::is_empty) {
271        return Err(Error::invalid("delegation scope must not be empty"));
272    }
273
274    let root_pid = EnvOps::root_pid().map_err(Error::from)?;
275    if cert.signer_pid == root_pid {
276        return Err(Error::invalid("delegation signer must not be root"));
277    }
278
279    let record = SubnetRegistryOps::get(cert.signer_pid)
280        .ok_or_else(|| Error::invalid("delegation signer must be registered to subnet"))?;
281    if record.role.is_root() {
282        return Err(Error::invalid("delegation signer role must not be root"));
283    }
284
285    Ok(())
286}