Deterministic CBOR. Domain-separated signatures.

Every byte that moves between client and server (and every byte that's signed) is deterministic CBOR per RFC 8949 §4.2.1. No ambiguity, no canonicalisation surprises, no JSON-style whitespace games.

Determinism rules

Maps are sorted by key length first, then lexicographically. Integers use the shortest encoding (no length-padded majors). Strings are UTF-8 NFC. There is exactly one valid CBOR encoding for every fd0 object — encode and re-encode produces byte-identical output, and that's what gets signed.

Domain separation

Every signature is computed over domain || cbor(object); every AEAD AAD carries a domain string. A ciphertext or signature valid under one domain is invalid under any other. Disjointness (no prefix-equal pair) is enforced by TestDomainSeparatorsDisjoint in internal/proto/proto_test.go.

Domains in use
fd0-event-v1ScopeEvent signatures
fd0-user-event-v1auth.set signatures (user chain)
fd0-card-v1Identity card signatures
fd0-http-request-v1Per-request HTTP auth
fd0-encrypted-super-priv-v1Auth-method AEAD (passphrase/yubikey wrap)
fd0-vault-body-v1Vault body AAD
fd0-vault-wrap-v1Vault wrap header AAD
fd0-recovery-key-v1Recovery file AEAD
fd0-translog-sth-v1STH signature input
fd0-witness-cosign-v1Witness cosign input

Event chains

Both the per-user chain (auth methods) and the per-scope chain (members + secrets) are linear append-only signed logs. Each event commits to its predecessor by prev_hash = SHA-256(cbor(prev.SignedPrefix)). The first event has prev_hash = nil; the server checks the chain on every push.

ScopeEvent — internal/proto/types.go
ScopeEvent = {
  signed_prefix : SignedPrefix,
  signature     : Signature,
}

SignedPrefix = {
  kind           : "member.change" / "secret.set",
  scope          : tstr / nil,                ; nil iff genesis
  prev_hash      : bstr .size 32 / nil,
  author         : bstr .size 32,             ; signer super_pub
  seq            : uint,
  oek_version    : uint,
  key_deliveries : [* KeyDelivery],           ; non-empty for member.change
  payload        : <kind-specific>,
}

event_id       = "e_" || base32(truncate_128(SHA-256(cbor(SignedPrefix))))
prev_hash[N+1] = SHA-256(cbor(SignedPrefix[N]))