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