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 runtime::env::EnvOps,
25 runtime::metrics::auth::record_attestation_refresh_failed,
26 },
27 workflow::rpc::request::handler::RootResponseWorkflow,
28};
29use root_client::RootAuthMaterialClient;
30
31mod metadata;
36mod root_client;
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 = RootAuthMaterialClient::new(root_pid)
246 .attestation_key_set()
247 .await?;
248 AuthOps::replace_attestation_key_set(key_set);
249 Ok(())
250 };
251
252 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
253 Ok(()) => Ok(()),
254 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
255 verify_flow::record_attestation_verifier_rejection(&err);
256 verify_flow::log_attestation_verifier_rejection(
257 &err,
258 attestation,
259 caller,
260 self_pid,
261 "cached",
262 );
263 Err(Self::map_auth_error(err.into()))
264 }
265 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
266 verify_flow::record_attestation_verifier_rejection(&trigger);
267 verify_flow::log_attestation_verifier_rejection(
268 &trigger,
269 attestation,
270 caller,
271 self_pid,
272 "cache_miss_refresh",
273 );
274 record_attestation_refresh_failed();
275 log!(
276 Topic::Auth,
277 Warn,
278 "role attestation refresh failed local={} caller={} key_id={} error={}",
279 self_pid,
280 caller,
281 attestation.key_id,
282 source
283 );
284 Err(Self::map_auth_error(source))
285 }
286 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
287 verify_flow::record_attestation_verifier_rejection(&err);
288 verify_flow::log_attestation_verifier_rejection(
289 &err,
290 attestation,
291 caller,
292 self_pid,
293 "post_refresh",
294 );
295 Err(Self::map_auth_error(err.into()))
296 }
297 }
298 }
299
300 pub async fn verify_internal_invocation_proof(
302 proof: &SignedInternalInvocationProofV1,
303 target_method: &str,
304 accepted_roles: &[CanisterRole],
305 ) -> Result<(), Error> {
306 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
307 .map_err(Error::from)?
308 .min_accepted_epoch_by_role
309 .get(proof.payload.role.as_str())
310 .copied();
311 let min_accepted_epoch =
312 verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
313
314 let caller = IcOps::msg_caller();
315 let self_pid = IcOps::canister_self();
316 let now_secs = IcOps::now_secs();
317 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
318 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
319
320 let verify = || {
321 AuthOps::verify_internal_invocation_proof_cached(
322 proof,
323 crate::ops::auth::InternalInvocationProofVerificationInput {
324 caller,
325 self_pid,
326 target_method,
327 accepted_roles,
328 verifier_subnet,
329 now_secs,
330 min_accepted_epoch,
331 },
332 )
333 .map(|_| ())
334 };
335 let refresh = || async {
336 let key_set = RootAuthMaterialClient::new(root_pid)
337 .attestation_key_set()
338 .await?;
339 AuthOps::replace_attestation_key_set(key_set);
340 Ok(())
341 };
342
343 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
344 Ok(()) => Ok(()),
345 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
346 verify_flow::record_attestation_verifier_rejection(&err);
347 log!(
348 Topic::Auth,
349 Warn,
350 "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
351 self_pid,
352 caller,
353 proof.payload.subject,
354 proof.payload.role,
355 proof.key_id,
356 proof.payload.audience,
357 proof.payload.audience_method,
358 proof.payload.epoch,
359 err
360 );
361 Err(Self::map_internal_invocation_verify_error(err))
362 }
363 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
364 verify_flow::record_attestation_verifier_rejection(&trigger);
365 record_attestation_refresh_failed();
366 log!(
367 Topic::Auth,
368 Warn,
369 "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
370 self_pid,
371 caller,
372 proof.key_id,
373 source
374 );
375 Err(Self::map_auth_error(source))
376 }
377 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
378 verify_flow::record_attestation_verifier_rejection(&err);
379 log!(
380 Topic::Auth,
381 Warn,
382 "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
383 self_pid,
384 caller,
385 proof.payload.subject,
386 proof.payload.role,
387 proof.key_id,
388 proof.payload.audience,
389 proof.payload.audience_method,
390 proof.payload.epoch,
391 err
392 );
393 Err(Self::map_internal_invocation_verify_error(err))
394 }
395 }
396 }
397
398 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
400 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
401 if !cfg.enabled {
402 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
403 }
404
405 Ok(cfg
406 .max_ttl_secs
407 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
408 }
409}
410
411impl AuthApi {
412 async fn request_delegation_remote(
414 request: DelegationProofIssueRequest,
415 ) -> Result<DelegationProof, Error> {
416 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
417 RootAuthMaterialClient::new(root_pid)
418 .request_delegation(request)
419 .await
420 .map_err(Self::map_auth_error)
421 }
422
423 pub async fn request_role_attestation_root(
425 request: RoleAttestationRequest,
426 ) -> Result<SignedRoleAttestation, Error> {
427 let request = metadata::with_root_attestation_request_metadata(request);
428 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
429 .await
430 .map_err(Self::map_auth_error)?;
431
432 match response {
433 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
434 _ => Err(Error::internal(
435 "invalid root response type for role attestation request",
436 )),
437 }
438 }
439
440 pub async fn request_internal_invocation_proof_root(
442 request: InternalInvocationProofRequest,
443 ) -> Result<SignedInternalInvocationProofV1, Error> {
444 let request = metadata::with_internal_invocation_proof_request_metadata(request);
445 let response =
446 RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
447 .await
448 .map_err(Self::map_auth_error)?;
449
450 match response {
451 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
452 _ => Err(Error::internal(
453 "invalid root response type for internal invocation proof request",
454 )),
455 }
456 }
457
458 async fn request_role_attestation_remote(
460 request: RoleAttestationRequest,
461 ) -> Result<SignedRoleAttestation, Error> {
462 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
463 RootAuthMaterialClient::new(root_pid)
464 .request_role_attestation(request)
465 .await
466 .map_err(Self::map_auth_error)
467 }
468
469 async fn request_internal_invocation_proof_remote(
471 request: InternalInvocationProofRequest,
472 ) -> Result<SignedInternalInvocationProofV1, Error> {
473 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
474 RootAuthMaterialClient::new(root_pid)
475 .request_internal_invocation_proof(request)
476 .await
477 .map_err(Self::map_auth_error)
478 }
479}