1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenIssueRequest,
6 DelegatedTokenMintRequest, DelegationProof, DelegationProofIssueRequest,
7 RoleAttestationRequest, SignedRoleAttestation,
8 },
9 error::Error,
10 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
11 },
12 error::InternalErrorClass,
13 log,
14 log::Topic,
15 ops::{
16 auth::{
17 AuthOps, SignDelegatedTokenInput, SignDelegationProofInput,
18 VerifyDelegatedTokenRuntimeInput,
19 },
20 config::ConfigOps,
21 ic::IcOps,
22 rpc::RpcOps,
23 runtime::env::EnvOps,
24 runtime::metrics::auth::record_attestation_refresh_failed,
25 },
26 protocol,
27 workflow::rpc::request::handler::RootResponseWorkflow,
28};
29
30mod metadata;
35mod session;
36mod verify_flow;
37
38pub struct AuthApi;
45
46impl AuthApi {
47 const DELEGATED_TOKENS_DISABLED: &str =
48 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
49 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
50 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
51 b"canic-session-bootstrap-token-fingerprint";
52
53 fn map_auth_error(err: crate::InternalError) -> Error {
55 match err.class() {
56 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
57 Error::internal(err.to_string())
58 }
59 _ => Error::from(err),
60 }
61 }
62
63 fn verify_token_material(
68 token: &DelegatedToken,
69 max_cert_ttl_secs: u64,
70 max_token_ttl_secs: u64,
71 required_scopes: &[String],
72 now_secs: u64,
73 ) -> Result<Principal, Error> {
74 AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
75 token,
76 max_cert_ttl_secs,
77 max_token_ttl_secs,
78 required_scopes,
79 now_secs,
80 })
81 .map(|verified| verified.subject)
82 .map_err(Self::map_auth_error)
83 }
84
85 pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
87 AuthOps::local_shard_public_key_sec1(IcOps::canister_self())
88 .await
89 .map_err(Self::map_auth_error)
90 }
91
92 pub async fn issue_token(request: DelegatedTokenIssueRequest) -> Result<DelegatedToken, Error> {
94 AuthOps::sign_token(SignDelegatedTokenInput {
95 proof: request.proof,
96 subject: request.subject,
97 audience: request.aud,
98 scopes: request.scopes,
99 ttl_secs: request.ttl_secs,
100 nonce: request.nonce,
101 })
102 .await
103 .map_err(Self::map_auth_error)
104 }
105
106 pub async fn mint_token(request: DelegatedTokenMintRequest) -> Result<DelegatedToken, Error> {
108 let proof = Self::request_delegation(DelegationProofIssueRequest {
109 shard_pid: IcOps::canister_self(),
110 scopes: request.scopes.clone(),
111 aud: request.aud.clone(),
112 cert_ttl_secs: request.cert_ttl_secs,
113 })
114 .await?;
115
116 Self::issue_token(DelegatedTokenIssueRequest {
117 proof,
118 subject: request.subject,
119 aud: request.aud,
120 scopes: request.scopes,
121 ttl_secs: request.token_ttl_secs,
122 nonce: request.nonce,
123 })
124 .await
125 }
126
127 pub async fn request_delegation(
129 request: DelegationProofIssueRequest,
130 ) -> Result<DelegationProof, Error> {
131 Self::request_delegation_remote(request).await
132 }
133
134 pub async fn issue_delegation_proof(
136 request: DelegationProofIssueRequest,
137 ) -> Result<DelegationProof, Error> {
138 EnvOps::require_root().map_err(Error::from)?;
139 let max_cert_ttl_secs = Self::delegated_token_max_ttl_secs()?;
140 let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
141 AuthOps::sign_delegation_proof(SignDelegationProofInput {
142 audience: request.aud,
143 scopes: request.scopes,
144 shard_pid: request.shard_pid,
145 cert_ttl_secs: request.cert_ttl_secs,
146 max_token_ttl_secs,
147 max_cert_ttl_secs,
148 issued_at: IcOps::now_secs(),
149 })
150 .await
151 .map_err(Self::map_auth_error)
152 }
153
154 pub async fn request_role_attestation(
156 request: RoleAttestationRequest,
157 ) -> Result<SignedRoleAttestation, Error> {
158 let request = metadata::with_root_attestation_request_metadata(request);
159 let response = Self::request_role_attestation_remote(request).await?;
160
161 match response {
162 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
163 _ => Err(Error::internal(
164 "invalid root response type for role attestation request",
165 )),
166 }
167 }
168
169 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
171 AuthOps::attestation_key_set()
172 .await
173 .map_err(Self::map_auth_error)
174 }
175
176 pub async fn publish_root_auth_material() -> Result<(), Error> {
178 EnvOps::require_root().map_err(Error::from)?;
179 AuthOps::publish_root_auth_material().await.map_err(|err| {
180 log!(
181 Topic::Auth,
182 Warn,
183 "root auth material publish failed: {err}"
184 );
185 Self::map_auth_error(err)
186 })
187 }
188
189 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
191 AuthOps::replace_attestation_key_set(key_set);
192 }
193
194 pub async fn verify_role_attestation(
196 attestation: &SignedRoleAttestation,
197 min_accepted_epoch: u64,
198 ) -> Result<(), Error> {
199 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
200 .map_err(Error::from)?
201 .min_accepted_epoch_by_role
202 .get(attestation.payload.role.as_str())
203 .copied();
204 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
205 min_accepted_epoch,
206 configured_min_accepted_epoch,
207 );
208
209 let caller = IcOps::msg_caller();
210 let self_pid = IcOps::canister_self();
211 let now_secs = IcOps::now_secs();
212 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
213 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
214
215 let verify = || {
216 AuthOps::verify_role_attestation_cached(
217 attestation,
218 caller,
219 self_pid,
220 verifier_subnet,
221 now_secs,
222 min_accepted_epoch,
223 )
224 .map(|_| ())
225 };
226 let refresh = || async {
227 let key_set: AttestationKeySet =
228 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
229 AuthOps::replace_attestation_key_set(key_set);
230 Ok(())
231 };
232
233 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
234 Ok(()) => Ok(()),
235 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
236 verify_flow::record_attestation_verifier_rejection(&err);
237 verify_flow::log_attestation_verifier_rejection(
238 &err,
239 attestation,
240 caller,
241 self_pid,
242 "cached",
243 );
244 Err(Self::map_auth_error(err.into()))
245 }
246 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
247 verify_flow::record_attestation_verifier_rejection(&trigger);
248 verify_flow::log_attestation_verifier_rejection(
249 &trigger,
250 attestation,
251 caller,
252 self_pid,
253 "cache_miss_refresh",
254 );
255 record_attestation_refresh_failed();
256 log!(
257 Topic::Auth,
258 Warn,
259 "role attestation refresh failed local={} caller={} key_id={} error={}",
260 self_pid,
261 caller,
262 attestation.key_id,
263 source
264 );
265 Err(Self::map_auth_error(source))
266 }
267 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(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 "post_refresh",
275 );
276 Err(Self::map_auth_error(err.into()))
277 }
278 }
279 }
280
281 fn delegated_token_max_ttl_secs() -> Result<u64, Error> {
283 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
284 if !cfg.enabled {
285 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
286 }
287
288 Ok(cfg
289 .max_ttl_secs
290 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
291 }
292}
293
294impl AuthApi {
295 async fn request_delegation_remote(
297 request: DelegationProofIssueRequest,
298 ) -> Result<DelegationProof, Error> {
299 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
300 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
301 .await
302 .map_err(Self::map_auth_error)
303 }
304
305 pub async fn request_role_attestation_root(
307 request: RoleAttestationRequest,
308 ) -> Result<SignedRoleAttestation, Error> {
309 let request = metadata::with_root_attestation_request_metadata(request);
310 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
311 .await
312 .map_err(Self::map_auth_error)?;
313
314 match response {
315 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
316 _ => Err(Error::internal(
317 "invalid root response type for role attestation request",
318 )),
319 }
320 }
321
322 async fn request_role_attestation_remote(
324 request: RoleAttestationRequest,
325 ) -> Result<RootCapabilityResponse, Error> {
326 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
327 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
328 .await
329 .map_err(Self::map_auth_error)
330 }
331}