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