Skip to main content

canic_core/api/auth/
mod.rs

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            DelegatedTokenOps, 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
29// Internal auth pipeline:
30// - `session` owns delegated-session ingress and replay/session state handling.
31// - `metadata` owns root request metadata construction.
32// - `verify_flow` owns verifier-side attestation refresh behavior.
33mod metadata;
34mod session;
35mod verify_flow;
36
37///
38/// DelegationApi
39///
40/// Requires auth.delegated_tokens.enabled = true in config.
41///
42
43pub struct DelegationApi;
44
45impl DelegationApi {
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    // Map internal auth failures onto public endpoint errors.
53    fn map_delegation_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    /// Resolve the local shard public key in SEC1 encoding.
63    pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
64        DelegatedTokenOps::local_shard_public_key_sec1(IcOps::canister_self())
65            .await
66            .map_err(Self::map_delegation_error)
67    }
68
69    /// Issue a delegated token from an explicit self-contained proof.
70    pub async fn issue_token(request: DelegatedTokenIssueRequest) -> Result<DelegatedToken, Error> {
71        DelegatedTokenOps::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_delegation_error)
81    }
82
83    /// Request a root proof, then issue a self-contained delegated token.
84    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    /// Full delegated token verification without verifier-local proof lookup.
105    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        DelegatedTokenOps::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_delegation_error)
121    }
122
123    /// Request a self-contained delegation proof from root over RPC.
124    pub async fn request_delegation(
125        request: DelegationProofIssueRequest,
126    ) -> Result<DelegationProof, Error> {
127        Self::request_delegation_remote(request).await
128    }
129
130    /// Issue a self-contained delegation proof from the local root.
131    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_auth_max_ttl_secs()?;
136        let max_token_ttl_secs = request.cert_ttl_secs.min(max_cert_ttl_secs);
137        DelegatedTokenOps::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_delegation_error)
148    }
149
150    /// Request a signed role attestation from root over RPC.
151    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    /// Return the current root role-attestation key set.
166    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
167        DelegatedTokenOps::attestation_key_set()
168            .await
169            .map_err(Self::map_delegation_error)
170    }
171
172    /// Warm the root delegation and attestation key caches once.
173    pub async fn prewarm_root_key_material() -> Result<(), Error> {
174        EnvOps::require_root().map_err(Error::from)?;
175        DelegatedTokenOps::prewarm_root_key_material()
176            .await
177            .map_err(|err| {
178                log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
179                Self::map_delegation_error(err)
180            })
181    }
182
183    /// Replace the verifier-local role-attestation key set.
184    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
185        DelegatedTokenOps::replace_attestation_key_set(key_set);
186    }
187
188    /// Verify a role attestation, refreshing root keys once on unknown key.
189    pub async fn verify_role_attestation(
190        attestation: &SignedRoleAttestation,
191        min_accepted_epoch: u64,
192    ) -> Result<(), Error> {
193        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
194            .map_err(Error::from)?
195            .min_accepted_epoch_by_role
196            .get(attestation.payload.role.as_str())
197            .copied();
198        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
199            min_accepted_epoch,
200            configured_min_accepted_epoch,
201        );
202
203        let caller = IcOps::msg_caller();
204        let self_pid = IcOps::canister_self();
205        let now_secs = IcOps::now_secs();
206        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
207        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
208
209        let verify = || {
210            DelegatedTokenOps::verify_role_attestation_cached(
211                attestation,
212                caller,
213                self_pid,
214                verifier_subnet,
215                now_secs,
216                min_accepted_epoch,
217            )
218            .map(|_| ())
219        };
220        let refresh = || async {
221            let key_set: AttestationKeySet =
222                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
223            DelegatedTokenOps::replace_attestation_key_set(key_set);
224            Ok(())
225        };
226
227        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
228            Ok(()) => Ok(()),
229            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
230                verify_flow::record_attestation_verifier_rejection(&err);
231                verify_flow::log_attestation_verifier_rejection(
232                    &err,
233                    attestation,
234                    caller,
235                    self_pid,
236                    "cached",
237                );
238                Err(Self::map_delegation_error(err.into()))
239            }
240            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
241                verify_flow::record_attestation_verifier_rejection(&trigger);
242                verify_flow::log_attestation_verifier_rejection(
243                    &trigger,
244                    attestation,
245                    caller,
246                    self_pid,
247                    "cache_miss_refresh",
248                );
249                record_attestation_refresh_failed();
250                log!(
251                    Topic::Auth,
252                    Warn,
253                    "role attestation refresh failed local={} caller={} key_id={} error={}",
254                    self_pid,
255                    caller,
256                    attestation.key_id,
257                    source
258                );
259                Err(Self::map_delegation_error(source))
260            }
261            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
262                verify_flow::record_attestation_verifier_rejection(&err);
263                verify_flow::log_attestation_verifier_rejection(
264                    &err,
265                    attestation,
266                    caller,
267                    self_pid,
268                    "post_refresh",
269                );
270                Err(Self::map_delegation_error(err.into()))
271            }
272        }
273    }
274
275    // Resolve the root-owned TTL ceiling from delegated-token config.
276    fn delegated_auth_max_ttl_secs() -> Result<u64, Error> {
277        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
278        if !cfg.enabled {
279            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
280        }
281
282        Ok(cfg
283            .max_ttl_secs
284            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS))
285    }
286}
287
288impl DelegationApi {
289    // Route a self-contained delegation proof request over RPC to root.
290    async fn request_delegation_remote(
291        request: DelegationProofIssueRequest,
292    ) -> Result<DelegationProof, Error> {
293        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
294        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
295            .await
296            .map_err(Self::map_delegation_error)
297    }
298
299    // Execute one local root role-attestation request.
300    pub async fn request_role_attestation_root(
301        request: RoleAttestationRequest,
302    ) -> Result<SignedRoleAttestation, Error> {
303        let request = metadata::with_root_attestation_request_metadata(request);
304        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
305            .await
306            .map_err(Self::map_delegation_error)?;
307
308        match response {
309            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
310            _ => Err(Error::internal(
311                "invalid root response type for role attestation request",
312            )),
313        }
314    }
315
316    // Route a canonical role-attestation request over RPC to root.
317    async fn request_role_attestation_remote(
318        request: RoleAttestationRequest,
319    ) -> Result<RootCapabilityResponse, Error> {
320        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
321        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
322            .await
323            .map_err(Self::map_delegation_error)
324    }
325}