Refactor attestation engine and synchronize project documentation

- Refine session and storage lifecycle handling
- Improve VM extension architecture across server, shared, and WASM runtimes
- Enhance synthetic gene mutation engine integration and parity guarantees
- Align deterministic state progression between server and browser execution paths
- Update configuration examples and deployment guidance
- Expand architecture, API, threat model, privacy, and WASM build documentation
- Refresh README with comprehensive project overview, operational workflows,
  browser integration details, storage backend documentation, and security model
- Document v0.6.0 refactoring outcomes and design rationale
- Improve consistency across documentation, configuration, and implementation

This commit consolidates the v0.6.0 architectural refactoring effort,
strengthening deterministic browser/server parity while improving
maintainability, operational clarity, and project documentation.
This commit is contained in:
thakares committed 2026-05-29 21:55:08 +05:30
1 parent 2b8afd54e0
commit 0ed3cb444d
15 files changed
+2357 -797

No files matched your search

+242 -85
View File
@@ -1,16 +1,56 @@
# ChronoSeal API Reference
ChronoSeal defines a small, deterministic API surface for browser attestation and heartbeat verification.
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 endpoints are relative to the server root. In development: `http://localhost:3000`. In production: the HTTPS origin of the protected site.
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
## POST /init
Production deployments should use HTTPS. The daemon itself can run behind a local reverse proxy.
Initialise a new browser session.
## 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
@@ -25,11 +65,18 @@ Content-Type: application/json
}
```
| Field | Type | Description |
|---|---|---|
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 public key generated by the WASM runtime |
| Field | Type | Required | Description |
|---|---|---:|---|
| `public_key` | string | yes | Browser-generated Ed25519 public key as 64 hex characters |
### Response `200 OK`
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
{
@@ -48,26 +95,26 @@ Content-Type: application/json
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Opaque session identifier for the current browser session |
| `salt` | `string` | Random 16-byte salt used to seed the hash chain |
| `opcodes_b64` | `string` | Base64-encoded randomized VM program executed on every heartbeat |
| `initial_hash` | `string` | Initial chain hash `H(0)` used as `prev_hash` for the first heartbeat |
| `expires_at` | `number` | Unix timestamp in milliseconds after which the session expires |
| `heartbeat_min_interval_ms` | `number` | Minimum heartbeat interval in milliseconds |
| `heartbeat_max_interval_ms` | `number` | Maximum heartbeat interval in milliseconds |
| `gene_size` | `number` | Size of the initial synthetic gene buffer |
| `mutation_step` | `number` | Initial mutation step expected on the first heartbeat |
| `mutation_order_b64` | `string` | Base64-encoded mutation order for gene commitment preview |
| `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
### Error Behavior
`POST /init` returns `500 Internal Server Error` only for server-side failures such as invalid public key length or persistence errors. No detailed error information is exposed to callers.
`/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
## `POST /hb`
Submit a heartbeat to continue the session.
Submits one heartbeat for an existing session.
### Request
@@ -92,7 +139,7 @@ Content-Type: application/json
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": 2,
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"mutation_step": 1,
@@ -101,45 +148,61 @@ Content-Type: application/json
}
```
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Session ID from `/init` |
| `prev_hash` | `string` | Previous hash chain head (`initial_hash` on first heartbeat) |
| `timestamp` | `number` | `Date.now()` in milliseconds |
| `entropy_data.events` | `array` | Mouse event list since the previous heartbeat |
| `stack_state.stack` | `array` | VM stack contents after program execution |
| `stack_state.ip` | `number` | VM instruction pointer after execution |
| `fingerprint.aspectRatio` | `string` | `screen.width / screen.height` to 10 decimal places |
| `fingerprint.devicePixelRatio` | `number` | `window.devicePixelRatio` |
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency || 1` |
| `mutation_step` | `number` | Current mutation step sent by the client |
| `gene_commitment` | `string` | Gene commitment produced by the WASM preview mutation engine |
| `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload |
| 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 | Positive hardware concurrency value |
| `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 client signs a canonical JSON object with top-level keys sorted alphabetically:
The signature covers a canonical JSON object with sorted top-level keys:
```json
{
"entropyData": { "events": [{ "t": ..., "x": ..., "y": ... }] },
"entropyData": { "events": [{ "t": 1234.567, "x": 412.0, "y": 308.5 }] },
"fingerprint": {
"aspectRatio": "...",
"devicePixelRatio": ...,
"hardwareConcurrency": ...
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"geneCommitment": "...",
"mutationStep": ...,
"prevHash": "...",
"sessionId": "...",
"stackState": { "ip": ..., "stack": [...] },
"timestamp": ...
"geneCommitment": "64-char hex",
"mutationStep": 1,
"prevHash": "64-char hex",
"sessionId": "64-char hex",
"stackState": { "ip": 42, "stack": [2971406957, 1234567890] },
"timestamp": 1234567890123
}
```
Note: the signed payload uses camelCase while the transport request uses snake_case.
Important details:
### Response `200 OK` — Accepted
- 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
{
@@ -152,12 +215,19 @@ Note: the signed payload uses camelCase while the transport request uses snake_c
| Field | Type | Description |
|---|---|---|
| `status` | `string` | Always `ok` |
| `next_salt` | `string` | Next server salt for the following heartbeat |
| `next_mutation_step` | `number` | Next mutation step to apply after acceptance |
| `next_mutation_order_b64` | `string` | Base64-encoded next mutation program |
| `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 |
### Response `200 OK` — Rejected
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
{
@@ -165,45 +235,132 @@ Note: the signed payload uses camelCase while the transport request uses snake_c
}
```
A rejected heartbeat omits `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
Rejected heartbeats omit:
This silent rejection model avoids giving attackers distinct failure signals.
- `next_salt`
- `next_mutation_step`
- `next_mutation_order_b64`
---
This response shape is intentional. The server does not reveal which validation stage failed.
## Validation Rules
### Heartbeat Validation Order
Heartbeats are rejected silently when any validation step fails:
The server currently validates heartbeats in this order:
* session missing or expired
* signature invalid
* hash chain mismatch
* mutation step mismatch
* gene commitment mismatch
* timestamp outside ±30 seconds
* insufficient mouse events
* insufficient mouse movement
* unrealistic speed profile
* missing pause intervals
* invalid fingerprint values
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 WASM module exports the following functions to JavaScript:
The generated `chronoseal_wasm` package exposes:
| Function | Signature | Description |
|---|---|---|
| `generate_keypair()` | `() -> string` | Generate a new Ed25519 keypair and return the public key hex |
| `get_public_key()` | `() -> string` | Return the current public key hex |
| `sign_message(msg)` | `(string) -> string` | Sign a UTF-8 string payload and return the hex signature |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | Compute the next Blake3 hash chain value |
| `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` | Initialise the synthetic gene buffer in WASM memory |
| `preview_gene_commitment(order_b64)` | `(string) -> string` | Preview the next gene commitment from a mutation order |
| `commit_gene_preview()` | `() -> bool` | Commit the previewed mutation after an accepted heartbeat |
| `discard_gene_preview()` | `() -> void` | Discard the previewed mutation after rejection or error |
| `current_gene_commitment()` | `() -> string` | Return the current committed gene commitment |
| `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 return `""` on error. Callers must handle empty values and boolean failures gracefully.
String-returning functions use `""` to signal failure. Callers must handle empty strings explicitly.