feat: implement v0.6.0 mutation engine and db_type runtime selection
Rust / build (push) Canceled after 0s
Rust / build (push) Canceled after 0s
This commit is contained in:
1 parent
217dc5f92b
commit
ba768da58e
27 files changed
+2615
-158
No files matched your search
+41
-11
@@ -39,7 +39,12 @@ Content-Type: application/json
|
||||
"salt": "32-char hex string (16 bytes)",
|
||||
"opcodes_b64": "base64-encoded VM program (8–16 opcodes)",
|
||||
"initial_hash": "64-char hex string (32 bytes Blake3)",
|
||||
"expires_at": 1234567890123
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -50,6 +55,11 @@ Content-Type: application/json
|
||||
| `opcodes_b64` | `string` | Base64 VM program; execute with `run_program()` on every heartbeat |
|
||||
| `initial_hash` | `string` | `H(0) = Blake3(session_id ║ pub_key ║ salt)`; the first `prev_hash` |
|
||||
| `expires_at` | `number` | Unix timestamp in milliseconds; session expires after 30 minutes of inactivity |
|
||||
| `heartbeat_min_interval_ms` | `number` | Lower bound for randomized heartbeat scheduling |
|
||||
| `heartbeat_max_interval_ms` | `number` | Upper bound for randomized heartbeat scheduling |
|
||||
| `gene_size` | `number` | Initial synthetic gene size used by server and WASM (default 512) |
|
||||
| `mutation_step` | `number` | Server-issued mutation order step expected on next heartbeat |
|
||||
| `mutation_order_b64` | `string` | Base64-encoded mutation opcode program for the current step |
|
||||
|
||||
#### Error
|
||||
|
||||
@@ -89,6 +99,8 @@ Content-Type: application/json
|
||||
"devicePixelRatio": "2",
|
||||
"hardwareConcurrency": 8
|
||||
},
|
||||
"mutation_step": 1,
|
||||
"gene_commitment": "64-char hex Blake3 commitment",
|
||||
"signature": "128-char hex Ed25519 signature"
|
||||
}
|
||||
```
|
||||
@@ -104,6 +116,8 @@ Content-Type: application/json
|
||||
| `fingerprint.aspectRatio` | `string` | `(screen.width / screen.height).toFixed(10)` |
|
||||
| `fingerprint.devicePixelRatio` | `string` | `String(window.devicePixelRatio)` |
|
||||
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency \|\| 1` |
|
||||
| `mutation_step` | `number` | Must match server-side pending mutation step |
|
||||
| `gene_commitment` | `string` | Commitment of the locally previewed candidate gene after applying `mutation_order_b64` |
|
||||
| `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload |
|
||||
|
||||
#### Canonical Signing Payload
|
||||
@@ -115,6 +129,8 @@ alphabetically. Nested object keys follow their natural serialisation order.
|
||||
{
|
||||
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] },
|
||||
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
|
||||
"geneCommitment":"…",
|
||||
"mutationStep": …,
|
||||
"prevHash": "…",
|
||||
"sessionId": "…",
|
||||
"stackState": { "ip": …, "stack": […] },
|
||||
@@ -123,22 +139,28 @@ alphabetically. Nested object keys follow their natural serialisation order.
|
||||
```
|
||||
|
||||
Note: field names in the signing payload use camelCase (`sessionId`,
|
||||
`prevHash`, `entropyData`, `stackState`) while the request body uses
|
||||
snake_case (`session_id`, `prev_hash`, `entropy_data`, `stack_state`).
|
||||
`prevHash`, `entropyData`, `stackState`, `mutationStep`, `geneCommitment`)
|
||||
while the request body uses snake_case (`session_id`, `prev_hash`,
|
||||
`entropy_data`, `stack_state`, `mutation_step`, `gene_commitment`).
|
||||
|
||||
#### Response `200 OK` — Accepted
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"next_salt": "32-char hex string (16 bytes)"
|
||||
"next_salt": "32-char hex string (16 bytes)",
|
||||
"next_mutation_step": 2,
|
||||
"next_mutation_order_b64": "base64-encoded mutation program"
|
||||
}
|
||||
```
|
||||
|
||||
The client must:
|
||||
1. Capture `sentSalt = currentSalt` before updating.
|
||||
2. Set `currentSalt = next_salt`.
|
||||
3. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`.
|
||||
1. Preview commitment locally from `mutation_order_b64` and send it in the heartbeat.
|
||||
2. Capture `sentSalt = currentSalt` before updating.
|
||||
3. Set `currentSalt = next_salt`.
|
||||
4. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`.
|
||||
5. Commit the previewed gene state.
|
||||
6. Replace pending mutation values with `next_mutation_step` and `next_mutation_order_b64`.
|
||||
|
||||
#### Response `200 OK` — Rejected
|
||||
|
||||
@@ -148,7 +170,8 @@ The client must:
|
||||
}
|
||||
```
|
||||
|
||||
`next_salt` is absent. The response body is intentionally identical in
|
||||
`next_salt`, `next_mutation_step`, and `next_mutation_order_b64` are absent.
|
||||
The response body is intentionally identical in
|
||||
structure. Rejections are silent — the caller cannot distinguish a validation
|
||||
failure from a rate limit hit or an expired session.
|
||||
|
||||
@@ -168,6 +191,8 @@ Heartbeats are rejected (silently) if any of the following checks fail:
|
||||
| Session expired | `current_time_ms > expires_at` |
|
||||
| Signature invalid | Ed25519 verification fails against stored public key |
|
||||
| Hash chain broken | `hex(prev_hash) ≠ stored last_hash` |
|
||||
| Mutation step mismatch | `mutation_step ≠ pending_mutation_step` |
|
||||
| Mutation commitment mismatch | `gene_commitment` does not match server-computed candidate commitment |
|
||||
| Timestamp drift | `\|server_now_ms - timestamp\| > 30 000` |
|
||||
| Insufficient mouse events | `events.len() < 3` |
|
||||
| Insufficient mouse distance | `total_dist < 10.0 px` |
|
||||
@@ -202,7 +227,7 @@ Rust types (serde derive, no custom ordering).
|
||||
|
||||
## WASM API
|
||||
|
||||
The WASM module (`antibot_wasm`) exports the following functions to JavaScript:
|
||||
The WASM module (`chronoseal_wasm`) exports the following functions to JavaScript:
|
||||
|
||||
| Function | Signature | Description |
|
||||
|---|---|---|
|
||||
@@ -211,6 +236,11 @@ The WASM module (`antibot_wasm`) exports the following functions to JavaScript:
|
||||
| `sign_message(msg)` | `(string) → string` | Sign UTF-8 string; return hex signature, or `""` if not initialised. |
|
||||
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) → string` | Compute next Blake3 chain hash; all inputs/output hex or JSON strings. |
|
||||
| `run_program(b64)` | `(string) → JsValue` | Execute base64 VM program; return `{ stack: u32[], ip: number }`. |
|
||||
| `init_gene_state(gene_size)` | `(u32) → bool` | Initialise synthetic gene state in WASM memory. |
|
||||
| `preview_gene_commitment(order_b64)` | `(string) → string` | Apply mutation order on preview state and return commitment hex. |
|
||||
| `commit_gene_preview()` | `() → bool` | Commit previewed mutation state after accepted heartbeat. |
|
||||
| `discard_gene_preview()` | `() → void` | Discard previewed mutation state after rejection/error. |
|
||||
| `current_gene_commitment()` | `() → string` | Return current committed gene commitment hex. |
|
||||
|
||||
All functions return empty strings on error rather than panicking.
|
||||
Callers must check for empty return values before using the result.
|
||||
String-returning functions return `""` on error rather than panicking. Callers
|
||||
must check for empty strings and boolean return values before use.
|
||||
@@ -1,5 +1,7 @@
|
||||
# ChronoSeal — Architecture
|
||||
|
||||
> Note (v0.6.0): Synthetic Gene Mutation flow and mutation handshake updates are documented in [REFRACTORING-v0.6.0.md](REFRACTORING-v0.6.0.md) and [API.md](API.md).
|
||||
|
||||
## Overview
|
||||
|
||||
ChronoSeal is a stateless, cryptographic browser attestation framework. Its
|
||||
@@ -42,7 +44,7 @@ synchronisation burden alone makes scaled operation expensive.
|
||||
### High-Level Design
|
||||
|
||||
- **Core**: Rust + Axum (async web framework)
|
||||
- **Storage**: In-memory SQLite (fast, ephemeral per process — restarts are clean)
|
||||
- **Storage**: `db_type` selectable (`sqlite-in-memory`, `sqlite-in-disk`, `valkey` compatibility mode)
|
||||
- **Client**: WASM + Rust (runs in browser for proof generation)
|
||||
- **Security Model**: Behavioral analysis + hash chaining + entropy scoring
|
||||
- **Deployment**: Static musl binary, systemd service, optional Docker
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# ChronoSeal v0.6.0 — Synthetic Gene Mutation System
|
||||
|
||||
## Overview & Motivation
|
||||
ChronoSeal v0.6.0 introduces a synthetic mutation chain model to strengthen attestation liveness and anti-replay guarantees while preserving privacy-first behavior. The core model combines:
|
||||
- a primary byte-oriented gene buffer (`Vec<u8>`), and
|
||||
- a bounded secondary environment map (`Vec<(u16 symbol, u32 quantity)>`).
|
||||
|
||||
Each heartbeat now carries deterministic mutation progression evidence (`mutation_step`, `gene_commitment`) that is validated server-side against the exact server-issued mutation order. This design increases attacker workload by coupling cryptographic chain continuity with stateful deterministic mutation parity.
|
||||
|
||||
## Architectural Goals
|
||||
1. Keep runtime behavior deterministic across server and WASM execution.
|
||||
2. Preserve ephemerality and low operational complexity.
|
||||
3. Minimize additional latency on the heartbeat path.
|
||||
4. Improve protocol resistance against replay and mutation tampering.
|
||||
5. Maintain a maintainable codebase with explicit invariants and focused modules.
|
||||
|
||||
## Design Decisions
|
||||
1. **Shared mutation engine**
|
||||
Mutation opcode semantics live in `shared/src/vm_extensions.rs` to guarantee server/client parity from one implementation.
|
||||
|
||||
2. **Deterministic gene commitment**
|
||||
A domain-separated BLAKE3 commitment (`chronoseal/gene/v1`) binds both gene bytes and sorted environment records.
|
||||
|
||||
3. **Bounded mutation complexity**
|
||||
Mutation program length is capped (`MAX_MUTATION_PROGRAM_BYTES`) and environment cardinality is capped (`MAX_ENV_RECORDS`).
|
||||
|
||||
4. **Strict validation on ingest**
|
||||
Environment payloads are validated for sortedness, uniqueness, non-zero quantity, and length constraints.
|
||||
|
||||
5. **Protocol-level mutation handshake**
|
||||
`InitResponse` and `Heartbeat` payloads now include mutation step/order and commitment fields.
|
||||
|
||||
6. **DB backend control via `db_type`**
|
||||
Server CLI/config now supports:
|
||||
- `sqlite-in-memory` (default)
|
||||
- `sqlite-in-disk` (active; uses `db_path`)
|
||||
- `valkey` (active compatibility mode; currently falls back to in-memory)
|
||||
|
||||
## Implementation Plan
|
||||
1. Add gene model + deterministic commitment in `shared/gene.rs`.
|
||||
2. Implement v0.6.0 mutation opcode set in shared VM extensions.
|
||||
3. Persist mutation state per session (`gene`, `environment`, `pending_mutation`, `pending_mutation_step`).
|
||||
4. Extend protocol schema for mutation fields in init/heartbeat exchange.
|
||||
5. Validate mutation step + commitment parity before accepting heartbeat updates.
|
||||
6. Add WASM preview/commit mutation lifecycle mirroring server behavior.
|
||||
7. Add `db_type` CLI/config flow and runtime backend initialization strategy.
|
||||
8. Add migration-safe schema extension (column existence checks + index creation).
|
||||
|
||||
## Testing Strategy (detailed section)
|
||||
ChronoSeal v0.6.0 test coverage is organized across unit, integration, and randomized/fuzz-style validation.
|
||||
|
||||
1. **Unit, integration, and property tests**
|
||||
- Unit tests for gene invariants and encoding/decoding.
|
||||
- Unit tests for every mutation opcode with stack-effect assertions.
|
||||
- Integration tests for full session lifecycle and heartbeat acceptance/rejection paths.
|
||||
- Table-driven randomized tests and fuzz-style random bytecode tests to validate deterministic failure/success symmetry.
|
||||
|
||||
2. **Server-client parity testing**
|
||||
- Shared opcode engine parity tests across seeded mutation sequences.
|
||||
- Multi-step mutation chain test (`test_mutation_chain`) asserting identical server/client final state.
|
||||
- 10+ heartbeat deterministic simulation tests in session integration suite.
|
||||
|
||||
3. **Evasion / attack simulation testing**
|
||||
- Replay attack simulation.
|
||||
- Mutation step mismatch rejection.
|
||||
- Mutation commitment tampering rejection.
|
||||
- Malformed server mutation payload rejection.
|
||||
- Stack underflow / unknown opcode / truncated program rejection.
|
||||
|
||||
4. **Performance regression testing**
|
||||
- Bounded execution checks through capped program size and bounded record counts.
|
||||
- Timing smoke regression test for mutation execution loops.
|
||||
- End-to-end heartbeat test coverage to detect behavior regressions on hot paths.
|
||||
|
||||
## Security Analysis
|
||||
1. **Replay resistance**
|
||||
Heartbeats are now tied to both chain hash and mutation step progression.
|
||||
|
||||
2. **Mutation tampering resistance**
|
||||
Server recomputes candidate gene state from authoritative pending mutation program and rejects commitment mismatch.
|
||||
|
||||
3. **Protocol ambiguity reduction**
|
||||
Canonical signing payload includes mutation fields, reducing exploitable unsigned state.
|
||||
|
||||
4. **Input hardening**
|
||||
Program size limits, stack underflow checks, and strict environment decoding reduce parser abuse and malformed payload amplification.
|
||||
|
||||
5. **Deterministic failure semantics**
|
||||
Invalid mutation instructions fail predictably and symmetrically across server and WASM paths.
|
||||
|
||||
## Performance Considerations
|
||||
1. Mutation instructions are lightweight and mostly O(1); only `INSERT`/`DELETE` are O(n) but bounded by max gene size.
|
||||
2. Environment operations use sorted-vector binary search with tight upper bound (`MAX_ENV_RECORDS`).
|
||||
3. Commitment hashing is linear in gene size and record count, both bounded.
|
||||
4. Shared engine avoids duplicate logic and divergence-induced debugging overhead.
|
||||
|
||||
## Migration & Backward Compatibility
|
||||
1. Schema migration is additive; new columns are created when missing.
|
||||
2. Existing deployments without mutation fields require updated client+server pair for heartbeat compatibility.
|
||||
3. `db_type` defaults to in-memory to preserve ephemeral behavior.
|
||||
4. `sqlite-in-disk` is now directly usable via `db_path`.
|
||||
5. `valkey` currently runs in compatibility mode (in-memory fallback) to avoid startup failure while preserving CLI contract.
|
||||
|
||||
## Risks & Mitigations
|
||||
1. **Risk: State divergence between server and client**
|
||||
Mitigation: shared opcode engine + deterministic seeded parity tests + multi-heartbeat integration tests.
|
||||
|
||||
2. **Risk: Mutation opcode abuse via malformed programs**
|
||||
Mitigation: strict parsing, length caps, explicit underflow/unknown-opcode errors.
|
||||
|
||||
3. **Risk: Performance regressions**
|
||||
Mitigation: bounded structures, smoke timing tests, and focused hot-path validation.
|
||||
|
||||
4. **Risk: Backend confusion during `db_type` rollout**
|
||||
Mitigation: explicit CLI command (`chronoseal db-type`), config output visibility, and clear runtime compatibility behavior.
|
||||
@@ -1,5 +1,9 @@
|
||||
bind = "0.0.0.0:3000"
|
||||
# sqlite-in-memory (default), sqlite-in-disk, valkey (v0.6.0 compatibility mode)
|
||||
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"
|
||||
# synthetic gene size (1..=65536), default 512
|
||||
gene_size = 512
|
||||
Reference in new issue
Block a user