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 sign_token(
86 token_version: u16,
87 claims: DelegatedTokenClaims,
88 proof: DelegationProof,
89 ) -> Result<DelegatedToken, Error> {
90 DelegatedTokenOps::sign_token(token_version, claims, proof)
91 .map_err(Self::map_delegation_error)
92 }
93
94 pub fn verify_token(
99 token: &DelegatedToken,
100 authority_pid: Principal,
101 now_secs: u64,
102 ) -> Result<(), Error> {
103 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
104 .map(|_| ())
105 .map_err(Self::map_delegation_error)
106 }
107
108 pub fn verify_token_verified(
113 token: &DelegatedToken,
114 authority_pid: Principal,
115 now_secs: u64,
116 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
117 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
118 .map(|verified| (verified.claims, verified.cert))
119 .map_err(Self::map_delegation_error)
120 }
121
122 pub async fn provision(
129 request: DelegationProvisionRequest,
130 ) -> Result<DelegationProvisionResponse, Error> {
131 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
132 if !cfg.enabled {
133 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
134 }
135
136 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
137 let caller = IcOps::msg_caller();
138 if caller != root_pid {
139 return Err(Error::forbidden(
140 "delegation provision requires root caller",
141 ));
142 }
143
144 validate_issuance_policy(&request.cert)?;
145 log!(
146 Topic::Auth,
147 Info,
148 "delegation provision start signer={} signer_targets={:?} verifier_targets={:?}",
149 request.cert.signer_pid,
150 request.signer_targets,
151 request.verifier_targets
152 );
153 DelegationWorkflow::provision(request)
154 .await
155 .map_err(Self::map_delegation_error)
156 }
157
158 pub async fn request_delegation(
162 request: DelegationRequest,
163 ) -> Result<DelegationProvisionResponse, Error> {
164 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
165 if !cfg.enabled {
166 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
167 }
168
169 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
170 if root_pid != IcOps::canister_self() {
171 return Err(Error::forbidden("delegation request must target root"));
172 }
173
174 let caller = IcOps::msg_caller();
175 if caller != request.signer_pid {
176 return Err(Error::forbidden(
177 "delegation request signer must match caller",
178 ));
179 }
180
181 if request.ttl_secs == 0 {
182 return Err(Error::invalid(
183 "delegation ttl_secs must be greater than zero",
184 ));
185 }
186
187 let now_secs = IcOps::now_secs();
188 let cert = DelegationCert {
189 v: 1,
190 signer_pid: request.signer_pid,
191 audiences: request.audiences,
192 scopes: request.scopes,
193 issued_at: now_secs,
194 expires_at: now_secs.saturating_add(request.ttl_secs),
195 };
196
197 validate_issuance_policy(&cert)?;
198
199 let response = DelegationWorkflow::provision(DelegationProvisionRequest {
200 cert,
201 signer_targets: vec![caller],
202 verifier_targets: request.verifier_targets,
203 })
204 .await
205 .map_err(Self::map_delegation_error)?;
206
207 if request.include_root_verifier {
208 DelegationStateOps::set_proof_from_dto(response.proof.clone());
209 }
210
211 Ok(response)
212 }
213
214 pub fn store_proof(
215 proof: DelegationProof,
216 kind: DelegationProvisionTargetKind,
217 ) -> Result<(), Error> {
218 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
219 if !cfg.enabled {
220 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
221 }
222
223 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
224 let caller = IcOps::msg_caller();
225 if caller != root_pid {
226 return Err(Error::forbidden(
227 "delegation proof store requires root caller",
228 ));
229 }
230
231 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
232 let local = IcOps::canister_self();
233 log!(
234 Topic::Auth,
235 Warn,
236 "delegation proof rejected kind={:?} local={} signer={} issued_at={} expires_at={} error={}",
237 kind,
238 local,
239 proof.cert.signer_pid,
240 proof.cert.issued_at,
241 proof.cert.expires_at,
242 err
243 );
244 return Err(Self::map_delegation_error(err));
245 }
246
247 DelegationStateOps::set_proof_from_dto(proof);
248 let local = IcOps::canister_self();
249 let stored = DelegationStateOps::proof_dto()
250 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
251 log!(
252 Topic::Auth,
253 Info,
254 "delegation proof stored kind={:?} local={} signer={} issued_at={} expires_at={}",
255 kind,
256 local,
257 stored.cert.signer_pid,
258 stored.cert.issued_at,
259 stored.cert.expires_at
260 );
261
262 Ok(())
263 }
264
265 pub fn require_proof() -> Result<DelegationProof, Error> {
266 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
267 if !cfg.enabled {
268 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
269 }
270
271 DelegationStateOps::proof_dto().ok_or_else(|| {
272 record_signer_mint_without_proof();
273 Error::not_found("delegation proof not set")
274 })
275 }
276}
277
278fn validate_issuance_policy(cert: &DelegationCert) -> Result<(), Error> {
279 if cert.expires_at <= cert.issued_at {
280 return Err(Error::invalid(
281 "delegation expires_at must be greater than issued_at",
282 ));
283 }
284
285 if cert.audiences.is_empty() {
286 return Err(Error::invalid("delegation audiences must not be empty"));
287 }
288
289 if cert.scopes.is_empty() {
290 return Err(Error::invalid("delegation scopes must not be empty"));
291 }
292
293 if cert.audiences.iter().any(String::is_empty) {
294 return Err(Error::invalid("delegation audience must not be empty"));
295 }
296
297 if cert.scopes.iter().any(String::is_empty) {
298 return Err(Error::invalid("delegation scope must not be empty"));
299 }
300
301 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
302 if cert.signer_pid == root_pid {
303 return Err(Error::invalid("delegation signer must not be root"));
304 }
305
306 let record = SubnetRegistryOps::get(cert.signer_pid)
307 .ok_or_else(|| Error::invalid("delegation signer must be registered to subnet"))?;
308 if record.role.is_root() {
309 return Err(Error::invalid("delegation signer role must not be root"));
310 }
311
312 Ok(())
313}