avp

Alt Vault Protocol, an open zero-knowledge spec for sharing alts across clients.

Examples

A worked end-to-end flow, representative message bodies, and runnable reference implementations, to make the spec concrete. The prose below is illustrative: hosts are vault.example, members are alice and bob, and the base64 key and ciphertext values shown in the worked flow are fixtures, not real cryptographic output. The runnable implementations, by contrast, do the real crypto (see Implementations below). 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/
Go go/server/ go/client/

All five examples implement the real envelope and key-wrap crypto (payload AEAD and X25519-HKDF-SHA256-AESGCM-v1), each verified byte-for-byte against ../vectors/. Cross-language wire compatibility is checked continuously by the examples-interop workflow: each language’s client runs against the Go reference server, and the Go reference client runs against each language’s server. Go and TypeScript are the fullest worked references for the cryptographic core.

For the gRPC profile, grpc/ is a runnable client and server that load the canonical ../proto/avp.proto directly and exercise all six Vault RPCs over a gRPC channel (the examples-grpc workflow runs it end to end). The message objects and crypto are identical to the HTTP/JSON examples; only the transport differs.

Conformance tooling lives in conformance/ (it checks the repo’s ../vectors/). From the repo root, task test (go-task) runs the conformance suite and every language’s example tests in one command; per-language targets (task go, task rust, …) run just one toolchain. 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.