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, ErrorCode},
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 AuthExpiryError, AuthOps, AuthOpsError, AuthValidationError, SignDelegatedTokenInput,
20 SignDelegationProofInput, 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 map_internal_invocation_verify_error(err: AuthOpsError) -> Error {
66 match err {
67 AuthOpsError::Validation(AuthValidationError::AttestationUnknownKeyId { .. }) => {
68 Error::new(ErrorCode::AuthKeyUnknown, err.to_string())
69 }
70 AuthOpsError::Expiry(AuthExpiryError::AttestationEpochRejected { .. }) => {
71 Error::new(ErrorCode::AuthMaterialStale, err.to_string())
72 }
73 AuthOpsError::Expiry(AuthExpiryError::AttestationExpired { .. }) => {
74 Error::new(ErrorCode::AuthProofExpired, err.to_string())
75 }
76 _ => Error::unauthorized(err.to_string()),
77 }
78 }
79
80 fn verify_token_material(
85 token: &DelegatedToken,
86 max_cert_ttl_secs: u64,
87 max_token_ttl_secs: u64,
88 required_scopes: &[String],
89 now_secs: u64,
90 ) -> Result<Principal, Error> {
91 AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
92 token,
93 max_cert_ttl_secs,
94 max_token_ttl_secs,
95 required_scopes,
96 now_secs,
97 })
98 .map(|verified| verified.subject)
99 .map_err(Self::map_auth_error)
100 }
101
102 pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
104 AuthOps::local_shard_public_key_sec1(IcOps::canister_self())
105 .await
106 .map_err(Self::map_auth_error)
107 }
108
109 pub async fn issue_token(request: DelegatedTokenIssueRequest) -> Result<DelegatedToken, Error> {
111 AuthOps::sign_token(SignDelegatedTokenInput {
112 proof: request.proof,
113 subject: request.subject,
114 audience: request.aud,
115 scopes: request.scopes,
116 ttl_secs: request.ttl_secs,
117 nonce: request.nonce,
118 })
119 .await
120 .map_err(Self::map_auth_error)
121 }
122
123 pub async fn mint_token(request: DelegatedTokenMintRequest) -> Result<DelegatedToken, Error> {
125 let proof = Self::request_delegation(DelegationProofIssueRequest {
126 shard_pid: IcOps::canister_self(),
127 scopes: request.scopes.clone(),
128 aud: request.aud.clone(),
129 cert_ttl_secs: request.cert_ttl_secs,
130 })
131 .await?;
132
133 Self::issue_token(DelegatedTokenIssueRequest {
134 proof,
135 subject: request.subject,
136 aud: request.aud,
137 scopes: request.scopes,
138 ttl_secs: request.token_ttl_secs,
139 nonce: request.nonce,
140 })
141 .await
142 }
143
144 pub async fn request_delegation(
146 request: DelegationProofIssueRequest,
147 ) -> Result<DelegationProof, Error> {
148 Self::request_delegation_remote(request).await
149 }
150
151 pub async fn issue_delegation_proof(
153 request: DelegationProofIssueRequest,
154 ) -> Result<DelegationProof, Error> {
155 EnvOps::require_root().map_err(Error::from)?;
156 let max_cert_ttl_secs = Self::delegated_token_max_ttl_secs()?;
157 let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
158 AuthOps::sign_delegation_proof(SignDelegationProofInput {
159 audience: request.aud,
160 scopes: request.scopes,
161 shard_pid: request.shard_pid,
162 cert_ttl_secs: request.cert_ttl_secs,
163 max_token_ttl_secs,
164 max_cert_ttl_secs,
165 issued_at: IcOps::now_secs(),
166 })
167 .await
168 .map_err(Self::map_auth_error)
169 }
170
171 pub async fn request_role_attestation(
173 request: RoleAttestationRequest,
174 ) -> Result<SignedRoleAttestation, Error> {
175 let request = metadata::with_root_attestation_request_metadata(request);
176 let response = Self::request_role_attestation_remote(request).await?;
177
178 match response {
179 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
180 _ => Err(Error::internal(
181 "invalid root response type for role attestation request",
182 )),
183 }
184 }
185
186 pub async fn request_internal_invocation_proof(
188 request: InternalInvocationProofRequest,
189 ) -> Result<SignedInternalInvocationProofV1, Error> {
190 let request = metadata::with_internal_invocation_proof_request_metadata(request);
191 let response = Self::request_internal_invocation_proof_remote(request).await?;
192
193 match response {
194 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
195 _ => Err(Error::internal(
196 "invalid root response type for internal invocation proof request",
197 )),
198 }
199 }
200
201 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
203 AuthOps::attestation_key_set()
204 .await
205 .map_err(Self::map_auth_error)
206 }
207
208 pub async fn publish_root_auth_material() -> Result<(), Error> {
210 EnvOps::require_root().map_err(Error::from)?;
211 AuthOps::publish_root_auth_material().await.map_err(|err| {
212 log!(
213 Topic::Auth,
214 Warn,
215 "root auth material publish failed: {err}"
216 );
217 Self::map_auth_error(err)
218 })
219 }
220
221 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
223 AuthOps::replace_attestation_key_set(key_set);
224 }
225
226 pub async fn verify_role_attestation(
228 attestation: &SignedRoleAttestation,
229 min_accepted_epoch: u64,
230 ) -> Result<(), Error> {
231 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
232 .map_err(Error::from)?
233 .min_accepted_epoch_by_role
234 .get(attestation.payload.role.as_str())
235 .copied();
236 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
237 min_accepted_epoch,
238 configured_min_accepted_epoch,
239 );
240
241 let caller = IcOps::msg_caller();
242 let self_pid = IcOps::canister_self();
243 let now_secs = IcOps::now_secs();
244 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
245 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
246
247 let verify = || {
248 AuthOps::verify_role_attestation_cached(
249 attestation,
250 caller,
251 self_pid,
252 verifier_subnet,
253 now_secs,
254 min_accepted_epoch,
255 )
256 .map(|_| ())
257 };
258 let refresh = || async {
259 let key_set: AttestationKeySet =
260 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
261 AuthOps::replace_attestation_key_set(key_set);
262 Ok(())
263 };
264
265 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
266 Ok(()) => Ok(()),
267 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
268 verify_flow::record_attestation_verifier_rejection(&err);
269 verify_flow::log_attestation_verifier_rejection(
270 &err,
271 attestation,
272 caller,
273 self_pid,
274 "cached",
275 );
276 Err(Self::map_auth_error(err.into()))
277 }
278 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
279 verify_flow::record_attestation_verifier_rejection(&trigger);
280 verify_flow::log_attestation_verifier_rejection(
281 &trigger,
282 attestation,
283 caller,
284 self_pid,
285 "cache_miss_refresh",
286 );
287 record_attestation_refresh_failed();
288 log!(
289 Topic::Auth,
290 Warn,
291 "role attestation refresh failed local={} caller={} key_id={} error={}",
292 self_pid,
293 caller,
294 attestation.key_id,
295 source
296 );
297 Err(Self::map_auth_error(source))
298 }
299 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
300 verify_flow::record_attestation_verifier_rejection(&err);
301 verify_flow::log_attestation_verifier_rejection(
302 &err,
303 attestation,
304 caller,
305 self_pid,
306 "post_refresh",
307 );
308 Err(Self::map_auth_error(err.into()))
309 }
310 }
311 }
312
313 pub async fn verify_internal_invocation_proof(
315 proof: &SignedInternalInvocationProofV1,
316 target_method: &str,
317 accepted_roles: &[CanisterRole],
318 ) -> Result<(), Error> {
319 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
320 .map_err(Error::from)?
321 .min_accepted_epoch_by_role
322 .get(proof.payload.role.as_str())
323 .copied();
324 let min_accepted_epoch =
325 verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
326
327 let caller = IcOps::msg_caller();
328 let self_pid = IcOps::canister_self();
329 let now_secs = IcOps::now_secs();
330 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
331 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
332
333 let verify = || {
334 AuthOps::verify_internal_invocation_proof_cached(
335 proof,
336 crate::ops::auth::InternalInvocationProofVerificationInput {
337 caller,
338 self_pid,
339 target_method,
340 accepted_roles,
341 verifier_subnet,
342 now_secs,
343 min_accepted_epoch,
344 },
345 )
346 .map(|_| ())
347 };
348 let refresh = || async {
349 let key_set: AttestationKeySet =
350 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
351 AuthOps::replace_attestation_key_set(key_set);
352 Ok(())
353 };
354
355 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
356 Ok(()) => Ok(()),
357 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
358 verify_flow::record_attestation_verifier_rejection(&err);
359 log!(
360 Topic::Auth,
361 Warn,
362 "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
363 self_pid,
364 caller,
365 proof.payload.subject,
366 proof.payload.role,
367 proof.key_id,
368 proof.payload.audience,
369 proof.payload.audience_method,
370 proof.payload.epoch,
371 err
372 );
373 Err(Self::map_internal_invocation_verify_error(err))
374 }
375 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
376 verify_flow::record_attestation_verifier_rejection(&trigger);
377 record_attestation_refresh_failed();
378 log!(
379 Topic::Auth,
380 Warn,
381 "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
382 self_pid,
383 caller,
384 proof.key_id,
385 source
386 );
387 Err(Self::map_auth_error(source))
388 }
389 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
390 verify_flow::record_attestation_verifier_rejection(&err);
391 log!(
392 Topic::Auth,
393 Warn,
394 "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
395 self_pid,
396 caller,
397 proof.payload.subject,
398 proof.payload.role,
399 proof.key_id,
400 proof.payload.audience,
401 proof.payload.audience_method,
402 proof.payload.epoch,
403 err
404 );
405 Err(Self::map_internal_invocation_verify_error(err))
406 }
407 }
408 }
409
410 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
412 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
413 if !cfg.enabled {
414 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
415 }
416
417 Ok(cfg
418 .max_ttl_secs
419 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
420 }
421}
422
423impl AuthApi {
424 async fn request_delegation_remote(
426 request: DelegationProofIssueRequest,
427 ) -> Result<DelegationProof, Error> {
428 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
429 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
430 .await
431 .map_err(Self::map_auth_error)
432 }
433
434 pub async fn request_role_attestation_root(
436 request: RoleAttestationRequest,
437 ) -> Result<SignedRoleAttestation, Error> {
438 let request = metadata::with_root_attestation_request_metadata(request);
439 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
440 .await
441 .map_err(Self::map_auth_error)?;
442
443 match response {
444 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
445 _ => Err(Error::internal(
446 "invalid root response type for role attestation request",
447 )),
448 }
449 }
450
451 pub async fn request_internal_invocation_proof_root(
453 request: InternalInvocationProofRequest,
454 ) -> Result<SignedInternalInvocationProofV1, Error> {
455 let request = metadata::with_internal_invocation_proof_request_metadata(request);
456 let response =
457 RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
458 .await
459 .map_err(Self::map_auth_error)?;
460
461 match response {
462 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
463 _ => Err(Error::internal(
464 "invalid root response type for internal invocation proof request",
465 )),
466 }
467 }
468
469 async fn request_role_attestation_remote(
471 request: RoleAttestationRequest,
472 ) -> Result<RootCapabilityResponse, Error> {
473 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
474 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
475 .await
476 .map_err(Self::map_auth_error)
477 }
478
479 async fn request_internal_invocation_proof_remote(
481 request: InternalInvocationProofRequest,
482 ) -> Result<RootCapabilityResponse, Error> {
483 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
484 RpcOps::call_rpc_result(
485 root_pid,
486 protocol::CANIC_REQUEST_INTERNAL_INVOCATION_PROOF,
487 request,
488 )
489 .await
490 .map_err(Self::map_auth_error)
491 }
492}