Skip to main content

canic_core/api/auth/
mod.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        auth::{
5            AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
6            DelegationCert, DelegationProof, DelegationProvisionResponse, DelegationRequest,
7            RoleAttestationRequest, SignedRoleAttestation,
8        },
9        error::{Error, ErrorCode},
10        rpc::{Request as RootRequest, Response as RootCapabilityResponse},
11    },
12    error::InternalErrorClass,
13    ids::cap,
14    log,
15    log::Topic,
16    ops::{
17        auth::{DelegatedTokenOps, audience},
18        config::ConfigOps,
19        ic::IcOps,
20        rpc::RpcOps,
21        runtime::env::EnvOps,
22        runtime::metrics::auth::{
23            record_attestation_refresh_failed, record_signer_issue_without_proof,
24        },
25        storage::auth::DelegationStateOps,
26    },
27    protocol,
28    workflow::rpc::request::handler::RootResponseWorkflow,
29};
30
31#[cfg(test)]
32use crate::ids::CanisterRole;
33
34// Internal auth pipeline:
35// - `session` owns delegated-session ingress and replay/session state handling.
36// - `admin` owns explicit root-driven fanout preparation and routing.
37// - `proof_store` owns proof-install validation and storage/cache side effects.
38//
39// Keep these modules free of lateral calls to each other. Coordination stays here,
40// and shared invariants should live in dedicated seams like `ops::auth::audience`.
41mod admin;
42mod metadata;
43mod proof_store;
44mod session;
45mod verify_flow;
46
47///
48/// DelegationApi
49///
50/// Requires auth.delegated_tokens.enabled = true in config.
51///
52
53pub struct DelegationApi;
54
55impl DelegationApi {
56    const DELEGATED_TOKENS_DISABLED: &str =
57        "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
58    const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
59    const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
60        b"canic-session-bootstrap-token-fingerprint:v1";
61
62    fn map_delegation_error(err: crate::InternalError) -> Error {
63        match err.class() {
64            InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
65                Error::internal(err.to_string())
66            }
67            _ => Error::from(err),
68        }
69    }
70
71    /// Full delegation proof verification (structure + signature).
72    ///
73    /// Purely local verification; does not read certified data or require a
74    /// query context.
75    pub fn verify_delegation_proof(
76        proof: &DelegationProof,
77        authority_pid: Principal,
78    ) -> Result<(), Error> {
79        DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
80            .map_err(Self::map_delegation_error)
81    }
82
83    #[cfg(canic_test_delegation_material)]
84    #[must_use]
85    pub fn current_signing_proof_for_test() -> Option<DelegationProof> {
86        DelegationStateOps::latest_proof_dto()
87    }
88
89    /// Return whether this canister currently has a local signing proof.
90    #[must_use]
91    pub fn has_signing_proof() -> bool {
92        DelegationStateOps::latest_proof_dto().is_some()
93    }
94
95    async fn sign_token(
96        claims: DelegatedTokenClaims,
97        proof: DelegationProof,
98    ) -> Result<DelegatedToken, Error> {
99        DelegatedTokenOps::sign_token(claims, proof)
100            .await
101            .map_err(Self::map_delegation_error)
102    }
103
104    /// Resolve the local shard public key in SEC1 encoding.
105    pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
106        DelegatedTokenOps::local_shard_public_key_sec1(IcOps::canister_self())
107            .await
108            .map_err(Self::map_delegation_error)
109    }
110
111    /// Issue a delegated token using a reusable local proof when possible.
112    ///
113    /// If the proof is missing or no longer valid for the requested claims, this
114    /// performs canonical shard-initiated setup and retries with the refreshed proof.
115    pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
116        let proof = Self::ensure_signing_proof(&claims).await?;
117        let claims = Self::canonicalize_claims_for_proof(claims, &proof);
118        Self::sign_token(claims, proof).await
119    }
120
121    /// Full delegated token verification (structure + signature).
122    ///
123    /// Purely local verification; does not read certified data or require a
124    /// query context.
125    pub fn verify_token(
126        token: &DelegatedToken,
127        authority_pid: Principal,
128        now_secs: u64,
129    ) -> Result<(), Error> {
130        DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
131            .map(|_| ())
132            .map_err(Self::map_delegation_error)
133    }
134
135    /// Verify a delegated token and return verified contents.
136    ///
137    /// This is intended for application-layer session construction.
138    /// It performs full verification and returns verified claims and cert.
139    pub fn verify_token_verified(
140        token: &DelegatedToken,
141        authority_pid: Principal,
142        now_secs: u64,
143    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
144        DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
145            .map(crate::ops::auth::VerifiedDelegatedToken::into_parts)
146            .map_err(Self::map_delegation_error)
147    }
148
149    /// Verify a delegated token and require its subject to match `msg_caller()`.
150    ///
151    /// This issuer-side helper does not require the old token audience to
152    /// include the local signer, which allows stale-audience reissue flows.
153    pub fn verify_token_for_caller(
154        token: &DelegatedToken,
155        authority_pid: Principal,
156        now_secs: u64,
157    ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
158        let verified = DelegatedTokenOps::verify_token_for_reissue(token, authority_pid, now_secs)
159            .map_err(Self::map_delegation_error)?;
160        Self::ensure_claims_bound_to_caller(&verified.claims.to_dto(), IcOps::msg_caller())?;
161        Ok(verified.into_parts())
162    }
163
164    /// Reissue a caller-bound token for a new audience without extending expiry.
165    ///
166    /// Scopes and `ext` are preserved. The replacement expiry is capped at the
167    /// old token expiry, so this refreshes audience only and does not renew the
168    /// session.
169    pub async fn reissue_token(
170        token: DelegatedToken,
171        aud: DelegationAudience,
172    ) -> Result<DelegatedToken, Error> {
173        let aud = Self::normalize_audience(aud)?;
174        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
175        let now_secs = IcOps::now_secs();
176        let (old_claims, _) = Self::verify_token_for_caller(&token, root_pid, now_secs)?;
177        let replacement_claims = DelegatedTokenClaims {
178            aud,
179            iat: now_secs,
180            ..old_claims.clone()
181        };
182
183        Self::reissue_token_from_verified(old_claims, replacement_claims).await
184    }
185
186    /// Ensure the caller has a valid delegated token for the requested audience.
187    ///
188    /// With no token, this mints a default `verify`-scoped token for
189    /// `msg_caller()`. With a caller-bound token, this returns it unchanged when
190    /// it already covers the audience or reissues it without extending expiry.
191    pub async fn ensure_token(
192        token: Option<DelegatedToken>,
193        aud: DelegationAudience,
194    ) -> Result<DelegatedToken, Error> {
195        let requested_aud = Self::normalize_audience(aud)?;
196        match token {
197            Some(token) => Self::ensure_existing_token_for_audience(token, requested_aud).await,
198            None => Self::issue_token_for_caller_audience(requested_aud).await,
199        }
200    }
201
202    /// Reissue a token from previously verified claims and proposed claims.
203    ///
204    /// CANIC enforces same `sub`, same `shard_pid`, no expiry extension, and a
205    /// default scope-subset rule.
206    pub async fn reissue_token_from_verified(
207        old_claims: DelegatedTokenClaims,
208        replacement_claims: DelegatedTokenClaims,
209    ) -> Result<DelegatedToken, Error> {
210        Self::ensure_reissue_claims_allowed(&old_claims, &replacement_claims)?;
211        let proof = Self::ensure_signing_proof(&replacement_claims).await?;
212        let replacement_claims = Self::canonicalize_reissue_claims_for_proof(
213            replacement_claims,
214            &proof,
215            old_claims.exp,
216        )?;
217        Self::sign_token(replacement_claims, proof).await
218    }
219
220    // Return an existing caller-bound token or reissue it to cover missing audience entries.
221    async fn ensure_existing_token_for_audience(
222        token: DelegatedToken,
223        requested_aud: DelegationAudience,
224    ) -> Result<DelegatedToken, Error> {
225        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
226        let now_secs = IcOps::now_secs();
227        let (old_claims, _) = Self::verify_token_for_caller(&token, root_pid, now_secs)?;
228        if audience::roles_subset(&requested_aud, &old_claims.aud) {
229            return Ok(token);
230        }
231
232        let aud = Self::merge_audience_for_reissue(old_claims.aud.clone(), requested_aud);
233        let replacement_claims = DelegatedTokenClaims {
234            aud,
235            iat: now_secs,
236            ..old_claims.clone()
237        };
238
239        Self::reissue_token_from_verified(old_claims, replacement_claims).await
240    }
241
242    // Issue the initial caller-bound token for an authenticated wallet/session principal.
243    async fn issue_token_for_caller_audience(
244        aud: DelegationAudience,
245    ) -> Result<DelegatedToken, Error> {
246        let caller = IcOps::msg_caller();
247        if let Err(reason) = crate::access::auth::validate_delegated_session_subject(caller) {
248            return Err(Error::forbidden(format!(
249                "delegated token caller rejected: {reason}"
250            )));
251        }
252
253        let now_secs = IcOps::now_secs();
254        let ttl_secs = ConfigOps::delegated_tokens_config()
255            .map_err(Error::from)?
256            .max_ttl_secs
257            .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
258        let claims = DelegatedTokenClaims {
259            sub: caller,
260            shard_pid: IcOps::canister_self(),
261            scopes: vec![cap::VERIFY.to_string()],
262            aud,
263            iat: now_secs,
264            exp: now_secs.saturating_add(ttl_secs),
265            ext: None,
266        };
267
268        Self::issue_token(claims).await
269    }
270
271    /// Canonical shard-initiated delegation request (user_shard -> root).
272    ///
273    /// Caller must match shard_pid and be registered to the subnet.
274    pub async fn request_delegation(
275        request: DelegationRequest,
276    ) -> Result<DelegationProvisionResponse, Error> {
277        let request = metadata::with_root_request_metadata(request);
278        Self::request_delegation_remote(request).await
279    }
280
281    pub async fn request_role_attestation(
282        request: RoleAttestationRequest,
283    ) -> Result<SignedRoleAttestation, Error> {
284        let request = metadata::with_root_attestation_request_metadata(request);
285        let response = Self::request_role_attestation_remote(request).await?;
286
287        match response {
288            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
289            _ => Err(Error::internal(
290                "invalid root response type for role attestation request",
291            )),
292        }
293    }
294
295    pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
296        DelegatedTokenOps::attestation_key_set()
297            .await
298            .map_err(Self::map_delegation_error)
299    }
300
301    /// Warm the root delegation and attestation key caches once.
302    pub async fn prewarm_root_key_material() -> Result<(), Error> {
303        EnvOps::require_root().map_err(Error::from)?;
304        DelegatedTokenOps::prewarm_root_key_material()
305            .await
306            .map_err(|err| {
307                log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
308                Self::map_delegation_error(err)
309            })
310    }
311
312    pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
313        DelegatedTokenOps::replace_attestation_key_set(key_set);
314    }
315
316    pub async fn verify_role_attestation(
317        attestation: &SignedRoleAttestation,
318        min_accepted_epoch: u64,
319    ) -> Result<(), Error> {
320        let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
321            .map_err(Error::from)?
322            .min_accepted_epoch_by_role
323            .get(attestation.payload.role.as_str())
324            .copied();
325        let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
326            min_accepted_epoch,
327            configured_min_accepted_epoch,
328        );
329
330        let caller = IcOps::msg_caller();
331        let self_pid = IcOps::canister_self();
332        let now_secs = IcOps::now_secs();
333        let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
334        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
335
336        let verify = || {
337            DelegatedTokenOps::verify_role_attestation_cached(
338                attestation,
339                caller,
340                self_pid,
341                verifier_subnet,
342                now_secs,
343                min_accepted_epoch,
344            )
345            .map(|_| ())
346        };
347        let refresh = || async {
348            let key_set: AttestationKeySet =
349                RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
350            DelegatedTokenOps::replace_attestation_key_set(key_set);
351            Ok(())
352        };
353
354        match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
355            Ok(()) => Ok(()),
356            Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
357                verify_flow::record_attestation_verifier_rejection(&err);
358                verify_flow::log_attestation_verifier_rejection(
359                    &err,
360                    attestation,
361                    caller,
362                    self_pid,
363                    "cached",
364                );
365                Err(Self::map_delegation_error(err.into()))
366            }
367            Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
368                verify_flow::record_attestation_verifier_rejection(&trigger);
369                verify_flow::log_attestation_verifier_rejection(
370                    &trigger,
371                    attestation,
372                    caller,
373                    self_pid,
374                    "cache_miss_refresh",
375                );
376                record_attestation_refresh_failed();
377                log!(
378                    Topic::Auth,
379                    Warn,
380                    "role attestation refresh failed local={} caller={} key_id={} error={}",
381                    self_pid,
382                    caller,
383                    attestation.key_id,
384                    source
385                );
386                Err(Self::map_delegation_error(source))
387            }
388            Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
389                verify_flow::record_attestation_verifier_rejection(&err);
390                verify_flow::log_attestation_verifier_rejection(
391                    &err,
392                    attestation,
393                    caller,
394                    self_pid,
395                    "post_refresh",
396                );
397                Err(Self::map_delegation_error(err.into()))
398            }
399        }
400    }
401
402    fn require_proof() -> Result<DelegationProof, Error> {
403        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
404        if !cfg.enabled {
405            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
406        }
407
408        DelegationStateOps::latest_proof_dto().ok_or_else(|| {
409            record_signer_issue_without_proof();
410            Error::not_found("delegation proof not installed")
411        })
412    }
413
414    // Resolve a proof that is currently usable for token issuance.
415    async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
416        let now_secs = IcOps::now_secs();
417
418        match Self::require_proof() {
419            Ok(proof)
420                if !DelegatedTokenOps::proof_reusable_for_claims(&proof, claims, now_secs) =>
421            {
422                Self::setup_delegation(claims).await
423            }
424            Ok(proof) => Ok(proof),
425            Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
426            Err(err) => Err(err),
427        }
428    }
429
430    // Provision a fresh delegation from root, then resolve the latest locally stored proof.
431    async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
432        let shard_public_key_sec1 =
433            DelegatedTokenOps::local_shard_public_key_sec1(claims.shard_pid)
434                .await
435                .map_err(Self::map_delegation_error)?;
436        let request = Self::delegation_request_from_claims(claims, shard_public_key_sec1)?;
437        let response = Self::request_delegation_remote(request).await?;
438        let proof = response.proof;
439        Self::store_local_signer_proof(proof.clone()).await?;
440        Ok(proof)
441    }
442
443    // Rebase claims onto a freshly issued proof window when delegation setup
444    // completed after the original token timestamps were chosen.
445    fn canonicalize_claims_for_proof(
446        claims: DelegatedTokenClaims,
447        proof: &DelegationProof,
448    ) -> DelegatedTokenClaims {
449        if claims.iat >= proof.cert.issued_at && claims.exp <= proof.cert.expires_at {
450            return claims;
451        }
452
453        DelegatedTokenClaims {
454            iat: proof.cert.issued_at,
455            exp: proof.cert.expires_at,
456            ..claims
457        }
458    }
459
460    // Bind verified token claims to the current IC caller.
461    fn ensure_claims_bound_to_caller(
462        claims: &DelegatedTokenClaims,
463        caller: Principal,
464    ) -> Result<(), Error> {
465        if claims.sub == caller {
466            Ok(())
467        } else {
468            Err(Error::forbidden(format!(
469                "delegated token subject '{}' does not match caller '{}'",
470                claims.sub, caller
471            )))
472        }
473    }
474
475    // Normalize caller-supplied audience roles with set semantics.
476    fn normalize_audience(audience: DelegationAudience) -> Result<DelegationAudience, Error> {
477        let DelegationAudience::Roles(roles) = audience else {
478            return Ok(DelegationAudience::Any);
479        };
480
481        let mut out = Vec::new();
482        for role in roles {
483            if !out.contains(&role) {
484                out.push(role);
485            }
486        }
487
488        if out.is_empty() {
489            return Err(Error::invalid("token audience role list must not be empty"));
490        }
491
492        Ok(DelegationAudience::Roles(out))
493    }
494
495    // Merge role-scoped audiences while preserving wildcard broadening semantics.
496    fn merge_audience_for_reissue(
497        current: DelegationAudience,
498        requested: DelegationAudience,
499    ) -> DelegationAudience {
500        match (current, requested) {
501            (DelegationAudience::Any, _) | (_, DelegationAudience::Any) => DelegationAudience::Any,
502            (DelegationAudience::Roles(mut current), DelegationAudience::Roles(requested)) => {
503                for role in requested {
504                    if !current.contains(&role) {
505                        current.push(role);
506                    }
507                }
508                DelegationAudience::Roles(current)
509            }
510        }
511    }
512
513    // Enforce same-session reissue invariants before resolving signing material.
514    fn ensure_reissue_claims_allowed(
515        old_claims: &DelegatedTokenClaims,
516        replacement_claims: &DelegatedTokenClaims,
517    ) -> Result<(), Error> {
518        if audience::has_empty_roles(&replacement_claims.aud) {
519            return Err(Error::invalid(
520                "replacement token audience role list must not be empty",
521            ));
522        }
523
524        if replacement_claims.sub != old_claims.sub {
525            return Err(Error::forbidden(format!(
526                "replacement token subject '{}' must match old subject '{}'",
527                replacement_claims.sub, old_claims.sub
528            )));
529        }
530
531        if replacement_claims.shard_pid != old_claims.shard_pid {
532            return Err(Error::forbidden(format!(
533                "replacement token shard '{}' must match old shard '{}'",
534                replacement_claims.shard_pid, old_claims.shard_pid
535            )));
536        }
537
538        if replacement_claims.exp > old_claims.exp {
539            return Err(Error::forbidden(
540                "replacement token expiry must not exceed old token expiry",
541            ));
542        }
543
544        if replacement_claims.exp < replacement_claims.iat {
545            return Err(Error::invalid(
546                "replacement token expiry must not precede issued_at",
547            ));
548        }
549
550        if !audience::strings_subset(&replacement_claims.scopes, &old_claims.scopes) {
551            return Err(Error::forbidden(
552                "replacement token scopes must be a subset of old token scopes",
553            ));
554        }
555
556        Ok(())
557    }
558
559    // Rebase reissue timing onto the resolved proof while preserving the old-expiry cap.
560    fn canonicalize_reissue_claims_for_proof(
561        claims: DelegatedTokenClaims,
562        proof: &DelegationProof,
563        old_exp: u64,
564    ) -> Result<DelegatedTokenClaims, Error> {
565        let iat = claims.iat.max(proof.cert.issued_at);
566        let exp = claims.exp.min(old_exp).min(proof.cert.expires_at);
567
568        if exp < iat {
569            return Err(Error::invalid(
570                "replacement token expiry is outside the current signing proof window",
571            ));
572        }
573
574        Ok(DelegatedTokenClaims { iat, exp, ..claims })
575    }
576
577    // Build a canonical delegation request from token claims.
578    fn delegation_request_from_claims(
579        claims: &DelegatedTokenClaims,
580        shard_public_key_sec1: Vec<u8>,
581    ) -> Result<DelegationRequest, Error> {
582        let ttl_secs = claims.exp.saturating_sub(claims.iat);
583        if ttl_secs == 0 {
584            return Err(Error::invalid(
585                "delegation ttl_secs must be greater than zero",
586            ));
587        }
588
589        let signer_pid = IcOps::canister_self();
590
591        Ok(DelegationRequest {
592            shard_pid: signer_pid,
593            scopes: claims.scopes.clone(),
594            aud: claims.aud.clone(),
595            ttl_secs,
596            shard_public_key_sec1,
597            metadata: None,
598        })
599    }
600
601    // Delegated audience invariants:
602    // 1. Some(empty) audiences are invalid; None means any registered verifier.
603    // 2. claims.aud must stay within proof.cert.aud.
604    // 3. proof installation on target T requires T's role to be allowed by proof.cert.aud.
605    // 4. token acceptance on canister C requires C's role to be allowed by claims.aud.
606    //
607    // Keep ingress, fanout, install, and runtime checks aligned to this block.
608}
609
610impl DelegationApi {
611    // Execute one local root delegation provisioning request.
612    pub async fn request_delegation_root(
613        request: DelegationRequest,
614    ) -> Result<DelegationProvisionResponse, Error> {
615        let request = metadata::with_root_request_metadata(request);
616        let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
617            .await
618            .map_err(Self::map_delegation_error)?;
619
620        match response {
621            RootCapabilityResponse::DelegationIssued(response) => Ok(response),
622            _ => Err(Error::internal(
623                "invalid root response type for delegation request",
624            )),
625        }
626    }
627
628    // Route a canonical delegation provisioning request over RPC to root.
629    async fn request_delegation_remote(
630        request: DelegationRequest,
631    ) -> Result<DelegationProvisionResponse, Error> {
632        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
633        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
634            .await
635            .map_err(Self::map_delegation_error)
636    }
637
638    // Execute one local root role-attestation request.
639    pub async fn request_role_attestation_root(
640        request: RoleAttestationRequest,
641    ) -> Result<SignedRoleAttestation, Error> {
642        let request = metadata::with_root_attestation_request_metadata(request);
643        let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
644            .await
645            .map_err(Self::map_delegation_error)?;
646
647        match response {
648            RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
649            _ => Err(Error::internal(
650                "invalid root response type for role attestation request",
651            )),
652        }
653    }
654
655    // Route a canonical role-attestation request over RPC to root.
656    async fn request_role_attestation_remote(
657        request: RoleAttestationRequest,
658    ) -> Result<RootCapabilityResponse, Error> {
659        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
660        RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
661            .await
662            .map_err(Self::map_delegation_error)
663    }
664}
665
666#[cfg(test)]
667mod tests;