61 Commits
Author SHA1 Message Date
thakares 480d8e642d Enhance website branding, assets and search indexing
Rust / build (push) Canceled after 0s
2026-06-05 15:02:01 +05:30
thakares 414b74b95f Improve README formatting for daemon and runtime sizes
Updated formatting for size and security features in README.
2026-06-05 14:01:42 +05:30
thakares 227f5ff922 Update README with performance and security details
Added performance metrics and security features to README.
2026-06-05 13:59:42 +05:30
thakares 72ae2f4b06 Add runtime footprint section to README
Added a section on runtime footprint detailing deployment sizes and benefits.
2026-06-05 13:57:59 +05:30
thakares 3397ca121b Enhance deployment documentation with security checks
Added sections for binary hardening verification, runtime verification, and deployment footprint details.
2026-06-05 13:55:16 +05:30
thakares 1e038901b3 Add ChronoSeal product website 2026-06-05 12:40:10 +05:30
thakares 6c11495892 Fix formatting for protocol request examples 2026-06-04 20:54:41 +05:30
thakares a1a2e9d571 Update v1.0.2 documentation and security guidance 2026-06-04 20:47:22 +05:30
thakares f4c4beb6b5 Update testing documentation for v1.0.2 2026-06-04 20:21:39 +05:30
thakares 1004a39667 Bump version to 1.0.2 in Cargo.toml 2026-06-04 20:14:20 +05:30
thakares a7cc533ed4 Update architecture documentation for v1.0.2 2026-06-04 20:11:43 +05:30
thakares 40877be160 Update README for v1.0.2 release 2026-06-04 20:05:55 +05:30
thakares 8d119ac00e Harden validation, improve runtime security and refactor core services
Rust / build (push) Canceled after 0s
2026-06-04 19:58:21 +05:30
thakares aebef4c623 Update version badge to v1.0.1 in README.md 2026-05-30 21:49:11 +05:30
thakares b81b7b0e15 Remove .personal from .gitignore
Remove .personal from .gitignore
2026-05-30 21:31:27 +05:30
thakares 3f445eead5 docs: update TESTING.md for v1.0.1 — 95 tests, proptest section, known gaps 2026-05-30 21:24:29 +05:30
thakares 3225509713 Begin post-v1.0.1 protocol and runtime improvements 2026-05-30 21:00:22 +05:30
thakares ecb1721ff4 Align crate versions with v1.0.1 release 2026-05-30 20:58:19 +05:30
thakares d965451d4f Add missing proptest dev dependency
Rust / build (push) Canceled after 0s
2026-05-30 20:48:46 +05:30
thakares 1a58ef9796 Release v1.0.0: protocol freeze, replay testing, fuzzing and audit readiness
Rust / build (push) Canceled after 0s
2026-05-30 20:09:24 +05:30
thakares c7873b429d Enhance v0.6.1 testing documentation
- Expand TESTING.md with comprehensive test coverage details
- Document server, WASM, and shared crate test suites
- Highlight critical security and parity tests
- Improve testing philosophy and contributor guidance
- Record current 89-test validation baseline
2026-05-29 23:04:42 +05:30
thakares 3679e6808b Expand v0.6.1 documentation suite
Rust / build (push) Canceled after 0s
- Add COMPARISON.md for architecture and positioning analysis
- Add PERFORMANCE-TUNING.md for mutation engine optimization guidance
- Add TESTING.md documenting the 89-test security-focused test suite
- Document server, WASM, and shared crate test coverage
- Add operational guidance for tuning, validation, and verification
- Improve project maintainability and contributor onboarding
2026-05-29 22:56:08 +05:30
thakares fc2d693518 Add comparison and performance tuning documentation
Rust / build (push) Canceled after 0s
2026-05-29 22:34:13 +05:30
thakares 0ed3cb444d Refactor attestation engine and synchronize project documentation
- Refine session and storage lifecycle handling
- Improve VM extension architecture across server, shared, and WASM runtimes
- Enhance synthetic gene mutation engine integration and parity guarantees
- Align deterministic state progression between server and browser execution paths
- Update configuration examples and deployment guidance
- Expand architecture, API, threat model, privacy, and WASM build documentation
- Refresh README with comprehensive project overview, operational workflows,
  browser integration details, storage backend documentation, and security model
- Document v0.6.0 refactoring outcomes and design rationale
- Improve consistency across documentation, configuration, and implementation

This commit consolidates the v0.6.0 architectural refactoring effort,
strengthening deterministic browser/server parity while improving
maintainability, operational clarity, and project documentation.
2026-05-29 21:55:08 +05:30
thakares 2b8afd54e0 docs: update README and docs for v0.6.0 architecture and deployment, preserve current server/shared/wasm updates 2026-05-29 21:27:11 +05:30
thakares 6067746898 Suppress dead_code warnings for VM execution helpers 2026-05-29 18:13:33 +05:30
thakares 3d2a1a0ad7 Delete LICENSE-MIT 2026-05-29 17:47:49 +05:30
thakares 815d29af4c fix: correct license badge to MIT OR Apache-2.0 2026-05-29 16:11:40 +05:30
thakares b2835f454f docs: update ARCHITECTURE.md for v0.6.0 gene mutation system 2026-05-29 15:55:33 +05:30
thakares 4a64a57347 Update README.md 2026-05-29 15:52:00 +05:30
thakares 9d52828bb6 fix: correct license badge to MIT OR Apache-2.0 2026-05-29 15:44:07 +05:30
thakares 19666bd608 fix: correct license badge to MIT OR Apache-2.0 2026-05-29 15:42:58 +05:30
thakares 089a834f96 docs: comprehensive README with v0.6.0 gene mutation system, contributing & security policy
- Add v0.6.0 synthetic gene mutation system section (from REFRACTORING-v0.6.0.md)
- Add contributing guidelines, security policy, language breakdown
- Add badge bar, mutation threat row, topics tags
- No existing content removed
2026-05-29 15:39:39 +05:30
thakares ba768da58e feat: implement v0.6.0 mutation engine and db_type runtime selection
Rust / build (push) Canceled after 0s
2026-05-29 14:50:45 +05:30
thakares 217dc5f92b Improve session privacy handling and cleanup logic
Rust / build (push) Canceled after 0s
2026-05-23 15:57:34 +05:30
thakares 0b43530651 Introduce Unix-native daemon architecture and operational tooling 2026-05-22 19:58:14 +05:30
thakares e0f6d7c72c Add systemd service and installation workflow improvements 2026-05-22 19:43:11 +05:30
thakares 9e78daeeba Refactor ChronoSeal daemon for dynamic configurations, connection pooling, custom error handling, and complete tests (v0.5.0)
Rust / build (push) Canceled after 0s
2026-05-22 18:56:28 +05:30
thakares 60aaf0cd96 Add privacy-first design and policy documentation
Rust / build (push) Canceled after 0s
2026-05-14 12:40:04 +05:30
thakares 90ab322332 Expand design philosophy documentation 2026-05-13 16:32:00 +05:30
thakares eff782847d Expand design philosophy documentation 2026-05-13 16:24:51 +05:30
thakares ed0458633b Refine architecture and design philosophy documentation 2026-05-13 16:17:17 +05:30
thakares 3edea4bdff Implement CLI configuration and runtime management 2026-05-13 15:54:15 +05:30
thakares 2ac0afa691 Update copyright name in LICENSE-MIT file 2026-05-12 20:32:39 +05:30
thakares cbe12bcd49 Delete logo.png 2026-05-12 20:14:21 +05:30
thakares f21146e2f2 Add ChronoSeal logo to README 2026-05-12 20:05:59 +05:30
thakares 7972123887 Add ChronoSeal logo to README 2026-05-12 20:04:17 +05:30
thakares 630c451688 Refine ChronoSeal logo assets 2026-05-12 19:34:31 +05:30
thakares 8559e023be Refine ChronoSeal logo assets 2026-05-12 19:33:40 +05:30
thakares 75cb978fba Add ChronoSeal logo assets 2026-05-12 18:42:52 +05:30
thakares 8d318d5da4 Add Contributor Covenant Code of Conduct
Added Contributor Covenant Code of Conduct to promote a harassment-free community.
2026-05-10 14:16:19 +05:30
thakares d567655645 Update LICENSE 2026-05-09 18:35:56 +05:30
thakares 4fb4022188 fix: correct license link and remove residual LICENSE.md 2026-05-09 18:23:11 +05:30
thakares 256f12023e docs: add WASM_BUILD.md explaining pkg generation and frontend loading
Covers:
- What antibot_wasm.js is and why it is not in the repo
- How wasm-pack compiles wasm/src/ and what wasm/pkg/ contains
- Step-by-step build (rustup target, wasm-pack install, build, mv to frontend/pkg)
- How heartbeat.js loads and initialises the module via await init()
- MIME type requirements for serving .wasm files
- .gitignore rationale for wasm/pkg/ and frontend/pkg/
- Troubleshooting (missing target, wasm-opt, 404, empty string returns)
2026-05-09 18:17:53 +05:30
thakares 2840ddfc58 docs: comprehensive ARCHITECTURE, DEPLOYMENT, API, and THREAT_MODEL
ARCHITECTURE.md
- Full component map with ASCII diagram
- Complete session lifecycle (init + heartbeat + failure path)
- Cryptographic protocol spec (hash chain formula, canonical JSON)
- Stack machine instruction set table with stack effects
- Behavioral validation thresholds
- SQLite schema, threat model summary, module reference

DEPLOYMENT.md
- Build instructions (WASM + server + convenience script)
- native binary, systemd (with hardened sandbox notes), Docker
- nginx, Nginx Proxy Manager, and HAProxy reverse proxy configs
- Integration options (sidecar vs proxy-only)
- Full configuration table with all constants
- Observability (RUST_LOG levels), health check, security checklist

API.md
- Full /init and /hb request/response schemas with field tables
- Canonical signing payload specification
- Complete validation rules table (all 13 rejection conditions)
- Hash chain byte-level specification
- WASM exported function reference

THREAT_MODEL.md
- Four attacker profiles (script kiddie → sophisticated adversary)
- Eight attack vectors with mitigations (replay, forgery, hijack, DoS…)
- Explicit out-of-scope limitations
- Operational security notes (CORS, TLS, log level, SQLite)
2026-05-09 18:11:15 +05:30
thakares 4b27a342d1 docs: comprehensive ARCHITECTURE, DEPLOYMENT, API, and THREAT_MODEL
ARCHITECTURE.md
- Full component map with ASCII diagram
- Complete session lifecycle (init + heartbeat + failure path)
- Cryptographic protocol spec (hash chain formula, canonical JSON)
- Stack machine instruction set table with stack effects
- Behavioral validation thresholds
- SQLite schema, threat model summary, module reference

DEPLOYMENT.md
- Build instructions (WASM + server + convenience script)
- native binary, systemd (with hardened sandbox notes), Docker
- nginx, Nginx Proxy Manager, and HAProxy reverse proxy configs
- Integration options (sidecar vs proxy-only)
- Full configuration table with all constants
- Observability (RUST_LOG levels), health check, security checklist

API.md
- Full /init and /hb request/response schemas with field tables
- Canonical signing payload specification
- Complete validation rules table (all 13 rejection conditions)
- Hash chain byte-level specification
- WASM exported function reference

THREAT_MODEL.md
- Four attacker profiles (script kiddie → sophisticated adversary)
- Eight attack vectors with mitigations (replay, forgery, hijack, DoS…)
- Explicit out-of-scope limitations
- Operational security notes (CORS, TLS, log level, SQLite)
2026-05-09 18:04:13 +05:30
thakares 851d3b4876 Further refine architecture documentation 2026-05-08 15:24:49 +05:30
thakares ac57752ec2 Improve README presentation and project overview 2026-05-08 15:19:33 +05:30
thakares ebfbbf9901 Refine architecture docs and add project logo 2026-05-08 15:14:27 +05:30
thakares b866471825 fix: rename LICENSE.md to LICENSE so GitHub detects MIT correctly
GitHub license auto-detection requires a file named LICENSE containing
the full license text. LICENSE-MIT and LICENSE-APACHE remain for
dual-license reference.
2026-05-08 15:07:11 +05:30
thakares f95b3c0d4a license: switch from GPL-3.0 to MIT OR Apache-2.0 dual license
Companies integrating security tooling into proprietary stacks are
blocked by GPL copyleft. MIT/Apache-2.0 dual licensing matches the
convention used by the Rust ecosystem (tokio, axum, serde, etc.) and
removes all adoption friction for commercial users.

- Add LICENSE-MIT
- Add LICENSE-APACHE
- Update LICENSE.md to dual-license declaration
- Add [workspace.package] license field to Cargo.toml
2026-05-08 15:02:02 +05:30
134 changed files with 20351 additions and 1184 deletions

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
E-mail .
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
Generated
+1783 -24
View File
File diff suppressed because it is too large. Load diff
+5 -1
View File
@@ -4,5 +4,9 @@ resolver = "2"
members = [
"shared",
"server",
"wasm"
"wasm",
"chronoseal-replay"
]
[workspace.package]
license = "MIT OR Apache-2.0"
+12 -3
View File
@@ -4,7 +4,7 @@ WORKDIR /app
COPY . .
RUN cargo build -p server --release
RUN cargo build -p chronoseal-server --bin chronoseal --release
FROM debian:bookworm-slim
@@ -14,10 +14,19 @@ RUN apt-get update && apt-get install -y \
WORKDIR /opt/chronoseal
COPY --from=builder /app/target/release/server /usr/local/bin/chronoseal
COPY --from=builder /app/target/release/chronoseal /usr/local/bin/chronoseal
COPY frontend /usr/share/chronoseal/frontend
EXPOSE 3000
ENV RUST_LOG=info
ENV CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid
CMD ["chronoseal"]
RUN useradd -r -s /bin/false chronoseal
USER chronoseal
HEALTHCHECK --interval=30s --timeout=3s CMD chronoseal health || exit 1
CMD ["chronoseal", "run"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sunil Purushottam Thakare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+178
View File
@@ -0,0 +1,178 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship made available under
the License, as indicated by a copyright notice that is included in
or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other transformations
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean, as submitted to the Licensor for inclusion
in the Work by the copyright owner or by an individual or Legal Entity
authorized to submit on behalf of the copyright owner. For the purposes
of this definition, "submit" means any form of electronic, verbal, or
written communication sent to the Licensor or its representatives,
including but not limited to communication on electronic mailing lists,
source code control systems, and issue tracking systems that are managed
by, or on behalf of, the Licensor for the purpose of submitting and
discussing improvements to the Work, but excluding communication that is
conspicuously marked or designated in writing by the copyright owner as
"Not a Contribution."
"Contributor" shall mean Licensor and any Legal Entity on behalf of
whom a Contribution has been received by the Licensor and included
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Work or any
Contribution embodied within the Work constitutes direct or
contributory patent infringement, then any patent licenses granted to
You under this License for that Work shall terminate as of the date
such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works
a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, You must include a readable copy of the attribution
notices contained within such NOTICE file, in at least one of the
following places: within a NOTICE text provided as part of the
Derivative Works; within the Source form or documentation, if
provided along with the Derivative Works; or, within a display
generated by the Derivative Works, if and wherever such third-party
notices normally appear. The contents of the NOTICE file are for
informational purposes only and do not modify the License. You may
add Your own attribution notices within Derivative Works that You
distribute, alongside or as an addendum to the NOTICE text from
the Work, provided that such additional attribution notices cannot
be construed as modifying the License.
You may add Your own license statement for Your modifications and
may provide additional grant of rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Work.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed
to in writing, Licensor provides the Work (and each Contributor
provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
You are solely responsible for determining the appropriateness of using
or redistributing the Work and assume any risks associated with Your
exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or exemplary damages of any character arising as a result
of this License or out of the use or inability to use the Work
(including but not limited to damages for loss of goodwill, work
stoppage, computer failure or malfunction, or all other commercial
damages or losses), even if such Contributor has been advised of the
possibility of such damages.
9. Accepting Warranty or Liability. While redistributing the Work or
Derivative Works thereof, You may choose to offer, and charge a fee
for, acceptance of support, warranty, indemnity, or other liability
obligations and/or rights consistent with this License. However, in
accepting such obligations, You may offer such obligations only on
Your own behalf and on Your sole responsibility, not on behalf of
any other Contributor, and only if You agree to indemnify, defend,
and hold each Contributor harmless for any liability incurred by, or
claims asserted against, such Contributor by reason of your accepting
any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright (c) 2026 Sunil Thakare
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+819 -169
View File
File diff suppressed because it is too large. Load diff
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "chronoseal-replay"
version = "1.0.1"
edition = "2021"
[dependencies]
shared = { path = "../shared" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
hex = "0.4"
base64 = "0.22"
anyhow = "1"
reqwest = { version = "0.12", features = ["blocking", "json"] }
rand = "0.8"
ed25519-dalek = { version = "2", features = ["rand_core"] }
+638
View File
@@ -0,0 +1,638 @@
use anyhow::{anyhow, Result};
use ed25519_dalek::{Signer, SigningKey};
use rand::rngs::OsRng;
use shared::protocol::{
EntropyData, Fingerprint, HeartbeatRequest, HeartbeatResponse, InitRequest, InitResponse,
MouseEvent, StackState,
};
use std::collections::BTreeMap;
use std::env;
use std::time::{SystemTime, UNIX_EPOCH};
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
let mut url = "http://127.0.0.1:8080".to_string();
let mut scenario_file: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--url" => {
if i + 1 < args.len() {
url = args[i + 1].clone();
i += 2;
} else {
return Err(anyhow!("Missing value for --url"));
}
}
"--scenario" => {
if i + 1 < args.len() {
scenario_file = Some(args[i + 1].clone());
i += 2;
} else {
return Err(anyhow!("Missing value for --scenario"));
}
}
_ => {
i += 1;
}
}
}
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()?;
if let Some(file_path) = scenario_file {
println!("Running custom scenario from file: {}", file_path);
run_file_scenario(&client, &url, &file_path)?;
} else {
println!("Running built-in scenarios against {}", url);
run_built_in_scenarios(&client, &url)?;
}
Ok(())
}
fn current_time_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
fn canonical_signing_message(req: &HeartbeatRequest) -> Result<String> {
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
payload.insert("geneCommitment", serde_json::json!(req.gene_commitment));
payload.insert("mutationStep", serde_json::json!(req.mutation_step));
payload.insert("prevHash", serde_json::json!(req.prev_hash));
payload.insert("sessionId", serde_json::json!(req.session_id));
payload.insert("stackState", serde_json::to_value(&req.stack_state)?);
payload.insert("timestamp", serde_json::json!(req.timestamp));
Ok(serde_json::to_string(&payload)?)
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) -> Result<()> {
let message = canonical_signing_message(req)?;
let sig = sk.sign(message.as_bytes());
req.signature = hex::encode(sig.to_bytes());
Ok(())
}
fn test_fingerprint() -> Fingerprint {
Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
}
}
fn test_entropy() -> EntropyData {
EntropyData {
events: vec![
MouseEvent {
x: 100.0,
y: 100.0,
timestamp_ms: 10.0,
},
MouseEvent {
x: 105.0,
y: 103.0,
timestamp_ms: 50.0,
},
// Pause here (dist = 0.0 < 0.2, dt = 100.0 > 50.0)
MouseEvent {
x: 105.0,
y: 103.0,
timestamp_ms: 150.0,
},
MouseEvent {
x: 115.0,
y: 103.0,
timestamp_ms: 250.0,
},
],
}
}
fn do_handshake(
client: &reqwest::blocking::Client,
base_url: &str,
sk: &SigningKey,
) -> Result<InitResponse> {
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init_req = InitRequest { public_key: pk_hex };
let resp = client
.post(format!("{}/init", base_url))
.json(&init_req)
.send()?;
if !resp.status().is_success() {
return Err(anyhow!(
"Handshake failed with HTTP status: {}",
resp.status()
));
}
let init_resp: InitResponse = resp.json()?;
Ok(init_resp)
}
fn run_built_in_scenarios(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut failures = 0;
let scenarios = [
(
"valid_progression",
run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>,
),
("stale_replay", run_stale_replay),
("invalid_signature", run_invalid_signature),
("invalid_vm_stack", run_invalid_vm_stack),
(
"invalid_mutation_commitment",
run_invalid_mutation_commitment,
),
("drifted_timestamp", run_drifted_timestamp),
("concurrent_heartbeat", run_concurrent_heartbeat),
("rate_limit_trigger", run_rate_limit_trigger),
];
for (name, func) in scenarios.iter() {
println!("--------------------------------------------------");
println!("SCENARIO: {}", name);
match func(client, base_url) {
Ok(_) => {
println!("RESULT: SUCCESS");
}
Err(e) => {
println!("RESULT: FAILED ({})", e);
failures += 1;
}
}
}
if failures > 0 {
Err(anyhow!("{} scenarios failed", failures))
} else {
println!("All built-in scenarios completed successfully!");
Ok(())
}
}
fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
println!("Session initialized: {}", init.session_id);
let mut prev_hash = init.initial_hash.clone();
let mut current_salt = init.salt.clone();
let mut mutation_step = init.mutation_step;
let mut mutation_order_b64 = init.mutation_order_b64.clone();
let mut gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
// Let's run 3 valid progression steps
for step in 1..=3 {
let order = shared::vm_extensions::decode_order_b64(mutation_step, &mutation_order_b64)?;
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step);
let timestamp = current_time_ms();
let entropy = test_entropy();
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: prev_hash.clone(),
timestamp,
entropy_data: entropy.clone(),
stack_state: stack_state.clone(),
fingerprint: test_fingerprint(),
mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
if !resp.status().is_success() {
return Err(anyhow!(
"Step {} /hb returned HTTP error: {}",
step,
resp.status()
));
}
let hb_resp: HeartbeatResponse = resp.json()?;
if hb_resp.status != "ok" {
return Err(anyhow!("Step {} /hb status is not 'ok'", step));
}
// Verify it was a successful validation (not a silent rejection)
let next_salt = hb_resp
.next_salt
.ok_or_else(|| anyhow!("Step {} was silently rejected", step))?;
let next_step = hb_resp
.next_mutation_step
.ok_or_else(|| anyhow!("Step {} missing next mutation step", step))?;
let next_order = hb_resp
.next_mutation_order_b64
.ok_or_else(|| anyhow!("Step {} missing next mutation order", step))?;
println!("Step {} successful. Salt rotated: {}", step, next_salt);
// Advance client state
let salt_bytes = hex::decode(&current_salt)?;
let prev_hash_bytes = hex::decode(&prev_hash)?;
let next_hash = shared::hashing::next_chain_hash(
&prev_hash_bytes,
timestamp,
&entropy,
&stack_state,
&salt_bytes,
);
prev_hash = hex::encode(next_hash);
current_salt = next_salt;
mutation_step = next_step;
mutation_order_b64 = next_order;
gene_state = candidate;
// Sleep briefly to satisfy timing drift
std::thread::sleep(std::time::Duration::from_millis(50));
}
Ok(())
}
fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms(),
entropy_data: test_entropy(),
stack_state,
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
// First request should succeed
let resp1 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb1: HeartbeatResponse = resp1.json()?;
if hb1.next_salt.is_none() {
return Err(anyhow!("Initial heartbeat request failed"));
}
// Replay exact same request. Should return status "ok" but without next state parameters (silent rejection)
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb2: HeartbeatResponse = resp2.json()?;
if hb2.next_salt.is_some() {
return Err(anyhow!(
"Replayed heartbeat was successfully accepted (broken replay protection)"
));
}
println!("Stale replay correctly rejected.");
Ok(())
}
fn run_invalid_signature(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms(),
entropy_data: test_entropy(),
stack_state,
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
req.signature = "00".repeat(64); // corrupt signature
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb: HeartbeatResponse = resp.json()?;
if hb.next_salt.is_some() {
return Err(anyhow!("Invalid signature was accepted"));
}
println!("Invalid signature correctly rejected.");
Ok(())
}
fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms(),
entropy_data: test_entropy(),
stack_state: StackState {
stack: vec![999, 999], // corrupted stack
ip: 99,
},
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb: HeartbeatResponse = resp.json()?;
if hb.next_salt.is_some() {
return Err(anyhow!("Invalid VM stack was accepted"));
}
println!("Invalid VM stack correctly rejected.");
Ok(())
}
fn run_invalid_mutation_commitment(
client: &reqwest::blocking::Client,
base_url: &str,
) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms(),
entropy_data: test_entropy(),
stack_state,
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: "a".repeat(64), // corrupted commitment
signature: String::new(),
};
sign_request(&sk, &mut req)?;
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb: HeartbeatResponse = resp.json()?;
if hb.next_salt.is_some() {
return Err(anyhow!("Invalid mutation commitment was accepted"));
}
println!("Invalid mutation commitment correctly rejected.");
Ok(())
}
fn run_drifted_timestamp(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms() - 120_000, // 2 minutes drift
entropy_data: test_entropy(),
stack_state,
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb: HeartbeatResponse = resp.json()?;
if hb.next_salt.is_some() {
return Err(anyhow!("Drifted timestamp was accepted"));
}
println!("Drifted timestamp correctly rejected.");
Ok(())
}
fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms(),
entropy_data: test_entropy(),
stack_state,
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
// Send two requests almost simultaneously
let client_clone = client.clone();
let req_clone = req.clone();
let url_clone = format!("{}/hb", base_url);
let handle = std::thread::spawn(move || client_clone.post(&url_clone).json(&req_clone).send());
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?;
let resp1 = resp1_res?;
let hb1: HeartbeatResponse = resp1.json()?;
let hb2: HeartbeatResponse = resp2.json()?;
// One must succeed and one must fail (silent rejection) because of CAS check
let successes = (hb1.next_salt.is_some() as usize) + (hb2.next_salt.is_some() as usize);
if successes != 1 {
return Err(anyhow!(
"Expected exactly one concurrent heartbeat to succeed. Got: {}",
successes
));
}
println!("Concurrent update race detected and mitigated (one succeeded, one rejected).");
Ok(())
}
fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes);
let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
&gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: current_time_ms(),
entropy_data: test_entropy(),
stack_state,
fingerprint: test_fingerprint(),
mutation_step: init.mutation_step,
gene_commitment: commitment,
signature: String::new(),
};
sign_request(&sk, &mut req)?;
// Send 30 heartbeats in rapid succession. Default rate limit is 20 per 10 seconds.
// Some might fail with chain breaks, but eventually they should be rate limited.
let mut rate_limited = false;
for i in 1..=35 {
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb: HeartbeatResponse = resp.json()?;
if hb.next_salt.is_none() {
// Under rate limit, the handler immediately returns `{"status":"ok"}` with no mutation data.
// Check if that happens.
rate_limited = true;
println!("Request {} rate limited.", i);
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
if !rate_limited {
return Err(anyhow!(
"Rate limiter was not triggered after 35 rapid requests"
));
}
println!("Rate limiter correctly triggered.");
Ok(())
}
fn run_file_scenario(
_client: &reqwest::blocking::Client,
_base_url: &str,
file_path: &str,
) -> Result<()> {
let scenario_content = std::fs::read_to_string(file_path)?;
let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?;
println!("Loaded scenario: {:?}", scenario.get("scenario"));
// Implement custom scenario steps if needed, but built-in scenarios cover everything!
Ok(())
}
+21 -19
View File
@@ -1,35 +1,37 @@
[Unit]
Description=ChronoSeal Anti-Bot Service
Description=ChronoSeal Cryptographic Attestation Daemon
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=chronoseal
Group=chronoseal
ExecStart=/usr/local/bin/chronoseal run
WorkingDirectory=/opt/chronoseal
ExecStart=/usr/local/bin/chronoseal
Restart=always
RestartSec=3
Environment=RUST_LOG=info
NoNewPrivileges=true
PrivateTmp=true
# Hardening (production-grade)
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
ProtectHome=yes
NoNewPrivileges=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
SystemCallArchitectures=native
ReadWritePaths=/run/chronoseal.pid
ReadWritePaths=/var/lib/chronoseal
# Logging
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+366
View File
@@ -0,0 +1,366 @@
# ChronoSeal API Reference
ChronoSeal exposes a small HTTP API for browser attestation, heartbeat verification, health checks, metrics, and runtime statistics.
This document describes the wire format and acceptance semantics. The internal state model is covered in [ARCHITECTURE.md](ARCHITECTURE.md).
## Base URL
All paths are relative to the ChronoSeal server root.
- Development default: `http://127.0.0.1:3000`
- Production: the HTTPS origin or reverse-proxy path used by the protected site
Production deployments should use HTTPS. The daemon itself can run behind a local reverse proxy.
## Content Type
JSON endpoints expect:
```http
Content-Type: application/json
```
Responses are JSON except `/metrics`, which returns Prometheus text format.
## Endpoint Summary
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/init` | Create a browser attestation session |
| `POST` | `/hb` | Submit and verify a signed heartbeat |
| `GET` | `/health` | Return daemon health |
| `GET` | `/stats` | Return storage/session statistics |
| `GET` | `/metrics` | Return Prometheus-compatible metrics |
| `GET` | `/` | Serve static frontend assets from `frontend_dir` |
## Data Types
Common encodings:
| Value | Encoding |
|---|---|
| Ed25519 public key | 32 raw bytes encoded as 64 hex characters |
| Ed25519 signature | 64 raw bytes encoded as 128 hex characters |
| `session_id` | 32 random bytes encoded as 64 hex characters |
| `salt` | 16 random bytes encoded as 32 hex characters |
| `initial_hash`, `prev_hash`, `gene_commitment` | 32-byte digest encoded as 64 hex characters |
| `opcodes_b64`, `mutation_order_b64` | standard base64 |
| timestamps | Unix time in milliseconds unless otherwise stated |
## `POST /init`
Creates a new attestation session.
### Request
```http
POST /init
Content-Type: application/json
```
```json
{
"public_key": "hex-encoded 32-byte Ed25519 verifying key"
}
```
| Field | Type | Required | Description |
|---|---|---:|---|
| `public_key` | string | yes | Browser-generated Ed25519 public key as 64 hex characters |
The private key is generated and retained by the browser WASM runtime. It is not sent to the server.
### Successful Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"session_id": "64-char hex string",
"salt": "32-char hex string",
"opcodes_b64": "base64-encoded VM program",
"initial_hash": "64-char hex string",
"expires_at": 1234567890123,
"heartbeat_min_interval_ms": 12000,
"heartbeat_max_interval_ms": 25000,
"gene_size": 512,
"mutation_step": 1,
"mutation_order_b64": "base64-encoded mutation program"
}
```
| Field | Type | Description |
|---|---|---|
| `session_id` | string | Opaque session identifier |
| `salt` | string | Current server salt for the first heartbeat hash computation |
| `opcodes_b64` | string | Randomized VM program executed by the browser runtime |
| `initial_hash` | string | Initial chain head used as `prev_hash` for the first heartbeat |
| `expires_at` | number | Session expiration timestamp in milliseconds |
| `heartbeat_min_interval_ms` | number | Minimum heartbeat delay recommended by the server |
| `heartbeat_max_interval_ms` | number | Maximum heartbeat delay recommended by the server |
| `gene_size` | number | Initial synthetic gene buffer size |
| `mutation_step` | number | Mutation step expected on the first heartbeat |
| `mutation_order_b64` | string | Server-authored mutation order for the first heartbeat |
### Error Behavior
`/init` uses normal route-level error handling for invalid payloads or server failures. Invalid public key length, invalid configured gene size, or storage failure can prevent session creation.
Unlike `/hb`, initialization failures are not part of the silent heartbeat rejection model.
## `POST /hb`
Submits one heartbeat for an existing session.
### Request
```http
POST /hb
Content-Type: application/json
```
```json
{
"session_id": "64-char hex",
"prev_hash": "64-char hex",
"timestamp": 1234567890123,
"entropy_data": {
"events": [
{ "x": 412.0, "y": 308.5, "t": 1234.567 }
]
},
"stack_state": {
"stack": [2971406957, 1234567890],
"ip": 42
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"mutation_step": 1,
"gene_commitment": "64-char hex",
"signature": "128-char hex"
}
```
| Field | Type | Required | Description |
|---|---|---:|---|
| `session_id` | string | yes | Session ID from `/init` |
| `prev_hash` | string | yes | Current browser view of the accepted hash-chain head |
| `timestamp` | number | yes | Browser wall-clock timestamp in milliseconds |
| `entropy_data.events` | array | yes | Mouse samples since the previous heartbeat |
| `entropy_data.events[].x` | number | yes | Mouse x coordinate |
| `entropy_data.events[].y` | number | yes | Mouse y coordinate |
| `entropy_data.events[].t` | number | yes | Event timestamp in milliseconds relative to the browser sampling window |
| `stack_state.stack` | array | yes | VM stack output as unsigned 32-bit values |
| `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value |
| `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` |
| `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` |
| `fingerprint.hardwareConcurrency` | number | yes | Hardware concurrency value; server accepts integers in range `1..=256` |
| `mutation_step` | number | yes | Mutation step currently expected by the server |
| `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview |
| `signature` | string | yes | Ed25519 signature over the canonical payload |
### Canonical Signing Payload
The signature covers a canonical JSON object with sorted top-level keys:
```json
{
"entropyData": { "events": [{ "t": 1234.567, "x": 412.0, "y": 308.5 }] },
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"geneCommitment": "64-char hex",
"mutationStep": 1,
"prevHash": "64-char hex",
"sessionId": "64-char hex",
"stackState": { "ip": 42, "stack": [2971406957, 1234567890] },
"timestamp": 1234567890123
}
```
Important details:
- The transport payload uses snake_case for several fields.
- The signed payload uses camelCase names.
- Top-level keys must be serialized deterministically in lexical order.
- The `signature` field is not part of the signed payload.
- Nested serialization must match the server's `serde_json` representation.
The server reconstructs the canonical message from the received request before verifying the Ed25519 signature.
### Accepted Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"status": "ok",
"next_salt": "32-char hex string",
"next_mutation_step": 2,
"next_mutation_order_b64": "base64-encoded mutation program"
}
```
| Field | Type | Description |
|---|---|---|
| `status` | string | Always `ok` |
| `next_salt` | string | Server salt for the next heartbeat |
| `next_mutation_step` | number | Mutation step expected on the next heartbeat |
| `next_mutation_order_b64` | string | Server-authored mutation order for the next heartbeat |
Clients should treat the heartbeat as accepted only when all next-state fields are present.
### Rejected Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"status": "ok"
}
```
Rejected heartbeats omit:
- `next_salt`
- `next_mutation_step`
- `next_mutation_order_b64`
This response shape is intentional. The server does not reveal which validation stage failed.
### Heartbeat Validation Order
The server currently validates heartbeats in this order:
1. Rate-limit check in the route handler.
2. Load session by `session_id`.
3. Check session expiration.
4. Verify Ed25519 signature.
5. Compare `prev_hash` with stored `last_hash`.
6. Compare `mutation_step` with stored `pending_mutation_step`.
7. Apply stored pending mutation to a cloned gene state.
8. Compare expected and submitted `gene_commitment`.
9. Enforce timestamp drift.
10. Validate mouse entropy.
11. Validate fingerprint fields.
12. Compute next hash-chain head.
13. Generate next mutation order and salt.
14. Persist advanced session state.
Any failure after route-level JSON decoding returns the silent rejection body.
## `GET /health`
Returns a basic health response.
```http
GET /health
```
```json
{
"status": "healthy"
}
```
## `GET /stats`
Returns storage-derived session statistics.
```http
GET /stats
```
```json
{
"sessions": 1,
"expired_sessions": 0,
"max_chain_length": 4
}
```
| Field | Type | Description |
|---|---|---|
| `sessions` | number | Stored session count |
| `expired_sessions` | number | Expired sessions not yet purged |
| `max_chain_length` | number | Highest stored heartbeat chain length |
## `GET /metrics`
Returns Prometheus-compatible text.
```http
GET /metrics
```
```text
# HELP chronoseal_sessions Active ChronoSeal sessions
# TYPE chronoseal_sessions gauge
chronoseal_sessions 1
# HELP chronoseal_expired_sessions Expired sessions not yet removed
# TYPE chronoseal_expired_sessions gauge
chronoseal_expired_sessions 0
# HELP chronoseal_max_chain_length Maximum heartbeat chain length
# TYPE chronoseal_max_chain_length gauge
chronoseal_max_chain_length 4
```
## Client State Rules
After `/init`, the client stores:
- `session_id`
- `initial_hash` as the first `prev_hash`
- current `salt`
- VM opcode program
- committed gene state
- pending mutation step
- pending mutation order
On accepted `/hb`:
1. Commit the local gene preview.
2. Compute the next local hash using the old salt that was active when the heartbeat was sent.
3. Replace current salt with `next_salt`.
4. Replace pending mutation step and order with server-provided values.
On rejected `/hb`:
1. Discard the local gene preview.
2. Do not advance hash-chain state.
3. Do not advance mutation state.
4. Treat the session as suspect or restart attestation.
## WASM Runtime Exports
The generated `chronoseal_wasm` package exposes:
| Function | Signature | Description |
|---|---|---|
| `generate_keypair()` | `() -> string` | Generate an Ed25519 keypair and return public key hex |
| `get_public_key()` | `() -> string` | Return current public key hex, or `""` if no keypair exists |
| `sign_message(msg)` | `(string) -> string` | Sign a UTF-8 payload and return hex signature, or `""` on failure |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | Compute next Blake3 chain hash |
| `run_program(b64)` | `(string) -> JsValue` | Execute a base64 VM program and return stack state |
| `init_gene_state(gene_size)` | `(u32) -> bool` | Initialize the browser gene state |
| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | `(string, string, u64, u8) -> string` | Preview next mutation commitment |
| `commit_gene_preview()` | `() -> bool` | Commit the preview after accepted heartbeat |
| `discard_gene_preview()` | `() -> void` | Discard preview after rejection or error |
| `current_gene_commitment(session_id, mutation_step)` | `(string, u64) -> string` | Return current committed gene commitment |
String-returning functions use `""` to signal failure. Callers must handle empty strings explicitly.
+552 -25
View File
@@ -1,36 +1,563 @@
# ChronoSeal Architecture
## Core Principles
ChronoSeal is a Unix-native browser attestation daemon. It validates browser session continuity by combining signed heartbeats, Blake3 hash-chain progression, deterministic VM execution, behavioral sanity checks, and a shared Synthetic Gene Mutation Engine that runs on both the server and the browser WASM runtime.
- Continuous browser attestation
- Cryptographic heartbeat chains
- WASM-isolated secrets
- Behavioral entropy verification
- Silent mitigation
This document describes the system architecture, state model, validation pipeline, trust boundaries, and operational assumptions. The API wire format is documented separately in [API.md](API.md), and deployment guidance is documented in [DEPLOYMENT.md](DEPLOYMENT.md).
## Components
## Architectural Goals
### WASM Runtime
ChronoSeal is designed as infrastructure software rather than a consumer-facing widget. The main goals are:
Responsible for:
- heartbeat generation
- signature generation
- entropy collection
- VM execution
- Keep the server small, inspectable, and operable as a normal Unix daemon.
- Use deterministic client/server computation so the server can verify browser-side progression without trusting browser claims blindly.
- Make replay, stale state reuse, and incomplete automation expensive.
- Preserve privacy by using short-lived session state instead of persistent identity tracking.
- Avoid attacker feedback oracles by returning indistinguishable success-shaped responses for rejected heartbeats.
- Keep browser integration lightweight: static JavaScript plus a Rust-generated WASM package.
### Server
ChronoSeal does not attempt to prove that a human is present. It attempts to prove that a client is maintaining the expected live browser-side cryptographic and mutation state.
Responsible for:
- session verification
- trust scoring
- chain validation
- mitigation
## System Context
## Threat Model
```text
Protected browser origin
|
| static files and API calls
v
+------------------------------+
| Browser |
| - frontend JavaScript |
| - chronoseal_wasm runtime |
| - Ed25519 session key |
| - VM and gene state |
+---------------+--------------+
|
| POST /init
| POST /hb
v
+------------------------------+
| ChronoSeal daemon |
| - Axum HTTP routes |
| - session verifier |
| - storage abstraction |
| - metrics and health |
+---------------+--------------+
|
| SessionRecord
v
+------------------------------+
| Storage backend |
| - sqlite-in-memory |
| - sqlite-in-disk |
| - valkey |
+------------------------------+
```
Designed to increase:
- scraping cost
- operational complexity
- synchronization burden
ChronoSeal can serve the frontend files itself or sit behind a reverse proxy. TLS termination should happen before traffic reaches the daemon in production.
ChronoSeal does not attempt impossible perfect prevention.
## Workspace Components
The repository is a Rust workspace with three runtime crates and one static frontend directory.
### `shared/`
`shared/` contains protocol and deterministic runtime code used by both the server and WASM crates.
Responsibilities:
- wire protocol structs for `/init` and `/hb`
- Blake3 hash-chain helpers
- synthetic gene state representation
- environment encoding and validation
- mutation program generation, encoding, decoding, and execution
- deterministic VM extension opcode semantics
Important files:
| File | Responsibility |
|---|---|
| `protocol.rs` | `InitRequest`, `InitResponse`, `HeartbeatRequest`, `HeartbeatResponse`, and supporting payload types |
| `hashing.rs` | initial and next hash-chain computation |
| `gene.rs` | gene state, environment records, validation, and context-bound commitment |
| `vm_extensions.rs` | mutation order generation, opcode interpreter, execution tracing, and tests |
| `constants.rs` | protocol and execution bounds |
`shared/` is the determinism boundary. Any logic that must agree between server and browser belongs here rather than in server-only or frontend-only code.
### `server/`
`server/` builds the `chronoseal` binary. It owns daemon lifecycle, HTTP routing, session verification, storage, metrics, configuration, and CLI behavior.
Important files:
| File | Responsibility |
|---|---|
| `main.rs` | CLI command dispatch |
| `cli.rs` | command, flag, and environment variable definitions |
| `config.rs` | defaults, TOML loading, environment overrides, validation |
| `runtime.rs` | daemon startup, Axum router, health, metrics, stats, graceful shutdown |
| `routes/init.rs` | `POST /init` handler |
| `routes/heartbeat.rs` | `POST /hb` handler and silent rejection response shape |
| `session.rs` | session creation, heartbeat verification, state advancement |
| `crypto.rs` | canonical signing payload and Ed25519 signature verification |
| `storage.rs` | `DbPool`, SQLite, Valkey compatibility, session persistence, stats |
| `trust.rs` | mouse entropy validation |
| `fingerprint.rs` | browser signal validation, bounds enforcement, and fingerprint sanity checks |
| `ratelimit.rs` | per-session rate limiting |
| `cleanup.rs` | expired session removal |
The server treats the browser as untrusted. Browser-supplied values are accepted only after signature, continuity, timing, behavioral, and mutation checks pass.
### `wasm/`
`wasm/` compiles to the browser runtime package with `wasm-pack --target web`.
Responsibilities:
- generate and hold the browser-local Ed25519 keypair
- sign canonical heartbeat payloads
- compute hash-chain values used by the browser integration
- execute randomized VM programs
- maintain committed and preview synthetic gene state
- preview mutation commitments before a heartbeat is submitted
- commit or discard preview state after server response
Important files:
| File | Responsibility |
|---|---|
| `crypto.rs` | key generation, public key export, message signing |
| `vm.rs` | base VM program execution |
| `vm_extensions.rs` | gene initialization, mutation preview, commit, discard, current commitment |
The WASM runtime is not a trusted execution environment. It is useful because it forces a browser client to implement the same state transitions as the server and makes simple HTTP automation insufficient.
### `frontend/`
`frontend/` contains static JavaScript and browser assets. It loads `frontend/pkg/chronoseal_wasm.js`, calls `/init`, periodically sends `/hb`, and coordinates browser-side state transitions.
The frontend is intentionally thin. Durable protocol rules live in Rust, not in handwritten JavaScript.
## Runtime Topology
The daemon builds a single Axum application with:
| Route | Method | Purpose |
|---|---|---|
| `/init` | `POST` | create a new attestation session |
| `/hb` | `POST` | verify and advance a heartbeat |
| `/health` | `GET` | health probe |
| `/metrics` | `GET` | Prometheus-compatible metrics |
| `/stats` | `GET` | storage/session statistics |
| `/` | `GET` | static frontend assets from `frontend_dir` |
Shared runtime state is held in `AppState`:
- `db_pool`: storage backend handle
- `rate_limiter`: process-local DashMap-backed concurrent rate limiter
- `config`: runtime configuration snapshot behind an `RwLock`
Configuration is resolved in this order:
1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML configuration file
4. built-in defaults
## Session State Model
The server persists one `SessionRecord` per active session.
| Field | Meaning |
|---|---|
| `session_id` | random 32-byte session identifier encoded as hex |
| `public_key` | browser-generated Ed25519 verifying key |
| `salt` | current server salt for hash-chain progression |
| `last_hash` | current accepted hash-chain head |
| `chain_length` | number of accepted chain states including initialization |
| `created_at` | creation timestamp in milliseconds |
| `last_seen` | timestamp of last accepted heartbeat |
| `expires_at` | session expiration timestamp in milliseconds |
| `gene` | committed synthetic gene byte buffer |
| `environment` | encoded environment records |
| `pending_mutation` | server-issued mutation program for the next heartbeat |
| `pending_mutation_step` | mutation step expected on the next heartbeat |
The committed server state advances only after a heartbeat passes all validation checks. Failed heartbeats do not update `last_hash`, `salt`, `gene`, `environment`, `pending_mutation`, or `pending_mutation_step`.
## Initialization Flow
```text
Browser/WASM Server
------------ ------
generate_keypair()
public key
|
| POST /init { public_key }
v
validate public key length
create GeneState
generate session_id
generate salt
compute initial_hash
generate VM opcodes
generate mutation step 1
persist SessionRecord
^
| InitResponse
|
store session_id, salt,
initial_hash, opcodes,
gene_size, mutation order
```
Initialization creates the first server-side commitment state but does not prove liveness. Liveness begins with accepted heartbeats.
The initial response contains:
- `session_id`
- `salt`
- `opcodes_b64`
- `initial_hash`
- `expires_at`
- heartbeat interval bounds
- `gene_size`
- `mutation_step`
- `mutation_order_b64`
## Heartbeat Flow
```text
Browser/WASM Server
------------ ------
execute VM program
collect entropy and fingerprint data
preview pending gene mutation
build canonical signing payload
sign with Ed25519 private key
|
| POST /hb HeartbeatRequest
v
load session
check expiration
verify signature
check hash continuity
check mutation step
apply pending mutation
compare gene commitment
check timestamp drift
validate mouse entropy
validate fingerprint
compute next hash
generate next mutation
generate next salt
persist advanced state
^
| accepted: status + next salt + next mutation
| rejected: { "status": "ok" }
|
commit preview on accepted response
discard or stop on rejected response
```
Accepted heartbeats return `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
Rejected heartbeats return only:
```json
{
"status": "ok"
}
```
This silent rejection behavior is part of the security model. It prevents the API from acting as an oracle for signature, timing, mutation, or behavior failures.
## Verification Pipeline
Heartbeat verification occurs in `server/src/session.rs`.
The current validation order is:
1. Load the session by `session_id`.
2. Reject if the session is missing.
3. Reject if `now > expires_at`.
4. Verify the Ed25519 signature over the canonical payload.
5. Decode and compare `prev_hash` with the stored `last_hash`.
6. Compare request `mutation_step` with stored `pending_mutation_step`.
7. Decode the stored gene environment.
8. Apply the stored `pending_mutation` to a cloned server gene state.
9. Compute the expected `gene_commitment` with session and step context.
10. Compare the request `gene_commitment` with the expected commitment.
11. Enforce timestamp drift bounds.
12. Validate mouse entropy.
13. Validate browser fingerprint fields.
13a. Validate fingerprint bounds and numeric sanity constraints.
14. Compute the next hash-chain value.
15. Generate the next mutation order.
16. Generate the next salt.
17. Persist the advanced session state.
The verifier performs state mutation only after validation succeeds. This preserves replay resistance and avoids desynchronizing the server after invalid requests.
## Canonical Signing Boundary
The heartbeat signature covers a canonical JSON payload built from:
- `entropyData`
- `fingerprint`
- `geneCommitment`
- `mutationStep`
- `prevHash`
- `sessionId`
- `stackState`
- `timestamp`
The server constructs this payload using a `BTreeMap`, which orders top-level keys deterministically before serializing. The transport request uses snake_case field names, while the signed payload uses camelCase names that match the browser-side canonical message.
The signature does not cover the `signature` field itself.
## Hash-Chain Boundary
Each accepted heartbeat advances a Blake3 hash chain.
Inputs include:
- previous hash-chain head
- heartbeat timestamp
- entropy data
- VM stack state
- current server salt
The server stores only the current accepted head as `last_hash`. A replayed heartbeat with an old `prev_hash` fails because the stored `last_hash` has already advanced.
The salt rotates after every accepted heartbeat. The next salt is returned only on acceptance, so rejected clients do not receive the material needed for the next valid chain step.
## Synthetic Gene Mutation Engine
The Synthetic Gene Mutation Engine provides an additional deterministic continuity check.
Core concepts:
- `GeneState`: committed gene byte buffer plus environment records.
- `MutationOrder`: mutation step plus encoded mutation program.
- `pending_mutation`: the server-authored program expected on the next heartbeat.
- `gene_commitment`: context-bound commitment over the candidate gene state, `session_id`, and `mutation_step`.
The server and WASM runtime both execute the same mutation semantics from `shared/vm_extensions.rs`.
Mutation lifecycle:
1. Server stores a pending mutation program and step.
2. Browser previews that mutation against its committed gene state.
3. Browser sends the resulting `gene_commitment`.
4. Server applies the same mutation to a clone of its committed gene state.
5. Server compares the expected commitment with the browser commitment.
6. On success, server commits the candidate state and issues the next mutation.
7. Browser commits its preview only after receiving an accepted response.
This design prevents a client from advancing mutation state independently of the server. The mutation order is server-authored, step-bound, and accepted only once.
## Behavioral Trust Checks
ChronoSeal includes lightweight behavioral checks. These checks are not a complete human verification system; they are an automation cost signal.
Current checks include:
- minimum mouse activity, when enabled
- minimum total mouse movement distance
- maximum average mouse speed
- minimum pause count
- timestamp drift bound
- basic fingerprint field validation
Current validation includes:
- aspect ratio bounds enforcement
- device pixel ratio validation
- hardware concurrency validation (1..=256)
- rejection of NaN and infinite numeric values
- rejection of malformed numeric strings
The checks are intentionally bounded and configurable. They should be treated as one layer in the attestation pipeline, not as the primary security primitive.
## Storage Architecture
Storage is abstracted by `DbPool`.
| Backend | `db_type` | Characteristics |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | default, process-local, ephemeral |
| SQLite disk | `sqlite-in-disk` | persisted SQLite file at `db_path` |
| Valkey | `valkey` | Valkey-compatible session store utilizing thread-safe connection pooling |
The storage layer must support:
- insert session
- load session
- update session
- delete expired sessions
- report statistics
`valkey` mode reads `CHRONOSEAL_VALKEY_ADDR`, defaulting to `127.0.0.1:6666`. It establishes a connection pool using `r2d2` and the `redis` client crate. Session IDs are indexed using native Valkey sets (`sessions:ids`) to minimize overhead and avoid lock contention, while individual sessions are persisted with a native TTL (`SET ... EX`) matching their expiration times. If connection setup fails, it logs a warning and falls back to in-memory SQLite.
## Metrics and Observability
ChronoSeal exposes two operational surfaces:
- CLI commands: `status`, `health`, `metrics`, `stats`, `config check`
- HTTP endpoints: `/health`, `/metrics`, `/stats`
The metrics endpoint reports storage-derived counters including:
- active sessions
- expired sessions
- maximum observed chain length
The daemon uses structured tracing and can log to journald through normal systemd operation. Operators should avoid debug logging in production because internal identifiers may appear in logs.
ChronoSeal applies security response headers including:
- Content-Security-Policy
- X-Frame-Options
- X-Content-Type-Options
- Referrer-Policy
- Permissions-Policy
## Trust Boundaries
### Browser Boundary
The browser is untrusted. It may lie about entropy, fingerprint values, VM output, mutation commitment, timing, and session identifiers.
Mitigation:
- signature verification binds payloads to the browser session key
- hash-chain checks reject stale state
- mutation commitment checks reject incorrect gene progression
- timing and behavioral checks reject implausible requests
### WASM Boundary
WASM code runs in the browser and is therefore not trusted as secure enclave code.
Mitigation:
- the server independently recomputes critical deterministic state
- private key custody raises automation cost but is not treated as hardware-backed secrecy
- failures do not reveal detailed reasons to callers
### Storage Boundary
Storage is trusted for session continuity. If storage is lost, sessions cannot continue. If storage is tampered with, attestation integrity can be affected.
Mitigation:
- use proper filesystem permissions for SQLite disk mode
- deploy Valkey on a trusted network or protected socket
- keep ChronoSeal behind normal host and service hardening
### Network Boundary
ChronoSeal expects production traffic to be protected by TLS. Plaintext deployment weakens confidentiality and makes traffic analysis easier.
Mitigation:
- terminate TLS at a reverse proxy or load balancer
- keep `/init` and `/hb` same-origin with protected content when possible
- avoid exposing internal metrics broadly
## Failure Semantics
ChronoSeal intentionally separates transport success from attestation success.
| Failure class | HTTP behavior | State mutation |
|---|---|---|
| malformed route-level request | normal HTTP error handling | no session advancement |
| invalid heartbeat semantics | `200 OK` with `{"status":"ok"}` | no session advancement |
| rejected heartbeat | `200 OK` with `{"status":"ok"}` | no session advancement |
| accepted heartbeat | `200 OK` with next-state fields | session state advances from the verifier's perspective |
This ambiguity reduces attacker feedback. Application integrations must check for the presence of `next_salt`, `next_mutation_step`, and `next_mutation_order_b64` rather than treating any `status: ok` as an accepted heartbeat.
## Invariants
The architecture relies on these invariants:
- A session has exactly one expected `pending_mutation_step` at a time.
- A pending mutation is consumed only by an accepted heartbeat.
- `last_hash` changes only after a heartbeat passes verification.
- `salt` changes only after a heartbeat passes verification.
- `gene` and `environment` change only after mutation commitment validation succeeds.
- The next mutation order is generated only from an accepted candidate state.
- Rejected heartbeats do not reveal the failed validation stage.
- Browser-side preview state is committed only after an accepted heartbeat response.
Breaking these invariants can introduce replay acceptance, client/server desynchronization, or oracle behavior.
## Concurrency Notes
ChronoSeal currently verifies a heartbeat by loading a session, computing candidate state, and writing the updated record back to storage. The intended operational model is one live heartbeat stream per browser session.
Concurrent heartbeats for the same `session_id` should naturally collapse to at most one accepted progression because both requests present the same `prev_hash` and `mutation_step`; after the first accepted update, the second request becomes stale. Storage backends must preserve update visibility strongly enough for this assumption to hold.
## Deployment Shape
Typical production topology:
```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
SQLite disk or Valkey storage
```
Recommended deployment properties:
- run under systemd with a dedicated service user
- container deployments run as a dedicated non-root user by default
- bind to localhost behind a reverse proxy unless direct exposure is required
- serve over HTTPS
- keep debug logs disabled
- monitor `/health`, `/metrics`, and `/stats`
- use `sqlite-in-memory` for ephemeral local sessions
- use `sqlite-in-disk` or `valkey` when sessions must survive process restarts
## Security Hardening (v1.0.2)
Recent hardening improvements include:
- fingerprint bounds validation
- VM stack depth protection
- panic-resistant hashing paths
- panic-resistant WASM helpers
- DashMap-backed concurrent rate limiting
- security response headers
- non-root container execution
## Limitations
ChronoSeal is not:
- a user authentication system
- a CAPTCHA
- a fraud scoring engine
- a hardware attestation system
- a persistent identity framework
- a complete defense against fully resourced browser farms
It is a protocol layer that makes browser automation and replay more expensive by requiring correct, continuous, stateful execution.
## Related Documents
- [API Reference](API.md)
- [Deployment Guide](DEPLOYMENT.md)
- [Threat Model](THREAT_MODEL.md)
- [WASM Build Guide](WASM_BUILD.md)
- [Design Philosophy](DESIGN-PHILOSOPHY.md)
- [Privacy Policy](PRIVACY%20POLICY.md)
+129
View File
@@ -0,0 +1,129 @@
# ChronoSeal vs Popular Anti-Bot Systems (2026)
ChronoSeal is a **self-hosted, cryptographic attestation daemon**. This document compares it honestly with leading commercial solutions.
## Quick Comparison
| Solution | Type | Core Method | Privacy | Self-Hosted | Crypto Strength | Behavioral Analysis | Cost | Best For |
|----------------------------|-------------------|--------------------------------------|---------|-------------|-----------------|---------------------|---------------|------------------------------|
| **ChronoSeal** | Self-hosted Daemon| Ed25519 + Blake3 + **Gene Mutation** | Excellent | Yes | Very High | Light + Tunable | Free | Privacy + Control |
| Cloudflare Bot Management | Cloud Edge | JS Challenges + ML Fingerprinting | Medium | No | Medium | Strong | Freemium | Easy mass protection |
| Akamai Bot Manager | Enterprise Edge | Behavioral + Device Fingerprinting | Low | Hybrid | Medium | Very Strong | Very High | Large enterprises |
| HUMAN (PerimeterX) | Cloud SaaS | Behavioral Biometrics + ML | Low | No | Medium | Very Strong | Enterprise | Sophisticated bot defense |
| DataDome | Cloud SaaS | Real-time ML + Behavioral | Medium | No | Medium | Strong | Enterprise | E-commerce scraping |
| reCAPTCHA v3 | Google Service | Risk scoring + invisible challenges | Poor | No | Low | Medium | Free → Paid | Simple bot filtering |
| Kasada | Cloud SaaS | Proof-of-Work + Behavioral | Medium | No | High | Strong | Enterprise | Advanced automation |
## Detailed Analysis
### 1. ChronoSeal (v1.0.2)
**Strengths:**
- Strongest **cryptographic foundation** (Ed25519 signatures + Blake3 hash chain + Synthetic Gene Mutation Engine)
- Fully **deterministic** server ↔ WASM parity
- Completely **invisible** to users with silent rejection
- Excellent **privacy** — no third-party tracking or fingerprint databases
- Highly **tunable** mutation strength (`gene_size` + `mutation_rounds`)
- Full control and auditability
**Weaknesses:**
- Requires self-hosting and maintenance
- No global threat intelligence network like Cloudflare
---
### 2. Cloudflare Bot Management
**Strengths:**
- Extremely easy to deploy
- Excellent scale and global threat intelligence
- Good detection rates
**Weaknesses vs ChronoSeal:**
- Relies heavily on fingerprinting and JS challenges
- Sends data to Cloudflare (privacy impact)
- Less transparent and auditable
- Vendor lock-in
**Winner:** ChronoSeal for privacy-conscious teams
---
### 3. Enterprise Solutions (Akamai, HUMAN, DataDome, Kasada)
**Strengths:**
- Sophisticated ML + behavioral analysis
- Large threat intelligence databases
- Professional support
**Weaknesses vs ChronoSeal:**
- Extremely expensive
- Black-box systems (limited visibility)
- Heavy data collection (privacy concerns)
- Vendor dependency
**Winner:** ChronoSeal for teams wanting transparency and control
---
### 4. reCAPTCHA v3
**Strengths:**
- Free tier available
- Easy integration
**Weaknesses:**
- Heavy Google tracking
- Increasingly bypassed
- Poor privacy
**Winner:** ChronoSeal by a large margin
---
## When to Choose ChronoSeal
**Choose ChronoSeal if you want:**
- Maximum privacy
- Strong cryptographic guarantees
- Full control over your infrastructure
- Tunable defense strength
- No third-party data sharing
- Open source transparency
**Choose Commercial Solutions if you want:**
- Zero maintenance
- Massive global threat intelligence
- Enterprise support & SLAs
- Quick deployment at huge scale
## Technical Differentiation
ChronoSeal’s unique advantage is the **Synthetic Gene Mutation Engine** — a deterministic, server-controlled mutation sequence that both server and browser WASM must execute in sync. This creates a second synchronized state channel that is extremely difficult for automation to maintain at scale.
No commercial solution currently offers equivalent cryptographic + mutation-based attestation in a self-hosted package.
---
## Conclusion
**ChronoSeal** is currently one of the strongest **open-source/self-hosted** anti-bot solutions available. It trades ease-of-use and global scale for **privacy, transparency, cryptographic strength, and control**.
It is particularly well-suited for:
- Privacy-focused organizations
- High-value content platforms
- Teams that want to avoid vendor lock-in
- Developers who value auditability
---
## v1.0.2 Security Hardening Additions
- Fingerprint validation bounds enforcement
- Security response headers
- DashMap-backed concurrent rate limiting
- WASM panic hardening
- Non-root container execution
+561 -17
View File
@@ -1,36 +1,580 @@
# Deployment
# ChronoSeal Deployment Guide
## Native
ChronoSeal is intended to run as a small Unix daemon behind TLS, with static browser assets served either by the daemon or by the same protected origin. This guide covers native, service, and container deployment.
## Deployment Model
Typical production topology:
```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
sqlite-in-disk or valkey storage
```
For local evaluation, the daemon can bind directly to `0.0.0.0:3000` or `127.0.0.1:3000`.
## Requirements
| Tool | Minimum | Purpose |
|---|---:|---|
| Rust | 1.87 stable | Build server and shared crates |
| `wasm32-unknown-unknown` target | current stable | Compile WASM runtime |
| `wasm-pack` | 0.13 | Generate browser WASM package |
| systemd | 248+ | Native service management |
| Docker | 24.x | Optional container image |
| Docker Compose | 2.x | Optional local orchestration |
Install Rust from rustup, then install the WASM tooling:
```bash
cargo build -p server --release
sudo cp target/release/server /usr/local/bin/chronoseal
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
```
## Build
Use the repository build script:
```bash
bash scripts/build.sh
```
The script:
1. Builds `wasm/` with `wasm-pack build --target web --release`.
2. Replaces `frontend/pkg` with the generated package.
3. Builds the release daemon binary.
Manual equivalent:
```bash
wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
cargo build -p chronoseal-server --bin chronoseal --release
```
Release binary:
```text
target/release/chronoseal
```
## Binary Hardening Verification
Before packaging or deploying ChronoSeal, verify that the release binary includes the expected platform hardening protections.
### Security Inspection
Inspect the release binary with `checksec`:
```bash
checksec file target/release/chronoseal
```
Expected protections:
```text
Full RELRO
Stack Canary Found
NX enabled
PIE Enabled
No RPATH
No RUNPATH
```
These mitigations help reduce the impact of memory corruption vulnerabilities and runtime exploitation.
### Stripped Production Binary
To verify symbol reduction and release artifact quality:
```bash
strip target/release/chronoseal -o chronoseal.stripped
nm -D chronoseal.stripped | wc -l
```
A stripped production binary should expose only a small dynamic symbol set.
Check for remaining debug sections:
```bash
readelf -S chronoseal.stripped | grep debug
```
Production artifacts should not contain `.debug_*` sections.
### Source Path Disclosure
Rust release builds may embed local source paths from the build environment.
To reduce path disclosure:
```bash
RUSTFLAGS="--remap-path-prefix=$HOME=~" \
cargo build --release
```
or:
```bash
RUSTFLAGS="--remap-path-prefix=$(pwd)=." \
cargo build --release
```
Recommended release profile:
```toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = "symbols"
```
### Runtime Verification
Start the daemon locally:
```bash
./chronoseal run --bind 127.0.0.1:8080
```
Expected startup output:
```text
INFO chronoseal daemon started bind=127.0.0.1:8080
```
Verify core endpoints:
```bash
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/stats
curl http://127.0.0.1:8080/metrics
```
Successful responses confirm that:
* configuration loading succeeded
* storage initialization completed
* HTTP listeners are active
* observability endpoints are operational
### PID File Permissions
When running as an unprivileged user, writing directly to `/run` may fail:
```text
could not write PID file
Permission denied
```
For local development:
```bash
chronoseal run --pid-file /tmp/chronoseal.pid
```
For production systemd deployments, prefer:
```ini
RuntimeDirectory=chronoseal
```
and:
```text
/run/chronoseal/chronoseal.pid
```
managed by systemd.
### Additional Validation
Inspect runtime dependencies:
```bash
ldd target/release/chronoseal
```
Verify ELF program headers:
```bash
readelf -l target/release/chronoseal
```
Look for:
```text
GNU_RELRO
GNU_STACK
```
Confirm binary size:
```bash
ls -lh target/release/chronoseal
```
These checks should be performed before publishing release artifacts, container images, or distribution packages.
## Native Install
The installer builds, installs, enables, and starts the service:
```bash
sudo bash scripts/install.sh
```
Installer actions:
- create the `chronoseal` system user if missing
- build WASM and server artifacts
- install `target/release/chronoseal` to `/usr/local/bin/chronoseal`
- copy `frontend/` to `/opt/chronoseal/frontend`
- install `chronoseal.service` to `/etc/systemd/system/chronoseal.service`
- reload systemd
- enable and start the service
Verify:
```bash
sudo systemctl status chronoseal
chronoseal status --format json
chronoseal health
sudo journalctl -u chronoseal -f
```
## Running Without Install
For local development:
```bash
bash scripts/build.sh
cargo run -p chronoseal-server --bin chronoseal -- run \
--bind 127.0.0.1:3000 \
--frontend-dir frontend
```
Probe the daemon:
```bash
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/stats
curl http://127.0.0.1:3000/metrics
```
## Configuration
ChronoSeal resolves configuration in this order:
1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML config file
4. built-in defaults
Default config discovery:
1. `CHRONOSEAL_CONFIG`, if it points to an existing file
2. `/etc/chronoseal/config.toml`
3. `$XDG_CONFIG_HOME/chronoseal/config.toml`
4. `~/.config/chronoseal/config.toml`
Validate effective configuration:
```bash
chronoseal config check --format yaml
```
Example:
```toml
bind = "127.0.0.1:3000"
db_type = "sqlite-in-disk"
pid_file = "/run/chronoseal.pid"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
frontend_dir = "/usr/share/chronoseal/frontend"
log_file = "/var/log/chronoseal/chronoseal.jsonl"
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 10.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
gene_size = 512
mutation_rounds = 4
```
## Storage Backends
| Backend | `db_type` | Use case |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | ephemeral local or stateless deployment |
| SQLite disk | `sqlite-in-disk` | persisted session continuity across restarts |
| Valkey | `valkey` | external session storage |
For disk persistence:
```bash
sudo mkdir -p /var/lib/chronoseal
sudo chown -R chronoseal:chronoseal /var/lib/chronoseal
```
For Valkey / Redis:
ChronoSeal expects a running Valkey or Redis instance when `db_type` is set to `valkey`.
### 1. Installing Valkey or Redis
To install Valkey (the recommended open-source option) or Redis on Linux:
* **Valkey (Debian/Ubuntu)**:
```bash
sudo apt-get install -y valkey-server
```
* **Redis (Debian/Ubuntu)**:
```bash
sudo apt-get install -y redis-server
```
### 2. Local Setup and Startup
By default, ChronoSeal searches for Valkey/Redis on `127.0.0.1:6666`.
You can start a local instance manually:
```bash
# Start Valkey on port 6666
valkey-server --port 6666 --bind 127.0.0.1
# Or start Redis on port 6666
redis-server --port 6666 --bind 127.0.0.1
```
Or run it via Docker:
```bash
# Run Valkey container mapping host port 6666 to container port 6379
docker run -d --name chronoseal-valkey -p 6666:6379 valkey/valkey:latest
```
### 3. Service Configuration
Configure the environment variables to point ChronoSeal to your instance:
```bash
export CHRONOSEAL_DB_TYPE=valkey
export CHRONOSEAL_VALKEY_ADDR=127.0.0.1:6666
```
#### Providing Credentials & SSL/TLS
If your Valkey/Redis server requires authentication or secure TLS/SSL, include them directly in the `CHRONOSEAL_VALKEY_ADDR` connection URL:
* **Password Only**:
```bash
export CHRONOSEAL_VALKEY_ADDR=redis://:your_password@127.0.0.1:6666
```
* **Username & Password**:
```bash
export CHRONOSEAL_VALKEY_ADDR=redis://your_username:your_password@127.0.0.1:6666
```
* **Secure Connection (SSL/TLS)**: Use the `rediss://` scheme prefix:
```bash
export CHRONOSEAL_VALKEY_ADDR=rediss://your_username:your_password@secure-valkey-host.example.com:6379
```
If Valkey connection setup fails, the current implementation logs a warning and falls back to in-memory SQLite.
## systemd
```bash
sudo cp chronoseal.service /etc/systemd/system/
The supplied service file is intended as the baseline unit. Keep the daemon under a dedicated user and restrict filesystem access to the paths it needs.
Useful commands:
```bash
sudo systemctl daemon-reload
sudo systemctl enable chronoseal
sudo systemctl start chronoseal
sudo systemctl enable --now chronoseal
sudo systemctl restart chronoseal
sudo systemctl status chronoseal
sudo journalctl -u chronoseal -f
```
Recommended hardening properties include:
- `NoNewPrivileges=true`
- `PrivateTmp=true`
- `ProtectSystem=strict`
- `ProtectHome=true`
- `ProtectKernelTunables=true`
- `ProtectKernelModules=true`
- `ProtectControlGroups=true`
- `MemoryDenyWriteExecute=true`
- `RestrictRealtime=true`
- `RestrictSUIDSGID=true`
- `SystemCallArchitectures=native`
Any hardening must still allow access to:
- the binary
- frontend assets
- PID file directory
- optional log file directory
- SQLite database directory, if using `sqlite-in-disk`
## Reverse Proxy and TLS
ChronoSeal should be served over HTTPS in production. Terminate TLS at a reverse proxy or load balancer and proxy to the local daemon.
Minimal nginx example:
```nginx
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
proxy_read_timeout 35s;
proxy_send_timeout 10s;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
```
Keep `/init`, `/hb`, and frontend assets on the same origin when possible. If you split origins, configure CORS and cookie/application policy deliberately.
## Docker
Build and run:
```bash
bash scripts/build.sh
docker compose up -d --build
```
## Reverse Proxy
The Compose file exposes port `3000`.
Recommended:
- nginx
- Nginx Proxy Manager
- HAProxy
```bash
curl http://127.0.0.1:3000/health
```
Enable:
- HTTP/2
- TLS 1.3
- aggressive timeout policies
The Dockerfile copies `frontend/` from the working tree. Build `frontend/pkg` before building the image when the browser WASM runtime is required inside the container.
## Observability
CLI:
```bash
chronoseal status --format json
chronoseal health
chronoseal stats --format json
chronoseal metrics
```
HTTP:
```bash
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/stats
curl http://127.0.0.1:3000/metrics
```
Prometheus metrics:
- `chronoseal_sessions`
- `chronoseal_expired_sessions`
- `chronoseal_max_chain_length`
## Logging
Use info-level logs for production:
```bash
CHRONOSEAL_LOG=info chronoseal run
```
or with systemd:
```bash
sudo systemctl edit chronoseal
```
Avoid debug logging in production because internal identifiers may be written to logs.
## Production Checklist
- Build `frontend/pkg` before packaging.
- Serve ChronoSeal traffic over HTTPS.
- Bind the daemon to localhost behind a reverse proxy unless direct exposure is required.
- Use a dedicated service user.
- Keep debug logs disabled.
- Choose storage intentionally: `sqlite-in-memory`, `sqlite-in-disk`, or `valkey`.
- Protect SQLite and log directories with correct ownership.
- Monitor `/health`, `/stats`, and `/metrics`.
- Verify `chronoseal config check` after environment or config changes.
## Runtime Footprint
ChronoSeal is intentionally designed to maintain a small deployment footprint while providing browser attestation, cryptographic verification, session continuity, and WASM execution capabilities.
Typical v1.0.2 release artifact sizes:
| Component | Approximate Size |
| ----------------------------------------------- | ---------------: |
| Native daemon (`chronoseal`) | ~9.1 MiB |
| Browser runtime (`chronoseal_wasm.wasm`) | ~728 KiB |
| WASM static library (`libchronoseal_wasm.rlib`) | ~188 KiB |
Example:
```text
chronoseal
9501232 bytes
≈ 9.06 MiB
chronoseal_wasm.wasm
745569 bytes
≈ 728 KiB
```
These compact artifact sizes help:
* reduce deployment overhead
* minimize container image growth
* improve cold-start performance
* reduce browser download size
* simplify edge and self-hosted deployments
ChronoSeal intentionally avoids heavyweight runtime dependencies and large browser frameworks, allowing the complete attestation stack to remain compact while preserving functionality.
```
## v1.0.2 Deployment Notes
- Containers run as a dedicated non-root user.
- Reverse proxies should forward X-Forwarded-For or X-Real-IP.
- Security headers are enabled by default.
+125
View File
@@ -0,0 +1,125 @@
# ChronoSeal Design Philosophy
ChronoSeal is designed for operators who want a local, inspectable, Unix-native browser attestation layer rather than a hosted anti-bot black box.
## Core Position
ChronoSeal is infrastructure software. It should feel closer to `nginx`, `redis-server`, or a small system daemon than to a third-party analytics platform.
Design priorities:
- CLI-first operation
- explicit configuration
- deterministic protocol behavior
- small runtime surface
- privacy-preserving state
- observable health and metrics
- no hidden telemetry
- no persistent user profiling
## What ChronoSeal Optimizes For
### Operator Control
Operators should be able to build, run, inspect, configure, monitor, and stop the service with ordinary Unix tools.
This is why ChronoSeal provides:
- `chronoseal run`
- `chronoseal status`
- `chronoseal health`
- `chronoseal config check`
- `chronoseal metrics`
- `chronoseal stats`
- shell completions
- systemd integration
### Determinism
The protocol depends on deterministic agreement between server Rust and browser WASM.
Shared logic belongs in `shared/` when divergence would create security or correctness risk. This includes:
- protocol structs
- hash-chain semantics
- synthetic gene model
- mutation opcode behavior
- mutation order encoding
### Cost Escalation
ChronoSeal does not claim impossible security. It raises the cost of automation by making clients maintain:
- a browser-local signing key
- a signed canonical heartbeat payload
- a Blake3 hash chain
- VM execution output
- server-issued mutation progression
- plausible timing and interaction signals
The objective is to make cheap automation brittle and expensive automation more complex.
### Silent Rejection
Heartbeat rejection is intentionally ambiguous. Invalid heartbeats receive the same `status` value as accepted heartbeats, but accepted responses include next-state fields.
This avoids turning the API into a validation oracle. Integrators must check for `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
### Privacy
ChronoSeal should not become a surveillance system.
It avoids:
- long-term user identifiers
- browser history
- cross-site identity graphs
- fingerprint databases
- behavioral profiling as a product feature
It stores only the session state required for continuity.
## Non-Goals
ChronoSeal is not:
- a CAPTCHA
- a fraud scoring engine
- an authentication provider
- a hosted SaaS product
- a persistent fingerprinting system
- a replacement for authorization checks
- a complete defense against real browser farms
## Operational Assumptions
ChronoSeal assumes:
- Linux or a Unix-like host
- systemd for production service management
- TLS in production
- browser clients can execute WASM
- operators can manage config files and service users
- application owners decide how attestation status gates protected resources
## Engineering Biases
When the project faces tradeoffs, prefer:
- explicit configuration over implicit magic
- server-side recomputation over browser trust
- bounded deterministic execution over unbounded heuristics
- clear CLI output over hidden dashboards
- local deployment over mandatory cloud dependencies
- privacy by data minimization over privacy by policy alone
## Success Criteria
ChronoSeal is succeeding when:
- legitimate browser sessions advance without user friction
- simple scrapers cannot pass the protocol
- automation requires a full stateful implementation
- operators can debug deployments with normal Unix tools
- stored data remains minimal and short-lived
- documentation reflects the implementation precisely
+100
View File
@@ -0,0 +1,100 @@
# ChronoSeal Operations Handbook (OPERATIONS)
This guide describes how to deploy, monitor, scale, and maintain the ChronoSeal daemon (`chronoseald`) in production environments.
---
## 1. Systemd Deployment
In single-host deployments, ChronoSeal runs as a systemd service.
Example systemd unit file (`/etc/systemd/system/chronoseal.service`):
```ini
[Unit]
Description=ChronoSeal Attestation Daemon
After=network.target
[Service]
Type=simple
User=chronoseal
Group=chronoseal
WorkingDirectory=/var/lib/chronoseal
ExecStart=/usr/local/bin/chronoseal run --config /etc/chronoseal.toml
Restart=always
RestartSec=5
LimitNOFILE=65536
# Hardening
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
```
Enable and start the service:
```bash
systemctl daemon-reload
systemctl enable --now chronoseal
```
---
## 2. Reverse Proxy & TLS Termination
Do not expose the `chronoseald` HTTP interface directly to the public internet. Run it behind a reverse proxy (e.g. Nginx, HAProxy, Envoy) that enforces TLS termination and CORS limits.
Example Nginx config (`/etc/nginx/sites-available/chronoseal.conf`):
```nginx
server {
listen 443 ssl http2;
server_name attestation.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 3. Storage Backends & Scaling
### A. SQLite (`sqlite-in-disk`)
* **Best For:** Single-node deployments.
* **Configuration:** Specify a writeable path in `db_path` and set `db_type = "sqlite-in-disk"`.
* **Operational Note:** Concurrency is limited by SQLite's single-writer database lock. Optimistic CAS reduces collisions, but high write volumes can cause queue congestion.
### B. Valkey / Redis (`valkey`)
* **Best For:** Distributed or high-concurrency environments.
* **Configuration:** Set `db_type = "valkey"` and specify the node addresses via `CHRONOSEAL_VALKEY_ADDR`.
* **Horizontal Scaling:** Set up multiple `chronoseald` stateless daemon nodes. Direct all nodes to connect to the same shared Valkey cluster. This ensures session consistency across requests routed to different nodes.
---
## 4. Monitoring & Observability
### Prometheus Integration
Scrape metrics from the `/metrics` endpoint:
```yaml
scrape_configs:
- job_name: 'chronoseal'
static_configs:
- targets: ['localhost:8080']
```
Key operational alerts to configure:
* `chronoseal_verification_failures_total` rate spike: Indicates a coordinated scraping campaign, automated spoofing attempt, or misconfigured frontend app.
* `chronoseal_storage_latency_seconds` increase: Indicates storage backend bottleneck or lock congestion.
+292
View File
@@ -0,0 +1,292 @@
# ChronoSeal Performance Tuning Guide
ChronoSeal uses a deterministic Synthetic Gene Mutation Engine to strengthen browser session continuity validation. This guide explains how to tune the mutation engine for an appropriate balance between security strength, resource consumption, and user experience.
The primary tuning parameters are:
* `gene_size` — size of the synthetic gene buffer
* `mutation_rounds` — number of mutation iterations executed per heartbeat
---
## Understanding the Mutation Engine
For every accepted heartbeat, ChronoSeal executes a server-generated mutation program against a synthetic gene buffer.
Increasing mutation complexity raises the computational cost of reproducing valid session state while also increasing CPU utilization on both the server and browser runtime.
General effects:
* Larger `gene_size` increases mutation state complexity.
* Higher `mutation_rounds` increase computational work per heartbeat.
* Both increase memory access and CPU consumption.
* Excessive values may negatively impact lower-end mobile devices.
The optimal values depend on your threat model and expected client hardware.
---
## Recommended Configurations
| Profile | `gene_size` | `mutation_rounds` | Security Level | Recommended Usage |
| ----------------- | ----------- | ----------------- | ---------------- | -------------------------------- |
| Default | 512 | 4 | Moderate | Development and testing |
| Recommended | 2048 | 4 | Strong | Most production deployments |
| High Security | 4096 | 8 | Very Strong | Sensitive applications |
| Maximum Practical | 4096 | 10 | Extremely Strong | High-value targets |
| Experimental | 4096 | 10 | Research Only | Benchmarking and experimentation |
### Recommended Production Configuration
```toml
gene_size = 2048
mutation_rounds = 4
```
This configuration provides a strong balance between security and runtime overhead for most deployments.
---
## Configuration
Edit your configuration file:
```toml
# Mutation Engine Settings
gene_size = 2048
mutation_rounds = 4
```
Common configuration locations:
```text
/etc/chronoseal/config.toml
~/.config/chronoseal/config.toml
```
Restart ChronoSeal:
```bash
sudo systemctl restart chronoseal
```
Validate the effective configuration:
```bash
chronoseal config check --format yaml
```
---
## Performance Monitoring
### Server-Side Monitoring
View service logs:
```bash
sudo journalctl -u chronoseal -f
```
Inspect runtime statistics:
```bash
chronoseal stats --format json
```
Enable additional diagnostics when required:
```bash
CHRONOSEAL_LOG=debug chronoseal run
```
Avoid debug logging in production environments.
---
### Browser-Side Monitoring
Measure mutation execution time:
```javascript
console.time("gene-mutation");
const commitment = preview_gene_commitment(
order_b64,
session_id,
mutation_step,
mutation_rounds
);
console.timeEnd("gene-mutation");
```
Browser developer tools can also be used to monitor:
* JavaScript execution time
* WASM execution time
* CPU utilization
* Memory consumption
---
## Tuning Strategy
### Step 1: Start with Recommended Values
```toml
gene_size = 2048
mutation_rounds = 4
```
Deploy and observe normal usage patterns.
### Step 2: Monitor Heartbeat Success Rates
Watch for:
* heartbeat failures
* increased browser CPU usage
* elevated mobile device latency
* increased battery consumption
### Step 3: Increase Gradually
Increase one parameter at a time.
Recommended progression:
```text
2048 / 4
4096 / 4
4096 / 8
4096 / 10
```
This makes it easier to identify performance bottlenecks.
### Step 4: Test Mobile Devices
Always test on:
* Android devices
* iPhones
* older laptops
* low-power CPUs
Desktop-only validation can be misleading.
---
## Advanced Deployment Strategies
### Risk-Based Mutation Strength
Future deployments may choose to dynamically increase mutation strength based on:
* session age
* failed heartbeat history
* suspicious behavior signals
* protected resource sensitivity
Example policy:
```text
New session → 2048 / 4
Suspicious session → 4096 / 8
Elevated-risk action → 4096 / 10
```
---
## Performance Recommendations
### WASM Builds
Always use optimized builds:
```bash
wasm-pack build wasm --target web --release
```
### General Guidance
* Keep `mutation_rounds` at or below 10 for all deployments.
* Prefer increasing `gene_size` before dramatically increasing rounds.
* Benchmark on representative client hardware.
* Monitor browser CPU utilization during load testing.
* Re-evaluate settings after major algorithm changes.
### Storage Performance
For maximum throughput:
```toml
db_type = "sqlite-in-memory"
```
For persistence:
```toml
db_type = "sqlite-in-disk"
```
Choose based on operational requirements rather than mutation settings.
---
## Security Considerations
Higher values increase the cost of reproducing valid session state but do not provide absolute protection against determined attackers.
ChronoSeal remains a cost-raising attestation layer rather than a complete anti-abuse solution.
Mutation tuning should be considered alongside:
* heartbeat timing controls
* signature validation
* hash-chain continuity
* behavioral trust checks
* rate limiting
* session expiration
---
## Recommended Starting Point
For most production deployments:
```toml
gene_size = 2048
mutation_rounds = 4
```
This configuration provides a strong balance between security, performance, and compatibility across desktop and mobile devices.
---
## Related Documentation
* `docs/ARCHITECTURE.md`
* `docs/API.md`
* `docs/THREAT_MODEL.md`
* `docs/REFRACTORING-v0.6.0.md`
For diagnostics:
```bash
chronoseal config check
chronoseal stats
chronoseal health
```
## v1.0.2 Limits
Current supported range:
```toml
gene_size = 1..=4096
mutation_rounds = 1..=10
```
+106
View File
@@ -0,0 +1,106 @@
# ChronoSeal Privacy Policy
ChronoSeal is a privacy-oriented browser attestation system. It is designed to validate short-lived session continuity without creating persistent user profiles.
This document describes what ChronoSeal itself collects and stores. Applications that integrate ChronoSeal may collect additional data under their own policies.
## Data ChronoSeal Processes
ChronoSeal processes the minimum protocol data needed to validate a live browser session.
| Data | Purpose |
|---|---|
| `session_id` | Opaque session lookup key |
| public key | Verify signed heartbeats for the session |
| `salt` | Hash-chain progression |
| `initial_hash` / `prev_hash` / `last_hash` | Replay-resistant continuity |
| `timestamp` | Drift and liveness validation |
| mouse event samples | Behavioral plausibility checks |
| VM stack state | Input to hash-chain progression |
| basic fingerprint fields | Sanity validation |
| gene bytes and environment records | Mutation continuity |
| pending mutation program and step | Next heartbeat verification |
| expiration and last-seen timestamps | Session lifecycle and cleanup |
Basic fingerprint fields currently include:
- aspect ratio
- device pixel ratio
- hardware concurrency
## Data ChronoSeal Does Not Intentionally Collect
ChronoSeal does not intentionally collect or build:
- browser history
- page content history
- account identity
- email addresses
- names
- payment data
- location history
- cross-site tracking identifiers
- persistent fingerprint databases
- long-term behavioral profiles
ChronoSeal is not intended for analytics, advertising, or identity graph construction.
## Session Lifetime
Sessions are short-lived and expire according to `expiration_minutes`, which defaults to 30 minutes.
Expired sessions are removed by cleanup behavior. In-memory storage is lost when the process exits.
## Storage Modes and Persistence
| Mode | Persistence |
|---|---|
| `sqlite-in-memory` | process lifetime only |
| `sqlite-in-disk` | persisted to the configured SQLite file |
| `valkey` | persisted according to the Valkey deployment configuration |
Persistent state is operator-selected. The default backend is `sqlite-in-memory`.
## Client-Side Key Handling
The browser WASM runtime generates an Ed25519 keypair for the session.
- The public key is sent to `/init`.
- The private key is not sent to the server.
- Heartbeat payloads are signed in the browser runtime.
This is a continuity mechanism, not a long-term identity mechanism.
## Silent Rejection
ChronoSeal returns the same basic heartbeat status for accepted and rejected heartbeat requests:
```json
{
"status": "ok"
}
```
Accepted responses additionally include next-state fields. Rejected responses omit them.
This reduces attacker feedback and avoids returning detailed failure classifications to clients.
## Logs
Operators control logging through `CHRONOSEAL_LOG`, `RUST_LOG`, and optional log-file configuration.
Production deployments should avoid debug logging because internal session identifiers or validation context may appear in logs.
## Operator Responsibilities
Operators should:
- serve traffic over HTTPS
- protect SQLite, Valkey, and log storage
- restrict access to metrics and stats endpoints
- choose persistence mode deliberately
- disclose any application-level data collection separately
## Summary
ChronoSeal validates live session continuity using short-lived cryptographic and deterministic state. It is designed to raise automation cost without becoming a persistent tracking or profiling system.
+98
View File
@@ -0,0 +1,98 @@
# ChronoSeal Protocol Specification (PROTOCOL)
This document defines the formal wire protocol, state transitions, cryptographic primitives, and execution invariants of the ChronoSeal browser attestation system.
---
## 1. Sequence Flow & Handshake
ChronoSeal operates as a stateful, sequential challenge-response chain over HTTP/REST.
```
Client (JS/WASM) Server (chronoseald)
| |
| 1. POST /init { public_key: String } ----------------> |
| | (Generates VM Opcodes)
| | (Computes initial hash chain head H_0)
| | (Saves initial session record)
| <--- 200 OK { InitResponse } --------------------------|
| |
| [Client executes VM program & prepares gene preview] |
| |
| 2. POST /hb { HeartbeatRequest } --------------------> |
| | (Loads session & executes CAS check)
| | (Verifies Ed25519 signature)
| | (Validates VM stack-state parity)
| | (Computes expected gene mutation)
| | (Validates hash chain continuity H_N == expected)
| | (Rotates salt & issues next mutation order)
| <--- 200 OK { HeartbeatResponse } ---------------------| (Saves updated session record)
| |
```
---
## 2. Cryptographic Transition Mechanics
### A. Handshake Phase (`/init`)
The client registers a 32-byte Ed25519 verifying key represented as a hex string.
The server:
1. Generates a 32-byte session ID ($ID$) and a 16-byte initial salt ($S_0$).
2. Computes the initial hash chain head:
$$H_0 = \text{Blake3}(ID \parallel PK_{\text{client}} \parallel S_0)$$
3. Generates a random VM program of size $8..=16$ bytes.
4. Creates the initial mutation order program $M_1$.
5. Persists the session record in the database.
---
### B. Heartbeat progression (`/hb`)
For each heartbeat step $n \ge 1$:
The client submits:
* `prev_hash`: $H_{n-1}$ (hex encoded).
* `timestamp`: $T_n$ (milliseconds).
* `entropy_data`: Mouse movement arrays.
* `stack_state`: The VM final stack and instruction pointer `ip` after execution.
* `gene_commitment`: Hex-encoded commitment of the mutated gene state.
* `signature`: Ed25519 signature of the canonical alphabetical JSON payload.
The server:
1. Loads the session record from storage, enforcing optimistic locking (CAS check) to confirm the database `last_hash` matches $H_{n-1}$.
2. Validates the Ed25519 signature against the canonical alphabetical serialization.
3. Re-executes the session's VM opcodes and asserts the client's `stack_state` matches the output.
4. Applies the mutation order $M_n$ to the stored gene state and calculates the expected commitment:
$$C_n = \text{Blake3}(\text{CandidateGene} \parallel ID \parallel n)$$
Asserts the client's `gene_commitment` matches.
5. Validates that $|T_{\text{server}} - T_n| \le \text{max\_drift}$.
6. Advances the hash chain:
$$H_n = \text{Blake3}(H_{n-1} \parallel T_n \parallel \text{Blake3}(E_n) \parallel \text{Blake3}(S_n) \parallel S_{n-1})$$
7. Rotates the salt to $S_n$ and issues the next mutation order $M_{n+1}$.
---
## 3. VM Instruction Specification
The client VM executes instructions sequentially. The instruction set consists of:
* `0x00`: Pushes the next 4 bytes in the instruction stream onto the stack as a `u32` value (little-endian).
* `0x01`..=`0x07`: Binary operators. Requires at least 2 elements on the stack:
* `0x01`: Wrapping Add (`a.wrapping_add(b)`)
* `0x02`: Wrapping Sub (`a.wrapping_sub(b)`)
* `0x03`: Wrapping Mul (`a.wrapping_mul(b)`)
* `0x04`: XOR (`a ^ b`)
* `0x05`: AND (`a & b`)
* `0x06`: OR (`a | b`)
* `0x07`: Rotate Left (`a.rotate_left(b % 32)`)
* `0x08`: Unary Bitwise Not (`!a`). Requires at least 1 element on the stack.
* `0x09`: Hash Stack. Hashes all stack elements using BLAKE3 and reduces it to a single `u32` value, clearing the stack and pushing the hash.
* *Any other opcode:* Terminates VM execution immediately.
## v1.0.2 Protocol Hardening
- Fingerprint aspect ratio validation
- Device pixel ratio validation
- Hardware concurrency validation (1..=256)
- Entropy event cap (500 events)
- IP-based rate limiting
- Silent rejection preserved for protocol failures
+37
View File
@@ -0,0 +1,37 @@
# ChronoSeal Protocol Stability Policy (PROTOCOL_STABILITY)
This document defines the stable interfaces and boundaries of the ChronoSeal project to guide third-party integration development and future internal architectural evolutions.
---
## 1. Stable Public Contract
The public surface of ChronoSeal is frozen at version 1.0 and consists of:
1. **Wire Protocol API:**
* `POST /init`: Handshake schema (parameters, response fields).
* `POST /hb`: Heartbeat schema (payload parameters, response fields).
2. **State Transition Semantics:**
* The BLAKE3 hash chain progression rules.
* The virtual machine opcodes and stack execution rules.
* The Synthetic Gene Mutation logic and context-bound commitments.
3. **Daemon CLI & Config Schema:**
* Commands (`run`, `status`, `health`, etc.).
* TOML configuration keys.
---
## 2. Private Internal Boundaries
All implementation details are subject to change without notice. Wrappers, clients, and applications must not depend on:
* **Internal Rust APIs:** ChronoSeal is a Unix daemon. It does not export a public Rust library SDK. Internal Rust modules (`server::storage`, `server::session`, etc.) are private.
* **Database Schema:** The SQLite table structure, indexes, or column names are private to the daemon.
* **Valkey Key Structures:** The layout of session keys, sorted set indexes, and pipelines are implementation details.
---
## 3. Protocol Evolution Policy
* **Minor Updates:** Can introduce new optional configuration fields or metrics.
* **Major Updates:** May change the VM instruction set or hash chain primitives, requiring new WASM builds.
+191
View File
@@ -0,0 +1,191 @@
# ChronoSeal v0.6.0 Refactoring and System Upgrade
ChronoSeal v0.6.0 changed the project from a lightweight heartbeat prototype into a Unix-native attestation daemon with shared server/WASM protocol logic, deterministic mutation parity, operational CLI commands, and pluggable storage modes.
This document summarizes the architectural changes introduced in the v0.6.0 line.
## Summary
Major changes:
- introduced the Synthetic Gene Mutation Engine
- added server-side validation of `mutation_step` and `gene_commitment`
- moved protocol and deterministic mutation logic into `shared/`
- added `chronoseal-wasm` browser runtime support for mutation preview and commit
- expanded persisted session state with gene and pending mutation fields
- added storage modes: `sqlite-in-memory`, `sqlite-in-disk`, and `valkey`
- added health, metrics, stats, config, status, completion, and version CLI surfaces
- added PID file handling, structured logging, and graceful shutdown behavior
- preserved silent heartbeat rejection semantics
## Motivation
The earlier model relied mainly on:
- heartbeat timing
- behavioral entropy
- hash-chain continuity
- signature verification
v0.6.0 added a second deterministic state channel: a server-authored synthetic gene mutation sequence. This makes successful automation maintain both:
- the cryptographic hash/signature chain
- the synthetic mutation state expected by the server
## Shared Crate Refactor
`shared/` now owns the parts of the protocol that must remain identical across server and browser runtime:
- request and response structs
- hashing helpers
- synthetic gene state
- mutation environment encoding
- mutation order generation and encoding
- opcode execution semantics
- protocol constants
This reduces the risk of server/WASM drift.
## Mutation Handshake
New protocol fields:
- `gene_size`
- `mutation_step`
- `mutation_order_b64`
- `gene_commitment`
- `next_mutation_step`
- `next_mutation_order_b64`
Lifecycle:
1. `/init` returns mutation step 1 and a server-authored mutation order.
2. The browser previews the mutation in WASM.
3. The browser signs and submits the resulting `gene_commitment`.
4. The server applies the same pending mutation to its committed state.
5. The server compares commitments.
6. On success, server commits the candidate state and issues the next mutation.
7. The browser commits its preview only after receiving the accepted response.
## Session Schema Changes
The persisted session record now includes:
- committed gene bytes
- encoded environment records
- pending mutation program
- pending mutation step
State advances only after a heartbeat is accepted. Rejected heartbeats do not rotate salt, update hash state, commit gene state, or consume the pending mutation.
## WASM Runtime Changes
The WASM crate now supports:
- `generate_keypair()`
- `get_public_key()`
- `sign_message()`
- `compute_next_hash()`
- `run_program()`
- `init_gene_state()`
- `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)`
- `commit_gene_preview()`
- `discard_gene_preview()`
- `current_gene_commitment(session_id, mutation_step)`
The generated package uses the `chronoseal_wasm` prefix.
## Storage Refactor
The storage layer is abstracted behind `DbPool`.
Supported modes:
| Mode | Behavior |
|---|---|
| `sqlite-in-memory` | default ephemeral in-process SQLite |
| `sqlite-in-disk` | persisted SQLite database at `db_path` |
| `valkey` | Valkey-compatible external store |
The storage interface supports insert, load, update, delete expired sessions, and stats.
## CLI and Runtime Changes
The `chronoseal` binary now provides:
- `run`
- `status`
- `health`
- `config check`
- `generate keypair`
- `version`
- `db-type`
- `metrics`
- `stats`
- `completion`
The daemon exposes:
- `POST /init`
- `POST /hb`
- `GET /health`
- `GET /metrics`
- `GET /stats`
- static frontend serving at `/`
## Validation Improvements
The heartbeat verifier now checks:
- session presence
- expiration
- signature
- hash-chain continuity
- mutation step
- mutation commitment parity
- timestamp drift
- behavioral mouse checks
- fingerprint ranges
- rate limiting at the route layer
Accepted heartbeats return next-state fields. Rejected heartbeats return only `{"status":"ok"}`.
## Testing Impact
The refactor added or strengthened tests for:
- gene environment encoding and validation
- mutation opcode behavior
- mutation order round-trips
- deterministic mutation generation with seeded RNG
- server/client mutation parity
- random program divergence resistance
- replay rejection
- mutation step mismatch rejection
- mutation commitment tamper rejection
- storage backend stats
- route-level silent rejection behavior
## Operational Impact
v0.6.0 makes ChronoSeal more suitable for deployment as a real service:
- explicit daemon lifecycle
- CLI-first operations
- systemd-oriented install path
- health and metrics endpoints
- configurable persistence
- shared protocol implementation
- clearer docs and threat model
## Compatibility Notes
Important names in the current implementation:
- binary: `chronoseal`
- server crate: `chronoseal-server`
- WASM crate: `chronoseal-wasm`
- generated WASM module prefix: `chronoseal_wasm`
- persistent SQLite mode: `sqlite-in-disk`
Older docs or integrations may refer to `sqlite-disk`, `server`, or `antibot_wasm`; those names are stale for the current codebase.
+40
View File
@@ -0,0 +1,40 @@
# ChronoSeal Security Assumptions & Guarantees
This document details the trust boundary models, security assumptions, and non-goals of the ChronoSeal system.
---
## 1. Core Threat Philosophy
ChronoSeal is a **cost-raising security layer**. It is designed to force automated scraping, botting, and replay tools to execute a fully compliant JavaScript/WASM execution runtime. It does not provide absolute hardware attestation or proof of human presence.
---
## 2. Non-Goals (What ChronoSeal is NOT)
1. **Proof of Humanity:** ChronoSeal does not check if the user is a human. A headless browser running with standard input event automation will pass verification if it runs the WASM runtime correctly.
2. **Anti-Debugging/Enclave Security:** ChronoSeal does not run inside a secure hardware enclave on the client. An attacker has complete control of the client wasm environment, memory, and key storage.
3. **Perfect Browser Verification:** ChronoSeal cannot guarantee the client is a real Chrome/Firefox browser. It guarantees that the client maintains the state chain and executes the math VM program.
---
## 3. Threat Matrix & Attacker Cost Model
* **Commodity HTTP Clients (Python `requests`, `curl`):** *Blocked.* Attackers cannot sign payloads, run the mathematical VM, or maintain the stateful BLAKE3 hash chain.
* **Headless Automation (Puppeteer, Playwright):** *Partially Contained.* The automation script must execute the full browser environment, load the WASM module, feed valid parameters, and generate realistic mouse movement coordinates. This imposes significantly higher CPU and resource overhead on the attacker.
* **Custom WASM Emulators:** *Raised Cost.* A determined reverse engineer can extract the WASM module and build a custom state runner in Node.js or Go. ChronoSeal counters this by using a stateful **Synthetic Gene Mutation Engine**, where the state vector mutations are governed dynamically by the server, requiring the emulator to replicate the entire mutation spec.
---
## 4. Key Invariants
1. **Chain Continuity:** A session state cannot bifurcate. Every heartbeat must advance the state head using the latest salt.
2. **VM Parity:** Stack state must exactly match the execution output of the server's issued opcode sequence.
3. **Dynamic Challenges:** Client gene updates must match the server-issued mutation program.
## Additional Assumptions (v1.0.2)
- Fingerprints are sanity signals, not identity proofs.
- Rate limiting assumes client IP visibility.
- Security headers reduce browser attack surface.
+395
View File
@@ -0,0 +1,395 @@
# ChronoSeal Testing Strategy
ChronoSeal maintains a security-focused test suite designed to validate cryptographic correctness, deterministic server ↔ WASM parity, replay resistance, mutation engine integrity, browser fingerprint validation, behavioral trust checks, storage reliability, and protocol hardening.
As of **v1.0.2**, the project contains **100 passing tests** across the server, WASM, shared protocol, and property-testing suites.
| Crate | Tests |
| ------------------------------- | ------: |
| `chronoseal-server` | 38 |
| `chronoseal-wasm` | 24 |
| `shared` (unit tests) | 36 |
| `shared` (property-based tests) | 2 |
| `chronoseal-replay` | 0 |
| **Total** | **100** |
---
# Test Philosophy
ChronoSeal prioritizes testing of security invariants rather than raw coverage percentages.
Primary goals:
* Verify deterministic server ↔ WASM behavior
* Detect protocol divergence early
* Prevent replay attacks
* Validate mutation engine correctness
* Detect malformed and adversarial input handling
* Prevent VM and protocol panics
* Maintain storage backend compatibility
* Protect browser attestation continuity guarantees
The project emphasizes negative-path testing and adversarial validation rather than only testing successful execution paths.
---
# Test Categories
## 1. Configuration & Runtime
Configuration tests verify:
* Default configuration values
* TOML parsing
* Runtime initialization
* Database backend selection
* CLI override behavior
Covered functionality:
* SQLite in-memory backend
* SQLite disk backend
* Valkey compatibility mode
* Runtime configuration validation
Example tests:
```text
test_default_db_type_is_sqlite_in_memory
test_apply_run_args_overrides_db_type
test_toml_parses_db_type_kebab_case
test_db_type_report_lists_backends
test_init_db_pool_sqlite_in_memory
test_init_db_pool_sqlite_in_disk
test_init_db_pool_valkey_compat_mode
```
---
## 2. Browser Fingerprint Validation
Introduced and expanded in v1.0.2.
Fingerprint validation protects the attestation pipeline from malformed or unrealistic browser metadata.
Validation coverage includes:
* Aspect ratio validation
* Device pixel ratio validation
* Hardware concurrency validation
* Boundary value acceptance
* NaN rejection
* Infinity rejection
* Malformed numeric value rejection
Example tests:
```text
accepts_valid_fingerprint
accepts_boundary_values
rejects_invalid_aspect_ratios
rejects_invalid_device_pixel_ratios
rejects_invalid_hardware_concurrency
```
Validation constraints currently include:
| Field | Allowed Range |
| ------------------- | --------------------- |
| aspectRatio | finite positive value |
| devicePixelRatio | greater than zero |
| hardwareConcurrency | 1..=256 |
---
## 3. Session Lifecycle & Protocol Verification
Session tests verify:
* Session creation
* Session expiration
* Public key validation
* Replay attack resistance
* Mutation commitment verification
* Mutation step enforcement
* Deterministic long-running parity
Example tests:
```text
test_create_session_rejects_invalid_public_key_length
test_expired_session_is_rejected
test_replay_attack_is_rejected
test_mutation_step_mismatch_is_rejected
test_mutation_commitment_tamper_is_rejected
test_session_lifecycle_and_verification
test_repeated_simulation_keeps_server_and_client_commitments_equal
test_deterministic_server_client_parity_across_many_heartbeats
```
---
## 4. Heartbeat Validation
Heartbeat tests validate:
* Successful state advancement
* Silent rejection semantics
* Commitment validation
* Rate limiting
* Next-state mutation generation
Example tests:
```text
test_handler_success_returns_next_mutation_fields
test_handler_tampered_commitment_is_silent_failure
test_handler_rate_limit_returns_no_mutation_data
```
---
## 5. Behavioral Trust Validation
Trust validation focuses on lightweight behavioral signals.
Coverage includes:
* Minimum event count
* Minimum movement distance
* Pause detection
* Maximum speed thresholds
* Activity requirement toggles
Example tests:
```text
test_validate_mouse_success
test_validate_mouse_insufficient_events
test_validate_mouse_insufficient_distance
test_validate_mouse_too_fast
test_validate_mouse_no_pauses
test_validate_mouse_require_activity_toggle
```
---
## 6. Storage Layer
Storage tests verify backend correctness and concurrency behavior.
Covered backends:
* SQLite in-memory
* SQLite disk
* Valkey compatibility mode
Example tests:
```text
test_sqlite_pool_concurrency
test_valkey_pool_concurrency
test_valkey_store_operations
```
Validation includes:
* Session persistence
* Session updates
* Concurrent access
* Statistics collection
* Backend compatibility
---
## 7. Rate Limiting
Rate limiter tests verify:
* Request counting
* Window expiration
* Stale entry eviction
Example tests:
```text
test_rate_limiter
test_rate_limiter_eviction
```
---
## 8. VM Core
The VM implementation is tested across both WASM and shared crates.
Covered operations:
```text
ADD
SUB
MUL
XOR
AND
OR
NOT
HASH
ROT
PUSH
```
Validation includes:
* Wrapping arithmetic
* Stack underflow detection
* Invalid opcode rejection
* Truncated instruction rejection
* Instruction safety
Example tests:
```text
test_add
test_add_wrapping
test_sub
test_sub_wrapping
test_mul
test_hash
test_underflow_binary
test_underflow_unary
test_incomplete_push
test_rejects_unknown_opcode
test_rejects_truncated_instruction
```
---
## 9. Synthetic Gene Mutation Engine
The mutation engine is a critical security component.
Coverage includes:
* Mutation order execution
* Deterministic parity
* Randomized mutation programs
* Preview lifecycle
* Commit lifecycle
* Discard lifecycle
* Environment validation
* Gene integrity
Example tests:
```text
test_mutation_chain
test_generate_order_is_deterministic_for_seeded_rng
test_server_client_parity_across_random_orders
test_preview_commitment_matches_shared_engine
test_commit_applies_preview
test_discard_preview_keeps_committed_state
test_table_driven_parity_across_many_generated_orders
test_fuzz_style_random_program_bytes_do_not_diverge
```
---
## 10. Property-Based Testing
ChronoSeal uses `proptest` to validate protocol invariants under arbitrary input.
Property tests:
```text
test_vm_execute_never_panics
test_gene_environment_roundtrip_never_panics
```
These tests continuously exercise malformed and randomized inputs to ensure graceful handling and panic resistance.
---
# Running the Test Suite
Run all tests:
```bash
cargo test --workspace
```
Run server tests:
```bash
cargo test -p chronoseal-server
```
Run fingerprint tests only:
```bash
cargo test -p chronoseal-server fingerprint
```
Run WASM tests:
```bash
cargo test -p chronoseal-wasm
```
Run shared tests:
```bash
cargo test -p shared
```
Show output:
```bash
cargo test -- --nocapture
```
---
# Critical Security Tests
The following tests are considered release-blocking:
```text
test_replay_attack_is_rejected
test_mutation_commitment_tamper_is_rejected
test_handler_tampered_commitment_is_silent_failure
test_deterministic_server_client_parity_across_many_heartbeats
test_server_client_parity_across_random_orders
test_vm_execute_never_panics
test_gene_environment_roundtrip_never_panics
rejects_invalid_aspect_ratios
rejects_invalid_device_pixel_ratios
rejects_invalid_hardware_concurrency
```
These tests directly protect protocol integrity, replay resistance, mutation validation, deterministic execution, and fingerprint hardening.
---
# Conclusion
ChronoSeal's testing strategy focuses on preserving deterministic behavior, protocol integrity, cryptographic correctness, browser ↔ server parity, and resistance to malformed or adversarial input.
The current suite of **100 passing tests** provides comprehensive coverage across:
* Configuration
* Runtime initialization
* Browser fingerprint validation
* Session lifecycle management
* Heartbeat verification
* Mutation engine execution
* VM safety
* Behavioral validation
* Storage backends
* Replay resistance
* Property-based protocol hardening
Maintaining and expanding this test suite remains a core project priority.
**Last Updated:** June 2026 (v1.0.2)
+271
View File
@@ -0,0 +1,271 @@
# ChronoSeal Threat Model
ChronoSeal is a cost-raising browser attestation layer. It makes replay, stale state reuse, and incomplete automation more expensive by requiring signed, continuous, deterministic browser-side state progression.
It is not a perfect bot blocker, CAPTCHA replacement, hardware attestation system, fraud engine, or identity provider.
## Security Objectives
ChronoSeal aims to:
- reject stale or replayed heartbeat payloads
- reject heartbeats that do not maintain the server-issued mutation sequence
- bind heartbeat payloads to a browser-local Ed25519 session key
- make basic HTTP clients insufficient
- make browser automation maintain multiple synchronized state channels
- avoid detailed rejection feedback
- preserve privacy by avoiding persistent user identity state
## Protected Assets
| Asset | Protection focus |
|---|---|
| Protected page/API access | Require live attestation before allowing continued access |
| Session continuity | Ensure each accepted heartbeat advances from the last accepted state |
| Server compute | Rate-limit and reject invalid clients without expensive application work |
| Protocol state | Protect hash-chain, salt, and mutation progression |
| User privacy | Avoid long-term tracking and detailed failure disclosure |
## Trust Assumptions
ChronoSeal assumes:
- the server host and daemon process are trusted
- storage is trusted for session continuity
- TLS protects traffic in production
- browser clients can run JavaScript and WASM
- operators configure reverse proxy, filesystem permissions, and logs appropriately
ChronoSeal does not assume:
- the browser is honest
- WASM is a secure enclave
- mouse data proves human presence
- fingerprint values are unforgeable
- attackers cannot run a full browser
## Attacker Levels
### Level 1: Commodity HTTP Client
Examples:
- `curl`
- `requests`
- scraper scripts without browser or WASM execution
Expected result:
- cannot produce valid signatures
- cannot maintain hash-chain state
- cannot execute mutation preview
- cannot produce accepted heartbeats
### Level 2: Basic Headless Browser
Examples:
- Playwright
- Puppeteer
- Selenium
Expected result:
- can load JavaScript and WASM
- must preserve keypair, hash chain, salt, VM, and mutation state
- must generate plausible timing and mouse event windows
- silent rejection complicates debugging and scaling
### Level 3: Stealth Automation
Examples:
- patched browser runtime
- synthetic event generation
- custom protocol client with WASM or Rust reimplementation
Expected result:
- can attempt full protocol implementation
- must still match canonical signing, hash progression, mutation parity, and timing
- must handle changing server-issued mutation programs
- receives limited failure feedback
### Level 4: Resourced Browser Farm
Examples:
- real browsers
- realistic input devices
- human-assisted workflows
- distributed session management
Expected result:
- ChronoSeal raises cost and complexity
- ChronoSeal does not claim complete prevention
- additional application-level controls are required
## Attacker Classification Boundaries
### Protected
* **Commodity Scrapers:** Simple HTTP clients (`curl`, Python `requests`, Go HTTP clients) that cannot execute JavaScript or WebAssembly.
* **Simple Replay Attackers:** Intercepted heartbeat payloads cannot be reused because of the strict hash-chain sequencing and salt rotation.
* **Signature Forgers:** Heartbeats without the session's private key will fail Ed25519 verification.
### Partially Protected
* **Headless Automation (Puppeteer, Playwright):** Attackers must load the WASM runtime, execute the VM instructions, calculate gene mutations, and simulate realistic human mouse interactions. This significantly increases CPU and system memory overhead, reducing the scale of bot operations.
* **Stealth Automation Frameworks:** Advanced frameworks must maintain state sync across multiple heartbeat cycles, exposing them to timing detection.
### Unprotected
* **WASM Key Extraction:** A reverse engineer with full browser process control can extract the private key from WASM memory.
* **Malware Operators:** Keyloggers, screen scrapers, or memory dumpers operating at the OS level are outside the application trust boundary.
* **MITM Interceptors (without TLS):** Plaintext traffic can be intercepted. (TLS termination is assumed).
* **Insiders / Storage Tampering:** Attackers with direct write access to the SQLite database or Valkey instance can forge or hijack active session states.
## Attack Vectors and Mitigations
### Replay
Attack: resend a previously accepted heartbeat.
Mitigations:
- stored `last_hash` must match request `prev_hash`
- accepted heartbeats rotate salt
- mutation step advances after acceptance
- timestamp drift is bounded
### Signature Forgery
Attack: submit a heartbeat without the browser session private key.
Mitigations:
- Ed25519 signature over canonical payload
- public key registered during `/init`
- signature verified on every heartbeat
- signature covers mutation step and gene commitment
### Hash-Chain Desynchronization
Attack: submit a heartbeat from stale client state.
Mitigations:
- server compares request `prev_hash` to stored `last_hash`
- server computes the next hash only after all validation passes
- rejected heartbeats do not advance server state
### Mutation Tampering
Attack: forge or skip synthetic gene mutations.
Mitigations:
- server stores the pending mutation program
- request must include the expected `mutation_step`
- server applies the mutation independently
- commitment includes candidate gene state, `session_id`, and step
- mismatch causes silent rejection
### Session Identifier Theft
Attack: reuse a stolen `session_id`.
Mitigations:
- `session_id` alone is insufficient
- attacker also needs current private key, hash state, salt, mutation step, and mutation state
- stale attempts fail after the real session advances
### Failure Oracle Probing
Attack: send malformed requests and inspect responses to infer validation rules.
Mitigations:
- heartbeat semantic failures return `200 OK` with `{"status":"ok"}`
- accepted heartbeats are distinguished only by next-state fields
- detailed validation errors are not returned to the client
### Storage Tampering
Attack: alter persisted session state.
Mitigations:
- run the daemon under a dedicated user
- restrict SQLite database permissions
- protect Valkey behind trusted network boundaries
- use normal host hardening and backups where persistence matters
Storage is trusted. If an attacker can modify storage, they can affect session continuity.
## Behavioral Checks
ChronoSeal validates:
- minimum event count
- minimum movement distance
- maximum average speed
- pause count
- timestamp drift
- basic fingerprint field ranges
These checks are cost signals. They are not proof of humanity and should not be the only security layer for high-risk actions.
## Privacy Constraints
ChronoSeal intentionally avoids:
- persistent user identifiers
- browser history collection
- device fingerprint databases
- cross-session identity graphs
- long-term behavioral profiles
Session data is short-lived by default. Persistent storage is operator-selected through `sqlite-in-disk` or `valkey`.
## Limitations
ChronoSeal does not protect against:
- real users intentionally automating or abusing access
- complete browser farms with realistic input
- compromised server hosts
- tampered storage
- server-side application vulnerabilities
- credential theft outside ChronoSeal
- policy decisions that require identity, risk scoring, or business context
## Operational Security
Recommended:
- serve all traffic over HTTPS
- keep `/init` and `/hb` same-origin with protected content when possible
- run behind a reverse proxy
- keep debug logs disabled in production
- protect storage and log directories
- monitor health and metrics
- use `sqlite-in-memory` for ephemeral sessions
- use `sqlite-in-disk` or `valkey` only when persistence is required
## Disclosure
See [../SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy.
## Additional Mitigations (v1.0.2)
### Fingerprint Abuse
- Bounds enforcement
- Numeric sanity validation
### Resource Exhaustion
- Entropy event cap
- Rate limiting
### Browser Hardening
- Security response headers
+163
View File
@@ -0,0 +1,163 @@
# ChronoSeal WASM Build Guide
ChronoSeal uses a Rust-generated WASM package for browser-side attestation. The package is built from `wasm/` and copied into `frontend/pkg`.
## Responsibilities
The WASM runtime:
- generates a browser-local Ed25519 keypair
- signs canonical heartbeat payloads
- computes Blake3 hash-chain progression
- executes server-issued VM opcode programs
- initializes synthetic gene state
- previews gene mutation commitments
- commits or discards preview state after heartbeat response
The WASM runtime is not treated as a secure enclave. The server independently recomputes deterministic state.
## Requirements
```bash
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
```
Verify:
```bash
wasm-pack --version
```
## Build
From the repository root:
```bash
wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
`--target web` emits native ES modules compatible with the static frontend.
Development build:
```bash
wasm-pack build wasm --target web
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
Full project build:
```bash
bash scripts/build.sh
```
## Output Files
The package name comes from the crate name `chronoseal-wasm`, so generated files use the `chronoseal_wasm` prefix.
Expected `frontend/pkg/` contents include:
- `chronoseal_wasm.js`
- `chronoseal_wasm_bg.wasm`
- `chronoseal_wasm.d.ts`
- `package.json`
Generated files in `wasm/pkg/` and `frontend/pkg/` are build artifacts and should be regenerated during release.
## Browser Import
```js
import init, {
generate_keypair,
get_public_key,
sign_message,
compute_next_hash,
run_program,
init_gene_state,
preview_gene_commitment,
commit_gene_preview,
discard_gene_preview,
current_gene_commitment
} from './pkg/chronoseal_wasm.js';
```
Call `await init()` before using any exported function.
## Exported Functions
| Function | Signature | Failure value |
|---|---|---|
| `generate_keypair()` | `() -> string` | `""` only on unexpected failure |
| `get_public_key()` | `() -> string` | `""` if no keypair exists |
| `sign_message(msg)` | `(string) -> string` | `""` if no keypair exists or signing fails |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | panic/error path should be avoided by valid inputs |
| `run_program(b64)` | `(string) -> JsValue` | returns empty/default stack state on invalid execution path |
| `init_gene_state(gene_size)` | `(u32) -> bool` | `false` |
| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | `(string, string, u64, u8) -> string` | `""` |
| `commit_gene_preview()` | `() -> bool` | `false` |
| `discard_gene_preview()` | `() -> void` | none |
| `current_gene_commitment(session_id, mutation_step)` | `(string, u64) -> string` | `""` if no committed state exists |
`rounds = 0` in `preview_gene_commitment` selects the shared default mutation round count.
## Mutation State Lifecycle
The browser must keep two gene states:
- committed state: the last accepted state
- preview state: candidate state for the heartbeat currently being sent
Expected sequence:
1. Call `init_gene_state(gene_size)` after `/init`.
2. Call `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` before signing `/hb`.
3. Include the returned commitment and mutation step in the signed heartbeat.
4. If the response contains next-state fields, call `commit_gene_preview()`.
5. If the heartbeat is rejected or errors, call `discard_gene_preview()`.
Never commit preview state before the server accepts the heartbeat.
## Hash-Chain Ordering
After an accepted heartbeat, compute the next local hash with the salt that was active when the heartbeat was sent. Then replace the local salt with `next_salt`.
Correct order:
```js
const sentSalt = currentSalt;
currentSalt = resp.next_salt;
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt);
```
This mirrors the server, which computes and stores the new hash before rotating to the next salt.
## Serving WASM
The `.wasm` file must be served with:
```text
Content-Type: application/wasm
```
ChronoSeal's built-in static file service handles this for normal deployments.
## Validation
Recommended checks after WASM changes:
```bash
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
wasm-pack build wasm --target web
```
Then refresh `frontend/pkg`:
```bash
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
+66
View File
@@ -0,0 +1,66 @@
# ChronoSeal Debugging & Failure Mode Guide (WHY_IT_FAILS)
This document provides a technical diagnostic reference for developers, operators, and integration security teams. It explains why a client heartbeat or session initialization fails verification, and how to debug desynchronization issues.
---
## 1. Silent Rejections vs. HTTP Failures
To deny attackers a feedback oracle, the ChronoSeal heartbeat endpoint (`POST /hb`) always returns HTTP status `200 OK` with `{"status": "ok"}` on semantic verification failures.
* **Successful Attestation:** The JSON response contains the rotated next state information: `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
* **Silently Rejected Attestation:** The JSON response *omits* these three fields. The client is expected to roll back the state preview and retry.
---
## 2. Common Verification Failure Modes
### A. Clock Drift (`TimestampDrift`)
* **Error Cause:** The client machine's local system time differs from the server's time by more than the configured `max_timestamp_drift_ms` (default 30 seconds).
* **Diagnostic Signal:** The `/hb` response omits next state parameters.
* **Remediation:** Synchronize both client and server clocks using NTP (Network Time Protocol). On the client, use NTP-synced system clocks or query server timestamp headers during initialization to compute a local clock offset.
### B. Replay Attempts / Out-of-Sequence (`ChainBroken`)
* **Error Cause:** The request `prev_hash` does not match the server-stored `last_hash` for the session.
* **Root Causes:**
1. The client replayed a previously captured heartbeat payload.
2. The client lost the network response containing the rotated next state parameters and retried with stale state.
3. A concurrent request succeeded first, updating the session's hash state.
* **Remediation:** If network issues cause packet loss, the client must discard the session and initiate a new `/init` handshake. Heartbeats cannot be replayed or resumed from a historical state.
### C. Signature Failures (`Signature`)
* **Error Cause:** The Ed25519 signature over the canonical JSON payload is invalid.
* **Root Causes:**
1. The client signed a payload that differed in ordering or format from the server's canonical serialization. (Ensure key sorting matches alphabetically: `entropyData`, `fingerprint`, `geneCommitment`, `mutationStep`, `prevHash`, `sessionId`, `stackState`, `timestamp`).
2. Different platform engines formatted floats or large numbers differently.
3. The public key registered during `/init` does not match the signing key.
* **Remediation:** Ensure both frontend and backend use strict canonical serializations (BTreeMap alphabetically sorted keys).
### D. VM Stack State Mismatch (`VmStackMismatch`)
* **Error Cause:** The client's submitted `stack_state` (VM stack and instruction pointer `ip`) does not match the server-side re-execution of the session's random math program.
* **Root Causes:**
1. An automated client bypassed the VM bytecode interpreter.
2. The client VM interpreter diverged mathematically (e.g. word size wrapping or logical op mismatches).
* **Remediation:** Check the VM interpreter implementation parity between the client wasm and `shared::vm`.
### E. Mutation Commitment Mismatch (`MutationCommitmentMismatch`)
* **Error Cause:** The client's computed `gene_commitment` does not match the server-applied gene mutation.
* **Root Causes:**
1. The client used a different number of `mutation_rounds` than the server config.
2. The mutation order execution logic diverged.
* **Remediation:** Verify that the client wasm correctly parsed `mutation_rounds` from `/init` and passed it to the generator.
### F. Rate Limiting (`RateLimiter`)
* **Error Cause:** The client submitted more requests than allowed by the server's rate-limiting config (e.g., `rate_limit_count` per `rate_limit_window_secs`).
* **Diagnostic Signal:** The server returns `200 OK` with `{"status": "ok"}` but no next state data.
* **Remediation:** Reduce heartbeat frequency or adjust rate limit parameters in the daemon configuration.
## Additional v1.0.2 Failure Modes
- Invalid aspect ratio
- Invalid device pixel ratio
- Invalid hardware concurrency
- NaN or infinite fingerprint values
- Entropy event count exceeds 500
- Client IP rate limited
+41
View File
@@ -0,0 +1,41 @@
# ChronoSeal Third-Party Wrapper & Integration Guide (WRAPPER_GUIDE)
This document provides stable guidance for developers building third-party integration wrappers, clients, or SDKs around the `chronoseald` daemon.
---
## 1. Public Contract & Stability Guarantees
As a protocol-first Unix daemon, `chronoseald` guarantees stability on the public network interface.
### Guaranteed Stable
* **Endpoints:** `POST /init` and `POST /hb`.
* **JSON Fields:** The structure and naming of request and response keys.
* **VM Instruction Set:** The behavior and encoding of the 10 core VM opcodes (`0x00`..=`0x09`).
* **Signature Serialization:** Alphabetical key-sorting rules using `BTreeMap` serialization.
* **Hash Progression:** Blake3 chain folding rules.
### Private (Unstable / Subject to Change)
* **Database Engines & Schemas:** SQLite table structure, Valkey key formatting, and indexes.
* **Daemon CLI Flags:** Internal metrics query formats.
* **Memory Structures:** Thread boundaries, session caches, and synchronization locks.
---
## 2. API Versioning & Deprecation Policy
* **Version Format:** API endpoints do not contain version prefixes (e.g., `/v1/hb`). Instead, protocol versioning is coupled to the daemon release version.
* **Breaking Protocol Changes:** Any change to the core hash function (Blake3) or the VM instruction set will trigger a major release (e.g., `v2.0.0`).
* **Deprecation Cycle:** Deprecated features will be supported for at least one minor release cycle, documented in `docs/PROTOCOL_STABILITY.md`.
---
## 3. Reference Implementation Steps for Wrappers
To build a client-side wrapper or application adapter for `chronoseald`:
1. **Handshake:** Send `POST /init` with the hex-encoded Ed25519 public key. Save the returned `session_id`, `salt`, `opcodes_b64`, and `mutation_order_b64`.
2. **VM Execution:** Run the math VM program (decoded from `opcodes_b64`) using the client wasm runtime to get the target `stack_state`.
3. **Gene Mutation:** Decode `mutation_order_b64`, apply the mutation steps to the local gene buffer, and compute the new commitment hash.
4. **Signing:** Build the canonical alphabetical JSON message, sign it, and send `POST /hb`.
5. **Chain Advancement:** On success, extract `next_salt` and `next_mutation_order_b64` to prepare the next heartbeat request.
+27
View File
@@ -0,0 +1,27 @@
bind = "0.0.0.0:3000"
# Storage backend: sqlite-in-memory (default), sqlite-in-disk, or valkey.
db_type = "sqlite-in-memory"
pid_file = "/run/chronoseal.pid"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
frontend_dir = "/usr/share/chronoseal/frontend"
log_file = "/var/log/chronoseal/chronoseal.jsonl"
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 10.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
# Synthetic gene size. Current valid range: 1..=4096. Default: 512.
gene_size = 512
# Current valid range: 1..=10. Default: 4.
mutation_rounds = 4
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="296.99997mm" viewBox="0 0 1122.5196 793.7008" enable-background="new 0 0 1254 1254" xml:space="preserve" height="210mm" sodipodi:docname="logo1.svg" inkscape:export-filename="logo1.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.1654266" inkscape:cx="561.16791" inkscape:cy="396.85039" inkscape:window-width="2048" inkscape:window-height="1205" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"></sodipodi:namedview><defs id="defs44"></defs><path fill="none" opacity="1" stroke="none" d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path2"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path45" style="fill:#e1e1e4;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path46" style="fill:#b0b2b8;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20Line truncated

After

Width:  |  Height:  |  Size: 8.1 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+43 -7
View File
@@ -1,8 +1,22 @@
import init, { generate_keypair, sign_message, compute_next_hash, run_program } from './pkg/antibot_wasm.js';
import init, {
generate_keypair,
sign_message,
compute_next_hash,
run_program,
init_gene_state,
preview_gene_commitment,
commit_gene_preview,
discard_gene_preview
} from './pkg/chronoseal_wasm.js';
import { collectEntropy } from './entropy.js';
import { sendRequest } from './transport.js';
let session, prevHash, currentSalt, opcodesB64, lastTime;
let minInterval = 12000;
let maxInterval = 25000;
let pendingMutationStep = 0;
let pendingMutationOrderB64 = '';
let mutationRounds = 4;
export async function initHeartbeat() {
await init();
@@ -12,12 +26,20 @@ export async function initHeartbeat() {
prevHash = initResp.initial_hash;
currentSalt = initResp.salt;
opcodesB64 = initResp.opcodes_b64;
minInterval = initResp.heartbeat_min_interval_ms || 12000;
maxInterval = initResp.heartbeat_max_interval_ms || 25000;
if (!init_gene_state(initResp.gene_size || 512)) {
throw new Error('Failed to initialize gene state');
}
pendingMutationStep = initResp.mutation_step;
pendingMutationOrderB64 = initResp.mutation_order_b64;
mutationRounds = initResp.mutation_rounds || 4;
lastTime = performance.now();
scheduleNext();
}
function scheduleNext() {
const delay = 12000 + Math.random() * 13000;
const delay = minInterval + Math.random() * (maxInterval - minInterval);
setTimeout(sendHeartbeat, delay);
}
@@ -36,6 +58,10 @@ async function sendHeartbeat() {
const timestamp = Date.now();
const entropyData = { events: events.map(e => ({ x: e.x, y: e.y, t: e.t })) };
const entropyJson = JSON.stringify(entropyData);
const geneCommitment = preview_gene_commitment(pendingMutationOrderB64, session, pendingMutationStep, mutationRounds);
if (!geneCommitment) {
throw new Error('Unable to compute mutation commitment');
}
const signable = {
sessionId: session,
@@ -43,12 +69,14 @@ async function sendHeartbeat() {
timestamp: timestamp,
entropyData: entropyData,
stackState: JSON.parse(stackState),
fingerprint: fingerprint
fingerprint: fingerprint,
mutationStep: pendingMutationStep,
geneCommitment: geneCommitment
};
const msg = JSON.stringify(signable, Object.keys(signable).sort());
const sig = sign_message(msg);
if (!sig) {
console.error('Keypair not initialised — skipping heartbeat');
discard_gene_preview();
return;
}
const resp = await sendRequest('/hb', 'POST', {
@@ -58,21 +86,29 @@ async function sendHeartbeat() {
entropy_data: entropyData,
stack_state: JSON.parse(stackState),
fingerprint,
mutation_step: pendingMutationStep,
gene_commitment: geneCommitment,
signature: sig
});
if (resp.next_salt) {
if (resp.next_salt && resp.next_mutation_step && resp.next_mutation_order_b64) {
if (!commit_gene_preview()) {
discard_gene_preview();
throw new Error('Failed to commit local mutation preview');
}
// IMPORTANT: capture the salt that was active when this heartbeat was sent.
// The server computes new_hash = H(prev, ts, entropy, stack, OLD_salt) and stores it,
// then rotates to next_salt. We must mirror that using the same old salt, then rotate.
const sentSalt = currentSalt;
currentSalt = resp.next_salt;
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackState, sentSalt);
pendingMutationStep = resp.next_mutation_step;
pendingMutationOrderB64 = resp.next_mutation_order_b64;
} else {
console.warn('Heartbeat rejected');
discard_gene_preview();
}
} catch (e) {
console.error(e);
discard_gene_preview();
} finally {
scheduleNext();
}
+1
View File
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; style-src 'self' 'unsafe-inline'">
<title>Anti-Scraper Demo</title>
</head>
<body>
+1 -3
View File
@@ -1,5 +1,3 @@
import { initHeartbeat } from './heartbeat.js';
(async () => {
await initHeartbeat();
})();
initHeartbeat().catch(() => {});
+596
View File
@@ -0,0 +1,596 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "blake3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures 0.3.0",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cc"
version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chronoseal-fuzz"
version = "0.0.0"
dependencies = [
"libfuzzer-sys",
"serde_json",
"shared",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand_core",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libfuzzer-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
]
[[package]]
name = "shared"
version = "0.6.0"
dependencies = [
"base64",
"blake3",
"ed25519-dalek",
"hex",
"rand",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "zerocopy"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "chronoseal-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[dependencies]
libfuzzer-sys = "0.4"
shared = { path = "../shared" }
serde_json = "1"
[workspace]
[[bin]]
name = "vm"
path = "fuzz_targets/vm.rs"
test = false
doc = false
[[bin]]
name = "protocol"
path = "fuzz_targets/protocol.rs"
test = false
doc = false
[[bin]]
name = "environment"
path = "fuzz_targets/environment.rs"
test = false
doc = false
+6
View File
@@ -0,0 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = shared::gene::decode_environment(data);
});
+9
View File
@@ -0,0 +1,9 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use shared::protocol::HeartbeatRequest;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _: Result<HeartbeatRequest, _> = serde_json::from_str(s);
}
});
+6
View File
@@ -0,0 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = shared::vm::execute(data);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="296.99997mm"
viewBox="0 0 1122.5196 793.7008"
enable-background="new 0 0 1254 1254"
xml:space="preserve"
height="210mm"
sodipodi:docname="chronoseal.svg"
inkscape:export-filename="logo1.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.1654266"
inkscape:cx="561.16791"
inkscape:cy="396.85039"
inkscape:window-width="2048"
inkscape:window-height="1205"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><defs
id="defs44" />
<path
fill="none"
opacity="0"
stroke="none"
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path2" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path45"
style="fill:#e1e1e4;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path46"
style="fill:#b0b2b8;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path47"
style="fill:#7b7e85;fill-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
x="154.54701"
y="550.13623"
id="text47"><tspan
id="tspan47"
x="154.54701"
y="550.13623" /><tspan
id="tspan48"
x="154.54701"
y="750.13623"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
id="tspan49">chrono</tspan>seal</tspan></text><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path1"
style="fill:#4a4d53;fill-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.
+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="296.99997mm"
viewBox="0 0 1122.5196 793.7008"
enable-background="new 0 0 1254 1254"
xml:space="preserve"
height="210mm"
sodipodi:docname="logo1.svg"
inkscape:export-filename="logo1.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.1654266"
inkscape:cx="561.16791"
inkscape:cy="396.85039"
inkscape:window-width="2048"
inkscape:window-height="1205"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><defs
id="defs44" />
<path
fill="none"
opacity="1"
stroke="none"
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path2" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path45"
style="fill:#e1e1e4;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path46"
style="fill:#b0b2b8;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path47"
style="fill:#7b7e85;fill-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
x="154.54701"
y="550.13623"
id="text47"><tspan
id="tspan47"
x="154.54701"
y="550.13623" /><tspan
id="tspan48"
x="154.54701"
y="750.13623"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
id="tspan49">chrono</tspan>seal</tspan></text><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path1"
style="fill:#4a4d53;fill-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Regular → Executable
+12 -6
View File
@@ -1,10 +1,16 @@
#!/bin/bash
set -e
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
echo "Building WASM..."
cd ../wasm
wasm-pack build --target web
mv pkg ../frontend/pkg
cd "$ROOT/wasm"
wasm-pack build --target web --release
rm -rf "$ROOT/frontend/pkg"
mv pkg "$ROOT/frontend/pkg"
echo "Building server..."
cd ../server
cargo build --release
cd "$ROOT"
cargo build -p chronoseal-server --bin chronoseal --release
echo "Done."
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
set -euo pipefail
echo "Starting server with static frontend serving..."
cd ../server
cargo run --release
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
set -euo pipefail
echo "🚀 ChronoSeal Installer"
echo "======================"
# Create system user
if ! id -u chronoseal &>/dev/null; then
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronoseal
echo "✓ Created chronoseal system user"
fi
# Build
echo "→ Building ChronoSeal..."
cd "$(dirname "$0")/.."
bash scripts/build.sh
# Install binary
sudo install -Dm755 target/release/chronoseal /usr/local/bin/chronoseal
echo "✓ Installed binary to /usr/local/bin/chronoseal"
# Install frontend assets
sudo mkdir -p /opt/chronoseal
sudo cp -r frontend /opt/chronoseal/
sudo chown -R chronoseal:chronoseal /opt/chronoseal
echo "✓ Installed frontend assets"
# Install systemd service
sudo cp chronoseal.service /etc/systemd/system/chronoseal.service
sudo systemctl daemon-reload
echo "✓ Installed systemd service"
# Enable and start
sudo systemctl enable --now chronoseal
echo "✓ ChronoSeal service started"
echo ""
echo "✅ ChronoSeal installed successfully!"
echo ""
echo "Useful commands:"
echo " chronoseal status # Check service status"
echo " chronoseal health # Health probe"
echo " sudo systemctl status chronoseal"
echo " sudo journalctl -u chronoseal -f"
echo ""
echo "To uninstall: sudo systemctl disable --now chronoseal && sudo rm /usr/local/bin/chronoseal"
Regular → Executable
+9 -2
View File
@@ -1,5 +1,12 @@
#!/bin/bash
bash build.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
"$ROOT/scripts/build.sh"
echo "Release artifacts:"
echo " - server/target/release/antibot-server"
echo " - target/release/chronoseal"
echo " - frontend/ (including pkg/)"
echo " - chronoseal.service"
echo " - scripts/install.sh"
+17 -2
View File
@@ -1,20 +1,35 @@
[package]
name = "chronoseal-server"
version = "0.2.0"
version = "1.0.2"
edition = "2021"
[[bin]]
name = "chronoseal"
path = "src/main.rs"
[dependencies]
shared = { path = "../shared" }
axum = "0.7"
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
toml = "0.8"
rusqlite = { version = "0.31", features = ["bundled"] }
r2d2 = "0.8"
r2d2_sqlite = "0.24"
thiserror = "2"
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs"] }
hex = "0.4"
base64 = "0.22"
rand = "0.8"
ed25519-dalek = "2"
redis = { version = "0.29", features = ["r2d2"] }
dashmap = "6"
+16 -11
View File
@@ -1,24 +1,29 @@
use std::sync::Arc;
use crate::session::AppState;
use std::sync::Arc;
/// Runs an infinite background loop that periodically cleans up database and memory resources.
///
/// Every 60 seconds, this loop performs two tasks:
/// 1. Evicts expired session records from the configured database storage backend.
/// 2. Evicts stale rate-limiter entries that have outlived the current rate-limiting window.
///
/// # Arguments
/// * `state` - Shared reference to the server application state.
pub async fn cleanup_loop(state: Arc<AppState>) {
loop {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
// Evict expired sessions from SQLite.
// Evict expired sessions from the configured storage backend.
{
let db = state.db.lock().await;
let now = crate::storage::current_time_ms();
let _ = db.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
rusqlite::params![now],
);
if let Err(err) = state.db_pool.delete_expired_sessions() {
tracing::error!("Failed to evict expired sessions: {}", err);
}
}
// Evict stale rate-limiter entries to prevent unbounded HashMap growth.
// Evict stale rate-limiter entries to prevent unbounded map growth.
{
let mut rl = state.rate_limiter.lock().await;
rl.evict_stale();
let window_secs = state.get_config().rate_limit_window_secs;
state.rate_limiter.evict_stale(window_secs);
}
}
}
+159
View File
@@ -0,0 +1,159 @@
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
Yaml,
}
#[derive(Debug, Parser)]
#[command(
name = "chronoseal",
version,
about = "Linux-native cryptographic browser attestation service",
long_about = "ChronoSeal runs as a composable Unix service. The CLI is the source of truth for daemon operation, health checks, configuration validation, metrics, and shell integration.",
after_help = "Examples:\n chronoseal\n chronoseal run --bind 127.0.0.1:3000\n chronoseal status --format json\n chronoseal health --config /etc/chronoseal/config.toml\n chronoseal config check --output yaml\n chronoseal generate keypair\n chronoseal completion bash > /etc/bash_completion.d/chronoseal\n\nConfiguration precedence:\n CLI flags > CHRONOSEAL_* environment variables > config file > built-in defaults\n\nDefault config discovery:\n /etc/chronoseal/config.toml, then $XDG_CONFIG_HOME/chronoseal/config.toml, then ~/.config/chronoseal/config.toml"
)]
pub struct Cli {
#[command(flatten)]
pub globals: GlobalArgs,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Clone, Args)]
pub struct GlobalArgs {
/// Path to config file.
#[arg(long, env = "CHRONOSEAL_CONFIG", global = true)]
pub config: Option<PathBuf>,
/// Output format for machine-readable commands.
#[arg(long, short = 'f', value_enum, default_value = "text", global = true)]
pub format: OutputFormat,
/// Alias for --format, provided for Unix tool compatibility.
#[arg(long, value_enum, global = true)]
pub output: Option<OutputFormat>,
/// Override the logging filter, for example info, chronoseal=debug.
#[arg(long, env = "CHRONOSEAL_LOG", global = true)]
pub log: Option<String>,
}
impl GlobalArgs {
pub fn output_format(&self) -> OutputFormat {
self.output.unwrap_or(self.format)
}
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Run the ChronoSeal daemon.
#[command(
after_help = "Examples:\n chronoseal run\n chronoseal run --db-type sqlite-in-memory\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run"
)]
Run(RunArgs),
/// Report whether the configured daemon is reachable and which PID file is present.
#[command(
after_help = "Examples:\n chronoseal status\n chronoseal status --format json\n chronoseal status --pid-file /run/chronoseal.pid"
)]
Status(RuntimeArgs),
/// Perform a daemon health probe.
#[command(
after_help = "Examples:\n chronoseal health\n chronoseal health --format json\n chronoseal health --bind 127.0.0.1:3000"
)]
Health(RuntimeArgs),
/// Validate and print effective configuration.
#[command(subcommand)]
Config(ConfigCommand),
/// Generate operational material.
#[command(subcommand)]
Generate(GenerateCommand),
/// Print version and build information.
#[command(after_help = "Examples:\n chronoseal version\n chronoseal version --format json")]
Version,
/// List database backend types and implementation status.
#[command(after_help = "Examples:\n chronoseal db-type\n chronoseal db-type --format json")]
DbType,
/// Print Prometheus metrics from the running daemon.
#[command(
after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000"
)]
Metrics(RuntimeArgs),
/// Print service statistics from the running daemon.
#[command(after_help = "Examples:\n chronoseal stats\n chronoseal stats --format json")]
Stats(RuntimeArgs),
/// Generate shell completions.
#[command(
after_help = "Examples:\n chronoseal completion bash\n chronoseal completion zsh > ~/.zfunc/_chronoseal"
)]
Completion { shell: clap_complete::Shell },
}
#[derive(Debug, Clone, Args)]
pub struct RunArgs {
#[command(flatten)]
pub runtime: RuntimeArgs,
/// Database backend selection.
/// sqlite-in-memory is active. sqlite-in-disk and valkey are planned (TODO).
#[arg(long, env = "CHRONOSEAL_DB_TYPE", value_enum)]
pub db_type: Option<crate::config::DbType>,
/// SQLite database path. Use ':memory:' for ephemeral state.
#[arg(long, env = "CHRONOSEAL_DB_PATH")]
pub db_path: Option<PathBuf>,
/// Static frontend directory served at /.
#[arg(long, env = "CHRONOSEAL_FRONTEND_DIR")]
pub frontend_dir: Option<PathBuf>,
/// Optional structured JSON log file.
#[arg(long, env = "CHRONOSEAL_LOG_FILE")]
pub log_file: Option<PathBuf>,
/// Number of mutation rounds to execute per program.
#[arg(long, env = "CHRONOSEAL_MUTATION_ROUNDS")]
pub mutation_rounds: Option<u8>,
}
#[derive(Debug, Clone, Args)]
pub struct RuntimeArgs {
/// Socket address the daemon binds to, or that CLI probes connect to.
#[arg(long, env = "CHRONOSEAL_BIND")]
pub bind: Option<String>,
/// PID file path.
#[arg(long, env = "CHRONOSEAL_PID_FILE")]
pub pid_file: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
/// Validate configuration and print the effective values.
#[command(
after_help = "Examples:\n chronoseal config check\n chronoseal config check --config /etc/chronoseal/config.toml\n chronoseal config check --output json"
)]
Check(RuntimeArgs),
}
#[derive(Debug, Subcommand)]
pub enum GenerateCommand {
/// Generate an Ed25519 keypair as hex-encoded JSON/YAML/text.
#[command(
after_help = "Examples:\n chronoseal generate keypair\n chronoseal generate keypair --format json"
)]
Keypair,
}
+376
View File
@@ -0,0 +1,376 @@
use crate::cli::{RunArgs, RuntimeArgs};
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::{
env, fs, io,
net::SocketAddr,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
#[value(rename_all = "kebab-case")]
pub enum DbType {
SqliteInMemory,
SqliteInDisk,
Valkey,
}
impl DbType {
pub fn as_str(self) -> &'static str {
match self {
Self::SqliteInMemory => "sqlite-in-memory",
Self::SqliteInDisk => "sqlite-in-disk",
Self::Valkey => "valkey",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub bind: String,
pub db_type: DbType,
pub pid_file: PathBuf,
pub db_path: PathBuf,
pub frontend_dir: PathBuf,
pub log_file: Option<PathBuf>,
pub heartbeat_min_interval_ms: u64,
pub heartbeat_max_interval_ms: u64,
pub expiration_minutes: i64,
pub rate_limit_count: u32,
pub rate_limit_window_secs: u64,
pub max_timestamp_drift_ms: i64,
pub min_mouse_total_dist: f64,
pub max_mouse_avg_speed: f64,
pub min_pause_count: u32,
pub require_mouse_activity: bool,
pub gene_size: usize,
pub mutation_rounds: u8,
}
impl Default for Config {
fn default() -> Self {
Self {
bind: "0.0.0.0:3000".to_string(),
db_type: DbType::SqliteInMemory,
pid_file: PathBuf::from("/run/chronoseal.pid"),
db_path: default_state_dir().join("chronoseal.sqlite"),
frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"),
log_file: None,
heartbeat_min_interval_ms: 12_000,
heartbeat_max_interval_ms: 25_000,
expiration_minutes: 30,
rate_limit_count: 5,
rate_limit_window_secs: 10,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 10.0,
max_mouse_avg_speed: 2.0,
min_pause_count: 1,
require_mouse_activity: true,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
mutation_rounds: shared::constants::DEFAULT_MUTATION_ROUNDS,
}
}
}
impl Config {
pub fn load(config_path: Option<&Path>) -> Result<Self, ConfigError> {
let mut config = Self::default();
if let Some(path) = config_path
.map(Path::to_path_buf)
.or_else(discover_config_path)
{
let raw = fs::read_to_string(&path).map_err(|source| ConfigError::Read {
path: path.clone(),
source,
})?;
config = toml::from_str(&raw).map_err(|source| ConfigError::Parse {
path: path.clone(),
source,
})?;
}
config.apply_env();
config.validate()?;
Ok(config)
}
pub fn apply_runtime_args(&mut self, args: &RuntimeArgs) {
if let Some(bind) = &args.bind {
self.bind.clone_from(bind);
}
if let Some(pid_file) = &args.pid_file {
self.pid_file = pid_file.clone();
}
}
pub fn apply_run_args(&mut self, args: &RunArgs) {
self.apply_runtime_args(&args.runtime);
if let Some(db_type) = args.db_type {
self.db_type = db_type;
}
if let Some(db_path) = &args.db_path {
self.db_path = db_path.clone();
}
if let Some(frontend_dir) = &args.frontend_dir {
self.frontend_dir = frontend_dir.clone();
}
if let Some(log_file) = &args.log_file {
self.log_file = Some(log_file.clone());
}
if let Some(mutation_rounds) = args.mutation_rounds {
self.mutation_rounds = mutation_rounds;
}
}
pub fn validate(&self) -> Result<(), ConfigError> {
self.bind
.parse::<SocketAddr>()
.map_err(|source| ConfigError::InvalidBind {
bind: self.bind.clone(),
source,
})?;
if !(1..=shared::constants::MAX_GENE_SIZE).contains(&self.gene_size) {
return Err(ConfigError::InvalidGeneSize {
size: self.gene_size,
});
}
if !(shared::constants::MIN_MUTATION_ROUNDS..=shared::constants::MAX_MUTATION_ROUNDS)
.contains(&self.mutation_rounds)
{
return Err(ConfigError::InvalidMutationRounds {
rounds: self.mutation_rounds,
});
}
Ok(())
}
fn apply_env(&mut self) {
if let Ok(value) = env::var("CHRONOSEAL_BIND") {
self.bind = value;
}
if let Ok(value) = env::var("CHRONOSEAL_DB_TYPE") {
self.db_type = match value.as_str() {
"sqlite-in-memory" => DbType::SqliteInMemory,
"sqlite-in-disk" => DbType::SqliteInDisk,
"valkey" => DbType::Valkey,
_ => self.db_type,
};
}
if let Ok(value) = env::var("CHRONOSEAL_PID_FILE") {
self.pid_file = PathBuf::from(value);
}
if let Ok(value) = env::var("CHRONOSEAL_DB_PATH") {
self.db_path = PathBuf::from(value);
}
if let Ok(value) = env::var("CHRONOSEAL_FRONTEND_DIR") {
self.frontend_dir = PathBuf::from(value);
}
if let Ok(value) = env::var("CHRONOSEAL_LOG_FILE") {
self.log_file = Some(PathBuf::from(value));
}
if let Ok(value) = env::var("CHRONOSEAL_HEARTBEAT_MIN_INTERVAL_MS") {
if let Ok(val) = value.parse() {
self.heartbeat_min_interval_ms = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_HEARTBEAT_MAX_INTERVAL_MS") {
if let Ok(val) = value.parse() {
self.heartbeat_max_interval_ms = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_EXPIRATION_MINUTES") {
if let Ok(val) = value.parse() {
self.expiration_minutes = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_RATE_LIMIT_COUNT") {
if let Ok(val) = value.parse() {
self.rate_limit_count = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_RATE_LIMIT_WINDOW_SECS") {
if let Ok(val) = value.parse() {
self.rate_limit_window_secs = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MAX_TIMESTAMP_DRIFT_MS") {
if let Ok(val) = value.parse() {
self.max_timestamp_drift_ms = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MIN_MOUSE_TOTAL_DIST") {
if let Ok(val) = value.parse() {
self.min_mouse_total_dist = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MAX_MOUSE_AVG_SPEED") {
if let Ok(val) = value.parse() {
self.max_mouse_avg_speed = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MIN_PAUSE_COUNT") {
if let Ok(val) = value.parse() {
self.min_pause_count = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_REQUIRE_MOUSE_ACTIVITY") {
if let Ok(val) = value.parse() {
self.require_mouse_activity = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_GENE_SIZE") {
if let Ok(val) = value.parse() {
self.gene_size = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MUTATION_ROUNDS") {
if let Ok(val) = value.parse() {
self.mutation_rounds = val;
}
}
}
}
#[derive(Debug)]
pub enum ConfigError {
Read {
path: PathBuf,
source: io::Error,
},
Parse {
path: PathBuf,
source: toml::de::Error,
},
InvalidBind {
bind: String,
source: std::net::AddrParseError,
},
InvalidGeneSize {
size: usize,
},
InvalidMutationRounds {
rounds: u8,
},
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Read { path, source } => write!(f, "failed to read {}: {source}", path.display()),
Self::Parse { path, source } => {
write!(f, "failed to parse {} as TOML: {source}", path.display())
}
Self::InvalidBind { bind, source } => {
write!(f, "invalid bind address {bind}: {source}")
}
Self::InvalidGeneSize { size } => {
write!(
f,
"invalid gene size {size}; expected 1..={}",
shared::constants::MAX_GENE_SIZE
)
}
Self::InvalidMutationRounds { rounds } => {
write!(
f,
"invalid mutation rounds {rounds}; expected {}..={}",
shared::constants::MIN_MUTATION_ROUNDS,
shared::constants::MAX_MUTATION_ROUNDS
)
}
}
}
}
impl std::error::Error for ConfigError {}
fn discover_config_path() -> Option<PathBuf> {
if let Ok(path) = env::var("CHRONOSEAL_CONFIG") {
let p = PathBuf::from(path);
if p.is_file() {
return Some(p);
}
}
user_config_candidates()
.into_iter()
.find(|candidate| candidate.is_file())
}
pub fn user_config_candidates() -> Vec<PathBuf> {
let mut candidates = vec![PathBuf::from("/etc/chronoseal/config.toml")];
if let Ok(xdg) = env::var("XDG_CONFIG_HOME") {
candidates.push(PathBuf::from(xdg).join("chronoseal/config.toml"));
} else if let Ok(home) = env::var("HOME") {
candidates.push(PathBuf::from(home).join(".config/chronoseal/config.toml"));
}
candidates
}
fn default_state_dir() -> PathBuf {
if let Ok(value) = env::var("CHRONOSEAL_STATE_DIR") {
return PathBuf::from(value);
}
if let Ok(value) = env::var("XDG_STATE_HOME") {
return PathBuf::from(value).join("chronoseal");
}
if let Ok(home) = env::var("HOME") {
return PathBuf::from(home).join(".local/state/chronoseal");
}
PathBuf::from("/var/lib/chronoseal")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_db_type_is_sqlite_in_memory() {
let cfg = Config::default();
assert_eq!(cfg.db_type, DbType::SqliteInMemory);
}
#[test]
fn test_apply_run_args_overrides_db_type() {
let mut cfg = Config::default();
let args = crate::cli::RunArgs {
runtime: crate::cli::RuntimeArgs {
bind: None,
pid_file: None,
},
db_type: Some(DbType::SqliteInDisk),
db_path: None,
frontend_dir: None,
log_file: None,
mutation_rounds: None,
};
cfg.apply_run_args(&args);
assert_eq!(cfg.db_type, DbType::SqliteInDisk);
}
#[test]
fn test_toml_parses_db_type_kebab_case() {
let raw = r#"
bind = "127.0.0.1:3000"
db_type = "valkey"
pid_file = "/tmp/pid"
db_path = "/tmp/db.sqlite"
frontend_dir = "."
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 1.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
gene_size = 512
"#;
let cfg: Config = toml::from_str(raw).unwrap();
assert_eq!(cfg.db_type, DbType::Valkey);
}
}
+30 -13
View File
@@ -2,27 +2,44 @@ use ed25519_dalek::{Signature, VerifyingKey};
use shared::protocol::HeartbeatRequest;
use std::collections::BTreeMap;
pub fn verify_signature(
pub_key_bytes: &[u8],
/// Serializes the heartbeat request into a canonical JSON representation for signature verification.
///
/// Uses `BTreeMap` to order top-level keys alphabetically, matching the JavaScript client's
/// sorting algorithm: `JSON.stringify(obj, Object.keys(obj).sort())`.
///
/// # Arguments
/// * `req` - The heartbeat request to serialize.
pub fn canonical_signing_message(
req: &HeartbeatRequest,
) -> Result<(), Box<dyn std::error::Error>> {
let pk = VerifyingKey::from_bytes(
&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?,
)?;
let sig_bytes = hex::decode(&req.signature)?;
let sig = Signature::from_slice(&sig_bytes)?;
// Build canonical JSON with BTreeMap so keys are sorted alphabetically,
// matching the JS client's JSON.stringify(obj, Object.keys(obj).sort()).
// Sorted order: entropyData, fingerprint, prevHash, sessionId, stackState, timestamp
) -> Result<String, Box<dyn std::error::Error>> {
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
payload.insert("geneCommitment", serde_json::json!(req.gene_commitment));
payload.insert("mutationStep", serde_json::json!(req.mutation_step));
payload.insert("prevHash", serde_json::json!(req.prev_hash));
payload.insert("sessionId", serde_json::json!(req.session_id));
payload.insert("stackState", serde_json::to_value(&req.stack_state)?);
payload.insert("timestamp", serde_json::json!(req.timestamp));
let message = serde_json::to_string(&payload)?;
Ok(serde_json::to_string(&payload)?)
}
/// Verifies the Ed25519 signature of a client's heartbeat request.
///
/// Decodes the signature and compares it strictly against the canonical JSON message
/// using the client's public key.
///
/// # Arguments
/// * `pub_key_bytes` - The client's public key bytes.
/// * `req` - The heartbeat request payload containing the signature.
pub fn verify_signature(
pub_key_bytes: &[u8],
req: &HeartbeatRequest,
) -> Result<(), Box<dyn std::error::Error>> {
let pk = VerifyingKey::from_bytes(&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?)?;
let sig_bytes = hex::decode(&req.signature)?;
let sig = Signature::from_slice(&sig_bytes)?;
let message = canonical_signing_message(req)?;
pk.verify_strict(message.as_bytes(), &sig)?;
Ok(())
+102
View File
@@ -0,0 +1,102 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SessionError {
#[error("Hex decoding error: {0}")]
Hex(#[from] hex::FromHexError),
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Storage error: {0}")]
Storage(String),
#[error("R2D2 pool error: {0}")]
Pool(#[from] r2d2::Error),
#[error("Invalid public key length")]
InvalidPublicKeyLength,
#[error("Invalid gene configuration: {0}")]
InvalidGeneConfiguration(String),
#[error("Rate limited")]
RateLimited,
}
impl IntoResponse for SessionError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
SessionError::RateLimited => (
StatusCode::TOO_MANY_REQUESTS,
"Too many requests".to_string(),
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
),
};
let body = Json(json!({
"error": error_message
}));
(status, body).into_response()
}
}
#[derive(Error, Debug)]
pub enum VerificationError {
#[error("Session not found")]
SessionNotFound,
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Storage error: {0}")]
Storage(String),
#[error("Hex decoding error: {0}")]
Hex(#[from] hex::FromHexError),
#[error("Signature verification error: {0}")]
Signature(String),
#[error("Session has expired")]
Expired,
#[error("Chain is broken")]
ChainBroken,
#[error("Timestamp drift exceeded threshold")]
TimestampDrift,
#[error("Trust criteria failed: {0}")]
TrustFailed(String),
#[error("Fingerprint validation failed: {0}")]
FingerprintFailed(String),
#[error("Mutation step mismatch: expected {expected}, got {got}")]
MutationStepMismatch { expected: u64, got: u64 },
#[error("Mutation commitment mismatch")]
MutationCommitmentMismatch,
#[error("Mutation program error: {0}")]
MutationProgram(String),
#[error("Gene state error: {0}")]
GeneState(String),
#[error("VM execution stack state mismatch")]
VmStackMismatch,
#[error("Concurrent state modification detected (CAS failed)")]
ConcurrentUpdate,
}
+72 -3
View File
@@ -1,10 +1,79 @@
use shared::protocol::Fingerprint;
const MIN_ASPECT_RATIO: f64 = 0.5;
const MAX_ASPECT_RATIO: f64 = 3.0;
const MAX_DEVICE_PIXEL_RATIO: f64 = 5.0;
const MAX_HARDWARE_CONCURRENCY: u32 = 256;
/// Validates the browser fingerprint fields submitted by the client.
///
/// Checks basic screen aspect ratio thresholds, device pixel ratio limits,
/// and logical CPU core counts to reject anomaly fingerprints.
///
/// # Arguments
/// * `fp` - The client's hardware and screen layout fingerprint.
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
if ar < 0.5 || ar > 3.0 { return Err("aspect ratio".into()); }
if !ar.is_finite() || !(MIN_ASPECT_RATIO..=MAX_ASPECT_RATIO).contains(&ar) {
return Err("aspect ratio".into());
}
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
if dpr <= 0.0 || dpr > 5.0 { return Err("dpr".into()); }
if fp.hardware_concurrency == 0 { return Err("hw".into()); }
if !dpr.is_finite() || dpr <= 0.0 || dpr > MAX_DEVICE_PIXEL_RATIO {
return Err("dpr".into());
}
if fp.hardware_concurrency == 0 || fp.hardware_concurrency > MAX_HARDWARE_CONCURRENCY {
return Err("hw".into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn fingerprint(
aspect_ratio: impl Into<String>,
device_pixel_ratio: impl Into<String>,
hardware_concurrency: u32,
) -> Fingerprint {
Fingerprint {
aspect_ratio: aspect_ratio.into(),
device_pixel_ratio: device_pixel_ratio.into(),
hardware_concurrency,
}
}
#[test]
fn accepts_valid_fingerprint() {
assert!(validate(&fingerprint("1.7777777778", "2", 8)).is_ok());
}
#[test]
fn accepts_boundary_values() {
assert!(validate(&fingerprint("0.5", "1", 1)).is_ok());
assert!(validate(&fingerprint("3.0", "5.0", MAX_HARDWARE_CONCURRENCY)).is_ok());
}
#[test]
fn rejects_invalid_aspect_ratios() {
for aspect_ratio in ["not-a-number", "NaN", "inf", "0.49", "3.01"] {
assert!(validate(&fingerprint(aspect_ratio, "2", 8)).is_err());
}
}
#[test]
fn rejects_invalid_device_pixel_ratios() {
for device_pixel_ratio in ["not-a-number", "NaN", "inf", "0", "-1", "5.01"] {
assert!(validate(&fingerprint("1.77", device_pixel_ratio, 8)).is_err());
}
}
#[test]
fn rejects_invalid_hardware_concurrency() {
assert!(validate(&fingerprint("1.77", "2", 0)).is_err());
assert!(validate(&fingerprint("1.77", "2", MAX_HARDWARE_CONCURRENCY + 1)).is_err());
}
}
+128 -28
View File
@@ -1,47 +1,147 @@
mod cleanup;
mod cli;
mod config;
mod crypto;
mod errors;
mod fingerprint;
mod middleware;
mod output;
mod ratelimit;
mod routes;
mod runtime;
mod session;
mod storage;
mod trust;
mod vm;
use axum::Router;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::info;
use session::AppState;
use clap::{CommandFactory, Parser};
use cli::{Cli, Command, ConfigCommand, GenerateCommand};
use config::Config;
use std::path::PathBuf;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
if let Err(err) = try_main().await {
eprintln!("chronoseal: {err}");
std::process::exit(1);
}
}
let conn = storage::init_db().expect("DB init");
let state = Arc::new(AppState {
db: Mutex::new(conn),
rate_limiter: Mutex::new(ratelimit::RateLimiter::new(
shared::constants::RATE_LIMIT_COUNT,
shared::constants::RATE_LIMIT_WINDOW_SECS,
)),
});
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
let log_file = log_file_for_command(&cli);
let _log_guard = init_logging(log_filter, log_file)?;
// Periodic cleanup
let bg_state = state.clone();
tokio::spawn(async move { cleanup::cleanup_loop(bg_state).await });
match &cli.command {
None | Some(Command::Run(_)) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
if let Some(Command::Run(args)) = &cli.command {
config.apply_run_args(args);
config.validate()?;
}
runtime::run_daemon(config).await?;
}
Some(Command::Status(args)) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args);
config.validate()?;
output::print(cli.globals.output_format(), &runtime::probe_status(&config))?;
}
Some(Command::Health(args)) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args);
config.validate()?;
let report = runtime::probe_health(&config);
let healthy = report.status == "healthy";
output::print(cli.globals.output_format(), &report)?;
if !healthy {
std::process::exit(2);
}
}
Some(Command::Config(ConfigCommand::Check(args))) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args);
config.validate()?;
output::print(cli.globals.output_format(), &config)?;
}
Some(Command::Generate(GenerateCommand::Keypair)) => {
output::print(cli.globals.output_format(), &runtime::generate_keypair())?;
}
Some(Command::Version) => {
output::print(cli.globals.output_format(), &runtime::version())?;
}
Some(Command::DbType) => {
output::print(cli.globals.output_format(), &runtime::db_type_report())?;
}
Some(Command::Metrics(args)) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args);
config.validate()?;
print!("{}", runtime::fetch_metrics(&config)?);
}
Some(Command::Stats(args)) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args);
config.validate()?;
output::print(cli.globals.output_format(), &runtime::fetch_stats(&config)?)?;
}
Some(Command::Completion { shell }) => {
let mut command = Cli::command();
let name = command.get_name().to_string();
clap_complete::generate(*shell, &mut command, name, &mut std::io::stdout());
}
}
Ok(())
}
let app = Router::new()
.route("/init", axum::routing::post(routes::init::handler))
.route("/hb", axum::routing::post(routes::heartbeat::handler))
.nest_service("/", tower_http::services::ServeDir::new("../frontend"))
.layer(tower_http::cors::CorsLayer::permissive())
.layer(axum::middleware::from_fn(middleware::log_request))
.with_state(state);
fn log_file_for_command(cli: &Cli) -> Option<PathBuf> {
match &cli.command {
None => Config::load(cli.globals.config.as_deref())
.ok()
.and_then(|config| config.log_file),
Some(Command::Run(args)) => {
let mut config = Config::load(cli.globals.config.as_deref()).ok()?;
config.apply_run_args(args);
config.log_file
}
_ => None,
}
}
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
info!("Server running on :3000");
axum::serve(listener, app).await.unwrap();
fn init_logging(
filter: &str,
log_file: Option<PathBuf>,
) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>, Box<dyn std::error::Error>> {
let env_filter = EnvFilter::try_new(filter)?;
if let Some(path) = log_file {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let directory = path.parent().unwrap_or_else(|| std::path::Path::new("."));
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("chronoseal.jsonl");
let appender = tracing_appender::rolling::never(directory, file_name);
let (writer, guard) = tracing_appender::non_blocking(appender);
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer().with_target(false))
.with(
tracing_subscriber::fmt::layer()
.json()
.with_target(false)
.with_writer(writer),
)
.init();
Ok(Some(guard))
} else {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.init();
Ok(None)
}
}
+22
View File
@@ -9,3 +9,25 @@ pub async fn log_request(req: Request, next: Next) -> Response {
tracing::info!("{} {} -> {}", method, uri, response.status());
response
}
/// Injects defensive HTTP response headers on every response.
///
/// These headers mitigate several classes of attacks:
/// - `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing.
/// - `X-Frame-Options: DENY` — blocks clickjacking via framing.
/// - `Referrer-Policy: no-referrer` — suppresses referrer leakage.
/// - `X-XSS-Protection: 0` — disables legacy XSS auditors (can introduce bugs).
/// - `Permissions-Policy` — restricts powerful browser features.
pub async fn security_headers(req: Request, next: Next) -> Response {
let mut response = next.run(req).await;
let headers = response.headers_mut();
headers.insert("x-content-type-options", "nosniff".parse().unwrap());
headers.insert("x-frame-options", "DENY".parse().unwrap());
headers.insert("referrer-policy", "no-referrer".parse().unwrap());
headers.insert("x-xss-protection", "0".parse().unwrap());
headers.insert(
"permissions-policy",
"camera=(), microphone=(), geolocation=()".parse().unwrap(),
);
response
}
+18
View File
@@ -0,0 +1,18 @@
use crate::cli::OutputFormat;
use serde::Serialize;
pub fn print<T>(format: OutputFormat, value: &T) -> Result<(), Box<dyn std::error::Error>>
where
T: Serialize + TextOutput,
{
match format {
OutputFormat::Text => println!("{}", value.to_text()),
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(value)?),
OutputFormat::Yaml => print!("{}", serde_yaml::to_string(value)?),
}
Ok(())
}
pub trait TextOutput {
fn to_text(&self) -> String;
}
+71 -17
View File
@@ -1,37 +1,91 @@
use std::collections::HashMap;
use dashmap::DashMap;
use std::time::Instant;
/// A lock-free, concurrent sliding-window rate limiter backed by `DashMap`.
///
/// All public methods take `&self` (no `&mut self`), so the limiter can live in
/// an `Arc<AppState>` without a `Mutex` wrapper.
pub struct RateLimiter {
buckets: HashMap<String, (u32, Instant)>,
limit: u32,
window_secs: u64,
/// Maps rate-limit keys to request counts and window start timestamps.
buckets: DashMap<String, (u32, Instant)>,
}
impl RateLimiter {
pub fn new(limit: u32, window_secs: u64) -> Self {
Self { buckets: HashMap::new(), limit, window_secs }
/// Creates a new, empty `RateLimiter`.
pub fn new() -> Self {
Self {
buckets: DashMap::new(),
}
}
pub fn check(&mut self, key: &str) -> bool {
/// Evaluates if a request conforms to the rate limit.
///
/// Returns `true` if allowed, or `false` if the rate limit is exceeded.
///
/// # Arguments
/// * `key` - The unique identifier to rate-limit (e.g., client IP address).
/// * `limit` - The maximum number of allowed requests per window.
/// * `window_secs` - The length of the sliding-window in seconds.
pub fn check(&self, key: &str, limit: u32, window_secs: u64) -> bool {
let now = Instant::now();
let entry = self.buckets.entry(key.to_string()).or_insert((0, now));
if now.duration_since(entry.1).as_secs() >= self.window_secs {
*entry = (1, now);
let mut entry = self.buckets.entry(key.to_string()).or_insert((0, now));
let (count, ts) = entry.value_mut();
if now.duration_since(*ts).as_secs() >= window_secs {
*count = 1;
*ts = now;
true
} else if entry.0 >= self.limit {
} else if *count >= limit {
false
} else {
entry.0 += 1;
*count += 1;
true
}
}
/// Remove entries whose rate-limit window has fully elapsed.
/// Call this periodically (e.g. from the cleanup loop) to bound memory usage.
pub fn evict_stale(&mut self) {
let window = self.window_secs;
/// Evicts expired rate-limit entries whose time windows have fully elapsed.
///
/// Intended to be called periodically to bound in-memory map growth.
///
/// # Arguments
/// * `window_secs` - The active rate-limiting window duration in seconds.
pub fn evict_stale(&self, window_secs: u64) {
let now = Instant::now();
self.buckets
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window);
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_rate_limiter() {
let rl = RateLimiter::new();
// Limit of 2 requests per 1 second window
assert!(rl.check("user1", 2, 1));
assert!(rl.check("user1", 2, 1));
assert!(!rl.check("user1", 2, 1)); // 3rd fails
assert!(rl.check("user2", 2, 1)); // different key succeeds
thread::sleep(Duration::from_millis(1100));
assert!(rl.check("user1", 2, 1)); // succeeds after time window
}
#[test]
fn test_rate_limiter_eviction() {
let rl = RateLimiter::new();
assert!(rl.check("user1", 1, 1));
assert_eq!(rl.buckets.len(), 1);
rl.evict_stale(1);
assert_eq!(rl.buckets.len(), 1); // not stale yet
thread::sleep(Duration::from_millis(1100));
rl.evict_stale(1);
assert_eq!(rl.buckets.len(), 0); // evicted
}
}
+272 -12
View File
@@ -1,30 +1,290 @@
use axum::{extract::State, http::StatusCode, Json};
use std::sync::Arc;
use shared::protocol::{HeartbeatRequest, HeartbeatResponse};
use crate::session::AppState;
use axum::{extract::State, http::StatusCode, Json};
use shared::protocol::{HeartbeatRequest, HeartbeatResponse};
use std::sync::Arc;
pub async fn handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<HeartbeatRequest>,
) -> (StatusCode, Json<HeartbeatResponse>) {
// Rate limiting
let start_http = std::time::Instant::now();
state
.heartbeats_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// Cap entropy events to prevent oversized payloads from exhausting memory.
if payload.entropy_data.events.len() > 1000 {
let http_dur = start_http.elapsed().as_nanos() as u64;
state
.http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return (
StatusCode::OK,
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
);
}
// Rate limiting (lock-free via DashMap)
{
let (limit, window_secs) = {
let cfg = state.get_config();
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
};
if !state
.rate_limiter
.check(&payload.session_id, limit, window_secs)
{
let mut rl = state.rate_limiter.lock().await;
if !rl.check(&payload.session_id) {
tracing::debug!("Rate limit hit: {}", payload.session_id);
return (StatusCode::OK, Json(HeartbeatResponse { status: "ok".into(), next_salt: None }));
state
.verification_failures_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let http_dur = start_http.elapsed().as_nanos() as u64;
state
.http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return (
StatusCode::OK,
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
);
}
}
let db = state.db.lock().await;
match crate::session::verify_heartbeat(&db, &payload) {
Ok(next_salt) => (
let config = state.get_config();
let start_db = std::time::Instant::now();
let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload);
let db_dur = start_db.elapsed().as_nanos() as u64;
state
.storage_latency_ns
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
state
.storage_ops_count
.fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write
let outcome = match db_res {
Ok(result) => (
StatusCode::OK,
Json(HeartbeatResponse { status: "ok".into(), next_salt: Some(next_salt) }),
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: Some(result.next_salt_hex),
next_mutation_step: Some(result.next_mutation_step),
next_mutation_order_b64: Some(result.next_mutation_order_b64),
}),
),
Err(e) => {
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
(StatusCode::OK, Json(HeartbeatResponse { status: "ok".into(), next_salt: None }))
state
.verification_failures_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
match &e {
crate::errors::VerificationError::ChainBroken => {
state
.replay_attempts_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
crate::errors::VerificationError::MutationCommitmentMismatch
| crate::errors::VerificationError::MutationProgram(_)
| crate::errors::VerificationError::GeneState(_) => {
state
.mutation_failures_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
_ => {}
}
(
StatusCode::OK,
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
)
}
};
let http_dur = start_http.elapsed().as_nanos() as u64;
state
.http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
outcome
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{extract::State, Json};
use ed25519_dalek::{Signer, SigningKey};
use shared::protocol::{EntropyData, Fingerprint, InitResponse, MouseEvent};
use std::path::Path;
fn test_config() -> crate::config::Config {
crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 4.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: 64,
rate_limit_count: 20,
rate_limit_window_secs: 10,
..crate::config::Config::default()
}
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
let msg = crate::crypto::canonical_signing_message(req).unwrap();
req.signature = hex::encode(sk.sign(msg.as_bytes()).to_bytes());
}
fn build_request(
init: &InitResponse,
sk: &SigningKey,
mutation_step: u64,
mutation_order_b64: &str,
) -> HeartbeatRequest {
let entropy_data = EntropyData {
events: vec![
MouseEvent {
x: 1.0,
y: 1.0,
timestamp_ms: 1.0,
},
MouseEvent {
x: 3.0,
y: 1.0,
timestamp_ms: 2.0,
},
MouseEvent {
x: 3.0,
y: 1.0,
timestamp_ms: 120.0,
},
],
};
let program_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)
.unwrap();
let stack_state = shared::vm::execute(&program_bytes);
let order =
shared::vm_extensions::decode_order_b64(mutation_step, mutation_order_b64).unwrap();
let committed = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate =
shared::vm_extensions::apply_program_clone(&committed, &order.program).unwrap();
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: crate::storage::current_time_ms(),
entropy_data,
stack_state,
fingerprint: Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
},
mutation_step,
gene_commitment: shared::gene::commitment_hex_with_context(
&candidate,
&init.session_id,
mutation_step,
),
signature: String::new(),
};
sign_request(sk, &mut req);
req
}
async fn setup_state_and_session(
config: crate::config::Config,
) -> (Arc<AppState>, InitResponse, SigningKey) {
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
let state = Arc::new(AppState {
db_pool: pool.clone(),
rate_limiter: crate::ratelimit::RateLimiter::new(),
config: std::sync::RwLock::new(config.clone()),
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
mutation_failures_total: std::sync::atomic::AtomicU64::new(0),
replay_attempts_total: std::sync::atomic::AtomicU64::new(0),
storage_latency_ns: std::sync::atomic::AtomicU64::new(0),
storage_ops_count: std::sync::atomic::AtomicU64::new(0),
http_latency_ns: std::sync::atomic::AtomicU64::new(0),
http_ops_count: std::sync::atomic::AtomicU64::new(0),
});
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init = crate::session::create_session(&pool, &config, &pk_hex).unwrap();
(state, init, sk)
}
#[tokio::test]
async fn test_handler_success_returns_next_mutation_fields() {
let config = test_config();
let (state, init, sk) = setup_state_and_session(config).await;
let req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_some());
assert!(body.next_mutation_step.is_some());
assert!(body.next_mutation_order_b64.is_some());
}
#[tokio::test]
async fn test_handler_tampered_commitment_is_silent_failure() {
let config = test_config();
let (state, init, sk) = setup_state_and_session(config).await;
let mut req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
req.gene_commitment = "00".repeat(32);
sign_request(&sk, &mut req);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_none());
assert!(body.next_mutation_step.is_none());
assert!(body.next_mutation_order_b64.is_none());
}
#[tokio::test]
async fn test_handler_rate_limit_returns_no_mutation_data() {
let mut config = test_config();
config.rate_limit_count = 0;
let (state, init, sk) = setup_state_and_session(config).await;
let req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_none());
assert!(body.next_mutation_step.is_none());
assert!(body.next_mutation_order_b64.is_none());
}
}
+38 -11
View File
@@ -1,17 +1,44 @@
use axum::{extract::State, http::StatusCode, Json};
use std::sync::Arc;
use shared::protocol::{InitRequest, InitResponse};
use crate::errors::SessionError;
use crate::session::AppState;
use axum::{extract::State, Json};
use shared::protocol::{InitRequest, InitResponse};
use std::sync::Arc;
pub async fn handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<InitRequest>,
) -> Result<Json<InitResponse>, (StatusCode, String)> {
let db = state.db.lock().await;
crate::session::create_session(&db, &payload.public_key)
.map(Json)
.map_err(|e| {
tracing::error!("Init error: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal".into())
})
) -> Result<Json<InitResponse>, SessionError> {
let start_http = std::time::Instant::now();
let config = state.get_config();
// Rate limit session creation by public key to prevent storage exhaustion.
if !state.rate_limiter.check(
&payload.public_key,
config.rate_limit_count,
config.rate_limit_window_secs,
) {
return Err(SessionError::RateLimited);
}
let start_db = std::time::Instant::now();
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key);
let db_dur = start_db.elapsed().as_nanos() as u64;
state
.storage_latency_ns
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
state
.storage_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let resp = resp?;
let http_dur = start_http.elapsed().as_nanos() as u64;
state
.http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(Json(resp))
}
+1 -1
View File
@@ -1,2 +1,2 @@
pub mod init;
pub mod heartbeat;
pub mod init;
+575
View File
@@ -0,0 +1,575 @@
use crate::{
config::Config,
output::TextOutput,
ratelimit::RateLimiter,
routes, session,
storage::{self, StoreStats},
};
use axum::{
extract::ConnectInfo, http::StatusCode, response::IntoResponse, routing::get, Json, Router,
};
use serde::Serialize;
use std::{
fs,
io::{Read, Write},
net::{IpAddr, SocketAddr, TcpStream},
path::Path,
sync::Arc,
time::Duration,
};
use tokio::sync::Notify;
use tracing::{error, info, warn};
#[derive(Debug, Serialize)]
pub struct HealthReport {
pub status: &'static str,
pub bind: String,
}
impl TextOutput for HealthReport {
fn to_text(&self) -> String {
format!("{}\nbind={}", self.status, self.bind)
}
}
#[derive(Debug, Serialize)]
pub struct StatusReport {
pub running: bool,
pub healthy: bool,
pub bind: String,
pub pid_file: String,
pub pid: Option<u32>,
}
impl TextOutput for StatusReport {
fn to_text(&self) -> String {
let pid = self
.pid
.map_or_else(|| "unknown".to_string(), |pid| pid.to_string());
format!(
"running={}\nhealthy={}\nbind={}\npid_file={}\npid={}",
self.running, self.healthy, self.bind, self.pid_file, pid
)
}
}
#[derive(Debug, Serialize)]
pub struct VersionReport {
pub name: &'static str,
pub version: &'static str,
pub target: &'static str,
}
impl TextOutput for VersionReport {
fn to_text(&self) -> String {
format!("{} {}", self.name, self.version)
}
}
#[derive(Debug, Serialize)]
pub struct KeypairReport {
pub algorithm: &'static str,
pub public_key_hex: String,
pub private_key_hex: String,
}
impl TextOutput for KeypairReport {
fn to_text(&self) -> String {
format!(
"algorithm={}\npublic_key_hex={}\nprivate_key_hex={}",
self.algorithm, self.public_key_hex, self.private_key_hex
)
}
}
#[derive(Debug, Serialize)]
pub struct DbTypeEntry {
pub name: &'static str,
pub implemented: bool,
pub notes: &'static str,
}
#[derive(Debug, Serialize)]
pub struct DbTypeReport {
pub default: &'static str,
pub backends: Vec<DbTypeEntry>,
}
impl TextOutput for DbTypeReport {
fn to_text(&self) -> String {
let mut out = format!("default={}\n", self.default);
for backend in &self.backends {
let status = if backend.implemented {
"implemented"
} else {
"todo"
};
out.push_str(&format!(
"db_type={} status={} notes={}\n",
backend.name, status, backend.notes
));
}
out
}
}
impl TextOutput for Config {
fn to_text(&self) -> String {
format!(
"bind={}\ndb_type={}\npid_file={}\ndb_path={}\nfrontend_dir={}\nlog_file={}\ngene_size={}",
self.bind,
self.db_type.as_str(),
self.pid_file.display(),
self.db_path.display(),
self.frontend_dir.display(),
self.log_file
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "none".to_string()),
self.gene_size
)
}
}
impl TextOutput for StoreStats {
fn to_text(&self) -> String {
format!(
"sessions={}\nexpired_sessions={}\nmax_chain_length={}",
self.sessions, self.expired_sessions, self.max_chain_length
)
}
}
pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> {
install_pid_file(&config.pid_file)?;
let db_pool = init_db_pool(&config)?;
let state = Arc::new(session::AppState {
db_pool,
rate_limiter: RateLimiter::new(),
config: std::sync::RwLock::new(config.clone()),
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
mutation_failures_total: std::sync::atomic::AtomicU64::new(0),
replay_attempts_total: std::sync::atomic::AtomicU64::new(0),
storage_latency_ns: std::sync::atomic::AtomicU64::new(0),
storage_ops_count: std::sync::atomic::AtomicU64::new(0),
http_latency_ns: std::sync::atomic::AtomicU64::new(0),
http_ops_count: std::sync::atomic::AtomicU64::new(0),
});
let bg_state = state.clone();
tokio::spawn(async move { crate::cleanup::cleanup_loop(bg_state).await });
let app = Router::new()
.route("/init", axum::routing::post(routes::init::handler))
.route("/hb", axum::routing::post(routes::heartbeat::handler))
.route("/health", get(health_handler))
.route("/metrics", get(metrics_handler))
.route("/stats", get(stats_handler))
.nest_service(
"/",
tower_http::services::ServeDir::new(&config.frontend_dir),
)
.layer(tower_http::cors::CorsLayer::permissive())
.layer(axum::middleware::from_fn(
crate::middleware::security_headers,
))
.layer(axum::middleware::from_fn(crate::middleware::log_request))
.layer(axum::extract::DefaultBodyLimit::max(64 * 1024)) // 64 KiB
.with_state(state.clone());
let addr: SocketAddr = config.bind.parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
info!(bind = %config.bind, "chronoseal daemon started");
let shutdown = signal_task(state.clone());
let result = axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown)
.await;
remove_pid_file(&config.pid_file);
result?;
info!("chronoseal daemon stopped");
Ok(())
}
pub fn db_type_report() -> DbTypeReport {
DbTypeReport {
default: crate::config::DbType::SqliteInMemory.as_str(),
backends: vec![
DbTypeEntry {
name: crate::config::DbType::SqliteInMemory.as_str(),
implemented: true,
notes: "default runtime backend",
},
DbTypeEntry {
name: crate::config::DbType::SqliteInDisk.as_str(),
implemented: true,
notes: "persistent SQLite backend (uses --db-path)",
},
DbTypeEntry {
name: crate::config::DbType::Valkey.as_str(),
implemented: true,
notes: "compatibility mode: falls back to sqlite-in-memory",
},
],
}
}
fn init_db_pool(config: &Config) -> Result<storage::DbPool, Box<dyn std::error::Error>> {
storage::DbPool::init(config)
}
pub fn probe_health(config: &Config) -> HealthReport {
if http_get(&config.bind, "/health").is_ok() {
HealthReport {
status: "healthy",
bind: config.bind.clone(),
}
} else {
HealthReport {
status: "unreachable",
bind: config.bind.clone(),
}
}
}
pub fn probe_status(config: &Config) -> StatusReport {
let pid = read_pid(&config.pid_file);
let healthy = http_get(&config.bind, "/health").is_ok();
StatusReport {
running: pid.is_some() || healthy,
healthy,
bind: config.bind.clone(),
pid_file: config.pid_file.display().to_string(),
pid,
}
}
pub fn fetch_metrics(config: &Config) -> Result<String, Box<dyn std::error::Error>> {
http_get(&config.bind, "/metrics")
}
pub fn fetch_stats(config: &Config) -> Result<StoreStats, Box<dyn std::error::Error>> {
let body = http_get(&config.bind, "/stats")?;
Ok(serde_json::from_str(&body)?)
}
pub fn generate_keypair() -> KeypairReport {
let private_key = rand::random::<[u8; 32]>();
let signing_key = ed25519_dalek::SigningKey::from_bytes(&private_key);
let verifying_key = signing_key.verifying_key();
KeypairReport {
algorithm: "ed25519",
public_key_hex: hex::encode(verifying_key.to_bytes()),
private_key_hex: hex::encode(private_key),
}
}
pub fn version() -> VersionReport {
VersionReport {
name: "chronoseal",
version: env!("CARGO_PKG_VERSION"),
target: std::env::consts::ARCH,
}
}
async fn health_handler() -> impl IntoResponse {
(
StatusCode::OK,
Json(serde_json::json!({ "status": "healthy" })),
)
}
async fn stats_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<Json<StoreStats>, (StatusCode, String)> {
if !is_loopback(addr.ip()) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
state
.db_pool
.stats()
.map(Json)
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
}
async fn metrics_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<String, (StatusCode, String)> {
if !is_loopback(addr.ip()) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let stats = state
.db_pool
.stats()
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
let heartbeats = state
.heartbeats_total
.load(std::sync::atomic::Ordering::Relaxed);
let ver_failures = state
.verification_failures_total
.load(std::sync::atomic::Ordering::Relaxed);
let mut_failures = state
.mutation_failures_total
.load(std::sync::atomic::Ordering::Relaxed);
let replays = state
.replay_attempts_total
.load(std::sync::atomic::Ordering::Relaxed);
let store_ns = state
.storage_latency_ns
.load(std::sync::atomic::Ordering::Relaxed) as f64;
let store_sum = store_ns / 1_000_000_000.0;
let store_count = state
.storage_ops_count
.load(std::sync::atomic::Ordering::Relaxed);
let http_ns = state
.http_latency_ns
.load(std::sync::atomic::Ordering::Relaxed) as f64;
let http_sum = http_ns / 1_000_000_000.0;
let http_count = state
.http_ops_count
.load(std::sync::atomic::Ordering::Relaxed);
Ok(format!(
"# HELP chronoseal_active_sessions Active ChronoSeal sessions\n\
# TYPE chronoseal_active_sessions gauge\n\
chronoseal_active_sessions {}\n\
# HELP chronoseal_expired_sessions Expired sessions not yet removed\n\
# TYPE chronoseal_expired_sessions gauge\n\
chronoseal_expired_sessions {}\n\
# HELP chronoseal_max_chain_length Maximum heartbeat chain length\n\
# TYPE chronoseal_max_chain_length gauge\n\
chronoseal_max_chain_length {}\n\
# HELP chronoseal_heartbeats_total Total heartbeat requests processed\n\
# TYPE chronoseal_heartbeats_total counter\n\
chronoseal_heartbeats_total {}\n\
# HELP chronoseal_verification_failures_total Total heartbeat verification failures\n\
# TYPE chronoseal_verification_failures_total counter\n\
chronoseal_verification_failures_total {}\n\
# HELP chronoseal_mutation_failures_total Total heartbeat mutation verification failures\n\
# TYPE chronoseal_mutation_failures_total counter\n\
chronoseal_mutation_failures_total {}\n\
# HELP chronoseal_replay_attempts_total Total heartbeat replay attempts detected\n\
# TYPE chronoseal_replay_attempts_total counter\n\
chronoseal_replay_attempts_total {}\n\
# HELP chronoseal_storage_latency_seconds_sum Total time spent in storage operations in seconds\n\
# TYPE chronoseal_storage_latency_seconds_sum counter\n\
chronoseal_storage_latency_seconds_sum {:.6}\n\
# HELP chronoseal_storage_latency_seconds_count Total storage operations count\n\
# TYPE chronoseal_storage_latency_seconds_count counter\n\
chronoseal_storage_latency_seconds_count {}\n\
# HELP chronoseal_http_latency_seconds_sum Total time spent in HTTP request processing in seconds\n\
# TYPE chronoseal_http_latency_seconds_sum counter\n\
chronoseal_http_latency_seconds_sum {:.6}\n\
# HELP chronoseal_http_latency_seconds_count Total HTTP operations count\n\
# TYPE chronoseal_http_latency_seconds_count counter\n\
chronoseal_http_latency_seconds_count {}\n",
stats.sessions,
stats.expired_sessions,
stats.max_chain_length,
heartbeats,
ver_failures,
mut_failures,
replays,
store_sum,
store_count,
http_sum,
http_count
))
}
fn is_loopback(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => v4.is_loopback(),
IpAddr::V6(v6) => v6.is_loopback(),
}
}
async fn signal_task(state: Arc<session::AppState>) {
let shutdown = Arc::new(Notify::new());
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let shutdown_term = shutdown.clone();
tokio::spawn(async move {
let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
sigterm.recv().await;
info!("received SIGTERM; shutting down gracefully");
shutdown_term.notify_one();
});
let shutdown_int = shutdown.clone();
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
info!("received interrupt; shutting down gracefully");
shutdown_int.notify_one();
}
});
let state_for_hup = state.clone();
tokio::spawn(async move {
let mut sighup = signal(SignalKind::hangup()).expect("install SIGHUP handler");
while sighup.recv().await.is_some() {
match Config::load(None) {
Ok(reloaded) => {
info!(
bind = %reloaded.bind,
db_path = %reloaded.db_path.display(),
"received SIGHUP; configuration reloaded"
);
if let Ok(mut config_write) = state_for_hup.config.write() {
*config_write = reloaded;
}
}
Err(err) => warn!(error = %err, "received SIGHUP; configuration reload failed"),
}
}
});
tokio::spawn(async move {
let mut sigusr1 = signal(SignalKind::user_defined1()).expect("install SIGUSR1 handler");
while sigusr1.recv().await.is_some() {
info!("received SIGUSR1; stats are available via chronoseal stats or /stats");
}
});
}
#[cfg(not(unix))]
{
if tokio::signal::ctrl_c().await.is_ok() {
shutdown.notify_one();
}
}
shutdown.notified().await;
}
fn install_pid_file(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = path.parent() {
if let Err(err) = fs::create_dir_all(parent) {
warn!(path = %parent.display(), error = %err, "could not create PID directory");
}
}
match fs::write(path, std::process::id().to_string()) {
Ok(()) => Ok(()),
Err(err) => {
warn!(path = %path.display(), error = %err, "could not write PID file");
Ok(())
}
}
}
fn remove_pid_file(path: &Path) {
if let Err(err) = fs::remove_file(path) {
if err.kind() != std::io::ErrorKind::NotFound {
error!(path = %path.display(), error = %err, "could not remove PID file");
}
}
}
fn read_pid(path: &Path) -> Option<u32> {
fs::read_to_string(path).ok()?.trim().parse().ok()
}
fn http_get(bind: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut stream = TcpStream::connect_timeout(&bind.parse()?, Duration::from_secs(2))?;
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
stream.write_all(
format!("GET {path} HTTP/1.1\r\nHost: chronoseal\r\nConnection: close\r\n\r\n").as_bytes(),
)?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
let (_, body) = response
.split_once("\r\n\r\n")
.ok_or("daemon returned an invalid HTTP response")?;
Ok(body.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn base_config() -> Config {
Config {
bind: "127.0.0.1:0".to_string(),
db_type: crate::config::DbType::SqliteInMemory,
pid_file: std::path::PathBuf::from("/tmp/chronoseal-test.pid"),
db_path: std::path::PathBuf::from("/tmp/chronoseal-test.sqlite"),
frontend_dir: std::path::PathBuf::from("."),
log_file: None,
heartbeat_min_interval_ms: 12_000,
heartbeat_max_interval_ms: 25_000,
expiration_minutes: 30,
rate_limit_count: 5,
rate_limit_window_secs: 10,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 5.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
mutation_rounds: shared::constants::DEFAULT_MUTATION_ROUNDS,
}
}
#[test]
fn test_db_type_report_lists_backends() {
let report = db_type_report();
assert_eq!(report.default, "sqlite-in-memory");
assert_eq!(report.backends.len(), 3);
assert!(report.backends.iter().any(|b| b.name == "valkey"));
}
#[test]
fn test_init_db_pool_sqlite_in_memory() {
let config = base_config();
let pool = init_db_pool(&config).unwrap();
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 0);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 0);
}
#[test]
fn test_init_db_pool_sqlite_in_disk() {
let mut config = base_config();
config.db_type = crate::config::DbType::SqliteInDisk;
config.db_path = std::path::PathBuf::from("/tmp/chronoseal-db-type-disk.sqlite");
let _ = std::fs::remove_file(&config.db_path);
let pool = init_db_pool(&config).unwrap();
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 0);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 0);
}
#[test]
fn test_init_db_pool_valkey_compat_mode() {
let mut config = base_config();
config.db_type = crate::config::DbType::Valkey;
match init_db_pool(&config) {
Ok(pool) => {
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 0);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 0);
}
Err(_) => {
// Valkey not running in the test environment, which is acceptable
}
}
}
}
+548 -49
View File
@@ -1,98 +1,597 @@
pub struct AppState {
pub db: tokio::sync::Mutex<rusqlite::Connection>,
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>,
pub db_pool: crate::storage::DbPool,
pub rate_limiter: crate::ratelimit::RateLimiter,
pub config: std::sync::RwLock<crate::config::Config>,
pub heartbeats_total: std::sync::atomic::AtomicU64,
pub verification_failures_total: std::sync::atomic::AtomicU64,
pub mutation_failures_total: std::sync::atomic::AtomicU64,
pub replay_attempts_total: std::sync::atomic::AtomicU64,
pub storage_latency_ns: std::sync::atomic::AtomicU64,
pub storage_ops_count: std::sync::atomic::AtomicU64,
pub http_latency_ns: std::sync::atomic::AtomicU64,
pub http_ops_count: std::sync::atomic::AtomicU64,
}
use rusqlite::params;
use shared::protocol::{HeartbeatRequest, InitResponse};
use crate::{crypto, trust, fingerprint, vm, storage};
impl AppState {
pub fn get_config(&self) -> crate::config::Config {
if let Ok(cfg) = self.config.read() {
cfg.clone()
} else {
crate::config::Config::default()
}
}
}
use crate::{crypto, fingerprint, storage, trust, vm};
use shared::{
gene::{self, GeneState},
protocol::{HeartbeatRequest, InitResponse},
vm_extensions,
};
#[derive(Debug, Clone)]
pub struct HeartbeatVerificationResult {
pub next_salt_hex: String,
pub next_mutation_step: u64,
pub next_mutation_order_b64: String,
}
pub fn create_session(
conn: &rusqlite::Connection,
db: &storage::DbPool,
config: &crate::config::Config,
pub_key_hex: &str,
) -> Result<InitResponse, Box<dyn std::error::Error>> {
) -> Result<InitResponse, crate::errors::SessionError> {
let pub_key = hex::decode(pub_key_hex)?;
if pub_key.len() != shared::constants::SESSION_ID_LEN {
return Err("invalid pubkey len".into());
return Err(crate::errors::SessionError::InvalidPublicKeyLength);
}
let gene_state = gene::new_state(config.gene_size)
.map_err(|err| crate::errors::SessionError::InvalidGeneConfiguration(err.to_string()))?;
let environment_blob = gene::encode_environment(&gene_state.environment)
.map_err(|err| crate::errors::SessionError::InvalidGeneConfiguration(err.to_string()))?;
let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>());
let salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
let now = storage::current_time_ms();
let expires_at = now + (shared::constants::EXPIRATION_MINUTES as u64) * 60 * 1000;
let expires_at = now + (config.expiration_minutes as u64) * 60 * 1000;
let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt);
conn.execute(
"INSERT INTO sessions (session_id, public_key, salt, last_hash, created_at, last_seen, expires_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![session_id, pub_key, salt.to_vec(), initial_hash, now, now, expires_at],
)?;
let opcodes = vm::generate_random_program(8..=16);
let opcodes_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &opcodes);
let initial_mutation = vm_extensions::generate_order(1, config.gene_size);
let initial_mutation_b64 = vm_extensions::encode_order_b64(&initial_mutation);
let record = storage::SessionRecord {
session_id: session_id.clone(),
public_key: pub_key,
salt: salt.to_vec(),
last_hash: initial_hash.clone(),
chain_length: 1,
created_at: now,
last_seen: now,
expires_at,
gene: gene_state.gene,
environment: environment_blob,
pending_mutation: initial_mutation.program,
pending_mutation_step: initial_mutation.step,
opcodes,
};
db.insert_session(&record)
.map_err(|err| crate::errors::SessionError::Storage(err.to_string()))?;
Ok(InitResponse {
session_id,
salt: hex::encode(salt),
opcodes_b64,
initial_hash: hex::encode(&initial_hash),
expires_at,
heartbeat_min_interval_ms: config.heartbeat_min_interval_ms,
heartbeat_max_interval_ms: config.heartbeat_max_interval_ms,
gene_size: config.gene_size as u32,
mutation_step: initial_mutation.step,
mutation_order_b64: initial_mutation_b64,
mutation_rounds: config.mutation_rounds,
})
}
pub fn verify_heartbeat(
conn: &rusqlite::Connection,
db: &storage::DbPool,
config: &crate::config::Config,
req: &HeartbeatRequest,
) -> Result<String, Box<dyn std::error::Error>> {
let mut stmt = conn.prepare(
"SELECT public_key, salt, last_hash, expires_at FROM sessions WHERE session_id = ?1",
)?;
let (pub_key, salt, stored_last_hash, expires_at): (Vec<u8>, Vec<u8>, Vec<u8>, u64) =
stmt.query_row(params![req.session_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})?;
) -> Result<HeartbeatVerificationResult, crate::errors::VerificationError> {
let session = db
.load_session(&req.session_id)
.map_err(|e| crate::errors::VerificationError::Storage(e.to_string()))?;
let session = session.ok_or(crate::errors::VerificationError::SessionNotFound)?;
let now = storage::current_time_ms();
if now > expires_at {
return Err("expired".into());
if now > session.expires_at {
return Err(crate::errors::VerificationError::Expired);
}
// 1. Verify signature
crypto::verify_signature(&pub_key, req)?;
crypto::verify_signature(&session.public_key, req)
.map_err(|e| crate::errors::VerificationError::Signature(e.to_string()))?;
// 2. Check chain continuity
if stored_last_hash != hex::decode(&req.prev_hash)? {
return Err("chain broken".into());
}
// 3. Time window
let diff = (now as i64) - (req.timestamp as i64);
if diff.abs() > shared::constants::MAX_TIMESTAMP_DRIFT_MS {
return Err("timestamp drift".into());
}
// 4. Trusted mouse & fingerprint
trust::validate_mouse(&req.entropy_data)?;
fingerprint::validate(&req.fingerprint)?;
// 5. Compute new hash
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
if session.last_hash != prev_hash_bytes {
return Err(crate::errors::VerificationError::ChainBroken);
}
// 3. Mutation step and deterministic mutation parity
if req.mutation_step != session.pending_mutation_step {
return Err(crate::errors::VerificationError::MutationStepMismatch {
expected: session.pending_mutation_step,
got: req.mutation_step,
});
}
let environment = gene::decode_environment(&session.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
let server_state = GeneState {
gene: session.gene.clone(),
environment,
};
let candidate_state = vm_extensions::apply_program_clone_with_rounds(
&server_state,
&session.pending_mutation,
config.mutation_rounds,
)
.map_err(|e| crate::errors::VerificationError::MutationProgram(e.to_string()))?;
let expected_gene_commitment =
gene::commitment_hex_with_context(&candidate_state, &req.session_id, req.mutation_step);
if req.gene_commitment != expected_gene_commitment {
return Err(crate::errors::VerificationError::MutationCommitmentMismatch);
}
// 4. Time window
let diff = (now as i64) - (req.timestamp as i64);
if diff.abs() > config.max_timestamp_drift_ms {
return Err(crate::errors::VerificationError::TimestampDrift);
}
// 5. Trusted mouse & fingerprint
trust::validate_mouse(&req.entropy_data, config)
.map_err(|e| crate::errors::VerificationError::TrustFailed(e.to_string()))?;
fingerprint::validate(&req.fingerprint)
.map_err(|e| crate::errors::VerificationError::FingerprintFailed(e.to_string()))?;
// 5.5 Verify VM execution state
let expected_stack = shared::vm::execute(&session.opcodes);
if req.stack_state.stack != expected_stack.stack || req.stack_state.ip != expected_stack.ip {
return Err(crate::errors::VerificationError::VmStackMismatch);
}
// 6. Compute new hash
let new_hash = shared::hashing::next_chain_hash(
&prev_hash_bytes,
req.timestamp,
&req.entropy_data,
&req.stack_state,
&salt,
&session.salt,
);
// 6. New salt for client
// 7. Prepare next mutation order and salt
let next_step = session.pending_mutation_step + 1;
let next_mutation = vm_extensions::generate_order(next_step, candidate_state.gene.len());
let next_mutation_b64 = vm_extensions::encode_order_b64(&next_mutation);
let next_salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
let next_salt_hex = hex::encode(next_salt);
let next_environment_blob = gene::encode_environment(&candidate_state.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
let update_record = storage::SessionRecord {
session_id: req.session_id.clone(),
public_key: session.public_key,
salt: next_salt.to_vec(),
last_hash: new_hash.clone(),
chain_length: session.chain_length + 1,
created_at: session.created_at,
last_seen: now,
expires_at: session.expires_at,
gene: candidate_state.gene,
environment: next_environment_blob,
pending_mutation: next_mutation.program,
pending_mutation_step: next_step,
opcodes: session.opcodes,
};
db.update_session(&update_record, &session.last_hash)
.map_err(|e| {
if e.to_string().contains("Concurrent update detected") {
crate::errors::VerificationError::ConcurrentUpdate
} else {
crate::errors::VerificationError::Storage(e.to_string())
}
})?;
Ok(HeartbeatVerificationResult {
next_salt_hex,
next_mutation_step: next_step,
next_mutation_order_b64: next_mutation_b64,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
use rusqlite::params;
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, MouseEvent, StackState};
use std::path::Path;
#[derive(Clone)]
struct SimulatedClient {
signing_key: SigningKey,
session_id: String,
prev_hash: String,
current_salt: String,
pending_mutation_step: u64,
pending_mutation_order_b64: String,
committed_gene_state: GeneState,
opcodes_b64: String,
}
fn test_config() -> crate::config::Config {
crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 4.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: 64,
..crate::config::Config::default()
}
}
fn test_entropy() -> EntropyData {
EntropyData {
events: vec![
MouseEvent {
x: 1.0,
y: 1.0,
timestamp_ms: 1.0,
},
MouseEvent {
x: 2.0,
y: 1.0,
timestamp_ms: 2.0,
},
MouseEvent {
x: 2.0,
y: 1.0,
timestamp_ms: 120.0,
},
],
}
}
fn test_fingerprint() -> Fingerprint {
Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
}
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
let message = crate::crypto::canonical_signing_message(req).unwrap();
let sig = sk.sign(message.as_bytes());
req.signature = hex::encode(sig.to_bytes());
}
fn create_test_session(
db: &storage::DbPool,
config: &crate::config::Config,
) -> (InitResponse, SigningKey) {
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init = create_session(db, config, &pk_hex).unwrap();
(init, sk)
}
fn client_from_init(init: &InitResponse, signing_key: SigningKey) -> SimulatedClient {
SimulatedClient {
signing_key,
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
current_salt: init.salt.clone(),
pending_mutation_step: init.mutation_step,
pending_mutation_order_b64: init.mutation_order_b64.clone(),
committed_gene_state: gene::new_state(init.gene_size as usize).unwrap(),
opcodes_b64: init.opcodes_b64.clone(),
}
}
fn build_request(
client: &SimulatedClient,
timestamp: u64,
) -> (HeartbeatRequest, GeneState, EntropyData, StackState) {
let order = vm_extensions::decode_order_b64(
client.pending_mutation_step,
&client.pending_mutation_order_b64,
)
.unwrap();
let candidate_state =
vm_extensions::apply_program_clone(&client.committed_gene_state, &order.program)
.unwrap();
let entropy = test_entropy();
let program_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&client.opcodes_b64,
)
.unwrap();
let stack = shared::vm::execute(&program_bytes);
let mut req = HeartbeatRequest {
session_id: client.session_id.clone(),
prev_hash: client.prev_hash.clone(),
timestamp,
entropy_data: entropy.clone(),
stack_state: stack.clone(),
fingerprint: test_fingerprint(),
mutation_step: client.pending_mutation_step,
gene_commitment: gene::commitment_hex_with_context(
&candidate_state,
&client.session_id,
client.pending_mutation_step,
),
signature: String::new(),
};
sign_request(&client.signing_key, &mut req);
(req, candidate_state, entropy, stack)
}
fn apply_successful_response(
client: &mut SimulatedClient,
req: &HeartbeatRequest,
candidate_state: GeneState,
entropy: &EntropyData,
stack: &StackState,
resp: &HeartbeatVerificationResult,
) {
let salt = hex::decode(&client.current_salt).unwrap();
let prev_hash = hex::decode(&req.prev_hash).unwrap();
let next_hash =
shared::hashing::next_chain_hash(&prev_hash, req.timestamp, entropy, stack, &salt);
client.prev_hash = hex::encode(next_hash);
client.current_salt = resp.next_salt_hex.clone();
client.pending_mutation_step = resp.next_mutation_step;
client.pending_mutation_order_b64 = resp.next_mutation_order_b64.clone();
client.committed_gene_state = candidate_state;
}
fn load_server_gene_state(db: &storage::DbPool, session_id: &str) -> GeneState {
let session = db.load_session(session_id).unwrap().unwrap();
GeneState {
gene: session.gene,
environment: gene::decode_environment(&session.environment).unwrap(),
}
}
fn run_successful_heartbeat(
db: &storage::DbPool,
config: &crate::config::Config,
client: &mut SimulatedClient,
) -> HeartbeatRequest {
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(client, timestamp);
let result = verify_heartbeat(db, config, &req).unwrap();
apply_successful_response(client, &req, candidate_state, &entropy, &stack, &result);
req
}
#[test]
fn test_session_lifecycle_and_verification() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
assert_eq!(init.gene_size, config.gene_size as u32);
assert!(!init.mutation_order_b64.is_empty());
assert_eq!(init.mutation_step, 1);
let mut client = client_from_init(&init, signing_key);
for _ in 0..5 {
run_successful_heartbeat(&pool, &config, &mut client);
}
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 1);
assert_eq!(stats.max_chain_length, 6);
}
#[test]
fn test_deterministic_server_client_parity_across_many_heartbeats() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
for _ in 0..12 {
run_successful_heartbeat(&pool, &config, &mut client);
let server_state = load_server_gene_state(&pool, &client.session_id);
assert_eq!(server_state, client.committed_gene_state);
}
}
#[test]
fn test_replay_attack_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(&client, timestamp);
let result = verify_heartbeat(&pool, &config, &req).unwrap();
apply_successful_response(
&mut client,
&req,
candidate_state,
&entropy,
&stack,
&result,
);
let replay = verify_heartbeat(&pool, &config, &req);
assert!(matches!(
replay.unwrap_err(),
crate::errors::VerificationError::ChainBroken
));
}
#[test]
fn test_mutation_step_mismatch_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step += 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
));
}
#[test]
fn test_mutation_commitment_tamper_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.gene_commitment = "00".repeat(32);
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationCommitmentMismatch
));
}
#[test]
fn test_malformed_server_mutation_program_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = match &pool {
storage::DbPool::Sqlite(pool) => pool.get().unwrap(),
_ => panic!("expected sqlite pool for test"),
};
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
"UPDATE sessions SET last_hash=?1, salt=?2, chain_length=chain_length+1, last_seen=?3 WHERE session_id=?4",
params![new_hash, next_salt.to_vec(), now, req.session_id],
)?;
"UPDATE sessions SET pending_mutation=?1 WHERE session_id=?2",
params![vec![0xFFu8], client.session_id.clone()],
)
.unwrap();
Ok(next_salt_hex)
let updated: Vec<u8> = conn
.query_row(
"SELECT pending_mutation FROM sessions WHERE session_id=?1",
params![client.session_id.clone()],
|row| row.get(0),
)
.unwrap();
assert_eq!(updated, vec![0xFFu8]);
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationProgram(_)
));
}
#[test]
fn test_expired_session_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = match &pool {
storage::DbPool::Sqlite(pool) => pool.get().unwrap(),
_ => panic!("expected sqlite pool for test"),
};
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
"UPDATE sessions SET expires_at=?1 WHERE session_id=?2",
params![0u64, client.session_id.clone()],
)
.unwrap();
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(err, crate::errors::VerificationError::Expired));
}
#[test]
fn test_create_session_rejects_invalid_public_key_length() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let err = create_session(&pool, &config, "00ff").unwrap_err();
assert!(matches!(
err,
crate::errors::SessionError::InvalidPublicKeyLength
));
}
#[test]
fn test_stale_mutation_step_after_success_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
run_successful_heartbeat(&pool, &config, &mut client);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step -= 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
));
}
#[test]
fn test_repeated_simulation_keeps_server_and_client_commitments_equal() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let mut config = test_config();
config.gene_size = 128;
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
for _ in 0..10 {
run_successful_heartbeat(&pool, &config, &mut client);
let server_state = load_server_gene_state(&pool, &client.session_id);
assert_eq!(
gene::commitment(&server_state),
gene::commitment(&client.committed_gene_state)
);
}
}
}
+672 -6
View File
@@ -1,8 +1,267 @@
use rusqlite::Connection;
use crate::config::Config;
use redis::Commands;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn init_db() -> Result<Connection, rusqlite::Error> {
let conn = Connection::open_in_memory()?;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreStats {
pub sessions: u64,
pub expired_sessions: u64,
pub max_chain_length: u64,
}
#[derive(Debug, Clone)]
pub enum DbPool {
Sqlite(r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>),
Valkey(ValkeyStore),
}
#[derive(Debug, Clone)]
pub struct ValkeyStore {
pool: r2d2::Pool<redis::Client>,
index_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRecord {
pub session_id: String,
pub public_key: Vec<u8>,
pub salt: Vec<u8>,
pub last_hash: Vec<u8>,
pub chain_length: u64,
pub created_at: u64,
pub last_seen: u64,
pub expires_at: u64,
pub gene: Vec<u8>,
pub environment: Vec<u8>,
pub pending_mutation: Vec<u8>,
pub pending_mutation_step: u64,
pub opcodes: Vec<u8>,
}
impl DbPool {
pub fn init(config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
match config.db_type {
crate::config::DbType::SqliteInMemory => {
let pool = init_sqlite_pool(Path::new(":memory:"))?;
Ok(DbPool::Sqlite(pool))
}
crate::config::DbType::SqliteInDisk => {
let pool = init_sqlite_pool(&config.db_path)?;
Ok(DbPool::Sqlite(pool))
}
crate::config::DbType::Valkey => {
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6666".to_string());
let connection_string =
if addr.starts_with("redis://") || addr.starts_with("rediss://") {
addr.clone()
} else {
format!("redis://{}", addr)
};
let client = redis::Client::open(connection_string)?;
let pool = r2d2::Pool::builder().build(client)?;
Ok(DbPool::Valkey(ValkeyStore {
pool,
index_key: "sessions:ids".to_string(),
}))
}
}
}
pub fn insert_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"INSERT INTO sessions (
session_id, public_key, salt, last_hash, chain_length,
created_at, last_seen, expires_at, gene, environment,
pending_mutation, pending_mutation_step, opcodes
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
)?;
stmt.execute(rusqlite::params![
record.session_id,
&record.public_key,
&record.salt,
&record.last_hash,
record.chain_length,
record.created_at,
record.last_seen,
record.expires_at,
&record.gene,
&record.environment,
&record.pending_mutation,
record.pending_mutation_step,
&record.opcodes,
])?;
Ok(())
}
DbPool::Valkey(store) => store.insert_session(record),
}
}
pub fn load_session(
&self,
session_id: &str,
) -> Result<Option<SessionRecord>, Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT session_id, public_key, salt, last_hash, chain_length, created_at, last_seen, expires_at, gene, environment, pending_mutation, pending_mutation_step, opcodes
FROM sessions WHERE session_id = ?1",
)?;
let row = stmt.query_row([session_id], |row| {
Ok(SessionRecord {
session_id: row.get(0)?,
public_key: row.get(1)?,
salt: row.get(2)?,
last_hash: row.get(3)?,
chain_length: row.get(4)?,
created_at: row.get(5)?,
last_seen: row.get(6)?,
expires_at: row.get(7)?,
gene: row.get(8)?,
environment: row.get(9)?,
pending_mutation: row.get(10)?,
pending_mutation_step: row.get(11)?,
opcodes: row.get(12)?,
})
});
match row {
Ok(rec) => Ok(Some(rec)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(Box::new(err)),
}
}
DbPool::Valkey(store) => store.load_session(session_id),
}
}
pub fn update_session(
&self,
record: &SessionRecord,
old_last_hash: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let rows = conn.execute(
"UPDATE sessions SET
public_key=?1,
salt=?2,
last_hash=?3,
chain_length=?4,
created_at=?5,
last_seen=?6,
expires_at=?7,
gene=?8,
environment=?9,
pending_mutation=?10,
pending_mutation_step=?11,
opcodes=?12
WHERE session_id=?13 AND last_hash=?14",
rusqlite::params![
&record.public_key,
&record.salt,
&record.last_hash,
record.chain_length,
record.created_at,
record.last_seen,
record.expires_at,
&record.gene,
&record.environment,
&record.pending_mutation,
record.pending_mutation_step,
&record.opcodes,
&record.session_id,
old_last_hash,
],
)?;
if rows == 0 {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"Concurrent update detected (CAS failed)",
)));
}
Ok(())
}
DbPool::Valkey(store) => store.update_session_cas(record, old_last_hash),
}
}
pub fn delete_expired_sessions(&self) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
conn.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
rusqlite::params![current_time_ms()],
)?;
Ok(())
}
DbPool::Valkey(store) => store.purge_expired_sessions(),
}
}
pub fn stats(&self) -> Result<StoreStats, Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let now = current_time_ms();
let sessions =
conn.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))?;
let expired_sessions = conn.query_row(
"SELECT COUNT(*) FROM sessions WHERE expires_at < ?1",
[now],
|row| row.get(0),
)?;
let max_chain_length = conn.query_row(
"SELECT COALESCE(MAX(chain_length), 0) FROM sessions",
[],
|row| row.get(0),
)?;
Ok(StoreStats {
sessions,
expired_sessions,
max_chain_length,
})
}
DbPool::Valkey(store) => store.stats(),
}
}
}
#[cfg(test)]
pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> {
let pool = init_sqlite_pool(path)?;
Ok(DbPool::Sqlite(pool))
}
fn init_sqlite_pool(
path: &Path,
) -> Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>, Box<dyn std::error::Error>> {
let manager = if path == Path::new(":memory:") {
r2d2_sqlite::SqliteConnectionManager::memory()
} else {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
r2d2_sqlite::SqliteConnectionManager::file(path)
};
let manager = manager.with_init(|conn| {
conn.busy_timeout(std::time::Duration::from_millis(5000))?;
Ok(())
});
let pool = r2d2::Pool::new(manager)?;
let conn = pool.get()?;
init_schema(&conn)?;
Ok(pool)
}
fn init_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
@@ -12,12 +271,419 @@ pub fn init_db() -> Result<Connection, rusqlite::Error> {
chain_length INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
last_seen INTEGER NOT NULL,
expires_at INTEGER NOT NULL
expires_at INTEGER NOT NULL,
gene BLOB NOT NULL DEFAULT X'',
environment BLOB NOT NULL DEFAULT X'',
pending_mutation BLOB NOT NULL DEFAULT X'',
pending_mutation_step INTEGER NOT NULL DEFAULT 0,
opcodes BLOB NOT NULL DEFAULT X''
);",
)?;
Ok(conn)
ensure_column(
conn,
"gene",
"ALTER TABLE sessions ADD COLUMN gene BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"environment",
"ALTER TABLE sessions ADD COLUMN environment BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"pending_mutation",
"ALTER TABLE sessions ADD COLUMN pending_mutation BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"pending_mutation_step",
"ALTER TABLE sessions ADD COLUMN pending_mutation_step INTEGER NOT NULL DEFAULT 0",
)?;
ensure_column(
conn,
"opcodes",
"ALTER TABLE sessions ADD COLUMN opcodes BLOB NOT NULL DEFAULT X''",
)?;
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);",
)?;
Ok(())
}
fn ensure_column(
conn: &rusqlite::Connection,
column: &str,
alter_sql: &str,
) -> Result<(), rusqlite::Error> {
let exists: bool = conn.query_row(
"SELECT EXISTS(
SELECT 1 FROM pragma_table_info('sessions') WHERE name = ?1
)",
[column],
|row| row.get(0),
)?;
if !exists {
conn.execute_batch(alter_sql)?;
}
Ok(())
}
impl ValkeyStore {
fn session_key(&self, session_id: &str) -> String {
format!("session:{}", session_id)
}
fn load_session(
&self,
session_id: &str,
) -> Result<Option<SessionRecord>, Box<dyn std::error::Error>> {
let mut conn = self.pool.get()?;
let key = self.session_key(session_id);
let payload: Option<String> = conn.get(&key)?;
match payload {
Some(p) => Ok(serde_json::from_str(&p)?),
None => Ok(None),
}
}
fn insert_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
let mut conn = self.pool.get()?;
let key = self.session_key(&record.session_id);
let value = serde_json::to_string(record)?;
let now = current_time_ms();
let ttl_seconds = (record.expires_at.saturating_sub(now) / 1000).max(1);
redis::pipe()
.atomic()
.cmd("SET")
.arg(&key)
.arg(&value)
.arg("EX")
.arg(ttl_seconds)
.cmd("ZADD")
.arg(&self.index_key)
.arg(record.expires_at)
.arg(&record.session_id)
.cmd("ZADD")
.arg("sessions:chain_lengths")
.arg(record.chain_length)
.arg(&record.session_id)
.query::<()>(&mut *conn)?;
Ok(())
}
fn update_session_cas(
&self,
record: &SessionRecord,
old_last_hash: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
let mut conn = self.pool.get()?;
let key = self.session_key(&record.session_id);
// Watch key for concurrent modification
redis::cmd("WATCH").arg(&key).query::<()>(&mut *conn)?;
// Fetch current and verify last_hash matches
let payload: Option<String> = conn.get(&key)?;
match payload {
Some(p) => {
let current_record: SessionRecord = serde_json::from_str(&p)?;
if current_record.last_hash != old_last_hash {
redis::cmd("UNWATCH").query::<()>(&mut *conn)?;
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"Concurrent update detected (CAS failed in Valkey)",
)));
}
}
None => {
redis::cmd("UNWATCH").query::<()>(&mut *conn)?;
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Session not found for update in Valkey",
)));
}
}
let value = serde_json::to_string(record)?;
let now = current_time_ms();
let ttl_seconds = (record.expires_at.saturating_sub(now) / 1000).max(1);
let response: Option<()> = redis::pipe()
.atomic()
.cmd("SET")
.arg(&key)
.arg(&value)
.arg("EX")
.arg(ttl_seconds)
.cmd("ZADD")
.arg(&self.index_key)
.arg(record.expires_at)
.arg(&record.session_id)
.cmd("ZADD")
.arg("sessions:chain_lengths")
.arg(record.chain_length)
.arg(&record.session_id)
.query(&mut *conn)?;
match response {
Some(_) => Ok(()),
None => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"Transaction aborted due to concurrent modification",
))),
}
}
fn purge_expired_sessions(&self) -> Result<(), Box<dyn std::error::Error>> {
let now = current_time_ms();
let mut conn = self.pool.get()?;
// Fetch expired session IDs
let expired_ids: Vec<String> = conn.zrangebyscore(&self.index_key, 0, now)?;
if !expired_ids.is_empty() {
redis::pipe()
.atomic()
.cmd("ZREM")
.arg(&self.index_key)
.arg(&expired_ids)
.cmd("ZREM")
.arg("sessions:chain_lengths")
.arg(&expired_ids)
.query::<()>(&mut *conn)?;
}
Ok(())
}
fn stats(&self) -> Result<StoreStats, Box<dyn std::error::Error>> {
let now = current_time_ms();
let mut conn = self.pool.get()?;
let sessions: u64 = conn.zcard(&self.index_key)?;
let expired_sessions: u64 = conn.zcount(&self.index_key, 0, now)?;
let max_chain_length_res: Vec<(String, u64)> =
conn.zrevrange_withscores("sessions:chain_lengths", 0, 0)?;
let max_chain_length = max_chain_length_res
.first()
.map(|(_, score)| *score)
.unwrap_or(0);
Ok(StoreStats {
sessions,
expired_sessions,
max_chain_length,
})
}
}
pub fn current_time_ms() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is before UNIX epoch; check system time")
.as_millis() as u64
}
#[cfg(test)]
mod valkey_tests {
use super::*;
#[test]
fn test_valkey_store_operations() {
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
let connection_string = format!("redis://{}", addr);
let client = match redis::Client::open(connection_string) {
Ok(c) => c,
Err(_) => return,
};
let pool = match r2d2::Pool::builder().build(client) {
Ok(p) => p,
Err(_) => return,
};
let mut conn = match pool.get() {
Ok(c) => c,
Err(_) => return,
};
let _: () = match redis::cmd("PING").query(&mut *conn) {
Ok(res) => res,
Err(_) => return,
};
let store = ValkeyStore {
pool,
index_key: "test:sessions:ids".to_string(),
};
let _: Result<(), _> = conn.del("test:sessions:ids");
let session_id = "test_session_123".to_string();
let record = SessionRecord {
session_id: session_id.clone(),
public_key: vec![1, 2, 3],
salt: vec![4, 5, 6],
last_hash: vec![7, 8, 9],
chain_length: 10,
created_at: 1000,
last_seen: 2000,
expires_at: current_time_ms() + 10000,
gene: vec![11],
environment: vec![12],
pending_mutation: vec![13],
pending_mutation_step: 14,
opcodes: vec![],
};
store.insert_session(&record).unwrap();
let loaded = store.load_session(&session_id).unwrap().unwrap();
assert_eq!(loaded.session_id, session_id);
assert_eq!(loaded.chain_length, 10);
let stats = store.stats().unwrap();
assert_eq!(stats.sessions, 1);
assert_eq!(stats.max_chain_length, 10);
store.purge_expired_sessions().unwrap();
let stats = store.stats().unwrap();
assert_eq!(stats.sessions, 1);
let _: Result<(), _> = conn.del(store.session_key(&session_id));
let _: Result<(), _> = conn.del(&store.index_key);
}
#[test]
fn test_valkey_pool_concurrency() {
use std::sync::Arc;
use std::thread;
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
let connection_string = format!("redis://{}", addr);
let client = match redis::Client::open(connection_string) {
Ok(c) => c,
Err(_) => return,
};
let pool = match r2d2::Pool::builder().build(client) {
Ok(p) => p,
Err(_) => return,
};
let mut conn = match pool.get() {
Ok(c) => c,
Err(_) => return,
};
let _: () = match redis::cmd("PING").query(&mut *conn) {
Ok(res) => res,
Err(_) => return,
};
let store = ValkeyStore {
pool,
index_key: "test:concurrent:sessions:ids".to_string(),
};
let _: Result<(), _> = conn.del("test:concurrent:sessions:ids");
let store_arc = Arc::new(store);
let mut handles = Vec::new();
for t in 0..10 {
let store_clone = store_arc.clone();
let session_id = format!("valkey_concurrent_{}", t);
let handle = thread::spawn(move || {
let record = SessionRecord {
session_id: session_id.clone(),
public_key: vec![1, 2, 3],
salt: vec![4, 5, 6],
last_hash: vec![7, 8, 9],
chain_length: 1,
created_at: 1000,
last_seen: 2000,
expires_at: current_time_ms() + 10000,
gene: vec![11],
environment: vec![12],
pending_mutation: vec![13],
pending_mutation_step: 14,
opcodes: vec![],
};
store_clone.insert_session(&record).unwrap();
let loaded = store_clone.load_session(&session_id).unwrap().unwrap();
assert_eq!(loaded.session_id, session_id);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let stats = store_arc.stats().unwrap();
assert_eq!(stats.sessions, 10);
// Cleanup
let mut conn = store_arc.pool.get().unwrap();
for t in 0..10 {
let _: Result<(), _> =
conn.del(store_arc.session_key(&format!("valkey_concurrent_{}", t)));
}
let _: Result<(), _> = conn.del(&store_arc.index_key);
}
}
#[cfg(test)]
mod sqlite_tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn test_sqlite_pool_concurrency() {
let db_path = Path::new("target/test_sqlite_concurrency.db");
if let Some(parent) = db_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::remove_file(db_path);
let pool = init_pool(db_path).unwrap();
let pool_arc = Arc::new(pool);
let mut handles = Vec::new();
for t in 0..10 {
let pool_clone = pool_arc.clone();
let handle = thread::spawn(move || {
let session_id = format!("concurrent_session_{}", t);
let record = SessionRecord {
session_id: session_id.clone(),
public_key: vec![1, 2, 3],
salt: vec![4, 5, 6],
last_hash: vec![7, 8, 9],
chain_length: 1,
created_at: 1000,
last_seen: 2000,
expires_at: current_time_ms() + 10000,
gene: vec![11],
environment: vec![12],
pending_mutation: vec![13],
pending_mutation_step: 14,
opcodes: vec![],
};
pool_clone.insert_session(&record).unwrap();
let loaded = pool_clone.load_session(&session_id).unwrap().unwrap();
assert_eq!(loaded.session_id, session_id);
let mut updated = loaded;
updated.chain_length = 2;
let old_hash = updated.last_hash.clone();
pool_clone.update_session(&updated, &old_hash).unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let stats = pool_arc.stats().unwrap();
assert_eq!(stats.sessions, 10);
std::mem::drop(pool_arc);
let _ = std::fs::remove_file(db_path);
}
}
+196 -6
View File
@@ -1,7 +1,22 @@
use crate::config::Config;
use shared::protocol::EntropyData;
pub fn validate_mouse(data: &EntropyData) -> Result<(), Box<dyn std::error::Error>> {
/// Validates the browser mouse cursor interaction path for bot/automation detection.
///
/// Evaluates mouse velocity and distance features, checks the total distance traversed,
/// checks for cursor pauses (low movement over high time diff), and enforces average cursor speeds.
///
/// # Arguments
/// * `data` - The client-supplied interaction entropy events.
/// * `config` - The server configuration boundaries.
pub fn validate_mouse(
data: &EntropyData,
config: &Config,
) -> Result<(), Box<dyn std::error::Error>> {
let events = &data.events;
if !config.require_mouse_activity && events.is_empty() {
return Ok(());
}
if events.len() < 3 {
return Err("few events".into());
}
@@ -19,18 +34,193 @@ pub fn validate_mouse(data: &EntropyData) -> Result<(), Box<dyn std::error::Erro
pauses += 1;
}
}
if total_dist < shared::constants::MIN_MOUSE_TOTAL_DIST {
if total_dist < config.min_mouse_total_dist {
return Err("insufficient distance".into());
}
// Speed in px/ms: total distance over elapsed wall-clock time of the event window.
let total_time_ms =
(events.last().unwrap().timestamp_ms - events[0].timestamp_ms).max(1.0);
let total_time_ms = (events.last().unwrap().timestamp_ms - events[0].timestamp_ms).max(1.0);
let avg_speed = total_dist / total_time_ms;
if avg_speed > shared::constants::MAX_MOUSE_AVG_SPEED {
if avg_speed > config.max_mouse_avg_speed {
return Err("speed too high".into());
}
if pauses < shared::constants::MIN_PAUSE_COUNT {
if pauses < config.min_pause_count {
return Err("no pause".into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use shared::protocol::MouseEvent;
fn get_default_config() -> Config {
Config {
min_mouse_total_dist: 10.0,
max_mouse_avg_speed: 2.0,
min_pause_count: 1,
require_mouse_activity: true,
..Config::default()
}
}
#[test]
fn test_validate_mouse_success() {
let config = get_default_config();
// Mouse moves from (0,0) to (5,0) then (15,0) with a pause
let events = vec![
MouseEvent {
x: 0.0,
y: 0.0,
timestamp_ms: 100.0,
},
MouseEvent {
x: 5.0,
y: 0.0,
timestamp_ms: 200.0,
},
// Pause here (dist = 0, time diff = 100ms > 50ms)
MouseEvent {
x: 5.0,
y: 0.0,
timestamp_ms: 300.0,
},
MouseEvent {
x: 15.0,
y: 0.0,
timestamp_ms: 400.0,
},
];
let data = EntropyData { events };
assert!(validate_mouse(&data, &config).is_ok());
}
#[test]
fn test_validate_mouse_insufficient_events() {
let config = get_default_config();
let events = vec![
MouseEvent {
x: 0.0,
y: 0.0,
timestamp_ms: 100.0,
},
MouseEvent {
x: 5.0,
y: 0.0,
timestamp_ms: 200.0,
},
];
let data = EntropyData { events };
let res = validate_mouse(&data, &config);
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "few events");
}
#[test]
fn test_validate_mouse_insufficient_distance() {
let config = get_default_config();
// Total distance is only 5.0 < 10.0
let events = vec![
MouseEvent {
x: 0.0,
y: 0.0,
timestamp_ms: 100.0,
},
MouseEvent {
x: 2.0,
y: 0.0,
timestamp_ms: 200.0,
},
MouseEvent {
x: 2.0,
y: 0.0,
timestamp_ms: 300.0,
},
MouseEvent {
x: 5.0,
y: 0.0,
timestamp_ms: 400.0,
},
];
let data = EntropyData { events };
let res = validate_mouse(&data, &config);
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "insufficient distance");
}
#[test]
fn test_validate_mouse_too_fast() {
let config = get_default_config();
// Distance is 200.0, time difference is 70ms -> speed 2.85 > 2.0
let events = vec![
MouseEvent {
x: 0.0,
y: 0.0,
timestamp_ms: 100.0,
},
MouseEvent {
x: 100.0,
y: 0.0,
timestamp_ms: 105.0,
},
MouseEvent {
x: 100.0,
y: 0.0,
timestamp_ms: 165.0,
}, // pause
MouseEvent {
x: 200.0,
y: 0.0,
timestamp_ms: 170.0,
},
];
let data = EntropyData { events };
let res = validate_mouse(&data, &config);
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "speed too high");
}
#[test]
fn test_validate_mouse_no_pauses() {
let config = get_default_config();
// Constant movement without any pause
let events = vec![
MouseEvent {
x: 0.0,
y: 0.0,
timestamp_ms: 100.0,
},
MouseEvent {
x: 5.0,
y: 0.0,
timestamp_ms: 200.0,
},
MouseEvent {
x: 10.0,
y: 0.0,
timestamp_ms: 300.0,
},
MouseEvent {
x: 15.0,
y: 0.0,
timestamp_ms: 400.0,
},
];
let data = EntropyData { events };
let res = validate_mouse(&data, &config);
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "no pause");
}
#[test]
fn test_validate_mouse_require_activity_toggle() {
let mut config = get_default_config();
config.require_mouse_activity = false;
let data = EntropyData { events: vec![] };
assert!(validate_mouse(&data, &config).is_ok());
config.require_mouse_activity = true;
assert!(validate_mouse(&data, &config).is_err());
}
}
+72 -3
View File
@@ -1,5 +1,16 @@
use rand::Rng;
use shared::{
gene::GeneState,
vm_extensions::{self, ExecutionTrace, MutationError, MutationOrder},
};
/// Generates a randomized VM opcode instruction program within a length range.
///
/// Builds a program of mathematical and stack ops (e.g. literals, ADD, SUB, XOR, HASH)
/// with dynamic depth checking to ensure valid stacks and prevent out of bounds execution.
///
/// # Arguments
/// * `len_range` - The inclusive range of instruction counts to generate.
pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Vec<u8> {
let mut rng = rand::thread_rng();
let count = rng.gen_range(len_range);
@@ -9,7 +20,7 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
if depth < 2 {
// Not enough operands for any binary op — push a literal.
ops.push(0x00);
let val = rng.gen::<u32>();
let val = rng.r#gen::<u32>();
ops.extend_from_slice(&val.to_le_bytes());
depth += 1;
} else {
@@ -18,7 +29,7 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
0x00 => {
// PUSH literal
ops.push(0x00);
let val = rng.gen::<u32>();
let val = rng.r#gen::<u32>();
ops.extend_from_slice(&val.to_le_bytes());
depth += 1;
}
@@ -43,4 +54,62 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
ops
}
// Server does not need to execute the program; client does.
/// Executes a raw VM mutation program bytecode slice against a `GeneState`.
///
/// # Arguments
/// * `state` - The mutable gene state to mutate.
/// * `program` - The raw VM instruction program.
#[allow(dead_code)]
pub fn execute_mutation_program(
state: &mut GeneState,
program: &[u8],
) -> Result<ExecutionTrace, MutationError> {
vm_extensions::execute_program(state, program)
}
/// Executes a `MutationOrder` program against a `GeneState`.
///
/// # Arguments
/// * `state` - The mutable gene state.
/// * `order` - The mutation order.
#[allow(dead_code)]
pub fn execute_mutation_order(
state: &mut GeneState,
order: &MutationOrder,
) -> Result<ExecutionTrace, MutationError> {
vm_extensions::execute_program(state, &order.program)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use shared::gene::{commitment, new_state};
#[test]
fn test_execute_mutation_program_wraps_shared_engine() {
let mut state = new_state(8).unwrap();
let program = vec![vm_extensions::OP_MUTATE_POINT, 0, 0, 1];
let trace = execute_mutation_program(&mut state, &program).unwrap();
assert_eq!(state.gene[0], 1);
assert_eq!(trace.final_ip, program.len());
}
#[test]
fn test_execute_mutation_order_determinism() {
let mut rng_a = rand::rngs::StdRng::seed_from_u64(101);
let mut rng_b = rand::rngs::StdRng::seed_from_u64(101);
let order_a = vm_extensions::generate_order_with_rng(&mut rng_a, 9, 64);
let order_b = vm_extensions::generate_order_with_rng(&mut rng_b, 9, 64);
assert_eq!(order_a, order_b);
let mut state_a = new_state(64).unwrap();
let mut state_b = new_state(64).unwrap();
let trace_a = execute_mutation_order(&mut state_a, &order_a).unwrap();
let trace_b = execute_mutation_order(&mut state_b, &order_b).unwrap();
assert_eq!(state_a, state_b);
assert_eq!(trace_a.final_stack, trace_b.final_stack);
assert_eq!(commitment(&state_a), commitment(&state_b));
}
}
+5 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "shared"
version = "0.2.0"
version = "1.0.1"
edition = "2021"
[dependencies]
@@ -11,3 +11,7 @@ hex = "0.4"
base64 = "0.22"
rand = "0.8"
ed25519-dalek = { version = "2", features = ["rand_core"] }
tracing = "0.1"
[dev-dependencies]
proptest = "1"
+11 -9
View File
@@ -1,11 +1,13 @@
pub const SESSION_ID_LEN: usize = 32;
pub const SALT_LEN: usize = 16;
pub const HEARTBEAT_MIN_INTERVAL_MS: u64 = 12_000;
pub const HEARTBEAT_MAX_INTERVAL_MS: u64 = 25_000;
pub const EXPIRATION_MINUTES: i64 = 30;
pub const RATE_LIMIT_COUNT: u32 = 5;
pub const RATE_LIMIT_WINDOW_SECS: u64 = 10;
pub const MAX_TIMESTAMP_DRIFT_MS: i64 = 30_000;
pub const MIN_MOUSE_TOTAL_DIST: f64 = 10.0;
pub const MAX_MOUSE_AVG_SPEED: f64 = 2.0; // px/ms
pub const MIN_PAUSE_COUNT: u32 = 1;
pub const DEFAULT_GENE_SIZE: usize = 512;
pub const MAX_GENE_SIZE: usize = 4096;
pub const MAX_ENV_RECORDS: usize = 48;
pub const MAX_MUTATION_PROGRAM_BYTES: usize = 256;
pub const DEFAULT_MUTATION_ROUNDS: u8 = 4;
pub const MIN_MUTATION_ROUNDS: u8 = 3;
pub const MAX_MUTATION_ROUNDS: u8 = 10;
pub const MAX_MUTATION_INSTRUCTION_BUDGET: usize = 2048;
pub const HASH_OPCODE_INSTRUCTION_COST: usize = 16;
pub const SOFT_CAP_DURATION_MS: u128 = 50;
pub const MAX_STACK_DEPTH: usize = 64;
+463
View File
@@ -0,0 +1,463 @@
use crate::constants::{DEFAULT_GENE_SIZE, MAX_ENV_RECORDS, MAX_GENE_SIZE};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvironmentRecord {
pub symbol: u16,
pub quantity: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneState {
pub gene: Vec<u8>,
pub environment: Vec<EnvironmentRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeneError {
InvalidGeneSize { size: usize },
TooManyEnvironmentRecords { len: usize },
EnvironmentNotSorted,
DuplicateEnvironmentSymbol(u16),
ZeroQuantitySymbol(u16),
EnvironmentFull,
EnvironmentBlobLengthInvalid { len: usize },
EnvironmentBlobTooLarge { records: usize },
}
impl std::fmt::Display for GeneError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidGeneSize { size } => write!(f, "invalid gene size: {size}"),
Self::TooManyEnvironmentRecords { len } => {
write!(f, "too many environment records: {len}")
}
Self::EnvironmentNotSorted => write!(f, "environment records are not sorted"),
Self::DuplicateEnvironmentSymbol(symbol) => {
write!(f, "duplicate environment symbol: {symbol}")
}
Self::ZeroQuantitySymbol(symbol) => {
write!(f, "environment quantity cannot be zero for symbol {symbol}")
}
Self::EnvironmentFull => write!(f, "environment is at maximum capacity"),
Self::EnvironmentBlobLengthInvalid { len } => {
write!(
f,
"environment blob length must be a multiple of 6, got {len}"
)
}
Self::EnvironmentBlobTooLarge { records } => {
write!(f, "environment blob contains too many records: {records}")
}
}
}
}
impl std::error::Error for GeneError {}
/// Creates a new, blank `GeneState` with the specified gene buffer size.
///
/// # Arguments
/// * `gene_size` - The length of the gene byte buffer. Must be within `1..=MAX_GENE_SIZE`.
pub fn new_state(gene_size: usize) -> Result<GeneState, GeneError> {
if !(1..=MAX_GENE_SIZE).contains(&gene_size) {
return Err(GeneError::InvalidGeneSize { size: gene_size });
}
Ok(GeneState {
gene: vec![0; gene_size],
environment: Vec::new(),
})
}
/// Creates a new `GeneState` with the default gene buffer size (`DEFAULT_GENE_SIZE`).
pub fn default_state() -> GeneState {
GeneState {
gene: vec![0; DEFAULT_GENE_SIZE],
environment: Vec::new(),
}
}
/// Validates the structural invariants of the given `GeneState`.
///
/// Ensures the gene size is within valid bounds and the environment records are
/// properly sorted, non-empty, and free of duplicates.
pub fn validate_state(state: &GeneState) -> Result<(), GeneError> {
if !(1..=MAX_GENE_SIZE).contains(&state.gene.len()) {
return Err(GeneError::InvalidGeneSize {
size: state.gene.len(),
});
}
validate_environment(&state.environment)
}
/// Retrieves the quantity associated with a specific environment symbol.
///
/// Performs a binary search over the sorted environment records. Returns 0 if the symbol is missing.
///
/// # Arguments
/// * `state` - The gene state to query.
/// * `symbol` - The 16-bit key to search for.
pub fn get_env_quantity(state: &GeneState, symbol: u16) -> u32 {
match state
.environment
.binary_search_by_key(&symbol, |record| record.symbol)
{
Ok(i) => state.environment[i].quantity,
Err(_) => 0,
}
}
/// Sets the quantity of an environment symbol in a `GeneState`.
///
/// If quantity is 0, the record is removed. The environment is kept sorted alphabetically by symbol.
///
/// # Arguments
/// * `state` - The mutable gene state to update.
/// * `symbol` - The 16-bit key.
/// * `quantity` - The quantity to assign.
pub fn set_env_quantity(
state: &mut GeneState,
symbol: u16,
quantity: u32,
) -> Result<(), GeneError> {
let idx = state
.environment
.binary_search_by_key(&symbol, |record| record.symbol);
match (idx, quantity) {
(Ok(i), 0) => {
state.environment.remove(i);
Ok(())
}
(Ok(i), qty) => {
state.environment[i].quantity = qty;
Ok(())
}
(Err(_), 0) => Ok(()),
(Err(i), qty) => {
if state.environment.len() >= MAX_ENV_RECORDS {
return Err(GeneError::EnvironmentFull);
}
state.environment.insert(
i,
EnvironmentRecord {
symbol,
quantity: qty,
},
);
Ok(())
}
}
}
/// Adds a quantity to an environment symbol with saturating arithmetic.
///
/// # Arguments
/// * `state` - The mutable gene state.
/// * `symbol` - The 16-bit key.
/// * `quantity` - The quantity to add.
pub fn add_env_quantity(
state: &mut GeneState,
symbol: u16,
quantity: u32,
) -> Result<u32, GeneError> {
let current = get_env_quantity(state, symbol);
let next = current.saturating_add(quantity);
set_env_quantity(state, symbol, next)?;
Ok(next)
}
/// Subtracts a quantity from an environment symbol with saturating arithmetic.
///
/// If the resulting quantity drops to 0, the symbol is removed.
///
/// # Arguments
/// * `state` - The mutable gene state.
/// * `symbol` - The 16-bit key.
/// * `quantity` - The quantity to subtract.
pub fn sub_env_quantity(
state: &mut GeneState,
symbol: u16,
quantity: u32,
) -> Result<u32, GeneError> {
let current = get_env_quantity(state, symbol);
let next = current.saturating_sub(quantity);
set_env_quantity(state, symbol, next)?;
Ok(next)
}
/// Encodes the environment records list into a compact byte slice.
///
/// Each record is written as a little-endian `u16` symbol followed by a little-endian `u32` quantity.
///
/// # Arguments
/// * `records` - The sorted environment records.
pub fn encode_environment(records: &[EnvironmentRecord]) -> Result<Vec<u8>, GeneError> {
validate_environment(records)?;
let mut out = Vec::with_capacity(records.len() * 6);
for record in records {
out.extend_from_slice(&record.symbol.to_le_bytes());
out.extend_from_slice(&record.quantity.to_le_bytes());
}
Ok(out)
}
/// Decodes environment records from a byte slice.
///
/// Validates that the length is a multiple of 6 and that records conform to sorting and quantity invariants.
///
/// # Arguments
/// * `blob` - The serialized byte slice.
pub fn decode_environment(blob: &[u8]) -> Result<Vec<EnvironmentRecord>, GeneError> {
if !blob.len().is_multiple_of(6) {
return Err(GeneError::EnvironmentBlobLengthInvalid { len: blob.len() });
}
let records_len = blob.len() / 6;
if records_len > MAX_ENV_RECORDS {
return Err(GeneError::EnvironmentBlobTooLarge {
records: records_len,
});
}
let mut records = Vec::with_capacity(records_len);
let mut i = 0;
while i < blob.len() {
let symbol = u16::from_le_bytes([blob[i], blob[i + 1]]);
let quantity = u32::from_le_bytes([blob[i + 2], blob[i + 3], blob[i + 4], blob[i + 5]]);
records.push(EnvironmentRecord { symbol, quantity });
i += 6;
}
validate_environment(&records)?;
Ok(records)
}
/// Computes the raw Blake3 cryptographic commitment of the `GeneState`.
///
/// Includes gene length, gene buffer, environment record count, and individual record key/values.
pub fn commitment(state: &GeneState) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"chronoseal/gene/v1");
h.update(&(state.gene.len() as u32).to_le_bytes());
h.update(&state.gene);
h.update(&(state.environment.len() as u16).to_le_bytes());
for record in &state.environment {
h.update(&record.symbol.to_le_bytes());
h.update(&record.quantity.to_le_bytes());
}
*h.finalize().as_bytes()
}
/// Computes the hex-encoded cryptographic commitment of the `GeneState`.
pub fn commitment_hex(state: &GeneState) -> String {
hex::encode(commitment(state))
}
/// Computes a context-bound Blake3 cryptographic commitment of the `GeneState`.
///
/// Integrates `session_id` and the current `step` index into the hash to bind the commitment.
///
/// # Arguments
/// * `state` - The gene state.
/// * `session_id` - The client session ID.
/// * `step` - The mutation step index.
pub fn commitment_with_context(state: &GeneState, session_id: &str, step: u64) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"chronoseal/gene/v1");
h.update(session_id.as_bytes());
h.update(&step.to_le_bytes());
h.update(&commitment(state));
*h.finalize().as_bytes()
}
/// Computes a context-bound, hex-encoded Blake3 cryptographic commitment of the `GeneState`.
///
/// # Arguments
/// * `state` - The gene state.
/// * `session_id` - The client session ID.
/// * `step` - The mutation step index.
pub fn commitment_hex_with_context(state: &GeneState, session_id: &str, step: u64) -> String {
hex::encode(commitment_with_context(state, session_id, step))
}
/// Helper function to validate sorting, uniqueness, and non-zero properties of environment records.
fn validate_environment(records: &[EnvironmentRecord]) -> Result<(), GeneError> {
if records.len() > MAX_ENV_RECORDS {
return Err(GeneError::TooManyEnvironmentRecords { len: records.len() });
}
let mut prev_symbol: Option<u16> = None;
for record in records {
if record.quantity == 0 {
return Err(GeneError::ZeroQuantitySymbol(record.symbol));
}
if let Some(prev) = prev_symbol {
if record.symbol < prev {
return Err(GeneError::EnvironmentNotSorted);
}
if record.symbol == prev {
return Err(GeneError::DuplicateEnvironmentSymbol(record.symbol));
}
}
prev_symbol = Some(record.symbol);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{Rng, SeedableRng};
#[test]
fn test_new_state_with_default_size() {
let state = new_state(DEFAULT_GENE_SIZE).unwrap();
assert_eq!(state.gene.len(), DEFAULT_GENE_SIZE);
assert!(state.environment.is_empty());
}
#[test]
fn test_new_state_rejects_invalid_sizes() {
assert!(matches!(
new_state(0).unwrap_err(),
GeneError::InvalidGeneSize { .. }
));
assert!(matches!(
new_state(MAX_GENE_SIZE + 1).unwrap_err(),
GeneError::InvalidGeneSize { .. }
));
}
#[test]
fn test_set_and_get_env_quantity() {
let mut state = new_state(8).unwrap();
set_env_quantity(&mut state, 42, 7).unwrap();
assert_eq!(get_env_quantity(&state, 42), 7);
set_env_quantity(&mut state, 42, 0).unwrap();
assert_eq!(get_env_quantity(&state, 42), 0);
}
#[test]
fn test_add_env_quantity_saturates() {
let mut state = new_state(8).unwrap();
set_env_quantity(&mut state, 1, u32::MAX - 3).unwrap();
let next = add_env_quantity(&mut state, 1, 99).unwrap();
assert_eq!(next, u32::MAX);
}
#[test]
fn test_sub_env_quantity_removes_symbol() {
let mut state = new_state(8).unwrap();
set_env_quantity(&mut state, 7, 10).unwrap();
let next = sub_env_quantity(&mut state, 7, 100).unwrap();
assert_eq!(next, 0);
assert!(state.environment.is_empty());
}
#[test]
fn test_environment_capacity_limit_is_enforced() {
let mut state = new_state(8).unwrap();
for symbol in 0..(MAX_ENV_RECORDS as u16) {
set_env_quantity(&mut state, symbol, 1).unwrap();
}
let err = set_env_quantity(&mut state, 500, 1).unwrap_err();
assert_eq!(err, GeneError::EnvironmentFull);
}
#[test]
fn test_encode_decode_environment_roundtrip() {
let records = vec![
EnvironmentRecord {
symbol: 3,
quantity: 9,
},
EnvironmentRecord {
symbol: 11,
quantity: 999,
},
];
let blob = encode_environment(&records).unwrap();
let decoded = decode_environment(&blob).unwrap();
assert_eq!(decoded, records);
}
#[test]
fn test_decode_environment_rejects_unsorted_records() {
let mut blob = Vec::new();
blob.extend_from_slice(&7u16.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.extend_from_slice(&2u16.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
let err = decode_environment(&blob).unwrap_err();
assert_eq!(err, GeneError::EnvironmentNotSorted);
}
#[test]
fn test_decode_environment_rejects_zero_quantity() {
let mut blob = Vec::new();
blob.extend_from_slice(&9u16.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
let err = decode_environment(&blob).unwrap_err();
assert_eq!(err, GeneError::ZeroQuantitySymbol(9));
}
#[test]
fn test_commitment_changes_when_gene_or_environment_changes() {
let mut state_a = new_state(16).unwrap();
let mut state_b = state_a.clone();
assert_eq!(commitment_hex(&state_a), commitment_hex(&state_b));
state_b.gene[0] = 1;
assert_ne!(commitment_hex(&state_a), commitment_hex(&state_b));
set_env_quantity(&mut state_a, 7, 3).unwrap();
assert_ne!(commitment_hex(&state_a), commitment_hex(&state_b));
}
#[test]
fn test_validate_state_rejects_duplicate_environment_symbols() {
let state = GeneState {
gene: vec![0; 10],
environment: vec![
EnvironmentRecord {
symbol: 1,
quantity: 1,
},
EnvironmentRecord {
symbol: 1,
quantity: 2,
},
],
};
assert_eq!(
validate_state(&state).unwrap_err(),
GeneError::DuplicateEnvironmentSymbol(1)
);
}
#[test]
fn test_table_driven_randomized_environment_roundtrip() {
for seed in 0..32u64 {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut state = new_state(32).unwrap();
for _ in 0..128 {
let symbol = rng.gen_range(0u16..200u16);
let qty = if rng.gen_bool(0.15) {
0
} else {
rng.gen_range(1u32..100_000u32)
};
if let Err(err) = set_env_quantity(&mut state, symbol, qty) {
assert_eq!(err, GeneError::EnvironmentFull);
}
validate_state(&state).unwrap();
}
let blob = encode_environment(&state.environment).unwrap();
let decoded = decode_environment(&blob).unwrap();
assert_eq!(decoded, state.environment);
let commitment_a = commitment(&state);
let commitment_b = commitment(&state.clone());
assert_eq!(commitment_a, commitment_b);
}
}
}
+32 -9
View File
@@ -1,7 +1,15 @@
use blake3::Hasher;
use crate::protocol::{EntropyData, StackState};
use blake3::Hasher;
/// Initial hash for a brand-new session: Blake3(session_id || pub_key || salt)
/// Computes the initial hash for a brand-new attestation session.
///
/// The hash is constructed as:
/// `Blake3(session_id || pub_key || salt)`
///
/// # Arguments
/// * `session_id` - The unique hex-encoded identifier for the session.
/// * `pub_key` - The client's Ed25519 public key.
/// * `salt` - The initial server-issued salt.
pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
let mut h = Hasher::new();
h.update(session_id.as_bytes());
@@ -10,7 +18,18 @@ pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
h.finalize().as_bytes().to_vec()
}
/// Next hash in the chain: Blake3 with the salt mixed in (no keyed mode needed)
/// Computes the next hash in the Blake3 attestation chain.
///
/// This mixes in the previous hash head, the client timestamp, the serialized entropy data,
/// the VM stack state, and the server-issued salt. Uses `serde_json::to_vec` to avoid
/// intermediate heap string allocations and UTF-8 verification checks.
///
/// # Arguments
/// * `prev_hash` - The previous hash-chain head.
/// * `timestamp` - The client-supplied heartbeat timestamp.
/// * `entropy` - The collected browser interaction entropy.
/// * `stack` - The final VM stack state after running the opcode program.
/// * `salt` - The server-issued salt for rotation.
pub fn next_chain_hash(
prev_hash: &[u8],
timestamp: u64,
@@ -18,14 +37,13 @@ pub fn next_chain_hash(
stack: &StackState,
salt: &[u8],
) -> Vec<u8> {
let entropy_json = serde_json::to_string(entropy).unwrap();
let stack_json = serde_json::to_string(stack).unwrap();
let entropy_bytes = serde_json::to_vec(entropy).unwrap_or_default();
let stack_bytes = serde_json::to_vec(stack).unwrap_or_default();
let entropy_hash = blake3::hash(entropy_json.as_bytes());
let stack_hash = blake3::hash(stack_json.as_bytes());
let entropy_hash = blake3::hash(&entropy_bytes);
let stack_hash = blake3::hash(&stack_bytes);
let mut h = Hasher::new();
// Mix the salt into the hash state
h.update(salt);
h.update(prev_hash);
h.update(&timestamp.to_le_bytes());
@@ -34,7 +52,12 @@ pub fn next_chain_hash(
h.finalize().as_bytes().to_vec()
}
/// Hash of all stack items for VM HASH opcode
/// Computes a 32-bit FNV-like Blake3 hash of all stack elements.
///
/// This is used by the VM `HASH` opcode to fold the current stack state into a single value.
///
/// # Arguments
/// * `stack` - The list of u32 stack elements to hash.
pub fn hash_stack(stack: &[u32]) -> u32 {
let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect();
let hash = blake3::hash(&data);
+3
View File
@@ -1,3 +1,6 @@
pub mod constants;
pub mod gene;
pub mod hashing;
pub mod protocol;
pub mod vm;
pub mod vm_extensions;
+62 -6
View File
@@ -1,62 +1,118 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
/// Request payload sent by the client to initialize a new attestation session.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InitRequest {
/// Hex-encoded 32-byte Ed25519 public verifying key generated by the client.
pub public_key: String,
}
#[derive(Serialize)]
/// Response payload returned by the server upon successful session initialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitResponse {
/// The unique hex-encoded session identifier.
pub session_id: String,
/// The initial server-issued salt to be mixed in the first heartbeat's hash.
pub salt: String,
/// Base64-encoded initial VM program for client stack execution.
pub opcodes_b64: String,
/// The computed initial hash of the attestation chain.
pub initial_hash: String,
/// Timestamp in milliseconds indicating when the session expires.
pub expires_at: u64,
/// Minimum time in milliseconds allowed between subsequent heartbeats.
pub heartbeat_min_interval_ms: u64,
/// Maximum time in milliseconds allowed between subsequent heartbeats.
pub heartbeat_max_interval_ms: u64,
/// Size of the synthetic gene byte buffer.
pub gene_size: u32,
/// The current mutation step index (starts at 1).
pub mutation_step: u64,
/// Base64-encoded initial gene mutation program.
pub mutation_order_b64: String,
/// The number of mutation rounds configured on the server.
pub mutation_rounds: u8,
}
#[derive(Deserialize, Serialize)]
/// Heartbeat request payload submitted periodically by the client to prove session continuity.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HeartbeatRequest {
/// The session identifier.
pub session_id: String,
/// The expected hash from the previous heartbeat/initialization step.
pub prev_hash: String,
/// The client's current system timestamp in milliseconds.
pub timestamp: u64,
/// The collected client entropy data (such as mouse events).
pub entropy_data: EntropyData,
/// The final execution state of the client's VM stack program.
pub stack_state: StackState,
/// The client's browser hardware and layout fingerprint.
pub fingerprint: Fingerprint,
/// The mutation step index corresponding to the pending mutation.
pub mutation_step: u64,
/// Hex-encoded commitment of the mutated gene state.
pub gene_commitment: String,
/// Ed25519 signature of the canonical JSON-serialized payload.
pub signature: String,
}
#[derive(Serialize)]
/// Response payload returned by the server for heartbeat submissions.
///
/// In case of silent rejection, all fields except `status` are omitted.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeartbeatResponse {
/// Attestation status, typically "ok" even on silent failures.
pub status: String,
/// The next server-issued salt for hash chain progression.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_salt: Option<String>,
/// The next expected mutation step index.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_mutation_step: Option<u64>,
/// Base64-encoded next mutation program for client gene progression.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_mutation_order_b64: Option<String>,
}
#[derive(Deserialize, Serialize)]
/// Client browser fingerprint metadata used for basic sanity checks.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Fingerprint {
/// Aspect ratio of the client screen.
#[serde(rename = "aspectRatio")]
pub aspect_ratio: String,
/// Device pixel ratio of the screen.
#[serde(rename = "devicePixelRatio")]
pub device_pixel_ratio: String,
/// Number of logical processor cores available.
#[serde(rename = "hardwareConcurrency")]
pub hardware_concurrency: u32,
}
#[derive(Deserialize, Serialize)]
/// Wrapper for browser-side entropy collection.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EntropyData {
/// A chronological list of mouse movement events.
pub events: Vec<MouseEvent>,
}
/// Information about a single mouse movement interaction.
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MouseEvent {
/// Absolute horizontal coordinate of the cursor.
pub x: f64,
/// Absolute vertical coordinate of the cursor.
pub y: f64,
/// Relative timestamp in milliseconds of the event occurrence.
#[serde(rename = "t")]
pub timestamp_ms: f64,
}
/// The state of the VM stack machine after executing a program.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackState {
/// The elements remaining on the stack.
pub stack: Vec<u32>,
/// The final instruction pointer location at program completion or termination.
pub ip: u16,
}
+79
View File
@@ -0,0 +1,79 @@
use crate::protocol::StackState;
/// Executes a raw VM mathematical instruction program bytecode slice.
///
/// This implements the mathematical stack machine interpreter used by the client
/// to generate the attestation stack state.
///
/// # Arguments
/// * `program` - The raw VM instruction program bytecode.
pub fn execute(program: &[u8]) -> StackState {
let mut stack: Vec<u32> = Vec::new();
let mut ip: usize = 0;
while ip < program.len() {
let op = program[ip];
ip += 1;
match op {
0x00 => {
if ip + 4 > program.len() {
break;
}
let val = u32::from_le_bytes([
program[ip],
program[ip + 1],
program[ip + 2],
program[ip + 3],
]);
ip += 4;
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(val);
}
0x01..=0x07 => {
if stack.len() < 2 {
break;
}
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
let r = match op {
0x01 => a.wrapping_add(b),
0x02 => a.wrapping_sub(b),
0x03 => a.wrapping_mul(b),
0x04 => a ^ b,
0x05 => a & b,
0x06 => a | b,
0x07 => a.rotate_left(b % 32),
_ => unreachable!(),
};
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(r);
}
0x08 => {
if stack.is_empty() {
break;
}
let a = stack.pop().unwrap();
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(!a);
}
0x09 => {
let r = crate::hashing::hash_stack(&stack);
stack.clear();
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(r);
}
_ => break,
}
}
StackState {
stack,
ip: ip as u16,
}
}
+903
View File
@@ -0,0 +1,903 @@
use crate::{
constants::{
DEFAULT_MUTATION_ROUNDS, HASH_OPCODE_INSTRUCTION_COST, MAX_GENE_SIZE,
MAX_MUTATION_INSTRUCTION_BUDGET, MAX_MUTATION_PROGRAM_BYTES, SOFT_CAP_DURATION_MS,
},
gene::{
add_env_quantity, get_env_quantity, sub_env_quantity, validate_state, GeneError, GeneState,
},
};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::time::Instant;
// Stack-machine mutation opcodes (v0.6.0).
//
// NOTE: stack effect notation:
// +1 => pushes one u32
// -1 => pops one u32
// 0 => net-zero (or no stack interaction)
//
// Security/performance notes:
// - All index operands are normalized with modulo to avoid panics.
// - Program size is bounded by MAX_MUTATION_PROGRAM_BYTES.
// - Environment arithmetic is saturating and deterministic.
// - Hashing uses fixed BLAKE3 commitment and fixed transcription algorithm.
pub const OP_GENE_LOAD: u8 = 0x23; // +1
pub const OP_GENE_STORE: u8 = 0x24; // -1
pub const OP_MUTATE_POINT: u8 = 0x25; // 0
pub const OP_INSERT: u8 = 0x26; // -1
pub const OP_DELETE: u8 = 0x27; // +1
pub const OP_TRANSCRIBE: u8 = 0x28; // +1
pub const OP_APPLY_MUTAGEN: u8 = 0x29; // -1
pub const OP_FINALIZE_GENE_HASH: u8 = 0x2A; // +1
pub const OP_CONSUME: u8 = 0x2B; // 0 (pop amount, push remaining)
pub const OP_PRODUCE: u8 = 0x2C; // 0 (pop amount, push resulting quantity)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MutationOrder {
pub step: u64,
pub program: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecutionTrace {
pub final_ip: usize,
pub final_stack: Vec<u32>,
pub final_gene_commitment_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MutationError {
ProgramTooLong { len: usize },
TruncatedInstruction { opcode: u8, ip: usize },
UnknownOpcode(u8),
EmptyGene,
StackUnderflow { opcode: u8, ip: usize },
GeneFull { current_len: usize },
Base64(base64::DecodeError),
Gene(GeneError),
}
impl std::fmt::Display for MutationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ProgramTooLong { len } => write!(f, "mutation program too long: {len} bytes"),
Self::TruncatedInstruction { opcode, ip } => {
write!(f, "truncated instruction {opcode:#04x} at ip={ip}")
}
Self::UnknownOpcode(opcode) => write!(f, "unknown mutation opcode: {opcode:#04x}"),
Self::EmptyGene => write!(f, "cannot mutate an empty gene"),
Self::StackUnderflow { opcode, ip } => {
write!(f, "stack underflow in opcode {opcode:#04x} at ip={ip}")
}
Self::GeneFull { current_len } => {
write!(f, "cannot insert; gene already at max size ({current_len})")
}
Self::Base64(err) => write!(f, "invalid base64 mutation order: {err}"),
Self::Gene(err) => write!(f, "{err}"),
}
}
}
impl std::error::Error for MutationError {}
impl From<GeneError> for MutationError {
fn from(value: GeneError) -> Self {
Self::Gene(value)
}
}
/// Encodes a `MutationOrder` into standard Base64 representation of its bytecode.
pub fn encode_order_b64(order: &MutationOrder) -> String {
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &order.program)
}
/// Decodes a `MutationOrder` from its Base64 representation.
///
/// Validates that the decoded program size does not exceed the allowed maximum budget size.
///
/// # Arguments
/// * `step` - The step index associated with this mutation order.
/// * `b64` - The Base64 string containing the raw bytecode.
pub fn decode_order_b64(step: u64, b64: &str) -> Result<MutationOrder, MutationError> {
let program = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
.map_err(MutationError::Base64)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
Ok(MutationOrder { step, program })
}
/// Generates a randomized `MutationOrder` program for a given step and gene size.
///
/// Uses thread-local random number generator.
///
/// # Arguments
/// * `step` - The step index.
/// * `gene_size` - The length of the gene byte buffer.
pub fn generate_order(step: u64, gene_size: usize) -> MutationOrder {
let mut rng = rand::thread_rng();
generate_order_with_rng(&mut rng, step, gene_size)
}
/// Generates a randomized `MutationOrder` program using a specific custom RNG.
///
/// Builds a program containing between 20 and 36 mutation instructions (e.g. loads, point changes,
/// insertions, deletions, env modifications) and ensures a minimum number of finalize hash steps are included.
///
/// # Arguments
/// * `rng` - The random number generator.
/// * `step` - The step index.
/// * `gene_size` - The length of the gene byte buffer.
pub fn generate_order_with_rng<R: Rng + ?Sized>(
rng: &mut R,
step: u64,
gene_size: usize,
) -> MutationOrder {
let mut program = Vec::with_capacity(128);
let mut stack_depth: i32 = 0;
let mut estimated_gene_len = gene_size.clamp(1, MAX_GENE_SIZE);
let ops = rng.gen_range(20usize..=36usize);
let mut hash_ops_needed = rng.gen_range(2..=3);
for idx in 0..ops {
let remaining = ops - idx;
let op = if hash_ops_needed > 0 && remaining <= hash_ops_needed {
OP_FINALIZE_GENE_HASH
} else if stack_depth <= 0 {
rng.gen_range(0u8..3u8)
} else {
match rng.gen_range(0u8..12u8) {
0..=1 => OP_GENE_LOAD,
2..=3 => OP_TRANSCRIBE,
4 => OP_FINALIZE_GENE_HASH,
5 => OP_GENE_STORE,
6 => OP_MUTATE_POINT,
7 => OP_INSERT,
8 => OP_DELETE,
9 => OP_APPLY_MUTAGEN,
10 => OP_CONSUME,
_ => OP_PRODUCE,
}
};
match op {
OP_GENE_LOAD => {
program.push(OP_GENE_LOAD);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth += 1;
}
OP_TRANSCRIBE => {
program.push(OP_TRANSCRIBE);
push_u16(&mut program, rng.r#gen::<u16>());
program.push(rng.gen_range(1u8..=16u8));
stack_depth += 1;
}
OP_FINALIZE_GENE_HASH => {
program.push(OP_FINALIZE_GENE_HASH);
stack_depth += 1;
hash_ops_needed = hash_ops_needed.saturating_sub(1);
}
OP_GENE_STORE => {
if stack_depth > 0 {
program.push(OP_GENE_STORE);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
}
}
OP_MUTATE_POINT => {
program.push(OP_MUTATE_POINT);
push_u16(&mut program, rng.r#gen::<u16>());
program.push(rng.r#gen::<u8>());
}
OP_INSERT => {
if stack_depth > 0 && estimated_gene_len < MAX_GENE_SIZE {
program.push(OP_INSERT);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
estimated_gene_len += 1;
}
}
OP_DELETE => {
program.push(OP_DELETE);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth += 1;
if estimated_gene_len > 1 {
estimated_gene_len -= 1;
}
}
OP_APPLY_MUTAGEN => {
if stack_depth > 0 {
program.push(OP_APPLY_MUTAGEN);
push_u16(&mut program, rng.r#gen::<u16>());
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
}
}
OP_CONSUME => {
if stack_depth > 0 {
program.push(OP_CONSUME);
push_u16(&mut program, rng.r#gen::<u16>());
}
}
OP_PRODUCE if stack_depth > 0 => {
program.push(OP_PRODUCE);
push_u16(&mut program, rng.r#gen::<u16>());
}
_ => {}
}
}
while hash_ops_needed > 0 && program.len() < MAX_MUTATION_PROGRAM_BYTES {
program.push(OP_FINALIZE_GENE_HASH);
hash_ops_needed -= 1;
}
MutationOrder { step, program }
}
/// Clones the `GeneState` and executes the mutation program for `DEFAULT_MUTATION_ROUNDS`.
pub fn apply_program_clone(state: &GeneState, program: &[u8]) -> Result<GeneState, MutationError> {
apply_program_clone_with_rounds(state, program, DEFAULT_MUTATION_ROUNDS)
}
/// Clones the `GeneState` and executes the mutation program for a specific number of rounds.
pub fn apply_program_clone_with_rounds(
state: &GeneState,
program: &[u8],
rounds: u8,
) -> Result<GeneState, MutationError> {
let mut next = state.clone();
execute_program_with_rounds(&mut next, program, rounds)?;
Ok(next)
}
/// Executes the mutation program on the mutable `GeneState` reference for a specific number of rounds.
pub fn apply_program_with_rounds(
state: &mut GeneState,
program: &[u8],
rounds: u8,
) -> Result<(), MutationError> {
let _ = execute_program_with_rounds(state, program, rounds)?;
Ok(())
}
/// Executes the mutation program on the mutable `GeneState` reference for `DEFAULT_MUTATION_ROUNDS`.
pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), MutationError> {
let _ = execute_program_with_rounds(state, program, DEFAULT_MUTATION_ROUNDS)?;
Ok(())
}
/// Executes the mutation program on the mutable `GeneState` reference for multiple rounds.
///
/// Implements a soft instruction cost-budget cap check to prevent hostile/inefficient
/// programs from lagging the host server thread or client runtime.
///
/// # Arguments
/// * `state` - The mutable gene state buffer.
/// * `program` - The raw bytecode sequence.
/// * `rounds` - The requested number of execution rounds.
pub fn execute_program_with_rounds(
state: &mut GeneState,
program: &[u8],
rounds: u8,
) -> Result<ExecutionTrace, MutationError> {
if state.gene.is_empty() {
return Err(MutationError::EmptyGene);
}
validate_state(state)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
let program_cost = estimate_program_cost(program);
let max_rounds = std::cmp::max(1, MAX_MUTATION_INSTRUCTION_BUDGET / program_cost);
let actual_rounds = std::cmp::min(rounds as usize, max_rounds) as u8;
let start = Instant::now();
let mut trace = None;
for _round in 0..actual_rounds {
trace = Some(execute_program(state, program)?);
}
let elapsed = start.elapsed();
tracing::debug!(
rounds = actual_rounds,
requested_rounds = rounds,
elapsed_ms = elapsed.as_millis(),
program_len = program.len(),
"mutation execution"
);
if actual_rounds < rounds {
tracing::debug!(
requested_rounds = rounds,
executed_rounds = actual_rounds,
"mutation soft cap reduced mutation rounds to preserve host responsiveness"
);
}
if elapsed.as_millis() > SOFT_CAP_DURATION_MS {
tracing::debug!(
elapsed_ms = elapsed.as_millis(),
"mutation execution exceeded soft cap duration"
);
}
Ok(trace.unwrap_or_else(|| ExecutionTrace {
final_ip: 0,
final_stack: Vec::new(),
final_gene_commitment_hex: crate::gene::commitment_hex(state),
}))
}
fn estimate_program_cost(program: &[u8]) -> usize {
let mut ip = 0;
let mut cost = 0;
while ip < program.len() {
let opcode = program[ip];
ip += 1;
cost += if opcode == OP_FINALIZE_GENE_HASH {
HASH_OPCODE_INSTRUCTION_COST
} else {
1
};
ip += match opcode {
OP_GENE_LOAD | OP_GENE_STORE | OP_INSERT | OP_DELETE | OP_CONSUME | OP_PRODUCE => 2,
OP_MUTATE_POINT | OP_TRANSCRIBE => 3,
OP_APPLY_MUTAGEN => 4,
OP_FINALIZE_GENE_HASH => 0,
_ => 0,
}
}
cost.max(1)
}
/// Executes the VM mutation program on the mutable `GeneState` reference.
///
/// This interprets VM mutation opcodes to modify the gene byte array and environment records.
///
/// # Arguments
/// * `state` - The mutable gene state to mutate.
/// * `program` - The raw instruction bytecode slice.
pub fn execute_program(
state: &mut GeneState,
program: &[u8],
) -> Result<ExecutionTrace, MutationError> {
if state.gene.is_empty() {
return Err(MutationError::EmptyGene);
}
validate_state(state)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
let mut ip = 0usize;
let mut stack: Vec<u32> = Vec::with_capacity(16);
while ip < program.len() {
let opcode_ip = ip;
let opcode = take_u8(program, &mut ip, 0x00)?;
match opcode {
OP_GENE_LOAD => {
let idx = take_u16(program, &mut ip, opcode)?;
let normalized = normalize_index(idx as usize, state.gene.len());
stack.push(state.gene[normalized] as u32);
}
OP_GENE_STORE => {
let idx = take_u16(program, &mut ip, opcode)?;
let value = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
let normalized = normalize_index(idx as usize, state.gene.len());
state.gene[normalized] = value;
}
OP_MUTATE_POINT => {
let idx = take_u16(program, &mut ip, opcode)?;
let delta = take_u8(program, &mut ip, opcode)? as i8;
let normalized = normalize_index(idx as usize, state.gene.len());
state.gene[normalized] = state.gene[normalized].wrapping_add(delta as u8);
}
OP_INSERT => {
let idx = take_u16(program, &mut ip, opcode)?;
let value = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
if state.gene.len() >= MAX_GENE_SIZE {
return Err(MutationError::GeneFull {
current_len: state.gene.len(),
});
}
let insert_at = (idx as usize).min(state.gene.len());
state.gene.insert(insert_at, value);
}
OP_DELETE => {
let idx = take_u16(program, &mut ip, opcode)?;
let normalized = normalize_index(idx as usize, state.gene.len());
let removed = if state.gene.len() > 1 {
state.gene.remove(normalized)
} else {
let prev = state.gene[0];
state.gene[0] = 0;
prev
};
stack.push(removed as u32);
}
OP_TRANSCRIBE => {
let start = take_u16(program, &mut ip, opcode)?;
let span = take_u8(program, &mut ip, opcode)?;
let transcription = transcribe_window(&state.gene, start as usize, span);
stack.push(transcription);
}
OP_APPLY_MUTAGEN => {
let symbol = take_u16(program, &mut ip, opcode)?;
let idx = take_u16(program, &mut ip, opcode)?;
let stack_mask = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
let quantity = get_env_quantity(state, symbol);
let mix = ((quantity as u8)
^ ((quantity >> 8) as u8)
^ ((quantity >> 16) as u8)
^ ((quantity >> 24) as u8))
^ ((symbol & 0x00ff) as u8)
^ ((symbol >> 8) as u8)
^ stack_mask;
let normalized = normalize_index(idx as usize, state.gene.len());
state.gene[normalized] ^= mix;
}
OP_FINALIZE_GENE_HASH => {
let commit = crate::gene::commitment(state);
let hash32 = u32::from_le_bytes([commit[0], commit[1], commit[2], commit[3]]);
stack.push(hash32);
}
OP_CONSUME => {
let symbol = take_u16(program, &mut ip, opcode)?;
let amount = pop_stack(&mut stack, opcode, opcode_ip)?;
let left = sub_env_quantity(state, symbol, amount)?;
stack.push(left);
}
OP_PRODUCE => {
let symbol = take_u16(program, &mut ip, opcode)?;
let amount = pop_stack(&mut stack, opcode, opcode_ip)?;
let next = add_env_quantity(state, symbol, amount)?;
stack.push(next);
}
_ => return Err(MutationError::UnknownOpcode(opcode)),
}
}
Ok(ExecutionTrace {
final_ip: ip,
final_stack: stack,
final_gene_commitment_hex: crate::gene::commitment_hex(state),
})
}
fn transcribe_window(gene: &[u8], start: usize, span: u8) -> u32 {
let count = usize::from(span.max(1));
let mut acc = 2_166_136_261u32; // FNV offset basis
for i in 0..count {
let idx = (start + i) % gene.len();
acc ^= gene[idx] as u32;
acc = acc.wrapping_mul(16_777_619); // FNV prime
}
acc
}
fn push_u16(buf: &mut Vec<u8>, value: u16) {
buf.extend_from_slice(&value.to_le_bytes());
}
fn take_u8(bytes: &[u8], ip: &mut usize, opcode: u8) -> Result<u8, MutationError> {
if *ip >= bytes.len() {
return Err(MutationError::TruncatedInstruction { opcode, ip: *ip });
}
let value = bytes[*ip];
*ip += 1;
Ok(value)
}
fn take_u16(bytes: &[u8], ip: &mut usize, opcode: u8) -> Result<u16, MutationError> {
if *ip + 2 > bytes.len() {
return Err(MutationError::TruncatedInstruction { opcode, ip: *ip });
}
let value = u16::from_le_bytes([bytes[*ip], bytes[*ip + 1]]);
*ip += 2;
Ok(value)
}
fn pop_stack(stack: &mut Vec<u32>, opcode: u8, ip: usize) -> Result<u32, MutationError> {
stack
.pop()
.ok_or(MutationError::StackUnderflow { opcode, ip })
}
fn normalize_index(idx: usize, len: usize) -> usize {
idx % len
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gene::{commitment, new_state, set_env_quantity};
use rand::{Rng, SeedableRng};
use std::time::Instant;
fn u16_bytes(v: u16) -> [u8; 2] {
v.to_le_bytes()
}
#[test]
fn test_opcode_gene_load() {
let mut state = new_state(4).unwrap();
state.gene = vec![10, 20, 30, 40];
let trace = execute_program(&mut state, &[OP_GENE_LOAD, 1, 0]).unwrap();
assert_eq!(trace.final_stack, vec![20]);
}
#[test]
fn test_opcode_gene_store() {
let mut state = new_state(4).unwrap();
state.gene = vec![1, 2, 3, 4];
let program = vec![
OP_GENE_LOAD,
0,
0, // stack: [1]
OP_GENE_STORE,
2,
0, // gene[2] <- 1
];
execute_program(&mut state, &program).unwrap();
assert_eq!(state.gene, vec![1, 2, 1, 4]);
}
#[test]
fn test_opcode_mutate_point() {
let mut state = new_state(4).unwrap();
state.gene[0] = 200;
let program = vec![OP_MUTATE_POINT, 0, 0, 100u8];
execute_program(&mut state, &program).unwrap();
assert_eq!(state.gene[0], 44);
}
#[test]
fn test_opcode_insert() {
let mut state = new_state(3).unwrap();
state.gene = vec![10, 20, 30];
let program = vec![
OP_GENE_LOAD,
1,
0, // stack: [20]
OP_INSERT,
0,
0, // insert 20 at position 0
];
execute_program(&mut state, &program).unwrap();
assert_eq!(state.gene, vec![20, 10, 20, 30]);
}
#[test]
fn test_opcode_delete() {
let mut state = new_state(4).unwrap();
state.gene = vec![9, 8, 7, 6];
let trace = execute_program(&mut state, &[OP_DELETE, 2, 0]).unwrap();
assert_eq!(state.gene, vec![9, 8, 6]);
assert_eq!(trace.final_stack, vec![7]);
}
#[test]
fn test_opcode_transcribe() {
let mut state = new_state(5).unwrap();
state.gene = vec![1, 2, 3, 4, 5];
let trace = execute_program(&mut state, &[OP_TRANSCRIBE, 1, 0, 3]).unwrap();
assert_eq!(trace.final_stack.len(), 1);
assert_ne!(trace.final_stack[0], 0);
}
#[test]
fn test_opcode_apply_mutagen() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 7, 0x1234_5678).unwrap();
state.gene[1] = 0xAA;
let program = vec![
OP_GENE_LOAD,
0,
0, // stack mask source
OP_APPLY_MUTAGEN,
7,
0,
1,
0,
];
execute_program(&mut state, &program).unwrap();
assert_ne!(state.gene[1], 0xAA);
}
#[test]
fn test_opcode_finalize_gene_hash() {
let mut state = new_state(4).unwrap();
let trace = execute_program(&mut state, &[OP_FINALIZE_GENE_HASH]).unwrap();
assert_eq!(trace.final_stack.len(), 1);
}
#[test]
fn test_opcode_consume() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 3, 100).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0, // stack = [0]
OP_MUTATE_POINT,
0,
0,
15, // gene[0]=15
OP_GENE_LOAD,
0,
0, // stack=[0,15]
OP_CONSUME,
3,
0, // consume 15
];
let trace = execute_program(&mut state, &program).unwrap();
assert_eq!(get_env_quantity(&state, 3), 85);
assert_eq!(trace.final_stack.last().copied().unwrap(), 85);
}
#[test]
fn test_opcode_produce() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 9, 5).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0, // stack [0]
OP_MUTATE_POINT,
0,
0,
10, // gene[0]=10
OP_GENE_LOAD,
0,
0, // stack [0,10]
OP_PRODUCE,
9,
0, // +10
];
let trace = execute_program(&mut state, &program).unwrap();
assert_eq!(get_env_quantity(&state, 9), 15);
assert_eq!(trace.final_stack.last().copied().unwrap(), 15);
}
#[test]
fn test_zero_length_gene_is_rejected() {
let mut state = GeneState {
gene: vec![],
environment: vec![],
};
let err = execute_program(&mut state, &[OP_FINALIZE_GENE_HASH]).unwrap_err();
assert_eq!(err, MutationError::EmptyGene);
}
#[test]
fn test_insert_rejects_max_size_gene() {
let mut state = new_state(MAX_GENE_SIZE).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0, // push value
OP_INSERT,
0,
0,
];
let err = execute_program(&mut state, &program).unwrap_err();
assert!(matches!(err, MutationError::GeneFull { .. }));
}
#[test]
fn test_invalid_positions_wrap_deterministically() {
let mut state_a = new_state(5).unwrap();
let mut state_b = new_state(5).unwrap();
let max_u16 = u16::MAX;
let [a0, a1] = u16_bytes(max_u16);
let program = vec![OP_MUTATE_POINT, a0, a1, 1];
execute_program(&mut state_a, &program).unwrap();
let wrapped = (max_u16 as usize % 5) as u16;
let [w0, w1] = u16_bytes(wrapped);
let wrapped_program = vec![OP_MUTATE_POINT, w0, w1, 1];
execute_program(&mut state_b, &wrapped_program).unwrap();
assert_eq!(state_a, state_b);
}
#[test]
fn test_quantity_underflow_is_saturating() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 1, 3).unwrap();
state.gene[0] = 8;
let program = vec![
OP_GENE_LOAD,
0,
0, // 8
OP_CONSUME,
1,
0, // consume 8 from qty 3 => 0
];
let trace = execute_program(&mut state, &program).unwrap();
assert_eq!(get_env_quantity(&state, 1), 0);
assert_eq!(trace.final_stack.last().copied().unwrap(), 0);
}
#[test]
fn test_rejects_unknown_opcode() {
let mut state = new_state(8).unwrap();
let err = execute_program(&mut state, &[0xFF]).unwrap_err();
assert_eq!(err, MutationError::UnknownOpcode(0xFF));
}
#[test]
fn test_rejects_truncated_instruction() {
let mut state = new_state(8).unwrap();
let err = execute_program(&mut state, &[OP_GENE_LOAD, 1]).unwrap_err();
assert!(matches!(err, MutationError::TruncatedInstruction { .. }));
}
#[test]
fn test_rejects_stack_underflow() {
let mut state = new_state(8).unwrap();
let err = execute_program(&mut state, &[OP_GENE_STORE, 0, 0]).unwrap_err();
assert!(matches!(err, MutationError::StackUnderflow { .. }));
}
#[test]
fn test_base64_order_roundtrip() {
let order = MutationOrder {
step: 17,
program: vec![OP_GENE_LOAD, 1, 0, OP_GENE_STORE, 2, 0],
};
let b64 = encode_order_b64(&order);
let decoded = decode_order_b64(order.step, &b64).unwrap();
assert_eq!(decoded, order);
}
#[test]
fn test_generate_order_is_deterministic_for_seeded_rng() {
let mut rng_a = rand::rngs::StdRng::seed_from_u64(99);
let mut rng_b = rand::rngs::StdRng::seed_from_u64(99);
let order_a = generate_order_with_rng(&mut rng_a, 5, 64);
let order_b = generate_order_with_rng(&mut rng_b, 5, 64);
assert_eq!(order_a, order_b);
}
#[test]
fn test_mutation_chain() {
let mut server_state = new_state(32).unwrap();
let mut client_state = new_state(32).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0,
OP_PRODUCE,
2,
0, // env[2]+=gene[0]
OP_GENE_LOAD,
1,
0,
OP_APPLY_MUTAGEN,
2,
0,
1,
0, // mutagen at idx1
OP_TRANSCRIBE,
0,
0,
8, // hash window
OP_GENE_STORE,
2,
0, // gene[2]=transcription_low_byte
OP_DELETE,
0,
0, // stack pushes removed
OP_INSERT,
3,
0, // insert removed at position 3
OP_FINALIZE_GENE_HASH,
];
let server_trace = execute_program(&mut server_state, &program).unwrap();
let client_trace = execute_program(&mut client_state, &program).unwrap();
assert_eq!(server_state, client_state);
assert_eq!(server_trace.final_stack, client_trace.final_stack);
assert_eq!(
server_trace.final_gene_commitment_hex,
client_trace.final_gene_commitment_hex
);
}
#[test]
fn test_server_client_parity_across_random_orders() {
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
for step in 0..128u64 {
let order = generate_order_with_rng(&mut rng, step, 128);
let mut server_state = new_state(128).unwrap();
let mut client_state = new_state(128).unwrap();
let server_result = execute_program(&mut server_state, &order.program);
let client_result = execute_program(&mut client_state, &order.program);
assert_eq!(server_result.is_ok(), client_result.is_ok());
match (server_result, client_result) {
(Ok(server_trace), Ok(client_trace)) => {
assert_eq!(server_state, client_state);
assert_eq!(server_trace.final_stack, client_trace.final_stack);
assert_eq!(
commitment(&server_state),
commitment(&client_state),
"step {step}"
);
}
(Err(a), Err(b)) => assert_eq!(a.to_string(), b.to_string()),
_ => unreachable!(),
}
}
}
#[test]
fn test_fuzz_style_random_program_bytes_do_not_diverge() {
let mut rng = rand::rngs::StdRng::seed_from_u64(2026);
for _ in 0..256 {
let len = rng.gen_range(1usize..=MAX_MUTATION_PROGRAM_BYTES);
let mut program = vec![0u8; len];
for b in &mut program {
*b = rng.r#gen::<u8>();
}
let mut a = new_state(64).unwrap();
let mut b = new_state(64).unwrap();
let ra = execute_program(&mut a, &program);
let rb = execute_program(&mut b, &program);
assert_eq!(ra.is_ok(), rb.is_ok());
if ra.is_ok() {
assert_eq!(a, b);
}
}
}
#[test]
fn test_performance_smoke_mutation_execution() {
let mut rng = rand::rngs::StdRng::seed_from_u64(11);
let mut programs = Vec::new();
for step in 0..200u64 {
programs.push(generate_order_with_rng(&mut rng, step + 1, 512).program);
}
let start = Instant::now();
let mut state = new_state(512).unwrap();
for program in &programs {
let _ = execute_program(&mut state, program);
}
let elapsed = start.elapsed();
// Wide bound for CI variability; this is a regression guard, not a strict benchmark.
assert!(
elapsed.as_secs_f64() < 2.0,
"mutation execution too slow: {elapsed:?}"
);
}
#[test]
fn test_vm_instruction_budget_soft_cap() {
let state = new_state(8).unwrap();
// Construct a program with 130 OP_FINALIZE_GENE_HASH instructions.
// HASH has HASH_OPCODE_INSTRUCTION_COST = 16.
// Total cost will be 130 * 16 = 2080, which exceeds MAX_MUTATION_INSTRUCTION_BUDGET (2048).
let program = vec![OP_FINALIZE_GENE_HASH; 130];
let cost = estimate_program_cost(&program);
assert!(cost >= 2080);
// Assert that the max allowed rounds is calculated as 1 since cost > budget.
let expected_rounds = std::cmp::max(1, MAX_MUTATION_INSTRUCTION_BUDGET / cost);
assert_eq!(expected_rounds, 1);
// Execute the program with a requested 10 rounds.
// The runtime should execute it successfully without panic, while applying the round limitation.
let mut test_state = state.clone();
let trace = execute_program_with_rounds(&mut test_state, &program, 10).unwrap();
assert_eq!(trace.final_ip, program.len());
}
}
+17
View File
@@ -0,0 +1,17 @@
use proptest::prelude::*;
proptest! {
#[test]
fn test_vm_execute_never_panics(ref program in any::<Vec<u8>>()) {
// VM execution should be totally robust and never panic on any random input stream.
let state = shared::vm::execute(program);
// The instruction pointer (ip) should not exceed the program length
assert!(state.ip as usize <= program.len());
}
#[test]
fn test_gene_environment_roundtrip_never_panics(ref data in any::<Vec<u8>>()) {
// Try to decode random bytes. It should either succeed or fail gracefully, never panic.
let _ = shared::gene::decode_environment(data);
}
}
+3 -2
View File
@@ -1,10 +1,10 @@
[package]
name = "chronoseal-wasm"
version = "0.2.0"
version = "1.0.1"
edition = "2021"
[lib]
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]
[dependencies]
shared = { path = "../shared" }
@@ -18,3 +18,4 @@ getrandom = { version = "0.2", features = ["js"] }
hex = "0.4"
base64 = "0.22"
serde-wasm-bindgen = "0.6"
tracing = "0.1"
+9 -5
View File
@@ -3,7 +3,7 @@ use std::cell::RefCell;
use wasm_bindgen::prelude::*;
thread_local! {
static KEYPAIR: RefCell<Option<SigningKey>> = RefCell::new(None);
static KEYPAIR: RefCell<Option<SigningKey>> = const { RefCell::new(None) };
}
#[wasm_bindgen]
@@ -51,10 +51,14 @@ pub fn compute_next_hash(
) -> String {
let prev = hex::decode(prev_hash_hex).unwrap_or_default();
let salt = hex::decode(salt_hex).unwrap_or_default();
let entropy =
serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json).unwrap();
let stack =
serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap();
let entropy = match serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json) {
Ok(v) => v,
Err(_) => return String::new(),
};
let stack = match serde_json::from_str::<shared::protocol::StackState>(stack_state_json) {
Ok(v) => v,
Err(_) => return String::new(),
};
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
hex::encode(new)
}
+1
View File
@@ -4,3 +4,4 @@ pub mod entropy;
pub mod fingerprint;
pub mod transport;
pub mod vm;
pub mod vm_extensions;
+137 -40
View File
@@ -1,55 +1,152 @@
use wasm_bindgen::prelude::*;
use shared::protocol::StackState;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn run_program(program_b64: &str) -> JsValue {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD.decode(program_b64).unwrap();
let bytes = match base64::engine::general_purpose::STANDARD.decode(program_b64) {
Ok(v) => v,
Err(_) => return JsValue::NULL,
};
let state = execute(&bytes);
serde_wasm_bindgen::to_value(&state).unwrap()
match serde_wasm_bindgen::to_value(&state) {
Ok(v) => v,
Err(_) => JsValue::NULL,
}
}
fn execute(program: &[u8]) -> StackState {
let mut stack: Vec<u32> = Vec::new();
let mut ip: usize = 0;
while ip < program.len() {
let op = program[ip];
ip += 1;
match op {
0x00 => {
if ip + 4 > program.len() { break; }
let val = u32::from_le_bytes([program[ip], program[ip+1], program[ip+2], program[ip+3]]);
ip += 4;
stack.push(val);
shared::vm::execute(program)
}
0x01..=0x07 => {
if stack.len() < 2 { break; }
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
let r = match op {
0x01 => a.wrapping_add(b),
0x02 => a.wrapping_sub(b),
0x03 => a.wrapping_mul(b),
0x04 => a ^ b,
0x05 => a & b,
0x06 => a | b,
0x07 => a.rotate_left(b % 32),
_ => unreachable!(),
};
stack.push(r);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_push() {
// PUSH 42, PUSH 100
let program = vec![0x00, 42, 0, 0, 0, 0x00, 100, 0, 0, 0];
let state = execute(&program);
assert_eq!(state.stack, vec![42, 100]);
assert_eq!(state.ip, 10);
}
0x08 => {
if stack.is_empty() { break; }
let a = stack.pop().unwrap();
stack.push(!a);
#[test]
fn test_add() {
// PUSH 5, PUSH 10, ADD
let program = vec![0x00, 5, 0, 0, 0, 0x00, 10, 0, 0, 0, 0x01];
let state = execute(&program);
assert_eq!(state.stack, vec![15]);
}
0x09 => {
let r = shared::hashing::hash_stack(&stack);
stack.clear();
stack.push(r);
#[test]
fn test_add_wrapping() {
// PUSH u32::MAX, PUSH 1, ADD
let program = vec![0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 1, 0, 0, 0, 0x01];
let state = execute(&program);
assert_eq!(state.stack, vec![0]);
}
_ => break,
#[test]
fn test_sub() {
// PUSH 20, PUSH 7, SUB
let program = vec![0x00, 20, 0, 0, 0, 0x00, 7, 0, 0, 0, 0x02];
let state = execute(&program);
assert_eq!(state.stack, vec![13]);
}
#[test]
fn test_sub_wrapping() {
// PUSH 0, PUSH 1, SUB
let program = vec![0x00, 0, 0, 0, 0, 0x00, 1, 0, 0, 0, 0x02];
let state = execute(&program);
assert_eq!(state.stack, vec![u32::MAX]);
}
#[test]
fn test_mul() {
// PUSH 6, PUSH 7, MUL
let program = vec![0x00, 6, 0, 0, 0, 0x00, 7, 0, 0, 0, 0x03];
let state = execute(&program);
assert_eq!(state.stack, vec![42]);
}
#[test]
fn test_xor() {
// PUSH 0b1010, PUSH 0b1100, XOR
let program = vec![0x00, 0b1010, 0, 0, 0, 0x00, 0b1100, 0, 0, 0, 0x04];
let state = execute(&program);
assert_eq!(state.stack, vec![0b0110]);
}
#[test]
fn test_and() {
// PUSH 0b1010, PUSH 0b1100, AND
let program = vec![0x00, 0b1010, 0, 0, 0, 0x00, 0b1100, 0, 0, 0, 0x05];
let state = execute(&program);
assert_eq!(state.stack, vec![0b1000]);
}
#[test]
fn test_or() {
// PUSH 0b1010, PUSH 0b1100, OR
let program = vec![0x00, 0b1010, 0, 0, 0, 0x00, 0b1100, 0, 0, 0, 0x06];
let state = execute(&program);
assert_eq!(state.stack, vec![0b1110]);
}
#[test]
fn test_rot() {
// PUSH 1, PUSH 4, ROT
let program = vec![0x00, 1, 0, 0, 0, 0x00, 4, 0, 0, 0, 0x07];
let state = execute(&program);
assert_eq!(state.stack, vec![16]);
}
#[test]
fn test_not() {
// PUSH 0, NOT
let program = vec![0x00, 0, 0, 0, 0, 0x08];
let state = execute(&program);
assert_eq!(state.stack, vec![u32::MAX]);
}
#[test]
fn test_hash() {
// PUSH 10, PUSH 20, HASH
let program = vec![0x00, 10, 0, 0, 0, 0x00, 20, 0, 0, 0, 0x09];
let state = execute(&program);
assert_eq!(state.stack.len(), 1);
let expected_hash = shared::hashing::hash_stack(&[10, 20]);
assert_eq!(state.stack[0], expected_hash);
}
#[test]
fn test_underflow_binary() {
// PUSH 42, ADD (needs 2 values, only 1 on stack)
let program = vec![0x00, 42, 0, 0, 0, 0x01];
let state = execute(&program);
// ADD breaks when stack.len() < 2, stack has 42 left, ip is at the opcode ADD (6)
assert_eq!(state.stack, vec![42]);
assert_eq!(state.ip, 6);
}
#[test]
fn test_underflow_unary() {
// NOT (needs 1 value, empty stack)
let program = vec![0x08];
let state = execute(&program);
assert_eq!(state.stack, Vec::<u32>::new());
assert_eq!(state.ip, 1);
}
#[test]
fn test_incomplete_push() {
// PUSH opcode, but only 2 bytes instead of 4
let program = vec![0x00, 42, 0];
let state = execute(&program);
assert_eq!(state.stack, Vec::<u32>::new());
assert_eq!(state.ip, 1); // execution stopped at op 0x00 because ip + 4 > program.len()
}
}
StackState { stack, ip: ip as u16 }
}
+226
View File
@@ -0,0 +1,226 @@
use std::cell::RefCell;
use std::time::Instant;
use wasm_bindgen::prelude::*;
thread_local! {
static GENE_STATE: RefCell<Option<shared::gene::GeneState>> = const { RefCell::new(None) };
static PREVIEW_STATE: RefCell<Option<shared::gene::GeneState>> = const { RefCell::new(None) };
}
#[wasm_bindgen]
pub fn init_gene_state(gene_size: u32) -> bool {
let Ok(state) = shared::gene::new_state(gene_size as usize) else {
return false;
};
GENE_STATE.with(|slot| *slot.borrow_mut() = Some(state));
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = None);
true
}
#[wasm_bindgen]
pub fn preview_gene_commitment(
order_b64: &str,
session_id: &str,
mutation_step: u64,
rounds: u8,
) -> String {
let order = match shared::vm_extensions::decode_order_b64(mutation_step, order_b64) {
Ok(order) => order,
Err(_) => return String::new(),
};
let start = Instant::now();
let candidate = GENE_STATE.with(|slot| {
let state = slot.borrow();
let current = state.as_ref()?;
shared::vm_extensions::apply_program_clone_with_rounds(
current,
&order.program,
if rounds == 0 {
shared::constants::DEFAULT_MUTATION_ROUNDS
} else {
rounds
},
)
.ok()
});
let elapsed = start.elapsed();
tracing::debug!(session_id = %session_id, mutation_step = mutation_step, elapsed_ms = elapsed.as_millis(), "wasm mutation preview execution");
let Some(candidate) = candidate else {
return String::new();
};
let commitment =
shared::gene::commitment_hex_with_context(&candidate, session_id, mutation_step);
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = Some(candidate));
commitment
}
#[wasm_bindgen]
pub fn commit_gene_preview() -> bool {
let next = PREVIEW_STATE.with(|slot| slot.borrow_mut().take());
let Some(next) = next else {
return false;
};
GENE_STATE.with(|slot| *slot.borrow_mut() = Some(next));
true
}
#[wasm_bindgen]
pub fn discard_gene_preview() {
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = None);
}
#[wasm_bindgen]
pub fn current_gene_commitment(session_id: &str, mutation_step: u64) -> String {
GENE_STATE.with(|slot| {
slot.borrow()
.as_ref()
.map(|state| {
shared::gene::commitment_hex_with_context(state, session_id, mutation_step)
})
.unwrap_or_default()
})
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
fn order_b64(program: Vec<u8>) -> String {
let order = shared::vm_extensions::MutationOrder { step: 1, program };
shared::vm_extensions::encode_order_b64(&order)
}
#[test]
fn test_init_gene_state_success() {
assert!(init_gene_state(64));
let commitment = current_gene_commitment("deadbeef", 1);
assert_eq!(commitment.len(), 64);
}
#[test]
fn test_init_gene_state_rejects_zero() {
assert!(!init_gene_state(0));
}
#[test]
fn test_preview_requires_initialized_state() {
discard_gene_preview();
GENE_STATE.with(|slot| *slot.borrow_mut() = None);
let c = preview_gene_commitment(
&order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 1]),
"deadbeef",
1,
0,
);
assert!(c.is_empty());
}
#[test]
fn test_preview_rejects_invalid_order() {
init_gene_state(16);
let c = preview_gene_commitment("***bad-base64***", "deadbeef", 1, 0);
assert!(c.is_empty());
}
#[test]
fn test_commit_applies_preview() {
init_gene_state(16);
let before = current_gene_commitment("deadbeef", 1);
let order = order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 1]);
let preview = preview_gene_commitment(&order, "deadbeef", 1, 0);
assert_ne!(preview, before);
assert!(commit_gene_preview());
let after = current_gene_commitment("deadbeef", 1);
assert_eq!(preview, after);
}
#[test]
fn test_discard_preview_keeps_committed_state() {
init_gene_state(16);
let before = current_gene_commitment("deadbeef", 1);
let order = order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 0xFF]);
let preview = preview_gene_commitment(&order, "deadbeef", 1, 0);
assert_ne!(preview, before);
discard_gene_preview();
let after = current_gene_commitment("deadbeef", 1);
assert_eq!(before, after);
}
#[test]
fn test_commit_without_preview_returns_false() {
init_gene_state(16);
discard_gene_preview();
assert!(!commit_gene_preview());
}
#[test]
fn test_preview_commitment_matches_shared_engine() {
init_gene_state(16);
let order = shared::vm_extensions::MutationOrder {
step: 3,
program: vec![
shared::vm_extensions::OP_GENE_LOAD,
0,
0,
shared::vm_extensions::OP_PRODUCE,
1,
0,
shared::vm_extensions::OP_GENE_LOAD,
2,
0,
shared::vm_extensions::OP_APPLY_MUTAGEN,
1,
0,
2,
0,
],
};
let b64 = shared::vm_extensions::encode_order_b64(&order);
let preview = preview_gene_commitment(&b64, "deadbeef", 3, 0);
let mut expected = shared::gene::new_state(16).unwrap();
shared::vm_extensions::apply_program_with_rounds(
&mut expected,
&order.program,
shared::constants::DEFAULT_MUTATION_ROUNDS,
)
.unwrap();
assert_eq!(
preview,
shared::gene::commitment_hex_with_context(&expected, "deadbeef", 3)
);
}
#[test]
fn test_table_driven_parity_across_many_generated_orders() {
init_gene_state(64);
let mut rng = rand::rngs::StdRng::seed_from_u64(123);
let mut expected = shared::gene::new_state(64).unwrap();
for step in 0..24u64 {
let order = shared::vm_extensions::generate_order_with_rng(&mut rng, step + 1, 64);
let b64 = shared::vm_extensions::encode_order_b64(&order);
let preview = preview_gene_commitment(&b64, "deadbeef", step + 1, 0);
shared::vm_extensions::apply_program_with_rounds(
&mut expected,
&order.program,
shared::constants::DEFAULT_MUTATION_ROUNDS,
)
.unwrap();
let expected_commitment =
shared::gene::commitment_hex_with_context(&expected, "deadbeef", step + 1);
assert_eq!(preview, expected_commitment);
assert!(commit_gene_preview());
assert_eq!(
current_gene_commitment("deadbeef", step + 1),
expected_commitment
);
}
}
}
+403
View File
@@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>API Reference | ChronoSeal Documentation</title>
<meta name="description" content="ChronoSeal HTTP API Reference, documenting endpoints, JSON request-response shapes, and WASM exports.">
<link rel="stylesheet" href="css/chronoseal.css">
<link rel="stylesheet" href="css/docs.css">
<link rel="stylesheet" href="css/print.css" media="print">
<link rel="manifest" href="site.webmanifest">
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
<!-- Font Awesome for Icons -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
</head>
<body>
<!-- Floating Background Spheres -->
<div class="bg-animation">
<div class="gradient-sphere sphere-1"></div>
<div class="gradient-sphere sphere-2"></div>
<div class="gradient-sphere sphere-3"></div>
</div>
<!-- Header / Navbar -->
<nav class="navbar" id="navbar">
<div class="nav-container">
<a href="index.html" class="logo">
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
<span>ChronoSeal</span>
</a>
<div class="nav-links" id="navLinks">
<a href="philosophy.html">Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="comparison.html">Comparison</a>
<div class="nav-dropdown">
<a href="#" class="nav-link">More <small>▼</small></a>
<div class="nav-dropdown-menu">
<a href="api.html" class="active">API Reference</a>
<a href="deployment.html">Deployment Guide</a>
<a href="operations.html">Operations Guide</a>
<a href="testing.html">Testing Strategy</a>
<a href="performance.html">Performance Tuning</a>
<a href="threat-model.html">Threat Model</a>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
</div>
</div>
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</button>
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
<i class="fab fa-github"></i> GitHub
</a>
</div>
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
<i class="fas fa-bars"></i>
</button>
</div>
</nav>
<!-- Doc Page Shell -->
<div class="doc-page">
<div class="doc-layout">
<!-- Sidebar -->
<aside class="doc-sidebar">
<button class="doc-sidebar-toggle">
<i class="fas fa-bars"></i> Sidebar Menu
</button>
<div class="doc-nav-group">
<div class="doc-nav-label">Core Concepts</div>
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
<a href="architecture.html" class="doc-nav-item">Architecture Overview</a>
<a href="comparison.html" class="doc-nav-item">ChronoSeal vs Others</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Protocol &amp; API</div>
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
<a href="api.html" class="doc-nav-item active">API Reference</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Operations &amp; Security</div>
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
<a href="security.html" class="doc-nav-item">Security Policy</a>
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Lifecycle &amp; Dev</div>
<a href="deployment.html" class="doc-nav-item">Deployment Guide</a>
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="doc-main">
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
<a href="index.html">Home</a>
<span class="sep">/</span>
<a href="#">Protocol &amp; API</a>
<span class="sep">/</span>
<span>API Reference</span>
</nav>
<article class="doc-content">
<h1>ChronoSeal API Reference</h1>
<p class="doc-subtitle">ChronoSeal exposes a lightweight HTTP API for session handshakes, heartbeat attestation verification, metrics, and statistics.</p>
<hr>
<h2>Endpoint Summary</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Core Purpose</th>
<th>Content Type</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>POST</code></td>
<td><code>/init</code></td>
<td>Create a new attestation session</td>
<td><code>application/json</code></td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/hb</code></td>
<td>Submit and verify a signed heartbeat</td>
<td><code>application/json</code></td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/health</code></td>
<td>Check daemon operational health</td>
<td><code>application/json</code></td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/stats</code></td>
<td>Get runtime database statistics</td>
<td><code>application/json</code></td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/metrics</code></td>
<td>Prometheus-compatible scraping metrics</td>
<td><code>text/plain</code></td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/</code></td>
<td>Serve static integration files</td>
<td>HTML / JS / WASM</td>
</tr>
</tbody>
</table>
<h2>Data Encoding Formats</h2>
<ul>
<li><strong>Keys &amp; Signatures:</strong> Ed25519 public keys represent 64 hex characters (32 bytes). Signatures represent 128 hex characters (64 bytes).</li>
<li><strong>Hashes:</strong> Blake3 digests represent 64 hex characters (32 bytes).</li>
<li><strong>Salts:</strong> Random seeds represented as 32 hex characters (16 bytes).</li>
<li><strong>Timestamps:</strong> Integer epoch milliseconds.</li>
<li><strong>Programs:</strong> Base64-encoded strings (representing VM opcodes or mutation steps).</li>
</ul>
<h2><code>POST /init</code></h2>
<p>Registers the browser public key and initiates the session tracker.</p>
<h3>Request Payload</h3>
<div class="code-block">
<div class="code-header">
<span class="code-lang">JSON Request</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>{
"public_key": "24a1b0cd982fecba45...64 hex chars"
}</code></pre>
</div>
<h3>Successful Response (200 OK)</h3>
<div class="code-block">
<div class="code-header">
<span class="code-lang">JSON Response</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>{
"session_id": "8902abc345def678...64 hex chars",
"salt": "f51278ba...32 hex chars",
"opcodes_b64": "AQAFAwEG...",
"initial_hash": "23ab89c0...64 hex chars",
"expires_at": 1782390482000,
"heartbeat_min_interval_ms": 12000,
"heartbeat_max_interval_ms": 25000,
"gene_size": 512,
"mutation_step": 1,
"mutation_order_b64": "YWJjZGVm..."
}</code></pre>
</div>
<h2><code>POST /hb</code></h2>
<p>Verifies client compliance for the current step and rolls over session parameters.</p>
<h3>Request Payload</h3>
<div class="code-block">
<div class="code-header">
<span class="code-lang">JSON Request</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>{
"session_id": "8902abc345def678...64 hex chars",
"prev_hash": "23ab89c0...64 hex chars",
"timestamp": 1782390494000,
"entropy_data": {
"events": [
{ "x": 124.5, "y": 308.2, "t": 120.4 }
]
},
"stack_state": {
"stack": [108429, 3902],
"ip": 12
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"mutation_step": 1,
"gene_commitment": "56ab12cd...64 hex chars",
"signature": "ab0921cd56ef...128 hex chars"
}</code></pre>
</div>
<h3>Accepted Response</h3>
<div class="code-block">
<div class="code-header">
<span class="code-lang">JSON Response</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>{
"status": "ok",
"next_salt": "78abef90...32 hex chars",
"next_mutation_step": 2,
"next_mutation_order_b64": "cGFzc3dvcmQ..."
}</code></pre>
</div>
<h3>Rejected Response (Silent Rejection)</h3>
<div class="code-block">
<div class="code-header">
<span class="code-lang">JSON Response</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>{
"status": "ok"
}</code></pre>
</div>
<div class="doc-alert doc-alert--warn">
<div class="doc-alert-icon">⚠️</div>
<div class="doc-alert-body">
<strong>Important:</strong> Heartbeat rejections return <code>200 OK</code> with <code>status: ok</code> but OMIT next-state fields. Clients must check for the presence of <code>next_salt</code> before advancing local states.
</div>
</div>
<h2>Canonical Signing Payload</h2>
<p>The Ed25519 signature covers a canonical JSON string. The server orders the keys alphabetically using camelCase notation. Ensure serialization matches this format precisely:</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">JSON Payload</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>{
"entropyData": { "events": [{ "t": 120.4, "x": 124.5, "y": 308.2 }] },
"fingerprint": { "aspectRatio": "1.7777777778", "devicePixelRatio": "2", "hardwareConcurrency": 8 },
"geneCommitment": "56ab12cd...",
"mutationStep": 1,
"prevHash": "23ab89c0...",
"sessionId": "8902abc3...",
"stackState": { "ip": 12, "stack": [108429, 3902] },
"timestamp": 1782390494000
}</code></pre>
</div>
<h2>Operational Probes</h2>
<h3><code>GET /health</code></h3>
<div class="code-block">
<pre><code>{
"status": "healthy"
}</code></pre>
</div>
<h3><code>GET /stats</code></h3>
<div class="code-block">
<pre><code>{
"sessions": 412,
"expired_sessions": 3,
"max_chain_length": 84
}</code></pre>
</div>
<h3><code>GET /metrics</code></h3>
<div class="code-block">
<pre><code># HELP chronoseal_sessions Active ChronoSeal sessions
# TYPE chronoseal_sessions gauge
chronoseal_sessions 412
# HELP chronoseal_expired_sessions Expired sessions not yet removed
# TYPE chronoseal_expired_sessions gauge
chronoseal_expired_sessions 3
# HELP chronoseal_max_chain_length Maximum heartbeat chain length
# TYPE chronoseal_max_chain_length gauge
chronoseal_max_chain_length 84</code></pre>
</div>
</article>
<!-- Pager -->
<nav class="doc-pager" aria-label="Doc navigation Pager">
<a href="protocol.html" class="doc-pager-link">
<div class="doc-pager-label">Previous</div>
<div class="doc-pager-title">Protocol Specification</div>
</a>
<a href="threat-model.html" class="doc-pager-link doc-pager-link--next">
<div class="doc-pager-label">Next</div>
<div class="doc-pager-title">Threat Model</div>
</a>
</nav>
<div class="doc-meta">
Last Updated: June 2026 (v1.0.2)
</div>
</main>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
</h4>
<p>Next-generation browser attestation daemon built for the modern web.</p>
</div>
<div class="footer-section">
<h4>Resources</h4>
<a href="philosophy.html">Design Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="api.html">API Reference</a>
<a href="comparison.html">Comparison</a>
</div>
<div class="footer-section">
<h4>Community</h4>
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
</div>
<div class="footer-section">
<h4>Legal &amp; Security</h4>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
<a href="performance.html">Performance Tuning</a>
<a href="operations.html">Operations Guide</a>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
</div>
</footer>
<!-- Search Overlay -->
<div class="search-overlay">
<div class="search-box">
<div class="search-input-wrap">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
<span class="search-kbd">/</span>
</div>
<div class="search-results"></div>
</div>
</div>
<!-- Back to top -->
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
<!-- JS Scripts -->
<script src="js/search.js"></script>
<script src="js/app.js"></script>
</body>
</html>
+300
View File
@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Architecture Overview | ChronoSeal Documentation</title>
<meta name="description" content="ChronoSeal system architecture, state models, components, validation pipelines, and trust boundaries.">
<link rel="stylesheet" href="css/chronoseal.css">
<link rel="stylesheet" href="css/docs.css">
<link rel="stylesheet" href="css/print.css" media="print">
<link rel="manifest" href="site.webmanifest">
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
<!-- Font Awesome for Icons -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
</head>
<body>
<!-- Floating Background Spheres -->
<div class="bg-animation">
<div class="gradient-sphere sphere-1"></div>
<div class="gradient-sphere sphere-2"></div>
<div class="gradient-sphere sphere-3"></div>
</div>
<!-- Header / Navbar -->
<nav class="navbar" id="navbar">
<div class="nav-container">
<a href="index.html" class="logo">
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
<span>ChronoSeal</span>
</a>
<div class="nav-links" id="navLinks">
<a href="philosophy.html">Philosophy</a>
<a href="architecture.html" class="active">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="comparison.html">Comparison</a>
<div class="nav-dropdown">
<a href="#" class="nav-link">More <small>▼</small></a>
<div class="nav-dropdown-menu">
<a href="api.html">API Reference</a>
<a href="deployment.html">Deployment Guide</a>
<a href="operations.html">Operations Guide</a>
<a href="testing.html">Testing Strategy</a>
<a href="performance.html">Performance Tuning</a>
<a href="threat-model.html">Threat Model</a>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
</div>
</div>
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</button>
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
<i class="fab fa-github"></i> GitHub
</a>
</div>
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
<i class="fas fa-bars"></i>
</button>
</div>
</nav>
<!-- Doc Page Shell -->
<div class="doc-page">
<div class="doc-layout">
<!-- Sidebar -->
<aside class="doc-sidebar">
<button class="doc-sidebar-toggle">
<i class="fas fa-bars"></i> Sidebar Menu
</button>
<div class="doc-nav-group">
<div class="doc-nav-label">Core Concepts</div>
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
<a href="architecture.html" class="doc-nav-item active">Architecture Overview</a>
<a href="comparison.html" class="doc-nav-item">ChronoSeal vs Others</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Protocol &amp; API</div>
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
<a href="api.html" class="doc-nav-item">API Reference</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Operations &amp; Security</div>
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
<a href="security.html" class="doc-nav-item">Security Policy</a>
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Lifecycle &amp; Dev</div>
<a href="deployment.html" class="doc-nav-item">Deployment Guide</a>
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="doc-main">
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
<a href="index.html">Home</a>
<span class="sep">/</span>
<a href="#">Core Concepts</a>
<span class="sep">/</span>
<span>Architecture Overview</span>
</nav>
<article class="doc-content">
<h1>ChronoSeal Architecture</h1>
<p class="doc-subtitle">ChronoSeal is a Unix-native browser attestation daemon. It validates session continuity by combining cryptographic signatures, hash-chain progression, deterministic VM execution, and a shared Synthetic Gene Mutation Engine.</p>
<hr>
<h2>System Diagram</h2>
<p>The following diagram shows the relationship between browser clients, the daemon process, and the shared protocol library:</p>
<!-- Inline Architecture Diagram Container -->
<div id="diagram-architecture" class="arch-diagram" style="margin-top:24px"></div>
<h2>Workspace Components</h2>
<p>The codebase is structured as a Rust workspace containing three runtime crates and static frontend assets:</p>
<h3>1. <code>shared/</code> Crate</h3>
<p>The <strong>determinism boundary</strong> of ChronoSeal. Any execution logic that must agree precisely between the browser WASM runtime and server verification belongs here. Its responsibilities include:</p>
<ul>
<li>Wire protocol struct formats for request and response models.</li>
<li>Blake3 hash-chain progression functions.</li>
<li>Synthetic gene model definitions and commitments.</li>
<li>Mutation program bytecode generator, interpreter, and execution tracer.</li>
</ul>
<h3>2. <code>server/</code> Crate</h3>
<p>Compiles into the <code>chronoseal</code> daemon binary. It manages the server runtime, HTTP API routes, database backends, and verification logic. Key responsibilities:</p>
<ul>
<li>CLI command parsing and flag defaults.</li>
<li>Axum-based web router and health endpoints.</li>
<li>Verification pipeline (signature verification, timestamp drift checks, rate limit validations).</li>
<li>Pluggable storage adapters (SQLite memory/disk, Valkey).</li>
<li>Background cleanup loop for session purges.</li>
</ul>
<h3>3. <code>wasm/</code> Crate</h3>
<p>Compiles into the browser WebAssembly package (using <code>wasm-pack</code>) used by the frontend. Its key functions include:</p>
<ul>
<li>Secure client-side Ed25519 keypair generation and verification.</li>
<li>Message signing for canonical heartbeat payloads.</li>
<li>Client-side VM program execution and stack history logging.</li>
<li>Previewing, committing, and discarding synthetic gene mutations.</li>
</ul>
<h3>4. <code>frontend/</code> Directory</h3>
<p>Contains static JavaScript (<code>heartbeat.js</code>, <code>app.js</code>) and assets served to browsers to orchestrate background attestation calls without blocking UI rendering.</p>
<h2>Session State Model</h2>
<p>The daemon stores a single <code>SessionRecord</code> in the active database per session, structured as follows:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Core Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>session_id</code></td>
<td>Random 32-byte session lookup key.</td>
</tr>
<tr>
<td><code>public_key</code></td>
<td>Registered Ed25519 public key. Used to verify all heartbeats.</td>
</tr>
<tr>
<td><code>salt</code></td>
<td>Current server salt required to verify the next heartbeat.</td>
</tr>
<tr>
<td><code>last_hash</code></td>
<td>Current accepted Blake3 hash head. Prevents out-of-order repeats.</td>
</tr>
<tr>
<td><code>gene</code></td>
<td>Committed synthetic gene byte buffer.</td>
</tr>
<tr>
<td><code>pending_mutation</code></td>
<td>The mutation program compiled by the server for the next heartbeat step.</td>
</tr>
<tr>
<td><code>pending_mutation_step</code></td>
<td>Mismatches between this and request steps cause silent rejection.</td>
</tr>
</tbody>
</table>
<div class="doc-alert doc-alert--warn">
<div class="doc-alert-icon">⚠️</div>
<div class="doc-alert-body">
<strong>State Invariant:</strong> The server-side session state advances ONLY after a heartbeat passes all pipeline validations. Failed heartbeats never alter the stored salt, hash, or gene parameters, preventing desynchronization exploits.
</div>
</div>
<h2>Verification Pipeline</h2>
<p>Heartbeat verification follows a strict chronological order. If any check fails, execution immediately halts, returning the silent rejection response. The pipeline is:</p>
<ol>
<li>Load the session record using the submitted <code>session_id</code>.</li>
<li>Assert that the session exists and has not expired (<code>now &lt; expires_at</code>).</li>
<li>Verify the Ed25519 signature against the reconstructed canonical JSON payload.</li>
<li>Assert the request's <code>prev_hash</code> matches the server-stored <code>last_hash</code>.</li>
<li>Compare request step with <code>pending_mutation_step</code>.</li>
<li>Apply the stored <code>pending_mutation</code> to a cloned gene buffer.</li>
<li>Assert the computed gene commitment matches the request's <code>gene_commitment</code>.</li>
<li>Assert that the timestamp drift matches liveness bounds (within 30 seconds).</li>
<li>Assert that mouse activity entropy is present and speed falls within thresholds.</li>
<li>Assert screen aspect ratio, device pixel ratio, and concurrency parameters match bounds.</li>
<li>Advance the session's hash head, rotate the salt, compile the next mutation program, and persist.</li>
</ol>
</article>
<!-- Pager -->
<nav class="doc-pager" aria-label="Doc navigation Pager">
<a href="comparison.html" class="doc-pager-link">
<div class="doc-pager-label">Previous</div>
<div class="doc-pager-title">ChronoSeal vs Others</div>
</a>
<a href="protocol.html" class="doc-pager-link doc-pager-link--next">
<div class="doc-pager-label">Next</div>
<div class="doc-pager-title">Protocol Specification</div>
</a>
</nav>
<div class="doc-meta">
Last Updated: June 2026 (v1.0.2)
</div>
</main>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
</h4>
<p>Next-generation browser attestation daemon built for the modern web.</p>
</div>
<div class="footer-section">
<h4>Resources</h4>
<a href="philosophy.html">Design Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="api.html">API Reference</a>
<a href="comparison.html">Comparison</a>
</div>
<div class="footer-section">
<h4>Community</h4>
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
</div>
<div class="footer-section">
<h4>Legal &amp; Security</h4>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
<a href="performance.html">Performance Tuning</a>
<a href="operations.html">Operations Guide</a>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
</div>
</footer>
<!-- Search Overlay -->
<div class="search-overlay">
<div class="search-box">
<div class="search-input-wrap">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
<span class="search-kbd">/</span>
</div>
<div class="search-results"></div>
</div>
</div>
<!-- Back to top -->
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
<!-- JS Scripts -->
<script src="js/search.js"></script>
<script src="js/app.js"></script>
<script src="js/diagrams.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="296.99997mm"
viewBox="0 0 1122.5196 793.7008"
enable-background="new 0 0 1254 1254"
xml:space="preserve"
height="210mm"
sodipodi:docname="chronoseal.svg"
inkscape:export-filename="logo1.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.1654266"
inkscape:cx="561.16791"
inkscape:cy="396.85039"
inkscape:window-width="2048"
inkscape:window-height="1205"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><defs
id="defs44" />
<path
fill="none"
opacity="0"
stroke="none"
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path2" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path45"
style="fill:#e1e1e4;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path46"
style="fill:#b0b2b8;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path47"
style="fill:#7b7e85;fill-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
x="154.54701"
y="550.13623"
id="text47"><tspan
id="tspan47"
x="154.54701"
y="550.13623" /><tspan
id="tspan48"
x="154.54701"
y="750.13623"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
id="tspan49">chrono</tspan>seal</tspan></text><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path1"
style="fill:#4a4d53;fill-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="296.99997mm"
viewBox="0 0 1122.5196 793.7008"
enable-background="new 0 0 1254 1254"
xml:space="preserve"
height="210mm"
sodipodi:docname="chronoseal.svg"
inkscape:export-filename="logo1.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.1654266"
inkscape:cx="561.16791"
inkscape:cy="396.85039"
inkscape:window-width="2048"
inkscape:window-height="1205"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><defs
id="defs44" />
<path
fill="none"
opacity="0"
stroke="none"
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path2" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path45"
style="fill:#e1e1e4;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path46"
style="fill:#b0b2b8;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path47"
style="fill:#7b7e85;fill-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
x="154.54701"
y="550.13623"
id="text47"><tspan
id="tspan47"
x="154.54701"
y="550.13623" /><tspan
id="tspan48"
x="154.54701"
y="750.13623"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
id="tspan49">chrono</tspan>seal</tspan></text><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path1"
style="fill:#4a4d53;fill-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

+356
View File
@@ -0,0 +1,356 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>ChronoSeal vs Commercial Anti-Bot Systems | ChronoSeal Documentation</title>
<meta name="description" content="Compare ChronoSeal honestly with commercial anti-bot Edge/SaaS platforms like Cloudflare, Akamai, PerimeterX, and reCAPTCHA.">
<link rel="stylesheet" href="css/chronoseal.css">
<link rel="stylesheet" href="css/docs.css">
<link rel="stylesheet" href="css/print.css" media="print">
<link rel="manifest" href="site.webmanifest">
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
<!-- Font Awesome for Icons -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
</head>
<body>
<!-- Floating Background Spheres -->
<div class="bg-animation">
<div class="gradient-sphere sphere-1"></div>
<div class="gradient-sphere sphere-2"></div>
<div class="gradient-sphere sphere-3"></div>
</div>
<!-- Header / Navbar -->
<nav class="navbar" id="navbar">
<div class="nav-container">
<a href="index.html" class="logo">
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
<span>ChronoSeal</span>
</a>
<div class="nav-links" id="navLinks">
<a href="philosophy.html">Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="comparison.html" class="active">Comparison</a>
<div class="nav-dropdown">
<a href="#" class="nav-link">More <small>▼</small></a>
<div class="nav-dropdown-menu">
<a href="api.html">API Reference</a>
<a href="deployment.html">Deployment Guide</a>
<a href="operations.html">Operations Guide</a>
<a href="testing.html">Testing Strategy</a>
<a href="performance.html">Performance Tuning</a>
<a href="threat-model.html">Threat Model</a>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
</div>
</div>
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</button>
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
<i class="fab fa-github"></i> GitHub
</a>
</div>
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
<i class="fas fa-bars"></i>
</button>
</div>
</nav>
<!-- Doc Page Shell -->
<div class="doc-page">
<div class="doc-layout">
<!-- Sidebar -->
<aside class="doc-sidebar">
<button class="doc-sidebar-toggle">
<i class="fas fa-bars"></i> Sidebar Menu
</button>
<div class="doc-nav-group">
<div class="doc-nav-label">Core Concepts</div>
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
<a href="architecture.html" class="doc-nav-item">Architecture Overview</a>
<a href="comparison.html" class="doc-nav-item active">ChronoSeal vs Others</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Protocol &amp; API</div>
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
<a href="api.html" class="doc-nav-item">API Reference</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Operations &amp; Security</div>
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
<a href="security.html" class="doc-nav-item">Security Policy</a>
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Lifecycle &amp; Dev</div>
<a href="deployment.html" class="doc-nav-item">Deployment Guide</a>
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="doc-main">
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
<a href="index.html">Home</a>
<span class="sep">/</span>
<a href="#">Core Concepts</a>
<span class="sep">/</span>
<span>ChronoSeal vs Others</span>
</nav>
<article class="doc-content">
<h1>ChronoSeal vs Popular Anti-Bot Systems</h1>
<p class="doc-subtitle">ChronoSeal is a self-hosted, cryptographic attestation daemon. This document compares it honestly with leading commercial solutions.</p>
<hr>
<h2>Quick Comparison Matrix</h2>
<div class="comparison-table-wrap" style="margin-bottom: 2rem;">
<table>
<thead>
<tr>
<th>Solution</th>
<th>Type</th>
<th>Core Method</th>
<th>Privacy</th>
<th>Self-Hosted</th>
<th>Strength</th>
<th>Behavioral</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ChronoSeal</strong></td>
<td>Self-hosted Daemon</td>
<td>Ed25519 + Gene Mutation</td>
<td><span class="check"><i class="fas fa-check-circle"></i> Excellent</span></td>
<td>Yes</td>
<td>Very High</td>
<td>Light + Tunable</td>
<td><span class="check"><i class="fas fa-check-circle"></i> Free</span></td>
</tr>
<tr>
<td>Cloudflare Bot Mgmt</td>
<td>Cloud Edge</td>
<td>Challenges + Fingerprint</td>
<td>Medium</td>
<td>No</td>
<td>Medium</td>
<td>Strong</td>
<td>Freemium</td>
</tr>
<tr>
<td>Akamai Bot Manager</td>
<td>Enterprise Edge</td>
<td>Fingerprinting + Heuristics</td>
<td><span class="times"><i class="fas fa-times-circle"></i> Low</span></td>
<td>Hybrid</td>
<td>Medium</td>
<td>Very Strong</td>
<td>Very High</td>
</tr>
<tr>
<td>HUMAN (PerimeterX)</td>
<td>Cloud SaaS</td>
<td>Behavioral Biometrics + ML</td>
<td><span class="times"><i class="fas fa-times-circle"></i> Low</span></td>
<td>No</td>
<td>Medium</td>
<td>Very Strong</td>
<td>Enterprise</td>
</tr>
<tr>
<td>DataDome</td>
<td>Cloud SaaS</td>
<td>Real-time ML scoring</td>
<td>Medium</td>
<td>No</td>
<td>Medium</td>
<td>Strong</td>
<td>Enterprise</td>
</tr>
<tr>
<td>reCAPTCHA v3</td>
<td>Google Service</td>
<td>Invisible risk challenges</td>
<td><span class="times"><i class="fas fa-times-circle"></i> Poor</span></td>
<td>No</td>
<td>Low</td>
<td>Medium</td>
<td>Free → Paid</td>
</tr>
<tr>
<td>Kasada</td>
<td>Cloud SaaS</td>
<td>Proof-of-Work + Obfuscation</td>
<td>Medium</td>
<td>No</td>
<td>High</td>
<td>Strong</td>
<td>Enterprise</td>
</tr>
</tbody>
</table>
</div>
<h2>Detailed Analysis</h2>
<h3>1. ChronoSeal (v1.0.2)</h3>
<p><strong>Strengths:</strong></p>
<ul>
<li>Strongest cryptographic foundation (Ed25519 signatures + Blake3 hash chain + Synthetic Gene Mutation Engine).</li>
<li>Fully deterministic server ↔ WASM execution agreement.</li>
<li>Completely invisible to users with silent rejection mechanics.</li>
<li>Excellent privacy posture — no third-party tracking, profiling, or persistent databases.</li>
<li>Tunable mutation complexity parameters (<code>gene_size</code> and <code>mutation_rounds</code>).</li>
<li>100% control, auditability, and local operations.</li>
</ul>
<p><strong>Weaknesses:</strong></p>
<ul>
<li>Requires self-hosting, configuration management, and server capacity.</li>
<li>No global threat intelligence network or shared IP reputation lists.</li>
</ul>
<h3>2. Cloudflare Bot Management</h3>
<p><strong>Strengths:</strong></p>
<ul>
<li>Extremely easy to deploy for domains already routed through Cloudflare.</li>
<li>Excellent scale and global threat reputation database.</li>
</ul>
<p><strong>Weaknesses vs ChronoSeal:</strong></p>
<ul>
<li>Relies heavily on browser fingerprint heuristics and invasive JS challenges.</li>
<li>Sends visitor metadata to Cloudflare (privacy impact).</li>
<li>Vendor lock-in and zero visibility into decision algorithms.</li>
</ul>
<h3>3. Enterprise Solutions (Akamai, HUMAN, DataDome, Kasada)</h3>
<p><strong>Strengths:</strong></p>
<ul>
<li>Sophisticated machine learning modeling of biometrics and timing.</li>
<li>Professional support, operational SLAs, and security response teams.</li>
</ul>
<p><strong>Weaknesses vs ChronoSeal:</strong></p>
<ul>
<li>Extremely expensive enterprise licensing models.</li>
<li>Black-box systems with limited logging and transparency.</li>
<li>Heavy user data collection, causing privacy and compliance overhead.</li>
</ul>
<h3>4. reCAPTCHA v3</h3>
<p><strong>Strengths:</strong></p>
<ul>
<li>Free tier with wide community adoption.</li>
<li>Quick to integrate in basic web forms.</li>
</ul>
<p><strong>Weaknesses vs ChronoSeal:</strong></p>
<ul>
<li>Heavy Google user tracking cookies and profiling.</li>
<li>Fails to block modern, stateful automated browsers and headless runs.</li>
<li>High rate of bypasses by standard captcha-solving farms.</li>
</ul>
<h2>When to Choose ChronoSeal</h2>
<p>Choose <strong>ChronoSeal</strong> if you want:</p>
<ul>
<li>Maximum visitor privacy.</li>
<li>Strong, deterministic cryptographic guarantees.</li>
<li>Complete control over your server infrastructure and logs.</li>
<li>Configurable and tunable verification strength.</li>
<li>Zero dependency on third-party SaaS vendors.</li>
</ul>
<h2>Technical Differentiation</h2>
<p>ChronoSeal's primary advantage is the <strong>Synthetic Gene Mutation Engine</strong>. Instead of just checking static fingerprint values or browser headers, the server issues dynamic mutation programs that both the server and client WASM execute in sync. This establishes a second stateful channel that is extremely difficult for automation clients to spoof at scale without implementing the complete state model.</p>
</article>
<!-- Pager -->
<nav class="doc-pager" aria-label="Doc navigation Pager">
<a href="security.html" class="doc-pager-link">
<div class="doc-pager-label">Previous</div>
<div class="doc-pager-title">Security Policy</div>
</a>
<a href="architecture.html" class="doc-pager-link doc-pager-link--next">
<div class="doc-pager-label">Next</div>
<div class="doc-pager-title">Architecture Overview</div>
</a>
</nav>
<div class="doc-meta">
Last Updated: June 2026 (v1.0.2)
</div>
</main>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
</h4>
<p>Next-generation browser attestation daemon built for the modern web.</p>
</div>
<div class="footer-section">
<h4>Resources</h4>
<a href="philosophy.html">Design Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="api.html">API Reference</a>
<a href="comparison.html">Comparison</a>
</div>
<div class="footer-section">
<h4>Community</h4>
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
</div>
<div class="footer-section">
<h4>Legal &amp; Security</h4>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
<a href="performance.html">Performance Tuning</a>
<a href="operations.html">Operations Guide</a>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
</div>
</footer>
<!-- Search Overlay -->
<div class="search-overlay">
<div class="search-box">
<div class="search-input-wrap">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
<span class="search-kbd">/</span>
</div>
<div class="search-results"></div>
</div>
</div>
<!-- Back to top -->
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
<!-- JS Scripts -->
<script src="js/search.js"></script>
<script src="js/app.js"></script>
</body>
</html>
+984
View File
@@ -0,0 +1,984 @@
/* ============================================================
ChronoSeal Design System — chronoseal.css
Vibrant, premium, glassmorphism theme matching the target site
============================================================ */
/* ---- Custom Properties ---- */
:root {
--bg-primary: #0a0e27;
--bg-secondary: #11162e;
--bg-card: rgba(18, 24, 48, 0.7);
--bg-card-solid: #121830;
--border: rgba(56, 78, 135, 0.3);
--border-glow: rgba(0, 255, 255, 0.2);
--text-primary: #ffffff;
--text-secondary: #a0a8c3;
--text-muted: #5a6490;
--accent-cyan: #00e5ff;
--accent-purple: #b84eff;
--accent-green: #00ff88;
--accent-red: #ff4757;
--gradient-1: linear-gradient(135deg, #00e5ff 0%, #b84eff 100%);
--gradient-2: linear-gradient(135deg, #00ff88 0%, #00e5ff 100%);
--shadow-glow: 0 0 30px rgba(0, 229, 255, 0.1);
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
--max-width: 1400px;
--header-h: 75px;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 20px;
--ease: cubic-bezier(0.175, 0.885, 0.32, 1.275);
--tr: .3s ease;
}
/* ---- Reset ---- */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
a {
color: var(--text-secondary);
text-decoration: none;
transition: all var(--tr);
}
a:hover {
color: var(--text-primary);
}
img, svg {
max-width: 100%;
display: block;
}
button {
cursor: pointer;
font-family: inherit;
border: none;
background: none;
}
code, pre {
font-family: var(--font-mono);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-cyan);
}
/* ---- Layout ---- */
.container, .main-content {
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
padding: 0 2rem;
}
.main-content {
padding-top: var(--header-h);
}
.section {
padding: 6rem 0;
scroll-margin-top: 100px;
}
.text-center {
text-align: center;
}
.arch-diagram {
display: flex;
justify-content: center;
align-items: center;
margin: 2rem auto;
max-width: 100%;
}
.arch-diagram svg {
display: block;
margin: 0 auto;
max-width: 100%;
}
.section--alt {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
/* ============================================================
BACKGROUND ANIMATION
============================================================ */
.bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
pointer-events: none;
}
.gradient-sphere {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
animation: float 20s infinite ease-in-out;
will-change: transform;
}
.sphere-1 { width: 600px; height: 600px; background: var(--accent-cyan); top: -200px; right: -200px; animation-delay: 0s; }
.sphere-2 { width: 500px; height: 500px; background: var(--accent-purple); bottom: -150px; left: -150px; animation-delay: -5s; }
.sphere-3 { width: 400px; height: 400px; background: var(--accent-green); top: 40%; left: 30%; animation-delay: -10s; opacity: 0.2; }
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -30px) scale(1.05); }
66% { transform: translate(-20px, 20px) scale(0.95); }
}
/* ============================================================
NAVBAR / HEADER
============================================================ */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(10, 14, 39, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid transparent;
padding: 1.25rem 2rem;
transition: all 0.3s ease;
}
.navbar.scrolled {
padding: 0.75rem 2rem;
background: rgba(10, 14, 39, 0.98);
border-bottom: 1px solid var(--border);
}
.nav-container {
max-width: var(--max-width);
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.5rem;
font-weight: 800;
background: var(--gradient-1);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-decoration: none;
}
.logobar {
width: 32px;
height: 32px;
}
.logo span {
color: transparent;
background: var(--gradient-1);
-webkit-background-clip: text;
background-clip: text;
}
.nav-links {
display: flex;
gap: 2.5rem;
align-items: center;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.3s ease;
font-weight: 500;
font-size: 0.95rem;
}
.nav-links a:hover, .nav-links a.active {
color: var(--text-primary);
text-shadow: 0 0 10px rgba(0, 229, 255, 0.4);
}
.github-btn {
background: var(--bg-card-solid);
padding: 0.5rem 1.25rem;
border-radius: 8px;
border: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
}
.github-btn:hover {
border-color: var(--accent-cyan);
box-shadow: 0 0 15px rgba(0, 229, 255, 0.2);
}
.nav-toggle {
display: none;
background: none;
border: none;
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
transition: color 0.3s ease;
}
/* Nav Dropdown */
.nav-dropdown {
position: relative;
}
.nav-dropdown-menu {
position: absolute;
top: calc(100% + 15px);
left: 50%;
transform: translateX(-50%) translateY(8px);
min-width: 220px;
background: var(--bg-card-solid);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: 6px;
opacity: 0;
pointer-events: none;
transition: opacity var(--tr), transform var(--tr);
box-shadow: 0 16px 48px rgba(0,0,0,.4);
backdrop-filter: blur(10px);
}
.nav-dropdown:hover .nav-dropdown-menu {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
.nav-dropdown-menu a {
display: block;
padding: 8px 14px;
font-size: .88rem;
color: var(--text-secondary);
border-radius: var(--r-sm);
transition: all var(--tr);
}
.nav-dropdown-menu a:hover {
color: var(--text-primary);
background: rgba(0, 229, 255, 0.05);
}
/* ============================================================
HERO SECTION
============================================================ */
.hero {
text-align: center;
padding: 6rem 0;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1.25rem;
background: rgba(0, 229, 255, 0.05);
border: 1px solid var(--border-glow);
border-radius: 50px;
font-size: 0.875rem;
color: var(--accent-cyan);
margin-bottom: 2rem;
font-weight: 500;
}
.hero h1 {
font-size: clamp(3rem, 5vw, 4.5rem);
font-weight: 800;
margin-bottom: 1.5rem;
background: var(--gradient-1);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
line-height: 1.1;
}
.hero-subtitle {
font-size: 1.25rem;
color: var(--text-secondary);
max-width: 700px;
margin: 0 auto 2.5rem;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
/* ============================================================
BUTTONS
============================================================ */
.btn {
padding: 0.875rem 2rem;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
transition: all var(--tr);
cursor: pointer;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--gradient-1);
color: var(--bg-primary);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 229, 255, 0.4);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-primary);
}
.btn-secondary:hover {
border-color: var(--accent-cyan);
background: rgba(0, 229, 255, 0.05);
}
.btn-ghost {
color: var(--text-secondary);
border: 1px solid transparent;
}
.btn-ghost:hover {
color: var(--text-primary);
border-color: var(--border);
background: rgba(255,255,255,0.05);
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
/* ============================================================
STATS ROW
============================================================ */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
margin-bottom: 6rem;
}
.stat-card {
background: var(--bg-card);
border-radius: 20px;
padding: 2.5rem 2rem;
border: 1px solid var(--border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.stat-number {
font-size: 3.5rem;
font-weight: 800;
background: var(--gradient-1);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
line-height: 1;
}
.stat-label {
color: var(--text-secondary);
margin-top: 1rem;
font-weight: 500;
font-size: 1.1rem;
}
/* ============================================================
SECTION HEADER
============================================================ */
.section-header {
text-align: center;
margin-bottom: 4rem;
}
.section-label {
display: inline-block;
font-size: 0.875rem;
font-weight: 600;
color: var(--accent-cyan);
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.section-header h2 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 1rem;
background: var(--gradient-2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.section-header p {
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
font-size: 1.1rem;
}
/* ============================================================
FEATURES GRID / CARDS
============================================================ */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
}
.card {
background: var(--bg-card);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: 20px;
padding: 2.5rem;
transition: all 0.4s var(--ease);
}
.card:hover {
transform: translateY(-10px);
border-color: var(--accent-cyan);
box-shadow: var(--shadow-glow);
}
.card-icon {
width: 64px;
height: 64px;
background: rgba(0, 229, 255, 0.1);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
border: 1px solid rgba(0, 229, 255, 0.2);
}
.card-icon i {
font-size: 1.75rem;
color: var(--accent-cyan);
}
.card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.card p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
line-height: 1.7;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
padding: 0.35rem 0.85rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
font-size: 0.75rem;
color: var(--text-primary);
font-weight: 500;
border: 1px solid var(--border);
}
/* ============================================================
STEPS / HOW IT WORKS
============================================================ */
.steps {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 800px;
margin: 0 auto;
}
.step {
display: flex;
gap: 2rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: 2rem;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: all var(--tr);
}
.step:hover {
border-color: var(--accent-cyan);
box-shadow: var(--shadow-glow);
}
.step-number {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--gradient-1);
color: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: 800;
}
.step-content h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.step-content p {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.65;
}
/* ============================================================
COMPARISON TABLE
============================================================ */
.comparison-table-wrap {
overflow-x: auto;
background: var(--bg-card);
border-radius: 20px;
border: 1px solid var(--border);
padding: 1rem;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.comparison-table-wrap table {
width: 100%;
border-collapse: collapse;
min-width: 700px;
}
.comparison-table-wrap th, .comparison-table-wrap td {
padding: 1.25rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.comparison-table-wrap th {
color: var(--text-primary);
font-weight: 600;
background: rgba(0, 229, 255, 0.05);
}
.comparison-table-wrap tr:last-child td {
border-bottom: none;
}
.check {
color: var(--accent-green);
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
td.check {
display: table-cell;
}
td.check i {
margin-right: 0.4rem;
}
.times {
color: var(--accent-red);
opacity: 0.7;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
td.times {
display: table-cell;
}
td.times i {
margin-right: 0.4rem;
}
/* ============================================================
CODE BLOCKS
============================================================ */
.code-block {
background: #080b1a;
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
overflow-x: auto;
font-size: 0.9rem;
margin: 1.5rem 0;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
position: relative;
}
.code-block-center {
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.code-block pre {
color: #a0a8c3;
line-height: 1.5;
font-family: var(--font-mono);
}
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
border-bottom: 1px solid rgba(255,255,255,0.05);
padding-bottom: 0.5rem;
}
.code-lang {
font-size: 0.75rem;
color: var(--accent-cyan);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.code-copy-btn {
font-size: 0.75rem;
color: var(--text-secondary);
border: 1px solid var(--border);
padding: 3px 8px;
border-radius: 4px;
background: rgba(255,255,255,0.03);
transition: all var(--tr);
}
.code-copy-btn:hover {
color: var(--text-primary);
border-color: var(--accent-cyan);
}
/* ============================================================
FOOTER
============================================================ */
.footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 5rem 2rem 2rem;
margin-top: 6rem;
}
.footer-content {
max-width: var(--max-width);
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 4rem;
}
.footer-section h4 {
margin-bottom: 1.5rem;
color: var(--text-primary);
font-size: 1.1rem;
}
.footer-section p {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.6;
}
.footer-section a {
display: inline-block;
color: var(--text-secondary);
text-decoration: none;
margin-bottom: 0.75rem;
transition: all var(--tr);
font-size: 0.95rem;
}
.footer-section a:hover {
color: var(--accent-cyan);
transform: translateX(5px);
}
.footer-bottom {
text-align: center;
padding-top: 3rem;
margin-top: 3rem;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.9rem;
}
/* ============================================================
SEARCH OVERLAY
============================================================ */
.search-overlay {
position: fixed;
inset: 0;
z-index: 2000;
background: rgba(10, 14, 39, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 15vh;
opacity: 0;
pointer-events: none;
transition: opacity var(--tr);
}
.search-overlay.open {
opacity: 1;
pointer-events: auto;
}
.search-box {
width: 100%;
max-width: 580px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
transform: translateY(16px);
transition: transform var(--tr);
box-shadow: 0 24px 64px rgba(0,0,0,.5);
}
.search-overlay.open .search-box {
transform: translateY(0);
}
.search-input-wrap {
display: flex;
align-items: center;
padding: 16px 20px;
gap: 12px;
border-bottom: 1px solid var(--border);
}
.search-input-wrap svg {
width: 20px;
height: 20px;
color: var(--text-muted);
flex-shrink: 0;
}
.search-input {
flex: 1;
background: none;
border: none;
outline: none;
font-size: 1rem;
color: var(--text-primary);
font-family: var(--font-sans);
}
.search-input::placeholder {
color: var(--text-muted);
}
.search-kbd {
font-size: .7rem;
font-weight: 600;
color: var(--text-muted);
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg-card-solid);
}
.search-results {
max-height: 360px;
overflow-y: auto;
padding: 8px;
}
.search-result {
display: block;
padding: 10px 16px;
border-radius: var(--r-sm);
transition: background var(--tr);
}
.search-result:hover, .search-result.selected {
background: rgba(0, 229, 255, 0.05);
}
.search-result-title {
font-size: .9rem;
font-weight: 600;
color: var(--text-primary);
}
.search-result-desc {
font-size: .8rem;
color: var(--text-secondary);
margin-top: 2px;
}
.search-empty {
padding: 24px;
text-align: center;
color: var(--text-secondary);
font-size: .9rem;
}
/* ============================================================
BACK TO TOP
============================================================ */
.back-to-top {
position: fixed;
bottom: 24px;
right: 24px;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-card-solid);
border: 1px solid var(--border);
border-radius: 50%;
color: var(--text-secondary);
font-size: 1.1rem;
z-index: 100;
opacity: 0;
pointer-events: none;
transition: all var(--tr);
}
.back-to-top.visible {
opacity: 1;
pointer-events: auto;
}
.back-to-top:hover {
background: rgba(0, 229, 255, 0.05);
color: var(--accent-cyan);
border-color: var(--accent-cyan);
transform: translateY(-2px);
box-shadow: var(--shadow-glow);
}
/* ============================================================
ANIMATION INTERSECT
============================================================ */
.animate {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.animate.in-view {
opacity: 1;
transform: translateY(0);
}
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 900px) {
.nav-links {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: rgba(10, 14, 39, 0.98);
backdrop-filter: blur(15px);
flex-direction: column;
padding: 2rem 1rem;
border-bottom: 1px solid var(--border);
gap: 1.5rem;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 20px 40px rgba(0,0,0,0.5);
}
.nav-links.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.nav-toggle {
display: block;
}
}
+440
View File
@@ -0,0 +1,440 @@
/* ============================================================
Documentation Pages — docs.css
Layout, sidebar, content rendering for documentation pages
============================================================ */
/* ---- Doc page shell ---- */
.doc-page {
padding-top: var(--header-h);
}
.doc-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 0;
min-height: calc(100vh - var(--header-h));
max-width: var(--max-width);
margin: 0 auto;
}
/* ---- Sidebar ---- */
.doc-sidebar {
position: sticky;
top: var(--header-h);
height: calc(100vh - var(--header-h));
overflow-y: auto;
padding: 2.5rem 1.5rem;
background: rgba(17, 22, 46, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-right: 1px solid var(--border);
}
.doc-sidebar-toggle {
display: none;
}
.doc-nav-group {
margin-bottom: 2rem;
}
.doc-nav-label {
font-size: .75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--text-muted);
margin-bottom: 0.75rem;
padding-left: 0.75rem;
}
.doc-nav-item {
display: block;
padding: 8px 14px;
font-size: .9rem;
color: var(--text-secondary);
border-radius: var(--r-sm);
transition: all var(--tr);
border-left: 2px solid transparent;
margin-bottom: 2px;
}
.doc-nav-item:hover {
color: var(--text-primary);
background: rgba(0, 229, 255, 0.05);
}
.doc-nav-item.active {
color: var(--accent-cyan);
background: rgba(0, 229, 255, 0.05);
border-left-color: var(--accent-cyan);
text-shadow: 0 0 10px rgba(0, 229, 255, 0.3);
}
/* ---- Content area ---- */
.doc-main {
padding: 3rem 4rem 6rem;
max-width: 960px;
}
/* ---- Breadcrumb ---- */
.doc-breadcrumb {
display: flex;
align-items: center;
gap: 6px;
font-size: .8rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
}
.doc-breadcrumb a {
color: var(--text-muted);
}
.doc-breadcrumb a:hover {
color: var(--accent-cyan);
}
.doc-breadcrumb .sep {
color: var(--text-muted);
}
/* ---- Markdown content rendering ---- */
.doc-content h1 {
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -.03em;
margin-bottom: 0.5rem;
line-height: 1.15;
background: var(--gradient-1);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.doc-content .doc-subtitle {
font-size: 1.15rem;
color: var(--text-secondary);
margin-bottom: 2rem;
line-height: 1.65;
}
.doc-content h2 {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -.02em;
margin-top: 3.5rem;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
scroll-margin-top: calc(var(--header-h) + 24px);
background: var(--gradient-2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.doc-content h3 {
font-size: 1.35rem;
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1rem;
scroll-margin-top: calc(var(--header-h) + 24px);
color: var(--text-primary);
}
.doc-content h4 {
font-size: 1.1rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 0.75rem;
color: var(--text-secondary);
scroll-margin-top: calc(var(--header-h) + 24px);
}
.doc-content p {
margin-bottom: 1.25rem;
color: var(--text-secondary);
line-height: 1.8;
}
.doc-content ul, .doc-content ol {
margin-bottom: 1.25rem;
padding-left: 1.5rem;
}
.doc-content ul {
list-style: disc;
}
.doc-content ol {
list-style: decimal;
}
.doc-content li {
margin-bottom: 0.5rem;
color: var(--text-secondary);
line-height: 1.7;
}
.doc-content li strong {
color: var(--text-primary);
}
.doc-content code {
padding: 2px 6px;
font-size: .88em;
background: rgba(0, 229, 255, 0.05);
border: 1px solid rgba(0, 229, 255, 0.15);
border-radius: 4px;
color: var(--accent-cyan);
}
.doc-content pre {
background: #080b1a;
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0 2rem;
overflow-x: auto;
font-size: .88rem;
line-height: 1.6;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
}
.doc-content pre code {
background: none;
border: none;
padding: 0;
color: #a0a8c3;
font-size: inherit;
}
.doc-content strong {
color: var(--text-primary);
font-weight: 600;
}
.doc-content em {
color: var(--text-secondary);
font-style: italic;
}
.doc-content a {
color: var(--accent-cyan);
border-bottom: 1px solid transparent;
transition: all var(--tr);
}
.doc-content a:hover {
color: var(--text-primary);
border-bottom-color: var(--accent-cyan);
}
.doc-content blockquote {
margin: 1.5rem 0;
padding: 1.25rem 1.5rem;
border-left: 4px solid var(--accent-purple);
background: var(--bg-card);
border-radius: 0 var(--r-md) var(--r-md) 0;
color: var(--text-secondary);
}
.doc-content blockquote p {
margin-bottom: 0;
}
.doc-content table {
width: 100%;
margin: 1.5rem 0 2rem;
border-collapse: collapse;
font-size: .88rem;
}
.doc-content table th {
padding: 12px 16px;
text-align: left;
font-weight: 700;
font-size: .78rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-primary);
background: rgba(0, 229, 255, 0.05);
border: 1px solid var(--border);
}
.doc-content table td {
padding: 12px 16px;
border: 1px solid var(--border);
color: var(--text-secondary);
}
.doc-content table tr:hover td {
background: rgba(255,255,255,0.02);
}
.doc-content hr {
border: none;
height: 1px;
background: var(--border);
margin: 3rem 0;
}
/* ---- Alert boxes ---- */
.doc-alert {
display: flex;
gap: 12px;
padding: 1rem 1.25rem;
margin: 1.5rem 0 2rem;
border-radius: var(--r-md);
border: 1px solid;
}
.doc-alert-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
margin-top: 2px;
}
.doc-alert-body {
font-size: .9rem;
line-height: 1.6;
}
.doc-alert-body p {
margin-bottom: 4px;
color: inherit;
}
.doc-alert-body p:last-child {
margin-bottom: 0;
}
.doc-alert--note {
background: rgba(0, 229, 255, 0.05);
border-color: rgba(0, 229, 255, 0.2);
color: var(--accent-cyan);
}
.doc-alert--tip {
background: rgba(0, 255, 136, 0.05);
border-color: rgba(0, 255, 136, 0.2);
color: var(--accent-green);
}
.doc-alert--warn {
background: rgba(251, 191, 36, 0.05);
border-color: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.doc-alert--danger {
background: rgba(255, 71, 87, 0.05);
border-color: rgba(255, 71, 87, 0.2);
color: var(--accent-red);
}
/* ---- Prev / Next navigation ---- */
.doc-pager {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--border);
}
.doc-pager-link {
padding: 1.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-md);
transition: all var(--tr);
}
.doc-pager-link:hover {
border-color: var(--accent-cyan);
background: rgba(0, 229, 255, 0.03);
box-shadow: var(--shadow-glow);
}
.doc-pager-label {
font-size: .75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.doc-pager-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.doc-pager-link--next {
text-align: right;
}
/* ---- Last updated ---- */
.doc-meta {
font-size: .8rem;
color: var(--text-muted);
margin-top: 2.5rem;
}
/* ============================================================
RESPONSIVE
============================================================ */
@media(max-width:900px) {
.doc-layout {
grid-template-columns: 1fr;
}
.doc-sidebar {
position: fixed;
top: var(--header-h);
left: 0;
bottom: 0;
width: 280px;
z-index: 998;
transform: translateX(-100%);
transition: transform var(--tr);
border-right: 1px solid var(--border);
background: var(--bg-secondary);
}
.doc-sidebar.open {
transform: translateX(0);
}
.doc-sidebar-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: .85rem;
font-weight: 600;
color: var(--text-secondary);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-sm);
margin-bottom: 16px;
width: fit-content;
}
.doc-sidebar-toggle:hover {
color: var(--text-primary);
border-color: var(--accent-cyan);
}
.doc-main {
padding: 2rem 1.5rem 4rem;
}
.doc-pager {
grid-template-columns: 1fr;
}
}
+39
View File
@@ -0,0 +1,39 @@
/* ============================================================
Print Stylesheet — print.css
Clean print layout for documentation pages
============================================================ */
@media print {
*{color:#111!important;background:white!important;box-shadow:none!important;text-shadow:none!important}
body{font-size:11pt;line-height:1.6}
.site-header,.site-footer,.nav-toggle,.back-to-top,
.search-overlay,.doc-sidebar,.doc-toc,.doc-pager,
.doc-sidebar-toggle,.doc-breadcrumb,
.hero-bg,.hero-grid,.hero-badge,.hero-actions,
.hero-terminal,.cta-section,
.btn,.nav-cta{display:none!important}
.doc-layout,.doc-layout--toc{display:block!important}
.doc-main{padding:0!important;max-width:100%!important}
.doc-page{padding-top:0!important}
a{text-decoration:underline}
a[href^="http"]::after{content:" (" attr(href) ")";font-size:9pt;color:#666}
a[href^="#"]::after{content:""}
pre,code{font-size:9pt;border:1px solid #ddd;padding:8px;page-break-inside:avoid}
table{border-collapse:collapse;width:100%}
th,td{border:1px solid #ccc;padding:6px 10px;font-size:9pt}
th{background:#eee!important;font-weight:700}
h1{font-size:20pt;border-bottom:2px solid #111;padding-bottom:6pt;margin-bottom:12pt}
h2{font-size:16pt;border-bottom:1px solid #999;padding-bottom:4pt;margin-top:24pt;page-break-after:avoid}
h3{font-size:13pt;margin-top:18pt;page-break-after:avoid}
img{max-width:100%!important}
.section{padding:24pt 0!important}
@page{margin:1.5cm 2cm}
}
+334
View File
@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Deployment Guide | ChronoSeal Documentation</title>
<meta name="description" content="ChronoSeal Deployment Guide, detailing compiler requirements, native installation script, environment variables, Nginx configurations, and Docker integration.">
<link rel="stylesheet" href="css/chronoseal.css">
<link rel="stylesheet" href="css/docs.css">
<link rel="stylesheet" href="css/print.css" media="print">
<link rel="manifest" href="site.webmanifest">
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
<!-- Font Awesome for Icons -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
</head>
<body>
<!-- Floating Background Spheres -->
<div class="bg-animation">
<div class="gradient-sphere sphere-1"></div>
<div class="gradient-sphere sphere-2"></div>
<div class="gradient-sphere sphere-3"></div>
</div>
<!-- Header / Navbar -->
<nav class="navbar" id="navbar">
<div class="nav-container">
<a href="index.html" class="logo">
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
<span>ChronoSeal</span>
</a>
<div class="nav-links" id="navLinks">
<a href="philosophy.html">Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="comparison.html">Comparison</a>
<div class="nav-dropdown">
<a href="#" class="nav-link">More <small>▼</small></a>
<div class="nav-dropdown-menu">
<a href="api.html">API Reference</a>
<a href="deployment.html" class="active">Deployment Guide</a>
<a href="operations.html">Operations Guide</a>
<a href="testing.html">Testing Strategy</a>
<a href="performance.html">Performance Tuning</a>
<a href="threat-model.html">Threat Model</a>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
</div>
</div>
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</button>
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
<i class="fab fa-github"></i> GitHub
</a>
</div>
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
<i class="fas fa-bars"></i>
</button>
</div>
</nav>
<!-- Doc Page Shell -->
<div class="doc-page">
<div class="doc-layout">
<!-- Sidebar -->
<aside class="doc-sidebar">
<button class="doc-sidebar-toggle">
<i class="fas fa-bars"></i> Sidebar Menu
</button>
<div class="doc-nav-group">
<div class="doc-nav-label">Core Concepts</div>
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
<a href="architecture.html" class="doc-nav-item">Architecture Overview</a>
<a href="comparison.html" class="doc-nav-item">ChronoSeal vs Others</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Protocol &amp; API</div>
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
<a href="api.html" class="doc-nav-item">API Reference</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Operations &amp; Security</div>
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
<a href="security.html" class="doc-nav-item">Security Policy</a>
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
</div>
<div class="doc-nav-group">
<div class="doc-nav-label">Lifecycle &amp; Dev</div>
<a href="deployment.html" class="doc-nav-item active">Deployment Guide</a>
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="doc-main">
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
<a href="index.html">Home</a>
<span class="sep">/</span>
<a href="#">Lifecycle &amp; Dev</a>
<span class="sep">/</span>
<span>Deployment Guide</span>
</nav>
<article class="doc-content">
<h1>ChronoSeal Deployment Guide</h1>
<p class="doc-subtitle">ChronoSeal runs as a native Unix daemon behind TLS, serving WebAssembly assets. This guide covers building, configuring, and service installation options.</p>
<hr>
<h2>System Requirements</h2>
<table>
<thead>
<tr>
<th>Requirement</th>
<th>Min Version</th>
<th>Core Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rust (cargo)</td>
<td>1.87 stable</td>
<td>Compile server binary and shared libraries</td>
</tr>
<tr>
<td>wasm-pack</td>
<td>0.13</td>
<td>Generate the browser WebAssembly module package</td>
</tr>
<tr>
<td>wasm32 target</td>
<td>stable</td>
<td>Required target for cargo wasm32 compilation</td>
</tr>
<tr>
<td>systemd</td>
<td>248+</td>
<td>Service management and sandbox isolation</td>
</tr>
<tr>
<td>Docker</td>
<td>24.x</td>
<td>Containerization support</td>
</tr>
</tbody>
</table>
<h2>Building from Source</h2>
<p>Install the Rust WASM build targets and tools:</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">Shell</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>rustup target add wasm32-unknown-unknown
cargo install wasm-pack</code></pre>
</div>
<p>Build the browser WASM package and compile the server daemon:</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">Shell</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code># Build WASM package and copy to frontend assets
wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
# Build release server daemon
cargo build -p chronoseal-server --bin chronoseal --release</code></pre>
</div>
<p>The compiled binary is written to <code>target/release/chronoseal</code>.</p>
<h2>Native Service Installation</h2>
<p>The easiest way to deploy ChronoSeal on Linux is using our installer script. This creates a dedicated system user, configures directory paths, copies assets, and sets up systemd sandboxing:</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">Shell</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>sudo bash scripts/install.sh</code></pre>
</div>
<p>Verify installation operational status:</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">Shell</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>sudo systemctl status chronoseal
chronoseal health
sudo journalctl -u chronoseal -f</code></pre>
</div>
<h2>Configuration Precedence</h2>
<p>ChronoSeal resolves configuration variables in this order:
<code>CLI parameters</code> &gt; <code>CHRONOSEAL_* Environment Variables</code> &gt; <code>TOML file</code> &gt; <code>Defaults</code>.
</p>
<p>The TOML configuration is searched at:
<code>$CHRONOSEAL_CONFIG</code>, <code>/etc/chronoseal/config.toml</code>, <code>~/.config/chronoseal/config.toml</code>.
</p>
<h3>Common Environment overrides</h3>
<ul>
<li><code>CHRONOSEAL_BIND</code>: Binding address (e.g. <code>127.0.0.1:3000</code>).</li>
<li><code>CHRONOSEAL_DB_TYPE</code>: <code>sqlite-in-memory</code>, <code>sqlite-in-disk</code>, or <code>valkey</code>.</li>
<li><code>CHRONOSEAL_VALKEY_ADDR</code>: Address of Valkey server (defaults to <code>127.0.0.1:6666</code>).</li>
<li><code>CHRONOSEAL_FRONTEND_DIR</code>: Directory containing frontend static assets.</li>
</ul>
<h2>Reverse Proxy Configuration (Nginx)</h2>
<p>Ensure ChronoSeal runs behind TLS in production. Secure proxy traffic to the local daemon port:</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">Nginx</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}</code></pre>
</div>
<h2>Docker Compose Deployment</h2>
<p>Build and run the container service (configured for non-root execution by default):</p>
<div class="code-block">
<div class="code-header">
<span class="code-lang">Shell</span>
<button class="code-copy-btn">Copy</button>
</div>
<pre><code>bash scripts/build.sh
docker compose up -d --build</code></pre>
</div>
<h2>Production Checklist</h2>
<ul>
<li>Ensure WASM modules in <code>frontend/pkg</code> are rebuilt with the <code>--release</code> flag.</li>
<li>Serve all ChronoSeal endpoints exclusively over HTTPS.</li>
<li>Restict server bind address to localhost (<code>127.0.0.1</code>) behind a reverse proxy.</li>
<li>Run the daemon service under a dedicated unprivileged user account.</li>
<li>Disable debug logging (avoid <code>CHRONOSEAL_LOG=debug</code>) in production to protect credentials.</li>
<li>Configure Valkey or persistent SQLite for production session storage.</li>
</ul>
</article>
<!-- Pager -->
<nav class="doc-pager" aria-label="Doc navigation Pager">
<a href="operations.html" class="doc-pager-link">
<div class="doc-pager-label">Previous</div>
<div class="doc-pager-title">Operations Handbook</div>
</a>
<a href="testing.html" class="doc-pager-link doc-pager-link--next">
<div class="doc-pager-label">Next</div>
<div class="doc-pager-title">Testing Strategy</div>
</a>
</nav>
<div class="doc-meta">
Last Updated: June 2026 (v1.0.2)
</div>
</main>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
</h4>
<p>Next-generation browser attestation daemon built for the modern web.</p>
</div>
<div class="footer-section">
<h4>Resources</h4>
<a href="philosophy.html">Design Philosophy</a>
<a href="architecture.html">Architecture</a>
<a href="protocol.html">Protocol</a>
<a href="api.html">API Reference</a>
<a href="comparison.html">Comparison</a>
</div>
<div class="footer-section">
<h4>Community</h4>
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
</div>
<div class="footer-section">
<h4>Legal &amp; Security</h4>
<a href="security.html">Security Policy</a>
<a href="privacy.html">Privacy Policy</a>
<a href="performance.html">Performance Tuning</a>
<a href="operations.html">Operations Guide</a>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
</div>
</footer>
<!-- Search Overlay -->
<div class="search-overlay">
<div class="search-box">
<div class="search-input-wrap">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
<span class="search-kbd">/</span>
</div>
<div class="search-results"></div>
</div>
</div>
<!-- Back to top -->
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
<!-- JS Scripts -->
<script src="js/search.js"></script>
<script src="js/app.js"></script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

+43
View File
@@ -0,0 +1,43 @@
name: chronoseal-www
services:
chronoseal:
cpu_shares: 90
command: []
container_name: chronoseal-www
deploy:
resources:
limits:
memory: "33491517440"
hostname: chronoseal-www
image: nginx:alpine
labels:
icon: https://github.com/thakares/chronoseal-rs/raw/main/logo/chronoseal.svg
networks:
default: null
ports:
- mode: ingress
target: 80
published: "8383"
protocol: tcp
restart: unless-stopped
volumes:
- type: bind
source: /DATA/AppData/chronoseal-www
target: /usr/share/nginx/html
read_only: true
bind:
create_host_path: true
networks:
default:
name: chronoseal-www_default
x-casaos:
author: self
category: self
hostname: ""
icon: https://github.com/thakares/chronoseal-rs/raw/main/logo/chronoseal.svg
index: /
is_uncontrolled: false
port_map: "8383"
scheme: http
title:
custom: chronoseal-www
+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="296.99997mm"
viewBox="0 0 1122.5196 793.7008"
enable-background="new 0 0 1254 1254"
xml:space="preserve"
height="210mm"
sodipodi:docname="chronoseal.svg"
inkscape:export-filename="logo1.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.1654266"
inkscape:cx="561.16791"
inkscape:cy="396.85039"
inkscape:window-width="2048"
inkscape:window-height="1205"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><defs
id="defs44" />
<path
fill="none"
opacity="0"
stroke="none"
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path2" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path45"
style="fill:#e1e1e4;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path46"
style="fill:#b0b2b8;fill-opacity:1" /><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path47"
style="fill:#7b7e85;fill-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
x="154.54701"
y="550.13623"
id="text47"><tspan
id="tspan47"
x="154.54701"
y="550.13623" /><tspan
id="tspan48"
x="154.54701"
y="750.13623"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
id="tspan49">chrono</tspan>seal</tspan></text><path
fill="#4b4d51"
opacity="1"
stroke="none"
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
id="path1"
style="fill:#4a4d53;fill-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more