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(
74 AuthExpiryError::AttestationExpired { .. }
75 | AuthExpiryError::AttestationNotYetValid { .. },
76 ) => Error::new(ErrorCode::AuthProofExpired, err.to_string()),
77 _ => Error::unauthorized(err.to_string()),
78 }
79 }
80
81 fn verify_token_material(
86 token: &DelegatedToken,
87 max_cert_ttl_secs: u64,
88 max_token_ttl_secs: u64,
89 required_scopes: &[String],
90 now_secs: u64,
91 ) -> Result<Principal, Error> {
92 AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
93 token,
94 max_cert_ttl_secs,
95 max_token_ttl_secs,
96 required_scopes,
97 now_secs,
98 })
99 .map(|verified| verified.subject)
100 .map_err(Self::map_auth_error)
101 }
102
103 pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
105 AuthOps::local_shard_public_key_sec1(IcOps::canister_self())
106 .await
107 .map_err(Self::map_auth_error)
108 }
109
110 pub async fn issue_token(request: DelegatedTokenIssueRequest) -> Result<DelegatedToken, Error> {
112 AuthOps::sign_token(SignDelegatedTokenInput {
113 proof: request.proof,
114 subject: request.subject,
115 audience: request.aud,
116 scopes: request.scopes,
117 ttl_secs: request.ttl_secs,
118 nonce: request.nonce,
119 })
120 .await
121 .map_err(Self::map_auth_error)
122 }
123
124 pub async fn mint_token(request: DelegatedTokenMintRequest) -> Result<DelegatedToken, Error> {
126 let proof = Self::request_delegation(DelegationProofIssueRequest {
127 shard_pid: IcOps::canister_self(),
128 scopes: request.scopes.clone(),
129 aud: request.aud.clone(),
130 cert_ttl_secs: request.cert_ttl_secs,
131 })
132 .await?;
133
134 Self::issue_token(DelegatedTokenIssueRequest {
135 proof,
136 subject: request.subject,
137 aud: request.aud,
138 scopes: request.scopes,
139 ttl_secs: request.token_ttl_secs,
140 nonce: request.nonce,
141 })
142 .await
143 }
144
145 pub async fn request_delegation(
147 request: DelegationProofIssueRequest,
148 ) -> Result<DelegationProof, Error> {
149 Self::request_delegation_remote(request).await
150 }
151
152 pub async fn issue_delegation_proof(
154 request: DelegationProofIssueRequest,
155 ) -> Result<DelegationProof, Error> {
156 EnvOps::require_root().map_err(Error::from)?;
157 Self::validate_delegation_request_caller(IcOps::msg_caller(), request.shard_pid)?;
158 let max_cert_ttl_secs = Self::delegated_token_max_ttl_secs()?;
159 let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
160 AuthOps::sign_delegation_proof(SignDelegationProofInput {
161 audience: request.aud,
162 scopes: request.scopes,
163 shard_pid: request.shard_pid,
164 cert_ttl_secs: request.cert_ttl_secs,
165 max_token_ttl_secs,
166 max_cert_ttl_secs,
167 issued_at: IcOps::now_secs(),
168 })
169 .await
170 .map_err(Self::map_auth_error)
171 }
172
173 pub async fn request_role_attestation(
175 request: RoleAttestationRequest,
176 ) -> Result<SignedRoleAttestation, Error> {
177 let request = metadata::with_root_attestation_request_metadata(request);
178 Self::request_role_attestation_remote(request).await
179 }
180
181 pub async fn request_internal_invocation_proof(
183 request: InternalInvocationProofRequest,
184 ) -> Result<SignedInternalInvocationProofV1, Error> {
185 let request = metadata::with_internal_invocation_proof_request_metadata(request);
186 Self::request_internal_invocation_proof_remote(request).await
187 }
188
189 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
191 AuthOps::attestation_key_set()
192 .await
193 .map_err(Self::map_auth_error)
194 }
195
196 pub async fn publish_root_auth_material() -> Result<(), Error> {
198 EnvOps::require_root().map_err(Error::from)?;
199 AuthOps::publish_root_auth_material().await.map_err(|err| {
200 log!(
201 Topic::Auth,
202 Warn,
203 "root auth material publish failed: {err}"
204 );
205 Self::map_auth_error(err)
206 })
207 }
208
209 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
211 AuthOps::replace_attestation_key_set(key_set);
212 }
213
214 pub async fn verify_role_attestation(
216 attestation: &SignedRoleAttestation,
217 min_accepted_epoch: u64,
218 ) -> Result<(), Error> {
219 crate::workflow::runtime::auth::RuntimeAuthWorkflow::verify_role_attestation(
220 attestation,
221 min_accepted_epoch,
222 )
223 .await
224 .map_err(Self::map_auth_error)
225 }
226
227 pub async fn verify_internal_invocation_proof(
229 proof: &SignedInternalInvocationProofV1,
230 target_method: &str,
231 accepted_roles: &[CanisterRole],
232 ) -> Result<(), Error> {
233 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
234 .map_err(Error::from)?
235 .min_accepted_epoch_by_role
236 .get(proof.payload.role.as_str())
237 .copied();
238 let min_accepted_epoch =
239 verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
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_internal_invocation_proof_cached(
249 proof,
250 crate::ops::auth::InternalInvocationProofVerificationInput {
251 caller,
252 self_pid,
253 target_method,
254 accepted_roles,
255 verifier_subnet,
256 now_secs,
257 min_accepted_epoch,
258 },
259 )
260 .map(|_| ())
261 };
262 let refresh = || async {
263 let key_set = RootAuthMaterialClient::new(root_pid)
264 .attestation_key_set()
265 .await?;
266 AuthOps::replace_attestation_key_set(key_set);
267 Ok(())
268 };
269
270 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
271 Ok(()) => Ok(()),
272 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
273 verify_flow::record_attestation_verifier_rejection(&err);
274 log!(
275 Topic::Auth,
276 Warn,
277 "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
278 self_pid,
279 caller,
280 proof.payload.subject,
281 proof.payload.role,
282 proof.key_id,
283 proof.payload.audience,
284 proof.payload.audience_method,
285 proof.payload.epoch,
286 err
287 );
288 Err(Self::map_internal_invocation_verify_error(err))
289 }
290 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
291 verify_flow::record_attestation_verifier_rejection(&trigger);
292 record_attestation_refresh_failed();
293 log!(
294 Topic::Auth,
295 Warn,
296 "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
297 self_pid,
298 caller,
299 proof.key_id,
300 source
301 );
302 Err(Self::map_auth_error(source))
303 }
304 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
305 verify_flow::record_attestation_verifier_rejection(&err);
306 log!(
307 Topic::Auth,
308 Warn,
309 "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
310 self_pid,
311 caller,
312 proof.payload.subject,
313 proof.payload.role,
314 proof.key_id,
315 proof.payload.audience,
316 proof.payload.audience_method,
317 proof.payload.epoch,
318 err
319 );
320 Err(Self::map_internal_invocation_verify_error(err))
321 }
322 }
323 }
324
325 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
327 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
328 if !cfg.enabled {
329 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
330 }
331
332 Ok(cfg
333 .max_ttl_secs
334 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
335 }
336
337 fn validate_delegation_request_caller(
338 caller: Principal,
339 shard_pid: Principal,
340 ) -> Result<(), Error> {
341 if caller == shard_pid {
342 return Ok(());
343 }
344
345 Err(Error::forbidden(format!(
346 "delegation request caller {caller} must match shard_pid {shard_pid}"
347 )))
348 }
349}
350
351impl AuthApi {
352 async fn request_delegation_remote(
354 request: DelegationProofIssueRequest,
355 ) -> Result<DelegationProof, Error> {
356 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
357 RootAuthMaterialClient::new(root_pid)
358 .request_delegation(request)
359 .await
360 .map_err(Self::map_auth_error)
361 }
362
363 pub async fn request_role_attestation_root(
365 request: RoleAttestationRequest,
366 ) -> Result<SignedRoleAttestation, Error> {
367 let request = metadata::with_root_attestation_request_metadata(request);
368 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
369 .await
370 .map_err(Self::map_auth_error)?;
371
372 match response {
373 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
374 _ => Err(Error::internal(
375 "invalid root response type for role attestation request",
376 )),
377 }
378 }
379
380 pub async fn request_internal_invocation_proof_root(
382 request: InternalInvocationProofRequest,
383 ) -> Result<SignedInternalInvocationProofV1, Error> {
384 let request = metadata::with_internal_invocation_proof_request_metadata(request);
385 let response =
386 RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
387 .await
388 .map_err(Self::map_auth_error)?;
389
390 match response {
391 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
392 _ => Err(Error::internal(
393 "invalid root response type for internal invocation proof request",
394 )),
395 }
396 }
397
398 async fn request_role_attestation_remote(
400 request: RoleAttestationRequest,
401 ) -> Result<SignedRoleAttestation, Error> {
402 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
403 RootAuthMaterialClient::new(root_pid)
404 .request_role_attestation(request)
405 .await
406 .map_err(Self::map_auth_error)
407 }
408
409 async fn request_internal_invocation_proof_remote(
411 request: InternalInvocationProofRequest,
412 ) -> Result<SignedInternalInvocationProofV1, Error> {
413 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
414 RootAuthMaterialClient::new(root_pid)
415 .request_internal_invocation_proof(request)
416 .await
417 .map_err(Self::map_auth_error)
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::AuthApi;
424 use crate::{
425 cdk::types::Principal,
426 dto::error::ErrorCode,
427 ops::auth::{AuthExpiryError, AuthOpsError},
428 };
429
430 fn p(id: u8) -> Principal {
431 Principal::from_slice(&[id; 29])
432 }
433
434 #[test]
435 fn internal_invocation_not_yet_valid_maps_to_non_retryable_proof_expiry() {
436 let err = AuthApi::map_internal_invocation_verify_error(AuthOpsError::Expiry(
437 AuthExpiryError::AttestationNotYetValid {
438 issued_at: 20,
439 now_secs: 10,
440 },
441 ));
442
443 assert_eq!(err.code, ErrorCode::AuthProofExpired);
444 }
445
446 #[test]
447 fn delegation_request_caller_must_match_requested_shard() {
448 AuthApi::validate_delegation_request_caller(p(2), p(2)).expect("matching shard");
449
450 let err = AuthApi::validate_delegation_request_caller(p(1), p(2))
451 .expect_err("mismatched caller must fail");
452
453 assert_eq!(err.code, ErrorCode::Forbidden);
454 assert!(err.message.contains("must match shard_pid"));
455 }
456}