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 RootRequest, 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 = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
111            .await
112            .map_err(Self::map_delegation_error)?;
113
114        match response {
115            RootCapabilityResponse::DelegationIssued(response) => Ok(response),
116            _ => Err(Error::internal(
117                "invalid root response type for delegation request",
118            )),
119        }
120    }
121
122    pub async fn request_role_attestation(
123        request: RoleAttestationRequest,
124    ) -> Result<SignedRoleAttestation, Error> {
125        let request = metadata::with_root_attestation_request_metadata(request);
126        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
127            .await
128            .map_err(Self::map_delegation_error)?;
129
130        match response {
131            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
132            _ => Err(Error::internal(
133                "invalid root response type for role attestation request",
134            )),
135        }
136    }
137
138    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
139        DelegatedTokenOps::attestation_key_set()
140            .await
141            .map_err(Self::map_delegation_error)
142    }
143
144    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
145        DelegatedTokenOps::replace_attestation_key_set(key_set);
146    }
147
148    pub async fn verify_role_attestation(
149        attestation: &SignedRoleAttestation,
150        min_accepted_epoch: u64,
151    ) -> Result<(), Error> {
152        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
153            .map_err(Error::from)?
154            .min_accepted_epoch_by_role
155            .get(attestation.payload.role.as_str())
156            .copied();
157        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
158            min_accepted_epoch,
159            configured_min_accepted_epoch,
160        );
161
162        let caller = IcOps::msg_caller();
163        let self_pid = IcOps::canister_self();
164        let now_secs = IcOps::now_secs();
165        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
166        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
167
168        let verify = || {
169            DelegatedTokenOps::verify_role_attestation_cached(
170                attestation,
171                caller,
172                self_pid,
173                verifier_subnet,
174                now_secs,
175                min_accepted_epoch,
176            )
177            .map(|_| ())
178        };
179        let refresh = || async {
180            let key_set: AttestationKeySet =
181                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
182            DelegatedTokenOps::replace_attestation_key_set(key_set);
183            Ok(())
184        };
185
186        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
187            Ok(()) => Ok(()),
188            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
189                verify_flow::record_attestation_verifier_rejection(&err);
190                verify_flow::log_attestation_verifier_rejection(
191                    &err,
192                    attestation,
193                    caller,
194                    self_pid,
195                    "cached",
196                );
197                Err(Self::map_delegation_error(err.into()))
198            }
199            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
200                verify_flow::record_attestation_verifier_rejection(&trigger);
201                verify_flow::log_attestation_verifier_rejection(
202                    &trigger,
203                    attestation,
204                    caller,
205                    self_pid,
206                    "cache_miss_refresh",
207                );
208                record_attestation_refresh_failed();
209                log!(
210                    Topic::Auth,
211                    Warn,
212                    "role attestation refresh failed local={} caller={} key_id={} error={}",
213                    self_pid,
214                    caller,
215                    attestation.key_id,
216                    source
217                );
218                Err(Self::map_delegation_error(source))
219            }
220            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
221                verify_flow::record_attestation_verifier_rejection(&err);
222                verify_flow::log_attestation_verifier_rejection(
223                    &err,
224                    attestation,
225                    caller,
226                    self_pid,
227                    "post_refresh",
228                );
229                Err(Self::map_delegation_error(err.into()))
230            }
231        }
232    }
233
234    pub async fn store_proof(
235        proof: DelegationProof,
236        kind: DelegationProvisionTargetKind,
237    ) -> Result<(), Error> {
238        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
239        if !cfg.enabled {
240            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
241        }
242
243        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
244        let caller = IcOps::msg_caller();
245        if caller != root_pid {
246            return Err(Error::forbidden(
247                "delegation proof store requires root caller",
248            ));
249        }
250
251        DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
252            .await
253            .map_err(Self::map_delegation_error)?;
254        if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
255            let local = IcOps::canister_self();
256            log!(
257                Topic::Auth,
258                Warn,
259                "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
260                kind,
261                local,
262                proof.cert.shard_pid,
263                proof.cert.issued_at,
264                proof.cert.expires_at,
265                err
266            );
267            return Err(Self::map_delegation_error(err));
268        }
269
270        DelegationStateOps::set_proof_from_dto(proof);
271        let local = IcOps::canister_self();
272        let stored = DelegationStateOps::proof_dto()
273            .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
274        log!(
275            Topic::Auth,
276            Info,
277            "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
278            kind,
279            local,
280            stored.cert.shard_pid,
281            stored.cert.issued_at,
282            stored.cert.expires_at
283        );
284
285        Ok(())
286    }
287
288    pub fn require_proof() -> Result<DelegationProof, Error> {
289        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
290        if !cfg.enabled {
291            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
292        }
293
294        DelegationStateOps::proof_dto().ok_or_else(|| {
295            record_signer_mint_without_proof();
296            Error::not_found("delegation proof not set")
297        })
298    }
299}
300
301#[cfg(test)]
302mod tests;