Skip to the content.

Examples

A worked end-to-end flow, representative message bodies, and runnable reference implementations, to make the spec concrete. Everything here is illustrative: hosts are vault.example, members are alice and bob, and the base64 key and ciphertext values are placeholders, not real cryptographic output. See ../SPEC.md for the normative rules and ../vectors/ for byte-exact construction vectors.

Field names below are the HTTP/JSON profile (proto field names in camelCase); the gRPC profile carries the same fields.

Implementations

Reference clients and servers, organized as examples/<language>/<client|server>/. Each implements the HTTP/JSON profile against in-memory state and is illustrative, not production.

Language Server Client
TypeScript typescript/server/ typescript/client/
Rust rust/server/ rust/client/
Python python/server/ python/client/
Java java/server/ java/client/

Conformance tooling lives in conformance/ (it checks the repo’s ../vectors/). More languages are welcome; see ../CONTRIBUTING.md.

The cast

1. alice creates a repository

alice generates a per-repo data key, wraps it to her own X25519 key, encrypts the initial alt payload, and calls createRepo. See create-repo-request.json. The server stores it and returns the manifest. The repository’s address is now:

avp://vault.example/3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f

2. bob asks to join

bob sends alice an invite request out of band: a base64url JSON of his public keys (SPEC section 8.1).

{ "v": 1, "ed25519PublicKey": "Ym9iLWVkMjU1MTktcHVibGljLWtleQ", "x25519PublicKey": "Ym9iLXgyNTUxOS1wdWJsaWMta2V5" }

base64url-encoded, that is one opaque token bob pastes to alice.

3. alice adds bob

alice wraps the repo data key to bob’s X25519 key (client-side) and calls addMember with the resulting MemberEntry. She then replies to bob with a repo locator:

{ "v": 1, "host": "vault.example", "repoId": "3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f", "schemeId": "X25519-HKDF-SHA256-AESGCM-v1", "keyEpoch": 0, "issuerJwksUrl": "https://idp.example/.well-known/jwks.json" }

base64url-encoded, this tells bob where the repository lives and which identity provider to trust for key bindings.

4. bob authenticates and pulls

bob decodes the locator, resolves a transport for vault.example, and authenticates with his own keypair (SPEC section 3): challenge returns a nonce, bob signs the raw nonce bytes, token returns a bearer token scoped to vault.example. bob then calls pull with knownPayloadVersion: 0 and gets the manifest and current envelope. See pull-response.json. bob unwraps the data key from his MemberEntry and decrypts the payload.

5. someone pushes a change

A member adds an alt, re-encrypts the payload at the next version, and calls push with expectedPayloadVersion set to the version they pulled. See push-request.json. If another member wrote first, the server returns { "accepted": false, "conflict": true } with the current version, and the client pulls, re-applies, and retries.

6. removing a member rotates the key

To remove a member, a remaining member rotates the data key to a new epoch, re-wraps it to everyone who stays, re-encrypts the payload, and calls removeMember with the removed id, the rotated envelope, the re-wrapped roster, and the new epoch, all applied atomically. The removed member’s old wrapped key cannot derive the new epoch’s data key, and the AAD makes a stale-epoch envelope fail authentication.

Anti-MITM in one line

When bob joins on a server he does not operate, he verifies each served MemberEntry.keyBindingSig against the issuer from the locator before wrapping a data key to it (SPEC section 9). The signed message is utf8(ed25519PublicKey + "|" + x25519PublicKey); see ../vectors/key-binding-message.json.