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