1use crate::{
8 cdk::types::Principal,
9 domain::policy::auth::{
10 RootDelegatedRoleGrantPolicy, RootDelegationAudiencePolicy, RootIssuerPolicy,
11 },
12 dto::{
13 auth::{
14 ActiveDelegationProofStatusResponse, DelegatedRoleGrant, DelegatedToken,
15 DelegatedTokenGetRequest, DelegatedTokenPrepareRequest, DelegatedTokenPrepareResponse,
16 DelegationAudience, InstallActiveDelegationProofRequest,
17 InstallActiveDelegationProofResponse, RoleAttestationGetRequest,
18 RoleAttestationPrepareResponse, RoleAttestationRequest,
19 RootDelegationProofBatchGetRequest, RootDelegationProofBatchGetResponse,
20 RootDelegationProofBatchInstallRequest, RootDelegationProofBatchInstallResponse,
21 RootDelegationProofBatchPrepareRequest, RootDelegationProofBatchPrepareResponse,
22 RootIssuerPolicyResponse, RootIssuerPolicyUpsertRequest, RootIssuerPolicyView,
23 SignedRoleAttestation,
24 },
25 error::Error,
26 },
27 error::InternalErrorClass,
28 ops::{
29 auth::{AuthOps, VerifyDelegatedTokenRuntimeInput},
30 config::ConfigOps,
31 ic::IcOps,
32 runtime::env::EnvOps,
33 storage::auth::AuthStateOps,
34 },
35 workflow::runtime::auth::RuntimeAuthWorkflow,
36};
37
38mod session;
41
42pub struct AuthApi;
50
51impl AuthApi {
52 const DELEGATED_TOKENS_DISABLED: &str =
53 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
54 const DELEGATED_TOKEN_ISSUER_DISABLED: &str = "delegated token issuer disabled for this canister; set subnets.<subnet>.canisters.<role>.auth.delegated_token_issuer=true in canic.toml";
55 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
56 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
57 b"canic-session-bootstrap-token-fingerprint";
58
59 fn map_auth_error(err: crate::InternalError) -> Error {
61 match err.class() {
62 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
63 Error::internal(err.to_string())
64 }
65 _ => Error::from(err),
66 }
67 }
68
69 fn require_delegated_token_issuer_enabled() -> Result<(), Error> {
70 let delegated_tokens_cfg =
71 ConfigOps::delegated_tokens_config().map_err(Self::map_auth_error)?;
72 if !delegated_tokens_cfg.enabled {
73 return Err(Error::invalid(Self::DELEGATED_TOKENS_DISABLED));
74 }
75
76 let canister_cfg = ConfigOps::current_canister().map_err(Self::map_auth_error)?;
77 if !canister_cfg.auth.delegated_token_issuer {
78 return Err(Error::forbidden(Self::DELEGATED_TOKEN_ISSUER_DISABLED));
79 }
80
81 Ok(())
82 }
83
84 fn verify_token_material(
89 token: &DelegatedToken,
90 max_cert_ttl_ns: u64,
91 max_token_ttl_ns: u64,
92 required_scopes: &[String],
93 now_ns: u64,
94 ) -> Result<Principal, Error> {
95 AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
96 token,
97 caller: IcOps::msg_caller(),
98 max_cert_ttl_ns,
99 max_token_ttl_ns,
100 required_scopes,
101 now_ns,
102 })
103 .map(|verified| verified.subject)
104 .map_err(Self::map_auth_error)
105 }
106
107 pub fn prepare_delegated_token(
109 request: DelegatedTokenPrepareRequest,
110 ) -> Result<DelegatedTokenPrepareResponse, Error> {
111 Self::require_delegated_token_issuer_enabled()?;
112 RuntimeAuthWorkflow::prepare_delegated_token(request).map_err(Self::map_auth_error)
113 }
114
115 pub fn get_delegated_token(request: DelegatedTokenGetRequest) -> Result<DelegatedToken, Error> {
117 Self::require_delegated_token_issuer_enabled()?;
118
119 AuthOps::get_delegated_token_issuer_proof(request.claims_hash, IcOps::msg_caller())
120 .map_err(Self::map_auth_error)
121 }
122
123 pub fn install_active_delegation_proof(
125 request: InstallActiveDelegationProofRequest,
126 ) -> Result<InstallActiveDelegationProofResponse, Error> {
127 Self::require_delegated_token_issuer_enabled()?;
128
129 let active_proof =
130 AuthOps::install_active_delegation_proof(request.proof, IcOps::msg_caller())
131 .map_err(Self::map_auth_error)?;
132
133 Ok(InstallActiveDelegationProofResponse { active_proof })
134 }
135
136 pub fn active_delegation_proof_status() -> Result<ActiveDelegationProofStatusResponse, Error> {
138 Self::require_delegated_token_issuer_enabled()?;
139 Ok(AuthOps::active_delegation_proof_status(IcOps::now_nanos()))
140 }
141
142 pub fn upsert_root_issuer_policy_root(
144 request: RootIssuerPolicyUpsertRequest,
145 ) -> Result<RootIssuerPolicyResponse, Error> {
146 EnvOps::require_root().map_err(Error::from)?;
147 validate_root_issuer_policy_upsert_request(&request)?;
148
149 let policy = root_issuer_policy_from_request(request);
150 AuthStateOps::upsert_root_issuer_policy(policy.clone());
151
152 Ok(RootIssuerPolicyResponse {
153 issuer: root_issuer_policy_view(&policy),
154 })
155 }
156
157 #[cfg(canic_test_delegation_material)]
159 pub fn test_upsert_root_issuer_policy(
160 issuer_pid: Principal,
161 allowed_audiences: Vec<DelegationAudience>,
162 allowed_grants: Vec<DelegatedRoleGrant>,
163 max_cert_ttl_ns: u64,
164 refresh_after_ratio_bps: u16,
165 ) -> Result<(), Error> {
166 Self::upsert_root_issuer_policy_root(RootIssuerPolicyUpsertRequest {
167 issuer_pid,
168 enabled: true,
169 allowed_audiences,
170 allowed_grants,
171 max_cert_ttl_ns,
172 refresh_after_ratio_bps,
173 })
174 .map(|_| ())
175 }
176
177 pub fn prepare_delegation_proof_batch_root(
179 request: RootDelegationProofBatchPrepareRequest,
180 ) -> Result<RootDelegationProofBatchPrepareResponse, Error> {
181 EnvOps::require_root().map_err(Error::from)?;
182 let max_cert_ttl_ns = Self::delegated_token_max_ttl_ns()?;
183 AuthOps::prepare_delegation_proof_batch(request, max_cert_ttl_ns, IcOps::now_nanos())
184 .map_err(Self::map_auth_error)
185 }
186
187 pub fn get_delegation_proof_batch_root(
189 request: RootDelegationProofBatchGetRequest,
190 ) -> Result<RootDelegationProofBatchGetResponse, Error> {
191 EnvOps::require_root().map_err(Error::from)?;
192 AuthOps::get_delegation_proof_batch(request).map_err(Self::map_auth_error)
193 }
194
195 pub async fn install_delegation_proof_batch_root(
197 request: RootDelegationProofBatchInstallRequest,
198 ) -> Result<RootDelegationProofBatchInstallResponse, Error> {
199 EnvOps::require_root().map_err(Error::from)?;
200 RuntimeAuthWorkflow::install_delegation_proof_batch_root(request)
201 .await
202 .map_err(Self::map_auth_error)
203 }
204
205 pub fn prepare_role_attestation_root(
207 request: RoleAttestationRequest,
208 ) -> Result<RoleAttestationPrepareResponse, Error> {
209 RuntimeAuthWorkflow::prepare_role_attestation_root(request).map_err(Self::map_auth_error)
210 }
211
212 pub fn get_role_attestation_root(
214 request: RoleAttestationGetRequest,
215 ) -> Result<SignedRoleAttestation, Error> {
216 EnvOps::require_root().map_err(Error::from)?;
217 AuthOps::get_role_attestation(IcOps::msg_caller(), request.payload_hash)
218 .map_err(Self::map_auth_error)
219 }
220
221 pub async fn verify_role_attestation(
223 attestation: &SignedRoleAttestation,
224 min_accepted_epoch: u64,
225 ) -> Result<(), Error> {
226 crate::workflow::runtime::auth::RuntimeAuthWorkflow::verify_role_attestation(
227 attestation,
228 min_accepted_epoch,
229 )
230 .await
231 .map_err(Self::map_auth_error)
232 }
233
234 fn delegated_token_max_ttl_ns() -> Result<u64, Error> {
236 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
237 if !cfg.enabled {
238 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
239 }
240
241 let max_ttl_secs = cfg
242 .max_ttl_secs
243 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
244 max_ttl_secs.checked_mul(1_000_000_000).ok_or_else(|| {
245 Error::invalid("auth.delegated_tokens.max_ttl_secs overflows nanoseconds")
246 })
247 }
248}
249
250fn validate_root_issuer_policy_upsert_request(
251 request: &RootIssuerPolicyUpsertRequest,
252) -> Result<(), Error> {
253 if request.max_cert_ttl_ns == 0 {
254 return Err(Error::invalid(
255 "root issuer max certificate TTL must be greater than zero",
256 ));
257 }
258 if request.refresh_after_ratio_bps == 0 || request.refresh_after_ratio_bps >= 10_000 {
259 return Err(Error::invalid(
260 "root issuer refresh ratio must be between 1 and 9999 basis points",
261 ));
262 }
263 if request.enabled && request.allowed_audiences.is_empty() {
264 return Err(Error::invalid(
265 "enabled root issuer policy must allow at least one audience",
266 ));
267 }
268 if request.enabled && request.allowed_grants.is_empty() {
269 return Err(Error::invalid(
270 "enabled root issuer policy must allow at least one grant",
271 ));
272 }
273 Ok(())
274}
275
276fn root_issuer_policy_from_request(request: RootIssuerPolicyUpsertRequest) -> RootIssuerPolicy {
277 RootIssuerPolicy {
278 issuer_pid: request.issuer_pid,
279 enabled: request.enabled,
280 allowed_audiences: request
281 .allowed_audiences
282 .iter()
283 .map(root_delegation_audience_policy)
284 .collect(),
285 allowed_grants: request
286 .allowed_grants
287 .iter()
288 .map(root_delegated_role_grant_policy)
289 .collect(),
290 max_cert_ttl_ns: request.max_cert_ttl_ns,
291 refresh_after_ratio_bps: request.refresh_after_ratio_bps,
292 }
293}
294
295fn root_issuer_policy_view(policy: &RootIssuerPolicy) -> RootIssuerPolicyView {
296 RootIssuerPolicyView {
297 issuer_pid: policy.issuer_pid,
298 enabled: policy.enabled,
299 allowed_audiences: policy
300 .allowed_audiences
301 .iter()
302 .map(root_delegation_audience_view)
303 .collect(),
304 allowed_grants: policy
305 .allowed_grants
306 .iter()
307 .map(root_delegated_role_grant_view)
308 .collect(),
309 max_cert_ttl_ns: policy.max_cert_ttl_ns,
310 refresh_after_ratio_bps: policy.refresh_after_ratio_bps,
311 }
312}
313
314fn root_delegation_audience_policy(audience: &DelegationAudience) -> RootDelegationAudiencePolicy {
315 match audience {
316 DelegationAudience::Canister(canister) => RootDelegationAudiencePolicy::Canister(*canister),
317 DelegationAudience::CanicSubnet(subnet) => {
318 RootDelegationAudiencePolicy::CanicSubnet(*subnet)
319 }
320 DelegationAudience::Project(project) => {
321 RootDelegationAudiencePolicy::Project(project.clone())
322 }
323 }
324}
325
326fn root_delegated_role_grant_policy(grant: &DelegatedRoleGrant) -> RootDelegatedRoleGrantPolicy {
327 RootDelegatedRoleGrantPolicy {
328 target: grant.target.clone(),
329 scopes: grant.scopes.clone(),
330 }
331}
332
333fn root_delegation_audience_view(policy: &RootDelegationAudiencePolicy) -> DelegationAudience {
334 match policy {
335 RootDelegationAudiencePolicy::Canister(canister) => DelegationAudience::Canister(*canister),
336 RootDelegationAudiencePolicy::CanicSubnet(subnet) => {
337 DelegationAudience::CanicSubnet(*subnet)
338 }
339 RootDelegationAudiencePolicy::Project(project) => {
340 DelegationAudience::Project(project.clone())
341 }
342 }
343}
344
345fn root_delegated_role_grant_view(policy: &RootDelegatedRoleGrantPolicy) -> DelegatedRoleGrant {
346 DelegatedRoleGrant {
347 target: policy.target.clone(),
348 scopes: policy.scopes.clone(),
349 }
350}