docs: update README and docs for v0.6.0 architecture and deployment, preserve current server/shared/wasm updates
This commit is contained in:
1 parent
6067746898
commit
2b8afd54e0
27 files changed
+1413
-2567
No files matched your search
+100
-137
@@ -1,20 +1,18 @@
|
||||
# ChronoSeal — API Reference
|
||||
# ChronoSeal API Reference
|
||||
|
||||
ChronoSeal defines a small, deterministic API surface for browser attestation and heartbeat verification.
|
||||
|
||||
## Base URL
|
||||
|
||||
All endpoints are relative to the server root. In development: `http://localhost:3000`.
|
||||
In production: your HTTPS domain via reverse proxy.
|
||||
All endpoints are relative to the server root. In development: `http://localhost:3000`. In production: the HTTPS origin of the protected site.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
## POST /init
|
||||
|
||||
### `POST /init`
|
||||
Initialise a new browser session.
|
||||
|
||||
Initialise a new session. Called once per page load, immediately after the
|
||||
WASM module generates an Ed25519 keypair.
|
||||
|
||||
#### Request
|
||||
### Request
|
||||
|
||||
```http
|
||||
POST /init
|
||||
@@ -29,17 +27,17 @@ Content-Type: application/json
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 verifying key generated by the WASM module |
|
||||
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 public key generated by the WASM runtime |
|
||||
|
||||
#### Response `200 OK`
|
||||
### Response `200 OK`
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "64-char hex string (32 bytes)",
|
||||
"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,
|
||||
"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,
|
||||
@@ -50,29 +48,28 @@ Content-Type: application/json
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `session_id` | `string` | Opaque session identifier; include in every heartbeat |
|
||||
| `salt` | `string` | Initial salt; used to compute `H(0)` and first `H(1)` |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
#### Error
|
||||
### Error
|
||||
|
||||
Returns `500 Internal Server Error` only on server-side failures (DB errors,
|
||||
invalid public key length). No meaningful error body is returned.
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
### `POST /hb`
|
||||
## POST /hb
|
||||
|
||||
Submit a heartbeat. Called every 12–25 seconds with uniform random jitter.
|
||||
Submit a heartbeat to continue the session.
|
||||
|
||||
#### Request
|
||||
### Request
|
||||
|
||||
```http
|
||||
POST /hb
|
||||
@@ -81,13 +78,12 @@ Content-Type: application/json
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "64-char hex",
|
||||
"prev_hash": "64-char hex",
|
||||
"timestamp": 1234567890123,
|
||||
"session_id": "64-char hex",
|
||||
"prev_hash": "64-char hex",
|
||||
"timestamp": 1234567890123,
|
||||
"entropy_data": {
|
||||
"events": [
|
||||
{ "x": 412.0, "y": 308.5, "t": 1234.567 },
|
||||
{ "x": 415.2, "y": 310.1, "t": 1285.123 }
|
||||
{ "x": 412.0, "y": 308.5, "t": 1234.567 }
|
||||
]
|
||||
},
|
||||
"stack_state": {
|
||||
@@ -95,74 +91,73 @@ Content-Type: application/json
|
||||
"ip": 42
|
||||
},
|
||||
"fingerprint": {
|
||||
"aspectRatio": "1.7777777778",
|
||||
"devicePixelRatio": "2",
|
||||
"aspectRatio": "1.7777777778",
|
||||
"devicePixelRatio": 2,
|
||||
"hardwareConcurrency": 8
|
||||
},
|
||||
"mutation_step": 1,
|
||||
"gene_commitment": "64-char hex Blake3 commitment",
|
||||
"signature": "128-char hex Ed25519 signature"
|
||||
"gene_commitment": "64-char hex",
|
||||
"signature": "128-char hex"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `session_id` | `string` | Session ID from `/init` |
|
||||
| `prev_hash` | `string` | Hash chain head from previous heartbeat (or `initial_hash` for the first) |
|
||||
| `timestamp` | `number` | `Date.now()` in milliseconds; must be within ±30s of server time |
|
||||
| `entropy_data.events` | `array` | Mouse events since previous heartbeat; each has `x`, `y` (px), `t` (performance.now ms) |
|
||||
| `stack_state.stack` | `array` | `u32[]` result of executing the VM program |
|
||||
| `stack_state.ip` | `number` | Instruction pointer after execution |
|
||||
| `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` |
|
||||
| `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 |
|
||||
|
||||
#### Canonical Signing Payload
|
||||
### Canonical Signing Payload
|
||||
|
||||
The client signs the following JSON object. Top-level keys must be sorted
|
||||
alphabetically. Nested object keys follow their natural serialisation order.
|
||||
The client signs a canonical JSON object with top-level keys sorted alphabetically:
|
||||
|
||||
```json
|
||||
{
|
||||
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] },
|
||||
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
|
||||
"geneCommitment":"…",
|
||||
"mutationStep": …,
|
||||
"prevHash": "…",
|
||||
"sessionId": "…",
|
||||
"stackState": { "ip": …, "stack": […] },
|
||||
"timestamp": …
|
||||
"entropyData": { "events": [{ "t": ..., "x": ..., "y": ... }] },
|
||||
"fingerprint": {
|
||||
"aspectRatio": "...",
|
||||
"devicePixelRatio": ...,
|
||||
"hardwareConcurrency": ...
|
||||
},
|
||||
"geneCommitment": "...",
|
||||
"mutationStep": ...,
|
||||
"prevHash": "...",
|
||||
"sessionId": "...",
|
||||
"stackState": { "ip": ..., "stack": [...] },
|
||||
"timestamp": ...
|
||||
}
|
||||
```
|
||||
|
||||
Note: field names in the signing payload use camelCase (`sessionId`,
|
||||
`prevHash`, `entropyData`, `stackState`, `mutationStep`, `geneCommitment`)
|
||||
while the request body uses snake_case (`session_id`, `prev_hash`,
|
||||
`entropy_data`, `stack_state`, `mutation_step`, `gene_commitment`).
|
||||
Note: the signed payload uses camelCase while the transport request uses snake_case.
|
||||
|
||||
#### Response `200 OK` — Accepted
|
||||
### Response `200 OK` — Accepted
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"next_salt": "32-char hex string (16 bytes)",
|
||||
"status": "ok",
|
||||
"next_salt": "32-char hex string",
|
||||
"next_mutation_step": 2,
|
||||
"next_mutation_order_b64": "base64-encoded mutation program"
|
||||
}
|
||||
```
|
||||
|
||||
The client must:
|
||||
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`.
|
||||
| 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 |
|
||||
|
||||
#### Response `200 OK` — Rejected
|
||||
### Response `200 OK` — Rejected
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -170,77 +165,45 @@ The client must:
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
A rejected heartbeat omits `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
|
||||
|
||||
The client should log a warning and continue scheduling heartbeats (they will
|
||||
continue to fail until the page is reloaded and a new session is established).
|
||||
This silent rejection model avoids giving attackers distinct failure signals.
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules (Server-Side)
|
||||
## Validation Rules
|
||||
|
||||
Heartbeats are rejected (silently) if any of the following checks fail:
|
||||
Heartbeats are rejected silently when any validation step fails:
|
||||
|
||||
| Check | Condition for rejection |
|
||||
|---|---|
|
||||
| Rate limit | > 5 requests per 10-second window for this `session_id` |
|
||||
| Session not found | `session_id` not in SQLite |
|
||||
| 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` |
|
||||
| Mouse speed too high | `total_dist / total_time_ms > 2.0 px/ms` |
|
||||
| No mouse pauses | `pause_count < 1` |
|
||||
| Invalid aspect ratio | `ar < 0.5` or `ar > 3.0` |
|
||||
| Invalid devicePixelRatio | `dpr ≤ 0.0` or `dpr > 5.0` |
|
||||
| Zero hardwareConcurrency | `hardware_concurrency == 0` |
|
||||
* 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
|
||||
|
||||
---
|
||||
|
||||
## Hash Chain Specification
|
||||
## WASM Runtime Exports
|
||||
|
||||
```
|
||||
H(0) = Blake3( session_id_bytes ║ pub_key_bytes ║ salt₀_bytes )
|
||||
|
||||
H(n) = Blake3(
|
||||
saltₙ₋₁_bytes
|
||||
║ H(n-1)_bytes
|
||||
║ timestamp_u64_le_bytes
|
||||
║ Blake3( UTF-8( JSON(entropy_data) ) )
|
||||
║ Blake3( UTF-8( JSON(stack_state) ) )
|
||||
)
|
||||
```
|
||||
|
||||
All inputs are concatenated in the order shown. `timestamp` is encoded as a
|
||||
64-bit unsigned integer in little-endian byte order. JSON serialisation of
|
||||
`entropy_data` and `stack_state` uses the field order defined by the shared
|
||||
Rust types (serde derive, no custom ordering).
|
||||
|
||||
---
|
||||
|
||||
## WASM API
|
||||
|
||||
The WASM module (`chronoseal_wasm`) exports the following functions to JavaScript:
|
||||
The WASM module exports the following functions to JavaScript:
|
||||
|
||||
| Function | Signature | Description |
|
||||
|---|---|---|
|
||||
| `generate_keypair()` | `() → string` | Generate Ed25519 keypair; return hex public key. Private key stored in WASM memory. |
|
||||
| `get_public_key()` | `() → string` | Return hex public key, or `""` if not initialised. |
|
||||
| `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. |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
String-returning functions return `""` on error rather than panicking. Callers
|
||||
must check for empty strings and boolean return values before use.
|
||||
String-returning functions return `""` on error. Callers must handle empty values and boolean failures gracefully.
|
||||
Reference in new issue
Block a user