Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
480d8e642d | ||
|
|
414b74b95f | ||
|
|
227f5ff922 | ||
|
|
72ae2f4b06 | ||
|
|
3397ca121b | ||
|
|
1e038901b3 | ||
|
|
6c11495892 | ||
|
|
a1a2e9d571 | ||
|
|
f4c4beb6b5 | ||
|
|
1004a39667 | ||
|
|
a7cc533ed4 | ||
|
|
40877be160 | ||
|
|
8d119ac00e | ||
|
|
aebef4c623 | ||
|
|
b81b7b0e15 | ||
|
|
3f445eead5 | ||
|
|
3225509713 | ||
|
|
ecb1721ff4 | ||
|
|
d965451d4f | ||
|
|
1a58ef9796 | ||
|
|
c7873b429d | ||
|
|
3679e6808b | ||
|
|
fc2d693518 | ||
|
|
0ed3cb444d | ||
|
|
2b8afd54e0 | ||
|
|
6067746898 | ||
|
|
3d2a1a0ad7 | ||
|
|
815d29af4c | ||
|
|
b2835f454f | ||
|
|
4a64a57347 | ||
|
|
9d52828bb6 | ||
|
|
19666bd608 | ||
|
|
089a834f96 | ||
|
|
ba768da58e | ||
|
|
217dc5f92b | ||
|
|
0b43530651 | ||
|
|
e0f6d7c72c | ||
|
|
9e78daeeba | ||
|
|
60aaf0cd96 | ||
|
|
90ab322332 | ||
|
|
eff782847d | ||
|
|
ed0458633b | ||
|
|
3edea4bdff | ||
|
|
2ac0afa691 | ||
|
|
cbe12bcd49 | ||
|
|
f21146e2f2 | ||
|
|
7972123887 | ||
|
|
630c451688 | ||
|
|
8559e023be | ||
|
|
75cb978fba | ||
|
|
8d318d5da4 | ||
|
|
d567655645 | ||
|
|
4fb4022188 | ||
|
|
256f12023e | ||
|
|
2840ddfc58 | ||
|
|
4b27a342d1 | ||
|
|
851d3b4876 | ||
|
|
ac57752ec2 | ||
|
|
ebfbbf9901 | ||
|
|
b866471825 | ||
|
|
f95b3c0d4a |
No files matched your search
@@ -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.
|
||||
@@ -4,5 +4,9 @@ resolver = "2"
|
||||
members = [
|
||||
"shared",
|
||||
"server",
|
||||
"wasm"
|
||||
"wasm",
|
||||
"chronoseal-replay"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>.
|
||||
@@ -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"] }
|
||||
@@ -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(¤t_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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -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 |
@@ -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"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,5 +1,3 @@
|
||||
import { initHeartbeat } from './heartbeat.js';
|
||||
|
||||
(async () => {
|
||||
await initHeartbeat();
|
||||
})();
|
||||
initHeartbeat().catch(() => {});
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = shared::gene::decode_environment(data);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = shared::vm::execute(data);
|
||||
});
|
||||
|
After Width: | Height: | Size: 168 KiB |
@@ -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 |
@@ -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 |
@@ -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,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
echo "Starting server with static frontend serving..."
|
||||
cd ../server
|
||||
cargo run --release
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 mut rl = state.rate_limiter.lock().await;
|
||||
if !rl.check(&payload.session_id) {
|
||||
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)
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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,2 +1,2 @@
|
||||
pub mod init;
|
||||
pub mod heartbeat;
|
||||
pub mod init;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()))?;
|
||||
|
||||
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],
|
||||
)?;
|
||||
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(next_salt_hex)
|
||||
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 pending_mutation=?1 WHERE session_id=?2",
|
||||
params![vec![0xFFu8], client.session_id.clone()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(×tamp.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);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod constants;
|
||||
pub mod gene;
|
||||
pub mod hashing;
|
||||
pub mod protocol;
|
||||
pub mod vm;
|
||||
pub mod vm_extensions;
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -4,3 +4,4 @@ pub mod entropy;
|
||||
pub mod fingerprint;
|
||||
pub mod transport;
|
||||
pub mod vm;
|
||||
pub mod vm_extensions;
|
||||
@@ -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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
0x08 => {
|
||||
if stack.is_empty() { break; }
|
||||
let a = stack.pop().unwrap();
|
||||
stack.push(!a);
|
||||
}
|
||||
0x09 => {
|
||||
let r = shared::hashing::hash_stack(&stack);
|
||||
stack.clear();
|
||||
stack.push(r);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
StackState { stack, ip: ip as u16 }
|
||||
shared::vm::execute(program)
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[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]);
|
||||
}
|
||||
|
||||
#[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]);
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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>© 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>
|
||||
@@ -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 & 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 & 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 & 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 < 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 & 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>© 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>
|
||||
|
After Width: | Height: | Size: 168 KiB |
@@ -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 |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 168 KiB |
@@ -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 |
@@ -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 & 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 & 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 & 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 & 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>© 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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 & 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 & 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 & 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 & 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> > <code>CHRONOSEAL_* Environment Variables</code> > <code>TOML file</code> > <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 & 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>© 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>
|
||||
|
After Width: | Height: | Size: 192 KiB |
@@ -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
|
||||
@@ -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 |