canic_core/api/
auth.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        auth::{
5            DelegatedToken, DelegatedTokenClaims, DelegationAdminCommand, DelegationAdminResponse,
6            DelegationCert, DelegationProof,
7        },
8        error::Error,
9    },
10    error::InternalErrorClass,
11    ops::{
12        auth::DelegatedTokenOps, config::ConfigOps, ic::IcOps, runtime::env::EnvOps,
13        storage::auth::DelegationStateOps,
14    },
15    workflow::auth::DelegationWorkflow,
16};
17use std::{sync::Arc, time::Duration};
18
19///
20/// DelegationApi
21///
22/// Requires auth.delegated_tokens.enabled = true in config.
23///
24
25pub struct DelegationApi;
26
27impl DelegationApi {
28    const DELEGATED_TOKENS_DISABLED: &str =
29        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
30
31    fn map_delegation_error(err: crate::InternalError) -> Error {
32        match err.class() {
33            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
34                Error::internal(err.to_string())
35            }
36            _ => Error::from(err),
37        }
38    }
39
40    /// Sign a delegation cert.
41    pub fn sign_delegation_cert(cert: DelegationCert) -> Result<DelegationProof, Error> {
42        DelegatedTokenOps::sign_delegation_cert(cert).map_err(Self::map_delegation_error)
43    }
44
45    /// Full delegation proof verification (structure + signature).
46    ///
47    /// Purely local verification; does not read certified data or require a
48    /// query context.
49    pub fn verify_delegation_proof(
50        proof: &DelegationProof,
51        authority_pid: Principal,
52    ) -> Result<(), Error> {
53        DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
54            .map_err(Self::map_delegation_error)
55    }
56
57    pub fn sign_token(
58        token_version: u16,
59        claims: DelegatedTokenClaims,
60        proof: DelegationProof,
61    ) -> Result<DelegatedToken, Error> {
62        DelegatedTokenOps::sign_token(token_version, claims, proof)
63            .map_err(Self::map_delegation_error)
64    }
65
66    /// Full delegated token verification (structure + signature).
67    ///
68    /// Purely local verification; does not read certified data or require a
69    /// query context.
70    pub fn verify_token(
71        token: &DelegatedToken,
72        authority_pid: Principal,
73        now_secs: u64,
74    ) -> Result<(), Error> {
75        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
76            .map(|_| ())
77            .map_err(Self::map_delegation_error)
78    }
79
80    /// Return verified claims after full token verification.
81    ///
82    /// Purely local verification; does not read certified data or require a
83    /// query context.
84    pub fn verify_token_claims(
85        token: &DelegatedToken,
86        authority_pid: Principal,
87        now_secs: u64,
88    ) -> Result<DelegatedTokenClaims, Error> {
89        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
90            .map(|verified| verified.claims)
91            .map_err(Self::map_delegation_error)
92    }
93
94    pub fn prepare_issue(cert: DelegationCert) -> Result<(), 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        // Update-only step for certified delegation signatures.
101        DelegationWorkflow::prepare_delegation(&cert).map_err(Self::map_delegation_error)
102    }
103
104    pub fn get_issue(cert: DelegationCert) -> Result<DelegationProof, Error> {
105        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
106        if !cfg.enabled {
107            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
108        }
109
110        // Query-only step; requires a data certificate in the query context.
111        DelegationWorkflow::get_delegation(cert).map_err(Self::map_delegation_error)
112    }
113
114    pub fn issue_and_store(cert: DelegationCert) -> Result<DelegationProof, Error> {
115        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
116        if !cfg.enabled {
117            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
118        }
119
120        DelegationWorkflow::issue_and_store(cert).map_err(Self::map_delegation_error)
121    }
122
123    pub fn store_proof(proof: DelegationProof) -> Result<(), Error> {
124        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
125        if !cfg.enabled {
126            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
127        }
128
129        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
130        let caller = IcOps::msg_caller();
131        if caller != root_pid {
132            return Err(Error::forbidden(
133                "delegation proof store requires root caller",
134            ));
135        }
136
137        DelegatedTokenOps::verify_delegation_proof(&proof, root_pid)
138            .map_err(Self::map_delegation_error)?;
139
140        DelegationStateOps::set_proof_from_dto(proof);
141        Ok(())
142    }
143
144    pub fn require_proof() -> Result<DelegationProof, Error> {
145        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
146        if !cfg.enabled {
147            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
148        }
149
150        DelegationStateOps::proof_dto().ok_or_else(|| Error::not_found("delegation proof not set"))
151    }
152}
153
154///
155/// DelegationAdminApi
156///
157/// Admin façade for delegation rotation control.
158///
159
160pub struct DelegationAdminApi;
161
162impl DelegationAdminApi {
163    pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
164        match cmd {
165            DelegationAdminCommand::StartRotation { interval_secs } => {
166                let started = Self::start_rotation(interval_secs).await?;
167                Ok(if started {
168                    DelegationAdminResponse::RotationStarted
169                } else {
170                    DelegationAdminResponse::RotationAlreadyRunning
171                })
172            }
173            DelegationAdminCommand::StopRotation => {
174                let stopped = Self::stop_rotation().await?;
175                Ok(if stopped {
176                    DelegationAdminResponse::RotationStopped
177                } else {
178                    DelegationAdminResponse::RotationNotRunning
179                })
180            }
181        }
182    }
183
184    #[allow(clippy::unused_async)]
185    pub async fn start_rotation(interval_secs: u64) -> Result<bool, Error> {
186        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
187        if !cfg.enabled {
188            return Err(Error::forbidden(DelegationApi::DELEGATED_TOKENS_DISABLED));
189        }
190
191        if interval_secs == 0 {
192            return Err(Error::invalid(
193                "rotation interval must be greater than zero",
194            ));
195        }
196
197        let template = rotation_template()?;
198        let template = Arc::new(template);
199        let interval = Duration::from_secs(interval_secs);
200
201        let started = DelegationWorkflow::start_rotation(
202            interval,
203            Arc::new({
204                let template = Arc::clone(&template);
205                move || {
206                    let now_secs = IcOps::now_secs();
207                    Ok(build_rotation_cert(template.as_ref(), now_secs))
208                }
209            }),
210            Arc::new(|proof| {
211                DelegationStateOps::set_proof_from_dto(proof);
212                Ok(())
213            }),
214        );
215
216        Ok(started)
217    }
218
219    #[allow(clippy::unused_async)]
220    pub async fn stop_rotation() -> Result<bool, Error> {
221        Ok(DelegationWorkflow::stop_rotation())
222    }
223}
224
225///
226/// DelegationRotationTemplate
227///
228
229struct DelegationRotationTemplate {
230    v: u16,
231    signer_pid: Principal,
232    audiences: Vec<String>,
233    scopes: Vec<String>,
234    ttl_secs: u64,
235}
236
237fn rotation_template() -> Result<DelegationRotationTemplate, Error> {
238    let proof = DelegationStateOps::proof_dto()
239        .ok_or_else(|| Error::not_found("delegation proof not set"))?;
240    let cert = proof.cert;
241
242    if cert.expires_at <= cert.issued_at {
243        return Err(Error::invalid(
244            "delegation cert expires_at must be greater than issued_at",
245        ));
246    }
247
248    let ttl_secs = cert.expires_at - cert.issued_at;
249
250    Ok(DelegationRotationTemplate {
251        v: cert.v,
252        signer_pid: cert.signer_pid,
253        audiences: cert.audiences,
254        scopes: cert.scopes,
255        ttl_secs,
256    })
257}
258
259fn build_rotation_cert(template: &DelegationRotationTemplate, now_secs: u64) -> DelegationCert {
260    DelegationCert {
261        v: template.v,
262        signer_pid: template.signer_pid,
263        audiences: template.audiences.clone(),
264        scopes: template.scopes.clone(),
265        issued_at: now_secs,
266        expires_at: now_secs.saturating_add(template.ttl_secs),
267    }
268}