- 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.
11 KiB
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.
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:
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
POST /init
Content-Type: application/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/1.1 200 OK
Content-Type: application/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
POST /hb
Content-Type: application/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 | 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 signature covers a canonical JSON object with sorted top-level keys:
{
"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
signaturefield is not part of the signed payload. - Nested serialization must match the server's
serde_jsonrepresentation.
The server reconstructs the canonical message from the received request before verifying the Ed25519 signature.
Accepted Response
HTTP/1.1 200 OK
Content-Type: application/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/1.1 200 OK
Content-Type: application/json
{
"status": "ok"
}
Rejected heartbeats omit:
next_saltnext_mutation_stepnext_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:
- Rate-limit check in the route handler.
- Load session by
session_id. - Check session expiration.
- Verify Ed25519 signature.
- Compare
prev_hashwith storedlast_hash. - Compare
mutation_stepwith storedpending_mutation_step. - Apply stored pending mutation to a cloned gene state.
- Compare expected and submitted
gene_commitment. - Enforce timestamp drift.
- Validate mouse entropy.
- Validate fingerprint fields.
- Compute next hash-chain head.
- Generate next mutation order and salt.
- Persist advanced session state.
Any failure after route-level JSON decoding returns the silent rejection body.
GET /health
Returns a basic health response.
GET /health
{
"status": "healthy"
}
GET /stats
Returns storage-derived session statistics.
GET /stats
{
"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.
GET /metrics
# 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_idinitial_hashas the firstprev_hash- current
salt - VM opcode program
- committed gene state
- pending mutation step
- pending mutation order
On accepted /hb:
- Commit the local gene preview.
- Compute the next local hash using the old salt that was active when the heartbeat was sent.
- Replace current salt with
next_salt. - Replace pending mutation step and order with server-provided values.
On rejected /hb:
- Discard the local gene preview.
- Do not advance hash-chain state.
- Do not advance mutation state.
- 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.