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