Wire format
Protocol objects move between the registrar, wallets, and verifiers as JSON, but JSON is only the transport. The bytes that are signed and hashed are always the canonical BCS encoding described here, carried inside the JSON as an opaque base64url string. A verifier checks signatures over those bytes, never over the JSON that transported them.
Canonical bytes and the domain tag
Section titled “Canonical bytes and the domain tag”The only bytes ever signed or hashed are the BCS serialisation of a pair: a version-tagged label identifying the object’s kind, and the object body.
canonical = BCS( ( "<version-tagged kind label>", body ) )The kind label is inside the signed bytes, not alongside them. This makes cross-object replay structurally impossible: a signature produced over one kind of object cannot be presented as a signature over a different kind, even when the two bodies would otherwise encode identically. Versioning the label means a future revision of an object’s shape is a distinct signing domain rather than an ambiguous re-parse of the old one.
Verification decodes only after the signature check succeeds, and only under the kind the caller expected. A single tampered byte therefore fails at the signature step, before the payload is ever interpreted.
Signed object envelopes
Section titled “Signed object envelopes”A signed protocol object travels in an envelope carrying the canonical bytes, the signing key, and the signature over those bytes:
{ "payload": "…", // base64url(no-pad) of the canonical bytes "signer_pk": "…", // lowercase-hex Ed25519 public key "sig": "…" // base64url(no-pad) Ed25519 signature over the payload bytes}The recipient verifies the signature over the decoded payload bytes and then decodes them under the expected object kind. Where a specific signer is required — the employer’s key, the registrar key named by the current epoch, the holder’s per-employer subject key — the check also pins the key, so a validly signed object presented by the wrong party is rejected.
Authenticated envelopes
Section titled “Authenticated envelopes”Operations that mutate state are authenticated by the signature on the object or envelope itself; there is no separate session concept in the protocol. Where the operation, and not merely the object inside it, needs to be authenticated, the object is wrapped in an envelope that adds a nonce and a timestamp:
{ "payload": "…", // base64url(no-pad) of the canonical bytes "signer_pk": "…", // lowercase-hex Ed25519 public key "sig": "…", // base64url(no-pad) Ed25519 signature over the payload bytes "nonce": "…", // unique per signer, single use "ts": 0 // unix seconds}Replay defence rests on both fields together:
- Nonces are unique per signer and recorded, so a captured envelope cannot be submitted a second time.
- Timestamps must fall within five minutes of the registrar’s clock, which bounds how long a captured envelope is even worth replaying and bounds the nonce history that must be retained.
- Share Grant nonces are single-use per audience, so a grant captured by one audience cannot be presented as consent to another.
Operation receipts
Section titled “Operation receipts”Every mutating operation that appends to the log returns an operation receipt, so the caller retains a witnessed position in the log rather than having to trust that the append happened:
{ "log_seq": 0, // the sequence number this operation was assigned "entry_hash": "…", // the hash of the appended entry "log_head": { … } // the Signed Head covering that sequence number}The head inside a receipt is itself a signed object, in exactly the envelope shape above: it is the registrar’s signature over the employer, epoch, sequence number, and head hash. A receipt is therefore self-contained evidence — its recipient can verify the head’s signature offline and later use it to detect a registrar that publishes a conflicting head at the same position.
Wallets retain receipts as inclusion evidence; see Log & epochs and Misbehavior & switching.