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