1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenIssueRequest,
6 DelegatedTokenMintRequest, DelegationProof, DelegationProofIssueRequest,
7 InternalInvocationProofRequest, RoleAttestationRequest,
8 SignedInternalInvocationProofV1, SignedRoleAttestation,
9 },
10 error::Error,
11 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
12 },
13 error::InternalErrorClass,
14 ids::CanisterRole,
15 log,
16 log::Topic,
17 ops::{
18 auth::{
19 AuthOps, SignDelegatedTokenInput, SignDelegationProofInput,
20 VerifyDelegatedTokenRuntimeInput,
21 },
22 config::ConfigOps,
23 ic::IcOps,
24 rpc::RpcOps,
25 runtime::env::EnvOps,
26 runtime::metrics::auth::record_attestation_refresh_failed,
27 },
28 protocol,
29 workflow::rpc::request::handler::RootResponseWorkflow,
30};
31
32mod metadata;
37mod session;
38mod verify_flow;
39
40pub struct AuthApi;
47
48impl AuthApi {
49 const DELEGATED_TOKENS_DISABLED: &str =
50 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
51 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
52 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
53 b"canic-session-bootstrap-token-fingerprint";
54
55 fn map_auth_error(err: crate::InternalError) -> Error {
57 match err.class() {
58 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
59 Error::internal(err.to_string())
60 }
61 _ => Error::from(err),
62 }
63 }
64
65 fn verify_token_material(
70 token: &DelegatedToken,
71 max_cert_ttl_secs: u64,
72 max_token_ttl_secs: u64,
73 required_scopes: &[String],
74 now_secs: u64,
75 ) -> Result<Principal, Error> {
76 AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
77 token,
78 max_cert_ttl_secs,
79 max_token_ttl_secs,
80 required_scopes,
81 now_secs,
82 })
83 .map(|verified| verified.subject)
84 .map_err(Self::map_auth_error)
85 }
86
87 pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
89 AuthOps::local_shard_public_key_sec1(IcOps::canister_self())
90 .await
91 .map_err(Self::map_auth_error)
92 }
93
94 pub async fn issue_token(request: DelegatedTokenIssueRequest) -> Result<DelegatedToken, Error> {
96 AuthOps::sign_token(SignDelegatedTokenInput {
97 proof: request.proof,
98 subject: request.subject,
99 audience: request.aud,
100 scopes: request.scopes,
101 ttl_secs: request.ttl_secs,
102 nonce: request.nonce,
103 })
104 .await
105 .map_err(Self::map_auth_error)
106 }
107
108 pub async fn mint_token(request: DelegatedTokenMintRequest) -> Result<DelegatedToken, Error> {
110 let proof = Self::request_delegation(DelegationProofIssueRequest {
111 shard_pid: IcOps::canister_self(),
112 scopes: request.scopes.clone(),
113 aud: request.aud.clone(),
114 cert_ttl_secs: request.cert_ttl_secs,
115 })
116 .await?;
117
118 Self::issue_token(DelegatedTokenIssueRequest {
119 proof,
120 subject: request.subject,
121 aud: request.aud,
122 scopes: request.scopes,
123 ttl_secs: request.token_ttl_secs,
124 nonce: request.nonce,
125 })
126 .await
127 }
128
129 pub async fn request_delegation(
131 request: DelegationProofIssueRequest,
132 ) -> Result<DelegationProof, Error> {
133 Self::request_delegation_remote(request).await
134 }
135
136 pub async fn issue_delegation_proof(
138 request: DelegationProofIssueRequest,
139 ) -> Result<DelegationProof, Error> {
140 EnvOps::require_root().map_err(Error::from)?;
141 let max_cert_ttl_secs = Self::delegated_token_max_ttl_secs()?;
142 let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
143 AuthOps::sign_delegation_proof(SignDelegationProofInput {
144 audience: request.aud,
145 scopes: request.scopes,
146 shard_pid: request.shard_pid,
147 cert_ttl_secs: request.cert_ttl_secs,
148 max_token_ttl_secs,
149 max_cert_ttl_secs,
150 issued_at: IcOps::now_secs(),
151 })
152 .await
153 .map_err(Self::map_auth_error)
154 }
155
156 pub async fn request_role_attestation(
158 request: RoleAttestationRequest,
159 ) -> Result<SignedRoleAttestation, Error> {
160 let request = metadata::with_root_attestation_request_metadata(request);
161 let response = Self::request_role_attestation_remote(request).await?;
162
163 match response {
164 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
165 _ => Err(Error::internal(
166 "invalid root response type for role attestation request",
167 )),
168 }
169 }
170
171 pub async fn request_internal_invocation_proof(
173 request: InternalInvocationProofRequest,
174 ) -> Result<SignedInternalInvocationProofV1, Error> {
175 let request = metadata::with_internal_invocation_proof_request_metadata(request);
176 let response = Self::request_internal_invocation_proof_remote(request).await?;
177
178 match response {
179 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
180 _ => Err(Error::internal(
181 "invalid root response type for internal invocation proof request",
182 )),
183 }
184 }
185
186 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
188 AuthOps::attestation_key_set()
189 .await
190 .map_err(Self::map_auth_error)
191 }
192
193 pub async fn publish_root_auth_material() -> Result<(), Error> {
195 EnvOps::require_root().map_err(Error::from)?;
196 AuthOps::publish_root_auth_material().await.map_err(|err| {
197 log!(
198 Topic::Auth,
199 Warn,
200 "root auth material publish failed: {err}"
201 );
202 Self::map_auth_error(err)
203 })
204 }
205
206 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
208 AuthOps::replace_attestation_key_set(key_set);
209 }
210
211 pub async fn verify_role_attestation(
213 attestation: &SignedRoleAttestation,
214 min_accepted_epoch: u64,
215 ) -> Result<(), Error> {
216 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
217 .map_err(Error::from)?
218 .min_accepted_epoch_by_role
219 .get(attestation.payload.role.as_str())
220 .copied();
221 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
222 min_accepted_epoch,
223 configured_min_accepted_epoch,
224 );
225
226 let caller = IcOps::msg_caller();
227 let self_pid = IcOps::canister_self();
228 let now_secs = IcOps::now_secs();
229 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
230 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
231
232 let verify = || {
233 AuthOps::verify_role_attestation_cached(
234 attestation,
235 caller,
236 self_pid,
237 verifier_subnet,
238 now_secs,
239 min_accepted_epoch,
240 )
241 .map(|_| ())
242 };
243 let refresh = || async {
244 let key_set: AttestationKeySet =
245 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
246 AuthOps::replace_attestation_key_set(key_set);
247 Ok(())
248 };
249
250 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
251 Ok(()) => Ok(()),
252 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
253 verify_flow::record_attestation_verifier_rejection(&err);
254 verify_flow::log_attestation_verifier_rejection(
255 &err,
256 attestation,
257 caller,
258 self_pid,
259 "cached",
260 );
261 Err(Self::map_auth_error(err.into()))
262 }
263 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
264 verify_flow::record_attestation_verifier_rejection(&trigger);
265 verify_flow::log_attestation_verifier_rejection(
266 &trigger,
267 attestation,
268 caller,
269 self_pid,
270 "cache_miss_refresh",
271 );
272 record_attestation_refresh_failed();
273 log!(
274 Topic::Auth,
275 Warn,
276 "role attestation refresh failed local={} caller={} key_id={} error={}",
277 self_pid,
278 caller,
279 attestation.key_id,
280 source
281 );
282 Err(Self::map_auth_error(source))
283 }
284 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
285 verify_flow::record_attestation_verifier_rejection(&err);
286 verify_flow::log_attestation_verifier_rejection(
287 &err,
288 attestation,
289 caller,
290 self_pid,
291 "post_refresh",
292 );
293 Err(Self::map_auth_error(err.into()))
294 }
295 }
296 }
297
298 pub async fn verify_internal_invocation_proof(
300 proof: &SignedInternalInvocationProofV1,
301 target_method: &str,
302 accepted_roles: &[CanisterRole],
303 ) -> Result<(), Error> {
304 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
305 .map_err(Error::from)?
306 .min_accepted_epoch_by_role
307 .get(proof.payload.role.as_str())
308 .copied();
309 let min_accepted_epoch =
310 verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
311
312 let caller = IcOps::msg_caller();
313 let self_pid = IcOps::canister_self();
314 let now_secs = IcOps::now_secs();
315 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
316 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
317
318 let verify = || {
319 AuthOps::verify_internal_invocation_proof_cached(
320 proof,
321 crate::ops::auth::InternalInvocationProofVerificationInput {
322 caller,
323 self_pid,
324 target_method,
325 accepted_roles,
326 verifier_subnet,
327 now_secs,
328 min_accepted_epoch,
329 },
330 )
331 .map(|_| ())
332 };
333 let refresh = || async {
334 let key_set: AttestationKeySet =
335 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
336 AuthOps::replace_attestation_key_set(key_set);
337 Ok(())
338 };
339
340 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
341 Ok(()) => Ok(()),
342 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
343 verify_flow::record_attestation_verifier_rejection(&err);
344 log!(
345 Topic::Auth,
346 Warn,
347 "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
348 self_pid,
349 caller,
350 proof.payload.subject,
351 proof.payload.role,
352 proof.key_id,
353 proof.payload.audience,
354 proof.payload.audience_method,
355 proof.payload.epoch,
356 err
357 );
358 Err(Self::map_auth_error(err.into()))
359 }
360 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
361 verify_flow::record_attestation_verifier_rejection(&trigger);
362 record_attestation_refresh_failed();
363 log!(
364 Topic::Auth,
365 Warn,
366 "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
367 self_pid,
368 caller,
369 proof.key_id,
370 source
371 );
372 Err(Self::map_auth_error(source))
373 }
374 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
375 verify_flow::record_attestation_verifier_rejection(&err);
376 log!(
377 Topic::Auth,
378 Warn,
379 "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
380 self_pid,
381 caller,
382 proof.payload.subject,
383 proof.payload.role,
384 proof.key_id,
385 proof.payload.audience,
386 proof.payload.audience_method,
387 proof.payload.epoch,
388 err
389 );
390 Err(Self::map_auth_error(err.into()))
391 }
392 }
393 }
394
395 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
397 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
398 if !cfg.enabled {
399 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
400 }
401
402 Ok(cfg
403 .max_ttl_secs
404 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
405 }
406}
407
408impl AuthApi {
409 async fn request_delegation_remote(
411 request: DelegationProofIssueRequest,
412 ) -> Result<DelegationProof, Error> {
413 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
414 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
415 .await
416 .map_err(Self::map_auth_error)
417 }
418
419 pub async fn request_role_attestation_root(
421 request: RoleAttestationRequest,
422 ) -> Result<SignedRoleAttestation, Error> {
423 let request = metadata::with_root_attestation_request_metadata(request);
424 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
425 .await
426 .map_err(Self::map_auth_error)?;
427
428 match response {
429 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
430 _ => Err(Error::internal(
431 "invalid root response type for role attestation request",
432 )),
433 }
434 }
435
436 pub async fn request_internal_invocation_proof_root(
438 request: InternalInvocationProofRequest,
439 ) -> Result<SignedInternalInvocationProofV1, Error> {
440 let request = metadata::with_internal_invocation_proof_request_metadata(request);
441 let response =
442 RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
443 .await
444 .map_err(Self::map_auth_error)?;
445
446 match response {
447 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
448 _ => Err(Error::internal(
449 "invalid root response type for internal invocation proof request",
450 )),
451 }
452 }
453
454 async fn request_role_attestation_remote(
456 request: RoleAttestationRequest,
457 ) -> Result<RootCapabilityResponse, Error> {
458 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
459 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
460 .await
461 .map_err(Self::map_auth_error)
462 }
463
464 async fn request_internal_invocation_proof_remote(
466 request: InternalInvocationProofRequest,
467 ) -> Result<RootCapabilityResponse, Error> {
468 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
469 RpcOps::call_rpc_result(
470 root_pid,
471 protocol::CANIC_REQUEST_INTERNAL_INVOCATION_PROOF,
472 request,
473 )
474 .await
475 .map_err(Self::map_auth_error)
476 }
477}