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 let max_cert_ttl_secs = Self::delegated_token_max_ttl_secs()?;
158 let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
159 AuthOps::sign_delegation_proof(SignDelegationProofInput {
160 audience: request.aud,
161 scopes: request.scopes,
162 shard_pid: request.shard_pid,
163 cert_ttl_secs: request.cert_ttl_secs,
164 max_token_ttl_secs,
165 max_cert_ttl_secs,
166 issued_at: IcOps::now_secs(),
167 })
168 .await
169 .map_err(Self::map_auth_error)
170 }
171
172 pub async fn request_role_attestation(
174 request: RoleAttestationRequest,
175 ) -> Result<SignedRoleAttestation, Error> {
176 let request = metadata::with_root_attestation_request_metadata(request);
177 Self::request_role_attestation_remote(request).await
178 }
179
180 pub async fn request_internal_invocation_proof(
182 request: InternalInvocationProofRequest,
183 ) -> Result<SignedInternalInvocationProofV1, Error> {
184 let request = metadata::with_internal_invocation_proof_request_metadata(request);
185 Self::request_internal_invocation_proof_remote(request).await
186 }
187
188 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
190 AuthOps::attestation_key_set()
191 .await
192 .map_err(Self::map_auth_error)
193 }
194
195 pub async fn publish_root_auth_material() -> Result<(), Error> {
197 EnvOps::require_root().map_err(Error::from)?;
198 AuthOps::publish_root_auth_material().await.map_err(|err| {
199 log!(
200 Topic::Auth,
201 Warn,
202 "root auth material publish failed: {err}"
203 );
204 Self::map_auth_error(err)
205 })
206 }
207
208 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
210 AuthOps::replace_attestation_key_set(key_set);
211 }
212
213 pub async fn verify_role_attestation(
215 attestation: &SignedRoleAttestation,
216 min_accepted_epoch: u64,
217 ) -> Result<(), Error> {
218 crate::workflow::runtime::auth::RuntimeAuthWorkflow::verify_role_attestation(
219 attestation,
220 min_accepted_epoch,
221 )
222 .await
223 .map_err(Self::map_auth_error)
224 }
225
226 pub async fn verify_internal_invocation_proof(
228 proof: &SignedInternalInvocationProofV1,
229 target_method: &str,
230 accepted_roles: &[CanisterRole],
231 ) -> Result<(), Error> {
232 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
233 .map_err(Error::from)?
234 .min_accepted_epoch_by_role
235 .get(proof.payload.role.as_str())
236 .copied();
237 let min_accepted_epoch =
238 verify_flow::resolve_min_accepted_epoch(0, configured_min_accepted_epoch);
239
240 let caller = IcOps::msg_caller();
241 let self_pid = IcOps::canister_self();
242 let now_secs = IcOps::now_secs();
243 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
244 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
245
246 let verify = || {
247 AuthOps::verify_internal_invocation_proof_cached(
248 proof,
249 crate::ops::auth::InternalInvocationProofVerificationInput {
250 caller,
251 self_pid,
252 target_method,
253 accepted_roles,
254 verifier_subnet,
255 now_secs,
256 min_accepted_epoch,
257 },
258 )
259 .map(|_| ())
260 };
261 let refresh = || async {
262 let key_set = RootAuthMaterialClient::new(root_pid)
263 .attestation_key_set()
264 .await?;
265 AuthOps::replace_attestation_key_set(key_set);
266 Ok(())
267 };
268
269 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
270 Ok(()) => Ok(()),
271 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
272 verify_flow::record_attestation_verifier_rejection(&err);
273 log!(
274 Topic::Auth,
275 Warn,
276 "internal invocation proof rejected phase=cached local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
277 self_pid,
278 caller,
279 proof.payload.subject,
280 proof.payload.role,
281 proof.key_id,
282 proof.payload.audience,
283 proof.payload.audience_method,
284 proof.payload.epoch,
285 err
286 );
287 Err(Self::map_internal_invocation_verify_error(err))
288 }
289 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
290 verify_flow::record_attestation_verifier_rejection(&trigger);
291 record_attestation_refresh_failed();
292 log!(
293 Topic::Auth,
294 Warn,
295 "internal invocation proof refresh failed local={} caller={} key_id={} error={}",
296 self_pid,
297 caller,
298 proof.key_id,
299 source
300 );
301 Err(Self::map_auth_error(source))
302 }
303 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
304 verify_flow::record_attestation_verifier_rejection(&err);
305 log!(
306 Topic::Auth,
307 Warn,
308 "internal invocation proof rejected phase=post_refresh local={} caller={} subject={} role={} key_id={} audience={} method={} epoch={} error={}",
309 self_pid,
310 caller,
311 proof.payload.subject,
312 proof.payload.role,
313 proof.key_id,
314 proof.payload.audience,
315 proof.payload.audience_method,
316 proof.payload.epoch,
317 err
318 );
319 Err(Self::map_internal_invocation_verify_error(err))
320 }
321 }
322 }
323
324 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
326 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
327 if !cfg.enabled {
328 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
329 }
330
331 Ok(cfg
332 .max_ttl_secs
333 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
334 }
335}
336
337impl AuthApi {
338 async fn request_delegation_remote(
340 request: DelegationProofIssueRequest,
341 ) -> Result<DelegationProof, Error> {
342 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
343 RootAuthMaterialClient::new(root_pid)
344 .request_delegation(request)
345 .await
346 .map_err(Self::map_auth_error)
347 }
348
349 pub async fn request_role_attestation_root(
351 request: RoleAttestationRequest,
352 ) -> Result<SignedRoleAttestation, Error> {
353 let request = metadata::with_root_attestation_request_metadata(request);
354 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
355 .await
356 .map_err(Self::map_auth_error)?;
357
358 match response {
359 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
360 _ => Err(Error::internal(
361 "invalid root response type for role attestation request",
362 )),
363 }
364 }
365
366 pub async fn request_internal_invocation_proof_root(
368 request: InternalInvocationProofRequest,
369 ) -> Result<SignedInternalInvocationProofV1, Error> {
370 let request = metadata::with_internal_invocation_proof_request_metadata(request);
371 let response =
372 RootResponseWorkflow::response(RootRequest::issue_internal_invocation_proof(request))
373 .await
374 .map_err(Self::map_auth_error)?;
375
376 match response {
377 RootCapabilityResponse::InternalInvocationProofIssued(response) => Ok(response),
378 _ => Err(Error::internal(
379 "invalid root response type for internal invocation proof request",
380 )),
381 }
382 }
383
384 async fn request_role_attestation_remote(
386 request: RoleAttestationRequest,
387 ) -> Result<SignedRoleAttestation, Error> {
388 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
389 RootAuthMaterialClient::new(root_pid)
390 .request_role_attestation(request)
391 .await
392 .map_err(Self::map_auth_error)
393 }
394
395 async fn request_internal_invocation_proof_remote(
397 request: InternalInvocationProofRequest,
398 ) -> Result<SignedInternalInvocationProofV1, Error> {
399 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
400 RootAuthMaterialClient::new(root_pid)
401 .request_internal_invocation_proof(request)
402 .await
403 .map_err(Self::map_auth_error)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::AuthApi;
410 use crate::{
411 dto::error::ErrorCode,
412 ops::auth::{AuthExpiryError, AuthOpsError},
413 };
414
415 #[test]
416 fn internal_invocation_not_yet_valid_maps_to_non_retryable_proof_expiry() {
417 let err = AuthApi::map_internal_invocation_verify_error(AuthOpsError::Expiry(
418 AuthExpiryError::AttestationNotYetValid {
419 issued_at: 20,
420 now_secs: 10,
421 },
422 ));
423
424 assert_eq!(err.code, ErrorCode::AuthProofExpired);
425 }
426}