Security and trust model

Openwake is a record. Its first security property is integrity, the record cannot be quietly altered, and its second is confidentiality for the small amount of private data it holds about customers. Most of the registry is public by design.

What we hold

DataSensitivityProtection
Vendor facts, quotes, snapshotspublic by designhash-chained ledger, signed entries, daily anchors
Watchlists (a customer's vendor list)reveals their stackorg-scoped; DB encryption at rest
Emails, Slack webhook URLssecretssealed at rest with AES-256-GCM (OPENWAKE_DATA_KEY); opened only at delivery
Counterparty checksan org's agent activityorg-scoped; data classes only: payloads are never accepted, by API shape
API keyssecretsstored as sha256; shown once
Peopleidentityprovider subject (google:…, microsoft:…, email:…, org:<id>:…), sealed email, sha256(email) for lookups; no password, ever. Sign-in is OpenID Connect straight to Google / Microsoft / the org's own issuer (PKCE, nonce, ID token verified against the issuer's keys), or a six-digit email code (hashed, ten minutes, five tries)
Second factorsecretsTOTP secret sealed; recovery codes hashed; enforced on every sign-in once enabled
Agent activityrecordkind, counterparty, data classes, outcome, policy verdict; detail sealed; every event is a ledger entry (agent_event), so the activity log is hash-chained and signed like checks
Org SSOsecretsthe org's OIDC client secret sealed; the email domain routes only after one verified sign-in
Sessionssecretsa sealed {uid, iat, exp, sid} in an HttpOnly cookie, 30 days; the API verifies it with the same data key and never stores it

The ledger

Every snapshot, fact set, change, document change and check is appended to one hash chain:

hash = sha256(prev_hash | kind | ref_id | payload_hash | created_at)
payload_hash = sha256(canonical JSON of the record)
signature = Ed25519(hash)   with OPENWAKE_SIGNING_KEY

Receipts

check_counterparty returns a self-contained receipt (version: openwake-receipt/1): the exact statement that was recorded (counterparty, data classes, decision, reasons, the fact set it was decided on and that fact set's own ledger entry), the ledger entry that includes it (seq, prev_hash, payload_hash, hash, signature, key_id, created_at), the public anchor that covers it when one exists, and the recipe. The caller stores it as its own evidence of what Openwake said, when.

Verify with nothing but the public key: no call to Openwake:

payload_hash == sha256(canonical(statement))        canonical = keys sorted recursively, no whitespace
hash         == sha256(prev_hash|kind|ref_id|payload_hash|created_at)
Ed25519.verify(signature, hash-as-hex-bytes, public key for key_id)

The shape follows the SCITT idea of a signed statement plus a transparency receipt; the content is what makes it Openwake's: the counterparty's cited data practices at decision time.

Keys

pnpm keys:generate prints OPENWAKE_SIGNING_KEY and OPENWAKE_DATA_KEY. Both are required in production; development falls back to ephemeral / fixed dev keys with a loud warning. Rotate the signing key by publishing the new public key alongside the old at /v1/keys (entries carry key_id).

Key rotation

Rules that never change

  1. The oracle takes data classes, never data.
  2. Facts are quoted or they are not facts.
  3. The ledger is append-only; corrections are new entries, never edits.
  4. Fail-open: Openwake is advisory. The caller's policy decides what happens when we are unreachable.