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    /// Verify a delegated token and return verified contents.
81    ///
82    /// This is intended for application-layer session construction.
83    /// It performs full verification and returns verified claims and cert.
84    pub fn verify_token_verified(
85        token: &DelegatedToken,
86        authority_pid: Principal,
87        now_secs: u64,
88    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
89        DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
90            .map(|verified| (verified.claims, verified.cert))
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
142        Ok(())
143    }
144
145    pub fn require_proof() -> Result<DelegationProof, Error> {
146        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
147        if !cfg.enabled {
148            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
149        }
150
151        DelegationStateOps::proof_dto().ok_or_else(|| Error::not_found("delegation proof not set"))
152    }
153}
154
155///
156/// DelegationAdminApi
157///
158/// Admin façade for delegation rotation control.
159///
160
161pub struct DelegationAdminApi;
162
163impl DelegationAdminApi {
164    pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
165        match cmd {
166            DelegationAdminCommand::StartRotation { interval_secs } => {
167                let started = Self::start_rotation(interval_secs).await?;
168                Ok(if started {
169                    DelegationAdminResponse::RotationStarted
170                } else {
171                    DelegationAdminResponse::RotationAlreadyRunning
172                })
173            }
174            DelegationAdminCommand::StopRotation => {
175                let stopped = Self::stop_rotation().await?;
176                Ok(if stopped {
177                    DelegationAdminResponse::RotationStopped
178                } else {
179                    DelegationAdminResponse::RotationNotRunning
180                })
181            }
182        }
183    }
184
185    #[allow(clippy::unused_async)]
186    pub async fn start_rotation(interval_secs: u64) -> Result<bool, Error> {
187        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
188        if !cfg.enabled {
189            return Err(Error::forbidden(DelegationApi::DELEGATED_TOKENS_DISABLED));
190        }
191
192        if interval_secs == 0 {
193            return Err(Error::invalid(
194                "rotation interval must be greater than zero",
195            ));
196        }
197
198        let template = rotation_template()?;
199        let template = Arc::new(template);
200        let interval = Duration::from_secs(interval_secs);
201
202        let started = DelegationWorkflow::start_rotation(
203            interval,
204            Arc::new({
205                let template = Arc::clone(&template);
206                move || {
207                    let now_secs = IcOps::now_secs();
208                    Ok(build_rotation_cert(template.as_ref(), now_secs))
209                }
210            }),
211            Arc::new(|proof| {
212                DelegationStateOps::set_proof_from_dto(proof);
213                Ok(())
214            }),
215        );
216
217        Ok(started)
218    }
219
220    #[allow(clippy::unused_async)]
221    pub async fn stop_rotation() -> Result<bool, Error> {
222        Ok(DelegationWorkflow::stop_rotation())
223    }
224}
225
226///
227/// DelegationRotationTemplate
228///
229
230struct DelegationRotationTemplate {
231    v: u16,
232    signer_pid: Principal,
233    audiences: Vec<String>,
234    scopes: Vec<String>,
235    ttl_secs: u64,
236}
237
238fn rotation_template() -> Result<DelegationRotationTemplate, Error> {
239    let proof = DelegationStateOps::proof_dto()
240        .ok_or_else(|| Error::not_found("delegation proof not set"))?;
241    let cert = proof.cert;
242
243    if cert.expires_at <= cert.issued_at {
244        return Err(Error::invalid(
245            "delegation cert expires_at must be greater than issued_at",
246        ));
247    }
248
249    let ttl_secs = cert.expires_at - cert.issued_at;
250
251    Ok(DelegationRotationTemplate {
252        v: cert.v,
253        signer_pid: cert.signer_pid,
254        audiences: cert.audiences,
255        scopes: cert.scopes,
256        ttl_secs,
257    })
258}
259
260fn build_rotation_cert(template: &DelegationRotationTemplate, now_secs: u64) -> DelegationCert {
261    DelegationCert {
262        v: template.v,
263        signer_pid: template.signer_pid,
264        audiences: template.audiences.clone(),
265        scopes: template.scopes.clone(),
266        issued_at: now_secs,
267        expires_at: now_secs.saturating_add(template.ttl_secs),
268    }
269}