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 Self::request_role_attestation_remote(request).await
177 }
178
179 pub async fn request_internal_invocation_proof(
181 request: InternalInvocationProofRequest,
182 ) -> Result<SignedInternalInvocationProofV1, Error> {
183 let request = metadata::with_internal_invocation_proof_request_metadata(request);
184 Self::request_internal_invocation_proof_remote(request).await
185 }
186
187 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
189 AuthOps::attestation_key_set()
190 .await
191 .map_err(Self::map_auth_error)
192 }
193
194 pub async fn publish_root_auth_material() -> Result<(), Error> {
196 EnvOps::require_root().map_err(Error::from)?;
197 AuthOps::publish_root_auth_material().await.map_err(|err| {
198 log!(
199 Topic::Auth,
200 Warn,
201 "root auth material publish failed: {err}"
202 );
203 Self::map_auth_error(err)
204 })
205 }
206
207 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
209 AuthOps::replace_attestation_key_set(key_set);
210 }
211
212 pub async fn verify_role_attestation(
214 attestation: &SignedRoleAttestation,
215 min_accepted_epoch: u64,
216 ) -> Result<(), Error> {
217 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
218 .map_err(Error::from)?
219 .min_accepted_epoch_by_role
220 .get(attestation.payload.role.as_str())
221 .copied();
222 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
223 min_accepted_epoch,
224 configured_min_accepted_epoch,
225 );
226
227 let caller = IcOps::msg_caller();
228 let self_pid = IcOps::canister_self();
229 let now_secs = IcOps::now_secs();
230 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
231 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
232
233 let verify = || {
234 AuthOps::verify_role_attestation_cached(
235 attestation,
236 caller,
237 self_pid,
238 verifier_subnet,
239 now_secs,
240 min_accepted_epoch,
241 )
242 .map(|_| ())
243 };
244 let refresh = || async {
245 let key_set: AttestationKeySet =
246 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
247 AuthOps::replace_attestation_key_set(key_set);
248 Ok(())
249 };
250
251 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
252 Ok(()) => Ok(()),
253 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
254 verify_flow::record_attestation_verifier_rejection(&err);
255 verify_flow::log_attestation_verifier_rejection(
256 &err,
257 attestation,
258 caller,
259 self_pid,
260 "cached",
261 );
262 Err(Self::map_auth_error(err.into()))
263 }
264 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
265 verify_flow::record_attestation_verifier_rejection(&trigger);
266 verify_flow::log_attestation_verifier_rejection(
267 &trigger,
268 attestation,
269 caller,
270 self_pid,
271 "cache_miss_refresh",
272 );
273 record_attestation_refresh_failed();
274 log!(
275 Topic::Auth,
276 Warn,
277 "role attestation refresh failed local={} caller={} key_id={} error={}",
278 self_pid,
279 caller,
280 attestation.key_id,
281 source
282 );
283 Err(Self::map_auth_error(source))
284 }
285 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
286 verify_flow::record_attestation_verifier_rejection(&err);
287 verify_flow::log_attestation_verifier_rejection(
288 &err,
289 attestation,
290 caller,
291 self_pid,
292 "post_refresh",
293 );
294 Err(Self::map_auth_error(err.into()))
295 }
296 }
297 }
298
299 pub async fn verify_internal_invocation_proof(
301 proof: &SignedInternalInvocationProofV1,
302 target_method: &str,
303 accepted_roles: &[CanisterRole],
304 ) -> Result<(), Error> {
305 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
306 .map_err(Error::from)?
307 .min_accepted_epoch_by_role
308 .get(proof.payload.role.as_str())
309 .copied();
310 let min_accepted_epoch =
311 verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
312
313 let caller = IcOps::msg_caller();
314 let self_pid = IcOps::canister_self();
315 let now_secs = IcOps::now_secs();
316 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
317 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
318
319 let verify = || {
320 AuthOps::verify_internal_invocation_proof_cached(
321 proof,
322 crate::ops::auth::InternalInvocationProofVerificationInput {
323 caller,
324 self_pid,
325 target_method,
326 accepted_roles,
327 verifier_subnet,
328 now_secs,
329 min_accepted_epoch,
330 },
331 )
332 .map(|_| ())
333 };
334 let refresh = || async {
335 let key_set: AttestationKeySet =
336 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
337 AuthOps::replace_attestation_key_set(key_set);
338 Ok(())
339 };
340
341 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
342 Ok(()) => Ok(()),
343 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
344 verify_flow::record_attestation_verifier_rejection(&err);
345 log!(
346 Topic::Auth,
347 Warn,
348 "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
349 self_pid,
350 caller,
351 proof.payload.subject,
352 proof.payload.role,
353 proof.key_id,
354 proof.payload.audience,
355 proof.payload.audience_method,
356 proof.payload.epoch,
357 err
358 );
359 Err(Self::map_internal_invocation_verify_error(err))
360 }
361 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
362 verify_flow::record_attestation_verifier_rejection(&trigger);
363 record_attestation_refresh_failed();
364 log!(
365 Topic::Auth,
366 Warn,
367 "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
368 self_pid,
369 caller,
370 proof.key_id,
371 source
372 );
373 Err(Self::map_auth_error(source))
374 }
375 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
376 verify_flow::record_attestation_verifier_rejection(&err);
377 log!(
378 Topic::Auth,
379 Warn,
380 "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
381 self_pid,
382 caller,
383 proof.payload.subject,
384 proof.payload.role,
385 proof.key_id,
386 proof.payload.audience,
387 proof.payload.audience_method,
388 proof.payload.epoch,
389 err
390 );
391 Err(Self::map_internal_invocation_verify_error(err))
392 }
393 }
394 }
395
396 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
398 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
399 if !cfg.enabled {
400 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
401 }
402
403 Ok(cfg
404 .max_ttl_secs
405 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
406 }
407}
408
409impl AuthApi {
410 async fn request_delegation_remote(
412 request: DelegationProofIssueRequest,
413 ) -> Result<DelegationProof, Error> {
414 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
415 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
416 .await
417 .map_err(Self::map_auth_error)
418 }
419
420 pub async fn request_role_attestation_root(
422 request: RoleAttestationRequest,
423 ) -> Result<SignedRoleAttestation, Error> {
424 let request = metadata::with_root_attestation_request_metadata(request);
425 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
426 .await
427 .map_err(Self::map_auth_error)?;
428
429 match response {
430 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
431 _ => Err(Error::internal(
432 "invalid root response type for role attestation request",
433 )),
434 }
435 }
436
437 pub async fn request_internal_invocation_proof_root(
439 request: InternalInvocationProofRequest,
440 ) -> Result<SignedInternalInvocationProofV1, Error> {
441 let request = metadata::with_internal_invocation_proof_request_metadata(request);
442 let response =
443 RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
444 .await
445 .map_err(Self::map_auth_error)?;
446
447 match response {
448 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
449 _ => Err(Error::internal(
450 "invalid root response type for internal invocation proof request",
451 )),
452 }
453 }
454
455 async fn request_role_attestation_remote(
457 request: RoleAttestationRequest,
458 ) -> Result<SignedRoleAttestation, Error> {
459 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
460 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
461 .await
462 .map_err(Self::map_auth_error)
463 }
464
465 async fn request_internal_invocation_proof_remote(
467 request: InternalInvocationProofRequest,
468 ) -> Result<SignedInternalInvocationProofV1, Error> {
469 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
470 RpcOps::call_rpc_result(
471 root_pid,
472 protocol::CANIC_REQUEST_INTERNAL_INVOCATION_PROOF,
473 request,
474 )
475 .await
476 .map_err(Self::map_auth_error)
477 }
478}