Skip to main content

canic_core/api/auth/
mod.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        auth::{
5            AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationCert,
6            DelegationProof, DelegationProvisionResponse, DelegationProvisionTargetKind,
7            DelegationRequest, RoleAttestationRequest, SignedRoleAttestation,
8        },
9        error::Error,
10        rpc::{Request as RootCapabilityRequest, Response as RootCapabilityResponse},
11    },
12    error::InternalErrorClass,
13    log,
14    log::Topic,
15    ops::{
16        auth::DelegatedTokenOps,
17        config::ConfigOps,
18        ic::IcOps,
19        rpc::RpcOps,
20        runtime::env::EnvOps,
21        runtime::metrics::auth::{
22            record_attestation_refresh_failed, record_signer_mint_without_proof,
23        },
24        storage::auth::DelegationStateOps,
25    },
26    protocol,
27    workflow::rpc::request::handler::RootResponseWorkflow,
28};
29
30mod metadata;
31mod verify_flow;
32
33///
34/// DelegationApi
35///
36/// Requires auth.delegated_tokens.enabled = true in config.
37///
38
39pub struct DelegationApi;
40
41impl DelegationApi {
42    const DELEGATED_TOKENS_DISABLED: &str =
43        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
44
45    fn map_delegation_error(err: crate::InternalError) -> Error {
46        match err.class() {
47            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
48                Error::internal(err.to_string())
49            }
50            _ => Error::from(err),
51        }
52    }
53
54    /// Full delegation proof verification (structure + signature).
55    ///
56    /// Purely local verification; does not read certified data or require a
57    /// query context.
58    pub fn verify_delegation_proof(
59        proof: &DelegationProof,
60        authority_pid: Principal,
61    ) -> Result<(), Error> {
62        DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
63            .map_err(Self::map_delegation_error)
64    }
65
66    pub async fn sign_token(
67        claims: DelegatedTokenClaims,
68        proof: DelegationProof,
69    ) -> Result<DelegatedToken, Error> {
70        DelegatedTokenOps::sign_token(claims, proof)
71            .await
72            .map_err(Self::map_delegation_error)
73    }
74
75    /// Full delegated token verification (structure + signature).
76    ///
77    /// Purely local verification; does not read certified data or require a
78    /// query context.
79    pub fn verify_token(
80        token: &DelegatedToken,
81        authority_pid: Principal,
82        now_secs: u64,
83    ) -> Result<(), Error> {
84        DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
85            .map(|_| ())
86            .map_err(Self::map_delegation_error)
87    }
88
89    /// Verify a delegated token and return verified contents.
90    ///
91    /// This is intended for application-layer session construction.
92    /// It performs full verification and returns verified claims and cert.
93    pub fn verify_token_verified(
94        token: &DelegatedToken,
95        authority_pid: Principal,
96        now_secs: u64,
97    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
98        DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
99            .map(|verified| (verified.claims, verified.cert))
100            .map_err(Self::map_delegation_error)
101    }
102
103    /// Canonical shard-initiated delegation request (user_shard -> root).
104    ///
105    /// Caller must match shard_pid and be registered to the subnet.
106    pub async fn request_delegation(
107        request: DelegationRequest,
108    ) -> Result<DelegationProvisionResponse, Error> {
109        let request = metadata::with_root_request_metadata(request);
110        let response =
111            RootResponseWorkflow::response(RootCapabilityRequest::IssueDelegation(request))
112                .await
113                .map_err(Self::map_delegation_error)?;
114
115        match response {
116            RootCapabilityResponse::DelegationIssued(response) => Ok(response),
117            _ => Err(Error::internal(
118                "invalid root response type for delegation request",
119            )),
120        }
121    }
122
123    pub async fn request_role_attestation(
124        request: RoleAttestationRequest,
125    ) -> Result<SignedRoleAttestation, Error> {
126        let request = metadata::with_root_attestation_request_metadata(request);
127        let response =
128            RootResponseWorkflow::response(RootCapabilityRequest::IssueRoleAttestation(request))
129                .await
130                .map_err(Self::map_delegation_error)?;
131
132        match response {
133            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
134            _ => Err(Error::internal(
135                "invalid root response type for role attestation request",
136            )),
137        }
138    }
139
140    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
141        DelegatedTokenOps::attestation_key_set()
142            .await
143            .map_err(Self::map_delegation_error)
144    }
145
146    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
147        DelegatedTokenOps::replace_attestation_key_set(key_set);
148    }
149
150    pub async fn verify_role_attestation(
151        attestation: &SignedRoleAttestation,
152        min_accepted_epoch: u64,
153    ) -> Result<(), Error> {
154        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
155            .map_err(Error::from)?
156            .min_accepted_epoch_by_role
157            .get(attestation.payload.role.as_str())
158            .copied();
159        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
160            min_accepted_epoch,
161            configured_min_accepted_epoch,
162        );
163
164        let caller = IcOps::msg_caller();
165        let self_pid = IcOps::canister_self();
166        let now_secs = IcOps::now_secs();
167        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
168        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
169
170        let verify = || {
171            DelegatedTokenOps::verify_role_attestation_cached(
172                attestation,
173                caller,
174                self_pid,
175                verifier_subnet,
176                now_secs,
177                min_accepted_epoch,
178            )
179            .map(|_| ())
180        };
181        let refresh = || async {
182            let key_set: AttestationKeySet =
183                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
184            DelegatedTokenOps::replace_attestation_key_set(key_set);
185            Ok(())
186        };
187
188        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
189            Ok(()) => Ok(()),
190            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
191                verify_flow::record_attestation_verifier_rejection(&err);
192                verify_flow::log_attestation_verifier_rejection(
193                    &err,
194                    attestation,
195                    caller,
196                    self_pid,
197                    "cached",
198                );
199                Err(Self::map_delegation_error(err.into()))
200            }
201            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
202                verify_flow::record_attestation_verifier_rejection(&trigger);
203                verify_flow::log_attestation_verifier_rejection(
204                    &trigger,
205                    attestation,
206                    caller,
207                    self_pid,
208                    "cache_miss_refresh",
209                );
210                record_attestation_refresh_failed();
211                log!(
212                    Topic::Auth,
213                    Warn,
214                    "role attestation refresh failed local={} caller={} key_id={} error={}",
215                    self_pid,
216                    caller,
217                    attestation.key_id,
218                    source
219                );
220                Err(Self::map_delegation_error(source))
221            }
222            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
223                verify_flow::record_attestation_verifier_rejection(&err);
224                verify_flow::log_attestation_verifier_rejection(
225                    &err,
226                    attestation,
227                    caller,
228                    self_pid,
229                    "post_refresh",
230                );
231                Err(Self::map_delegation_error(err.into()))
232            }
233        }
234    }
235
236    pub async fn store_proof(
237        proof: DelegationProof,
238        kind: DelegationProvisionTargetKind,
239    ) -> Result<(), Error> {
240        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
241        if !cfg.enabled {
242            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
243        }
244
245        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
246        let caller = IcOps::msg_caller();
247        if caller != root_pid {
248            return Err(Error::forbidden(
249                "delegation proof store requires root caller",
250            ));
251        }
252
253        DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
254            .await
255            .map_err(Self::map_delegation_error)?;
256        if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
257            let local = IcOps::canister_self();
258            log!(
259                Topic::Auth,
260                Warn,
261                "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
262                kind,
263                local,
264                proof.cert.shard_pid,
265                proof.cert.issued_at,
266                proof.cert.expires_at,
267                err
268            );
269            return Err(Self::map_delegation_error(err));
270        }
271
272        DelegationStateOps::set_proof_from_dto(proof);
273        let local = IcOps::canister_self();
274        let stored = DelegationStateOps::proof_dto()
275            .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
276        log!(
277            Topic::Auth,
278            Info,
279            "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
280            kind,
281            local,
282            stored.cert.shard_pid,
283            stored.cert.issued_at,
284            stored.cert.expires_at
285        );
286
287        Ok(())
288    }
289
290    pub fn require_proof() -> Result<DelegationProof, Error> {
291        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
292        if !cfg.enabled {
293            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
294        }
295
296        DelegationStateOps::proof_dto().ok_or_else(|| {
297            record_signer_mint_without_proof();
298            Error::not_found("delegation proof not set")
299        })
300    }
301}
302
303#[cfg(test)]
304mod tests;