← Developers

05.2 — Signed Ingest & Evidence

Spec matching api v1

The Invariant

The signature covers the wire bytes, not the data. X-Edge-Signature: <alg>.<b64> is computed over the exact HTTP body (req.rawBody) — reserialized JSON would fail verification. Integrity and attribution are properties of the channel, not of a reconstructed payload. Two algorithms are accepted server-side: ed25519 (software keys, the default issued at claim) and es256 (ECDSA P-256/SHA-256, for TPM-bound signers).

The Contract

Surface Mechanism Fail mode
POST /sync/ingest x-edge-signature on raw body 403 EDGE_SIGNATURE_REQUIRED / EDGE_SIGNATURE_INVALID
POST /sync/edge-audit same, on the NDJSON string same
POST /bootstrap/claim body carries node_pubkey (SPKI DER b64) + optional hw_meta invalid key → 400

Nodes with a registered pubkey are fail-closed: unsigned → EDGE_SIGNATURE_REQUIRED, wrong key → EDGE_SIGNATURE_INVALID. Nodes without one (pre-rollout claims, manual env provisioning) are accepted — the soft-rollout whitelist, declared in the threat model. Replay is harmless: cp_event_dedup makes retransmission a no-op, so signatures carry no timestamp.

Signed ingest batches are stored verbatim in cp_ingest_batches: raw_body (bytea) + signature + node_id + event_count. Every event carries batch_id back to its signed envelope — the oracle re-verifies the original bytes.

The Flow

edge:  claim → keypair on device → privkey to edge-credentials.json (0600)
sync:  body bytes → sign → X-Edge-Signature → CP verifies vs registered pubkey
store: signed batch → cp_ingest_batches (raw_body + sig) → events.batch_id
audit: GET /public/verify/event/:id → signature_valid + membership + untampered

Payload Example

curl -X POST $CP/api/v1/sync/ingest \
  -H "Authorization: Bearer $EDGE_API_KEY" \
  -H "x-edge-node: $NODE_ID" \
  -H "x-edge-signature: ed25519.$(sign_raw_body)" \
  -H 'content-type: application/json' \
  --data-binary @batch.json        # exact bytes signed — no re-serialization

Known Failures

Declared threat model — what each layer does and does not cover:

Layer / symptom Protects against / cause → fix
Privkey at 0600 on device Remote/software compromise — not physical SD/SSD extraction
Ed25519 over rawBody In-transit tampering, wrong-node forgery — not replay (absorbed by dedup, by design)
cp_ingest_batches Post-ingest alteration of signed events — not row deletion (oracle answers found:false) nor unsigned legacy events
EDGE_SIGNATURE_INVALID Signature computed on reserialized JSON → sign the literal request body bytes
Outbox ax_cdc_outbox Network blackout — writes stay committed locally and drain on reconnect; not at-rest encryption (protect the host disk separately)