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
25pub 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 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 prepare_delegation_cert_signature(cert: &DelegationCert) -> Result<(), Error> {
59 DelegatedTokenOps::prepare_delegation_cert_signature(cert)
60 .map_err(Self::map_delegation_error)
61 }
62
63 pub fn get_delegation_cert_signature(cert: DelegationCert) -> Result<DelegationProof, Error> {
64 DelegatedTokenOps::get_delegation_cert_signature(cert).map_err(Self::map_delegation_error)
65 }
66
67 pub fn prepare_token_signature(
68 token_version: u16,
69 claims: &DelegatedTokenClaims,
70 proof: &DelegationProof,
71 ) -> Result<(), Error> {
72 DelegatedTokenOps::prepare_token_signature(token_version, claims, proof)
73 .map_err(Self::map_delegation_error)
74 }
75
76 pub fn get_token_signature(
77 token_version: u16,
78 claims: DelegatedTokenClaims,
79 proof: DelegationProof,
80 ) -> Result<DelegatedToken, Error> {
81 DelegatedTokenOps::get_token_signature(token_version, claims, proof)
82 .map_err(Self::map_delegation_error)
83 }
84
85 pub fn issue_token_prepare(
86 token_version: u16,
87 claims: DelegatedTokenClaims,
88 ) -> Result<(), Error> {
89 let proof = Self::require_proof()?;
90 DelegationWorkflow::issue_token_prepare(token_version, claims, proof)
91 .map_err(Self::map_delegation_error)
92 }
93
94 pub fn issue_token_get() -> Result<DelegatedToken, 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 DelegationWorkflow::issue_token_get().map_err(Self::map_delegation_error)
101 }
102
103 pub fn verify_token(
108 token: &DelegatedToken,
109 authority_pid: Principal,
110 now_secs: u64,
111 ) -> Result<(), Error> {
112 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
113 .map(|_| ())
114 .map_err(Self::map_delegation_error)
115 }
116
117 pub fn verify_token_verified(
122 token: &DelegatedToken,
123 authority_pid: Principal,
124 now_secs: u64,
125 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
126 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
127 .map(|verified| (verified.claims, verified.cert))
128 .map_err(Self::map_delegation_error)
129 }
130
131 pub fn provision_prepare(request: DelegationProvisionRequest) -> Result<(), Error> {
138 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
139 if !cfg.enabled {
140 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
141 }
142
143 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
144 let caller = IcOps::msg_caller();
145 if caller != root_pid {
146 return Err(Error::forbidden(
147 "delegation provision requires root caller",
148 ));
149 }
150
151 validate_issuance_policy(&request.cert)?;
152 log!(
153 Topic::Auth,
154 Info,
155 "delegation provision prepare signer={} signer_targets={:?} verifier_targets={:?}",
156 request.cert.signer_pid,
157 request.signer_targets,
158 request.verifier_targets
159 );
160
161 DelegationWorkflow::provision_prepare(request, false).map_err(Self::map_delegation_error)
162 }
163
164 pub fn provision_get() -> Result<DelegationProof, Error> {
165 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
166 if !cfg.enabled {
167 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
168 }
169
170 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
171 let caller = IcOps::msg_caller();
172 if caller != root_pid {
173 return Err(Error::forbidden(
174 "delegation provision requires root caller",
175 ));
176 }
177
178 DelegationWorkflow::provision_get().map_err(Self::map_delegation_error)
179 }
180
181 pub async fn provision_finalize(
182 proof: DelegationProof,
183 ) -> Result<DelegationProvisionResponse, Error> {
184 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
185 if !cfg.enabled {
186 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
187 }
188
189 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
190 let caller = IcOps::msg_caller();
191 if caller != root_pid {
192 return Err(Error::forbidden(
193 "delegation provision requires root caller",
194 ));
195 }
196
197 DelegationWorkflow::provision_finalize(proof)
198 .await
199 .map_err(Self::map_delegation_error)
200 }
201
202 pub fn request_delegation_prepare(request: DelegationRequest) -> Result<(), Error> {
206 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
207 if !cfg.enabled {
208 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
209 }
210
211 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
212 if root_pid != IcOps::canister_self() {
213 return Err(Error::forbidden("delegation request must target root"));
214 }
215
216 let caller = IcOps::msg_caller();
217 if caller != request.signer_pid {
218 return Err(Error::forbidden(
219 "delegation request signer must match caller",
220 ));
221 }
222
223 if request.ttl_secs == 0 {
224 return Err(Error::invalid(
225 "delegation ttl_secs must be greater than zero",
226 ));
227 }
228
229 let now_secs = IcOps::now_secs();
230 let cert = DelegationCert {
231 v: 1,
232 signer_pid: request.signer_pid,
233 audiences: request.audiences,
234 scopes: request.scopes,
235 issued_at: now_secs,
236 expires_at: now_secs.saturating_add(request.ttl_secs),
237 };
238
239 validate_issuance_policy(&cert)?;
240
241 DelegationWorkflow::provision_prepare(
242 DelegationProvisionRequest {
243 cert,
244 signer_targets: vec![caller],
245 verifier_targets: request.verifier_targets,
246 },
247 request.include_root_verifier,
248 )
249 .map_err(Self::map_delegation_error)
250 }
251
252 pub fn request_delegation_get() -> Result<DelegationProof, Error> {
253 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
254 if !cfg.enabled {
255 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
256 }
257
258 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
259 if root_pid != IcOps::canister_self() {
260 return Err(Error::forbidden("delegation request must target root"));
261 }
262
263 DelegationWorkflow::provision_get().map_err(Self::map_delegation_error)
264 }
265
266 pub async fn request_delegation_finalize(
267 proof: DelegationProof,
268 ) -> Result<DelegationProvisionResponse, Error> {
269 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
270 if !cfg.enabled {
271 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
272 }
273
274 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
275 if root_pid != IcOps::canister_self() {
276 return Err(Error::forbidden("delegation request must target root"));
277 }
278
279 DelegationWorkflow::provision_finalize(proof)
280 .await
281 .map_err(Self::map_delegation_error)
282 }
283
284 pub fn store_proof(
285 proof: DelegationProof,
286 kind: DelegationProvisionTargetKind,
287 ) -> Result<(), Error> {
288 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
289 if !cfg.enabled {
290 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
291 }
292
293 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
294 let caller = IcOps::msg_caller();
295 if caller != root_pid {
296 return Err(Error::forbidden(
297 "delegation proof store requires root caller",
298 ));
299 }
300
301 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
302 let local = IcOps::canister_self();
303 log!(
304 Topic::Auth,
305 Warn,
306 "delegation proof rejected kind={:?} local={} signer={} issued_at={} expires_at={} error={}",
307 kind,
308 local,
309 proof.cert.signer_pid,
310 proof.cert.issued_at,
311 proof.cert.expires_at,
312 err
313 );
314 return Err(Self::map_delegation_error(err));
315 }
316
317 DelegationStateOps::set_proof_from_dto(proof);
318 let local = IcOps::canister_self();
319 let stored = DelegationStateOps::proof_dto()
320 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
321 log!(
322 Topic::Auth,
323 Info,
324 "delegation proof stored kind={:?} local={} signer={} issued_at={} expires_at={}",
325 kind,
326 local,
327 stored.cert.signer_pid,
328 stored.cert.issued_at,
329 stored.cert.expires_at
330 );
331
332 Ok(())
333 }
334
335 pub fn require_proof() -> Result<DelegationProof, Error> {
336 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
337 if !cfg.enabled {
338 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
339 }
340
341 DelegationStateOps::proof_dto().ok_or_else(|| {
342 record_signer_mint_without_proof();
343 Error::not_found("delegation proof not set")
344 })
345 }
346}
347
348fn validate_issuance_policy(cert: &DelegationCert) -> Result<(), Error> {
349 if cert.expires_at <= cert.issued_at {
350 return Err(Error::invalid(
351 "delegation expires_at must be greater than issued_at",
352 ));
353 }
354
355 if cert.audiences.is_empty() {
356 return Err(Error::invalid("delegation audiences must not be empty"));
357 }
358
359 if cert.scopes.is_empty() {
360 return Err(Error::invalid("delegation scopes must not be empty"));
361 }
362
363 if cert.audiences.iter().any(String::is_empty) {
364 return Err(Error::invalid("delegation audience must not be empty"));
365 }
366
367 if cert.scopes.iter().any(String::is_empty) {
368 return Err(Error::invalid("delegation scope must not be empty"));
369 }
370
371 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
372 if cert.signer_pid == root_pid {
373 return Err(Error::invalid("delegation signer must not be root"));
374 }
375
376 let record = SubnetRegistryOps::get(cert.signer_pid)
377 .ok_or_else(|| Error::invalid("delegation signer must be registered to subnet"))?;
378 if record.role.is_root() {
379 return Err(Error::invalid("delegation signer role must not be root"));
380 }
381
382 Ok(())
383}