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.
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 = {
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]))