1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationCert,
6 DelegationProof, DelegationProvisionResponse, DelegationProvisionTargetKind,
7 DelegationRequest, RoleAttestationRequest, SignedRoleAttestation,
8 },
9 error::Error,
10 rpc::{Request as RootCapabilityRequest, Response as RootCapabilityResponse},
11 },
12 error::InternalErrorClass,
13 log,
14 log::Topic,
15 ops::{
16 auth::DelegatedTokenOps,
17 config::ConfigOps,
18 ic::IcOps,
19 rpc::RpcOps,
20 runtime::env::EnvOps,
21 runtime::metrics::auth::{
22 record_attestation_refresh_failed, record_signer_mint_without_proof,
23 },
24 storage::auth::DelegationStateOps,
25 },
26 protocol,
27 workflow::rpc::request::handler::RootResponseWorkflow,
28};
29
30mod metadata;
31mod verify_flow;
32
33pub struct DelegationApi;
40
41impl DelegationApi {
42 const DELEGATED_TOKENS_DISABLED: &str =
43 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
44
45 fn map_delegation_error(err: crate::InternalError) -> Error {
46 match err.class() {
47 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
48 Error::internal(err.to_string())
49 }
50 _ => Error::from(err),
51 }
52 }
53
54 pub fn verify_delegation_proof(
59 proof: &DelegationProof,
60 authority_pid: Principal,
61 ) -> Result<(), Error> {
62 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
63 .map_err(Self::map_delegation_error)
64 }
65
66 pub async fn sign_token(
67 claims: DelegatedTokenClaims,
68 proof: DelegationProof,
69 ) -> Result<DelegatedToken, Error> {
70 DelegatedTokenOps::sign_token(claims, proof)
71 .await
72 .map_err(Self::map_delegation_error)
73 }
74
75 pub fn verify_token(
80 token: &DelegatedToken,
81 authority_pid: Principal,
82 now_secs: u64,
83 ) -> Result<(), Error> {
84 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
85 .map(|_| ())
86 .map_err(Self::map_delegation_error)
87 }
88
89 pub fn verify_token_verified(
94 token: &DelegatedToken,
95 authority_pid: Principal,
96 now_secs: u64,
97 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
98 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
99 .map(|verified| (verified.claims, verified.cert))
100 .map_err(Self::map_delegation_error)
101 }
102
103 pub async fn request_delegation(
107 request: DelegationRequest,
108 ) -> Result<DelegationProvisionResponse, Error> {
109 let request = metadata::with_root_request_metadata(request);
110 let response =
111 RootResponseWorkflow::response(RootCapabilityRequest::IssueDelegation(request))
112 .await
113 .map_err(Self::map_delegation_error)?;
114
115 match response {
116 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
117 _ => Err(Error::internal(
118 "invalid root response type for delegation request",
119 )),
120 }
121 }
122
123 pub async fn request_role_attestation(
124 request: RoleAttestationRequest,
125 ) -> Result<SignedRoleAttestation, Error> {
126 let request = metadata::with_root_attestation_request_metadata(request);
127 let response =
128 RootResponseWorkflow::response(RootCapabilityRequest::IssueRoleAttestation(request))
129 .await
130 .map_err(Self::map_delegation_error)?;
131
132 match response {
133 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
134 _ => Err(Error::internal(
135 "invalid root response type for role attestation request",
136 )),
137 }
138 }
139
140 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
141 DelegatedTokenOps::attestation_key_set()
142 .await
143 .map_err(Self::map_delegation_error)
144 }
145
146 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
147 DelegatedTokenOps::replace_attestation_key_set(key_set);
148 }
149
150 pub async fn verify_role_attestation(
151 attestation: &SignedRoleAttestation,
152 min_accepted_epoch: u64,
153 ) -> Result<(), Error> {
154 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
155 .map_err(Error::from)?
156 .min_accepted_epoch_by_role
157 .get(attestation.payload.role.as_str())
158 .copied();
159 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
160 min_accepted_epoch,
161 configured_min_accepted_epoch,
162 );
163
164 let caller = IcOps::msg_caller();
165 let self_pid = IcOps::canister_self();
166 let now_secs = IcOps::now_secs();
167 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
168 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
169
170 let verify = || {
171 DelegatedTokenOps::verify_role_attestation_cached(
172 attestation,
173 caller,
174 self_pid,
175 verifier_subnet,
176 now_secs,
177 min_accepted_epoch,
178 )
179 .map(|_| ())
180 };
181 let refresh = || async {
182 let key_set: AttestationKeySet =
183 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
184 DelegatedTokenOps::replace_attestation_key_set(key_set);
185 Ok(())
186 };
187
188 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
189 Ok(()) => Ok(()),
190 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
191 verify_flow::record_attestation_verifier_rejection(&err);
192 verify_flow::log_attestation_verifier_rejection(
193 &err,
194 attestation,
195 caller,
196 self_pid,
197 "cached",
198 );
199 Err(Self::map_delegation_error(err.into()))
200 }
201 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
202 verify_flow::record_attestation_verifier_rejection(&trigger);
203 verify_flow::log_attestation_verifier_rejection(
204 &trigger,
205 attestation,
206 caller,
207 self_pid,
208 "cache_miss_refresh",
209 );
210 record_attestation_refresh_failed();
211 log!(
212 Topic::Auth,
213 Warn,
214 "role attestation refresh failed local={} caller={} key_id={} error={}",
215 self_pid,
216 caller,
217 attestation.key_id,
218 source
219 );
220 Err(Self::map_delegation_error(source))
221 }
222 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
223 verify_flow::record_attestation_verifier_rejection(&err);
224 verify_flow::log_attestation_verifier_rejection(
225 &err,
226 attestation,
227 caller,
228 self_pid,
229 "post_refresh",
230 );
231 Err(Self::map_delegation_error(err.into()))
232 }
233 }
234 }
235
236 pub async fn store_proof(
237 proof: DelegationProof,
238 kind: DelegationProvisionTargetKind,
239 ) -> Result<(), Error> {
240 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
241 if !cfg.enabled {
242 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
243 }
244
245 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
246 let caller = IcOps::msg_caller();
247 if caller != root_pid {
248 return Err(Error::forbidden(
249 "delegation proof store requires root caller",
250 ));
251 }
252
253 DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
254 .await
255 .map_err(Self::map_delegation_error)?;
256 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
257 let local = IcOps::canister_self();
258 log!(
259 Topic::Auth,
260 Warn,
261 "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
262 kind,
263 local,
264 proof.cert.shard_pid,
265 proof.cert.issued_at,
266 proof.cert.expires_at,
267 err
268 );
269 return Err(Self::map_delegation_error(err));
270 }
271
272 DelegationStateOps::set_proof_from_dto(proof);
273 let local = IcOps::canister_self();
274 let stored = DelegationStateOps::proof_dto()
275 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
276 log!(
277 Topic::Auth,
278 Info,
279 "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
280 kind,
281 local,
282 stored.cert.shard_pid,
283 stored.cert.issued_at,
284 stored.cert.expires_at
285 );
286
287 Ok(())
288 }
289
290 pub fn require_proof() -> Result<DelegationProof, Error> {
291 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
292 if !cfg.enabled {
293 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
294 }
295
296 DelegationStateOps::proof_dto().ok_or_else(|| {
297 record_signer_mint_without_proof();
298 Error::not_found("delegation proof not set")
299 })
300 }
301}
302
303#[cfg(test)]
304mod tests;