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
- alice and bob: two members, each an Ed25519 keypair (plus an X25519 key for wrapping). Their Ed25519 public key is their member id.
vault.example: the server that hosts the repository. Either member could be pointed at a different server by default; they reach this repository by its address.
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.