diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd5166..4f4a3f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,17 @@ implementation's version. exists: the Ed25519 primitive anchors the challenge signature and the payload-AEAD epoch-tamper case anchors rotation correctness; dedicated challenge/token and rotation vectors are noted as welcome additions rather than implied to already exist. +- Specified that the federation tokens (SPEC section 8.1) use minified JSON in the member order shown as + the RECOMMENDED encoding, while a decoder MUST accept any valid JSON object with the required members. +- Added `THREATMODEL.md`, the adversary model (network, honest-but-curious and malicious server, insider, + removed member, compromised IdP, stolen token/key), cryptographic assumptions, non-goals, and residual + risks; linked from SPEC section 12 and `SECURITY.md`. ### Repository +- Added `vectors/federation.json` and a conformance test for it: the base64url join-handshake tokens + (invite request, repo locator) as a decode oracle plus canonical-encode round-trip, and `avp://` + repository URI parse/format (SPEC section 8). Indexed in `vectors/index.json`. - Added a black-box conformance harness in `harness/`: point it at any running server and it drives the full wire contract, asserting the normative MUSTs (auth failure modes, optimistic-concurrency conflict, membership authorization, key rotation, and zero-knowledge: the plaintext never surfaces in diff --git a/CLAUDE.md b/CLAUDE.md index e611b35..876c009 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,10 @@ repo with Jekyll. The README is the home page. byte-for-byte). **Never edit a vector value without re-verifying all three.** `vectors/index.json` is a machine-readable index of the files (kind, spec section, RFC anchors); keep it in sync when adding or removing a vector. See - `vectors/README.md`. + `vectors/README.md`. `vectors/federation.json` is the exception to the crypto + pattern: base64url join-handshake tokens and `avp://` URI vectors (SPEC §8), + deterministic encoding rather than crypto, using `tokens`/`uris` arrays not + `cases`. - `examples/` are illustrative reference clients and servers (Go, TypeScript, Rust, Python, Java) plus a Node conformance runner that also checks schema/example/index/openapi consistency. They are not production code. @@ -41,6 +44,9 @@ repo with Jekyll. The README is the home page. optimistic-concurrency conflict, membership authorization, key rotation, zero-knowledge). It reuses the vector-tested example crypto; it is the executable check for a *server*, the vectors are the check for the *crypto*. +- `THREATMODEL.md` is the adversary model (what AVP defends, what it does not, the + residual risks). Keep it consistent with SPEC §10 invariants and §12 open items + when the security posture changes. When you change one of `SPEC.md` / `proto/avp.proto` / `schema/avp.schema.json` / `openapi.yaml`, check whether the others need the matching change. They describe diff --git a/README.md b/README.md index 8a321ae..7af036e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,9 @@ A browser or Node client can implement AVP with no gRPC toolchain at all. | [`openapi.yaml`](openapi.yaml) | OpenAPI 3.1 route description for the HTTP/JSON profile. | | [`vectors/`](vectors/) | Conformance test vectors (indexed by [`vectors/index.json`](vectors/index.json)). | | [`examples/`](examples/) | A worked end-to-end flow, representative message bodies, and runnable reference implementations. | +| [`harness/`](harness/) | Black-box conformance suite: run it against any server to assert the wire-contract MUSTs. | | [`Taskfile.yml`](Taskfile.yml) | One-command local runner: `task test` runs the conformance suite and every example. | +| [`THREATMODEL.md`](THREATMODEL.md) | What AVP defends against, what it does not, and the residual risks. | | [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to help. | | [`SECURITY.md`](SECURITY.md) | How to report a vulnerability. | | [`llms.txt`](llms.txt) | Structured entry point for LLMs and AI agents. | diff --git a/SECURITY.md b/SECURITY.md index 8447fc5..fa0b2bb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,6 +11,9 @@ machine-readable artifacts that could lead implementers to build something insec Vulnerabilities in a specific client, server, or library that implements AVP belong to that project, not here. Please report those to the relevant project. +[`THREATMODEL.md`](THREATMODEL.md) describes what AVP defends against, what it does not, and the known +residual risks. A report that sharpens or breaks part of that model is exactly what is most useful here. + ## Reporting Please report security issues **privately**, not in a public issue or pull request. diff --git a/SPEC.md b/SPEC.md index 0793ec7..a05899b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -248,7 +248,10 @@ X25519 key), the joiner publishes its keys first. Two base64url-encoded JSON tok `issuerJwksUrl` MAY be absent when the deployment publishes no key binding. Tokens are base64url (RFC 4648 §5, no padding) over the compact JSON above. They carry only public data -and are safe to relay over any channel. +and are safe to relay over any channel. The RECOMMENDED encoding is minified JSON with members in the +order shown; a decoder MUST accept any valid JSON object that carries the required members, regardless of +member order or insignificant whitespace. See [`vectors/federation.json`](vectors/federation.json) for +byte-exact token and `avp://` URI vectors. ### 8.2 Discovery (optional) @@ -307,7 +310,9 @@ An implementation is conformant if it satisfies every MUST above and reproduces - the **envelope compositions**: payload AEAD (`payload-aead.json`) and the key wrap/unwrap (`key-wrap.json`). The payload-AEAD case includes the epoch-tamper assertion (decryption fails when the AAD's `keyEpoch` changes), which is the cryptographic core of rotation correctness (§10): a - stale-epoch envelope cannot authenticate. + stale-epoch envelope cannot authenticate; +- the **federation encodings**: the join-handshake tokens (§8.1) and `avp://` URIs (§8) in + `federation.json`, base64url and JSON constructions with no key material. See [`vectors/README.md`](vectors/README.md). A **server** additionally proves conformance by passing the black-box harness in [`harness/`](harness/), which drives the full wire contract and asserts the MUSTs of @@ -319,6 +324,9 @@ in [`examples/`](examples/). ## 12. Security considerations and open items +[`THREATMODEL.md`](THREATMODEL.md) gives the full adversary model (what AVP defends, what it does not, and +the residual risks). The open items below are the unresolved pieces of it. + - **Algorithm negotiation.** `schemeId` names the AEAD/KDF/wrap scheme; the choreography for upgrading a repository's scheme over its lifetime is not yet specified. - **Cross-IdP trust (federation).** §9 lets a client verify member key bindings against a single IdP diff --git a/THREATMODEL.md b/THREATMODEL.md new file mode 100644 index 0000000..794e932 --- /dev/null +++ b/THREATMODEL.md @@ -0,0 +1,195 @@ +# AVP threat model + +This document states what the Alt Vault Protocol defends against, what it deliberately does not, and the +residual risks an operator or implementer should understand. It is informative; the normative contract is +[`SPEC.md`](SPEC.md). To report a vulnerability, follow [`SECURITY.md`](SECURITY.md). + +AVP shares alt accounts across clients through a zero-knowledge, federated server. All cryptography +happens on the client; the server stores only ciphertext, per-member wrapped keys, public keys, version +and epoch counters, and opaque signatures. The threat model follows from that single design choice. + +## Assets + +In rough order of sensitivity: + +1. **The alt plaintext.** The account credentials inside the encrypted payload (`accessToken`, and the + `username`, `uuid`, `ban`, and provenance fields around it). This is what AVP exists to protect. +2. **The repository data key.** The per-repo symmetric AES-256 key that encrypts the payload. Anyone who + holds it can read every alt in the repo. +3. **Member private keys.** Each member's Ed25519 identity key (authenticates, and is the member id) and + X25519 key (unwraps the data key). +4. **Bearer tokens.** Short-lived, per-host credentials that authorize vault operations. +5. **Metadata.** The membership roster (public keys), repository sizes, and access patterns. Lower value, + and partly exposed to the server by design (see Non-goals). + +## Actors and trust boundaries + +- **Client.** Trusted. It generates keys, performs all cryptography, and is the only place plaintext, the + data key, and private keys exist. +- **Vault server.** Untrusted for confidentiality and integrity of content; relied on only for storage + and availability. It is zero-knowledge by construction. +- **Identity provider (IdP).** A trust anchor. It issues bearer tokens (§3) and MAY sign member key + bindings (§9). A deployment may run the IdP and vault server together or separately. +- **Members.** Authorized insiders. Every member holds the data key and can read the whole repo. +- **Network.** Untrusted. Transport security is provided by TLS, not by the protocol messages. + +## Adversaries + +### A1. Passive network observer + +*Capability:* reads traffic between client and server. + +*Defended:* payloads and wrapped keys are encrypted (AES-256-GCM, X25519 + HKDF wrap); the data key and +plaintext never appear on the wire. Transport MUST be TLS, so even the ciphertext envelope and bearer +tokens are not exposed in transit. + +*Residual:* without TLS, an observer sees ciphertext, public keys, counters, and bearer tokens. TLS is +mandatory for exactly this reason. + +### A2. Active network attacker (MITM) + +*Capability:* intercepts, modifies, replays, or reorders messages. + +*Defended:* authentication is an Ed25519 challenge over a single-use, short-TTL nonce, so a captured auth +exchange cannot be replayed. Every payload binds `(repoId, payloadVersion, keyEpoch)` into the AES-GCM +AAD, so an envelope cannot be replayed under a different repository, version, or epoch without failing +authentication. Tampering with ciphertext fails the GCM tag. + +*Residual:* an attacker who also controls or impersonates the server is covered by A4. Bearer tokens are +themselves bearer credentials, so TLS plus short token lifetimes are required to limit a capture-and-reuse +window (see A8). + +### A3. Honest-but-curious server + +*Capability:* sees and stores everything the protocol sends it, and tries to learn the alts. + +*Defended:* zero-knowledge is an invariant (SPEC §10). After a create and push, no stored value decodes to +a plaintext alt field. The server holds ciphertext, base64 wrapped-key blobs, raw public keys, counters, +and opaque signatures, and can decrypt none of it. Provenance (`sourceClient`, `sourceUser`) lives inside +the encrypted payload, so cross-client attribution does not weaken this. + +*Residual:* the server learns **metadata**: the membership roster (a set of public keys), repository and +payload sizes, and the timing and frequency of operations. AVP does not hide this from the server it talks +to. See Non-goals. + +### A4. Malicious server + +*Capability:* in addition to A3, returns wrong, stale, or withheld data, and serves chosen key material. + +*Defended:* it still cannot read or forge plaintext (it has no data key). Against the specific attack of +serving a **wrong X25519 key** for a member (to trick an inviter into wrapping the data key to an +attacker), §9 key binding defends: the IdP signs each member's two public keys, and a joiner verifies the +binding before wrapping. This verification SHOULD be done always and MUST be done when the repository's +host is one the client does not itself operate. + +*Residual:* a malicious server can still attack **availability and freshness**. It can withhold updates, +serve a stale but validly-encrypted manifest and envelope, or refuse service. The monotonic +`payloadVersion` and the epoch-bound AAD let a client detect a rollback of content it can compare against, +but a client with no independent reference cannot, on its own, distinguish "no newer version exists" from +"the server is hiding it." Detecting equivocation or stale-state attacks requires out-of-band comparison +or version pinning, which AVP does not currently specify. + +### A5. Malicious or careless member (insider) + +*Capability:* a current member with the data key. + +*Defended:* nothing, by design, with respect to the contents of their own repository. Membership is the +trust boundary: every member can decrypt every alt. Integrity of writes is bounded by optimistic +concurrency (a member cannot silently clobber a concurrent write). + +*Residual:* a member can exfiltrate every alt they can see, and under the v1 policy any member may invite +another (the `addMember` authorization is "any member"). Treat repository membership as a full grant of +read access to everything in that repository, present and future, until the member is removed. + +### A6. Removed member + +*Capability:* a former member who retains keys and anything they previously pulled. + +*Defended:* `removeMember` rotates the data key to a new epoch and re-wraps it only to the remaining +members. The removed member's old wrapped key cannot derive the new epoch's data key, and the epoch in the +AAD makes a stale-epoch envelope fail authentication. They lose access to all **future** writes. + +*Residual:* there is **no backward secrecy**. A removed member keeps every alt they already decrypted +while a member. Rotation protects new data, not old. Rotate credentials (the alts themselves) out of band +if a departure requires revoking access to data already shared. + +### A7. Compromised IdP + +*Capability:* controls token issuance and key-binding signatures. + +*Defended:* the IdP is an explicit trust anchor, not an attacker AVP defends against. It is the smallest +sensible anchor: tokens carry only `sub` and `kind`, with no account or role claims, and a vault server +authorizes purely on membership. + +*Residual:* a compromised IdP can mint a token for any identity (authenticating as anyone at the transport +layer) and can forge key bindings, defeating the A4 defense. In the current single-issuer model a +repository trusts one IdP, named out of band in the repo locator. Cross-issuer trust (pinned-issuer sets, +web-of-trust) is deferred (SPEC §12). Run the IdP with the same care as any signing authority, and rotate +its key set if it is suspected compromised. + +### A8. Stolen bearer token + +*Capability:* possesses a valid, unexpired token. + +*Defended:* tokens are scoped to a single host and to the bearer's repository memberships, and are +short-lived. They never leave TLS. + +*Residual:* until it expires, a stolen token impersonates its subject at that host. Keep token TTLs short +and transport encrypted; a longer TTL is a longer theft window. + +### A9. Stolen member private key + +*Capability:* possesses a member's Ed25519 and/or X25519 private key. + +*Defended:* out of scope for the protocol (see Non-goals); how a client stores keys at rest is a client +concern. + +*Residual:* whoever holds the Ed25519 key can authenticate as that member; whoever holds the X25519 key +can unwrap every data key wrapped to that member. A stolen private key is a full member compromise and is +handled the same as A5 plus removal (A6) once detected. + +## Cryptographic assumptions + +AVP assumes the standard security of its primitives and their correct, side-channel-aware implementation: + +- **Ed25519** (RFC 8032) for identity and signatures; **X25519** (RFC 7748) for key agreement; + **HKDF-SHA256** (RFC 5869) for key derivation; **AES-256-GCM** (RFC 5116) for the payload and wrap AEAD. +- **TLS** for all transport. Bearer tokens and the ciphertext envelope rely on it. +- A **cryptographically secure RNG** for the data key, ephemeral wrap keys, IVs, and challenge nonces. +- **GCM IV uniqueness per key.** A fresh random 12-byte (96-bit) IV is generated per write under a given + data key. Reusing an IV under the same key is catastrophic for GCM (it leaks the XOR of plaintexts and + enables forgery). With random 96-bit IVs the birthday bound keeps collision probability negligible for + the small write volumes a vault sees; implementations SHOULD nonetheless treat key-epoch rotation as the + mechanism that resets the IV space and SHOULD rotate well before approaching 2^32 writes under a single + data key. + +## Non-goals + +AVP deliberately does **not** provide: + +- **Client endpoint security.** Malware on a member's machine, or insecure key storage at rest, defeats + the protocol. Protecting the client device and its key store is the implementer's responsibility. +- **Metadata privacy from the server.** The server it talks to learns the membership roster, repository + and payload sizes, and access timing. AVP hides content, not the shape of the traffic. +- **Availability or anti-censorship guarantees.** A server can refuse or degrade service (A4). AVP has no + built-in replication or quorum. +- **Backward secrecy on removal.** Removal protects future writes only (A6). +- **Server-to-server replication or relay.** Out of scope for this version (SPEC §8.3). +- **Anything about the accounts themselves.** Whether obtaining or using a given account complies with a + service's terms or with applicable law is the implementer's responsibility, not the protocol's. + +## Residual risks and open items + +These are tracked as open items in [`SPEC.md`](SPEC.md) §12 and are the most useful places to contribute a +security review: + +- **Freshness and equivocation against a malicious server** (A4): no in-protocol mechanism proves a + client is seeing the latest, non-forked state. +- **Single-issuer trust** (A7): cross-IdP trust is unspecified. +- **No backward secrecy** (A6): removal does not revoke already-seen data. +- **Metadata exposure to the server** (A3). +- **Bearer-token theft window** (A8): mitigated by TLS and short TTLs, not eliminated. +- **Algorithm agility:** `schemeId` names the AEAD/KDF/wrap scheme, but the choreography for upgrading a + repository's scheme over its lifetime is not yet specified. + +Found something exploitable? Please follow [`SECURITY.md`](SECURITY.md) rather than opening a public issue. diff --git a/examples/conformance/test/federation.test.ts b/examples/conformance/test/federation.test.ts new file mode 100644 index 0000000..ae1fb44 --- /dev/null +++ b/examples/conformance/test/federation.test.ts @@ -0,0 +1,69 @@ +/** + * Federation handshake and addressing vectors (SPEC section 8): the base64url join-handshake tokens + * (invite request, repo locator) and the avp:// repository URI. Token cases are a decode oracle and a + * canonical-encode check; the URI cases pin parse and format of avp:///. + * + * SPDX-License-Identifier: MIT + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import Ajv2020 from "ajv/dist/2020.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, "..", "..", ".."); + +const schema = JSON.parse(readFileSync(join(root, "schema", "avp.schema.json"), "utf8")); +const fed = JSON.parse(readFileSync(join(root, "vectors", "federation.json"), "utf8")); + +const ajv = new Ajv2020({ strict: false, allErrors: true }); +ajv.addSchema(schema, "avp"); + +for (const tc of fed.tokens as Array) { + test(`federation token ${tc.name}: decoding base64url yields the object`, () => { + const obj = JSON.parse(Buffer.from(tc.base64url, "base64url").toString("utf8")); + assert.deepEqual(obj, tc.decoded); + }); + + test(`federation token ${tc.name}: canonical minified encoding round-trips`, () => { + const json = JSON.stringify(tc.decoded); + assert.equal(json, tc.canonicalJson, "canonical JSON mismatch (field order or whitespace)"); + assert.equal(Buffer.from(json, "utf8").toString("base64url"), tc.base64url, "base64url mismatch"); + }); + + test(`federation token ${tc.name}: decoded object validates against ${tc.schema}`, () => { + const validate = ajv.getSchema(`avp#/$defs/${tc.schema}`); + assert.ok(validate, `schema is missing $def ${tc.schema}`); + assert.ok(validate(tc.decoded), JSON.stringify(validate.errors, null, 2)); + }); +} + +// avp:///: the authority is the host (or host:port); the repoId is the single opaque +// path segment and never encodes the host. +const AVP_URI = /^avp:\/\/([^/]+)\/(.+)$/; + +function parseAvpUri(uri: string): { host: string; repoId: string } { + const m = AVP_URI.exec(uri); + if (!m) { + throw new Error(`not an avp:// uri: ${uri}`); + } + return { host: m[1], repoId: m[2] }; +} + +function formatAvpUri(host: string, repoId: string): string { + return `avp://${host}/${repoId}`; +} + +for (const tc of fed.uris as Array) { + test(`avp uri ${tc.name}: parses into host and repoId`, () => { + assert.deepEqual(parseAvpUri(tc.uri), { host: tc.host, repoId: tc.repoId }); + }); + + test(`avp uri ${tc.name}: formats back to the same string`, () => { + assert.equal(formatAvpUri(tc.host, tc.repoId), tc.uri); + }); +} diff --git a/llms.txt b/llms.txt index 46f7f8c..2b37eab 100644 --- a/llms.txt +++ b/llms.txt @@ -34,10 +34,12 @@ Notes for implementers: field names are normative; do not rename or renumber the - [Ed25519 vectors](https://raw.githubusercontent.com/trqlmao/avp/main/vectors/ed25519.json): RFC 8032 Ed25519 vectors anchoring the signature primitive. - [Payload AEAD vectors](https://raw.githubusercontent.com/trqlmao/avp/main/vectors/payload-aead.json): AES-256-GCM payload encryption with the AVP AAD composition. - [Key-wrap vectors](https://raw.githubusercontent.com/trqlmao/avp/main/vectors/key-wrap.json): Full X25519-HKDF-SHA256-AESGCM-v1 wrap composition anchored to RFC 7748. +- [Federation vectors](https://raw.githubusercontent.com/trqlmao/avp/main/vectors/federation.json): Base64url join-handshake tokens (invite request, repo locator) and avp:// repository URIs (SPEC section 8); decode oracle plus canonical-encode and URI parse/format. ## Project - [README](https://raw.githubusercontent.com/trqlmao/avp/main/README.md): Overview, implementer guide, and ways to help. +- [Threat model](https://raw.githubusercontent.com/trqlmao/avp/main/THREATMODEL.md): What AVP defends against, the adversary model (network, honest-but-curious and malicious server, insider, removed member, compromised IdP, stolen token/key), cryptographic assumptions, non-goals, and residual risks. - [Contributing guide](https://raw.githubusercontent.com/trqlmao/avp/main/CONTRIBUTING.md): How to propose changes and the vendor-neutrality rule. - [Security policy](https://raw.githubusercontent.com/trqlmao/avp/main/SECURITY.md): How to report a vulnerability privately. diff --git a/vectors/README.md b/vectors/README.md index f04fb64..5d4352b 100644 --- a/vectors/README.md +++ b/vectors/README.md @@ -19,6 +19,14 @@ this repository: - [`key-binding-message.json`](key-binding-message.json), the canonical key-binding message from SPEC §9: `utf8(ed25519PublicKey + "|" + x25519PublicKey)`. Each case gives the inputs and the expected message as a UTF-8 string. +- [`federation.json`](federation.json), the federation handshake and addressing from SPEC §8. Unlike the + others it uses top-level `tokens` and `uris` arrays rather than `cases`. Each token case (the invite + request and the repo locator) is a **decode oracle**: base64url-decoding `base64url` MUST yield + `decoded`. `canonicalJson` is the recommended minified encoding (members in the order shown) and + `base64url` is its base64url (RFC 4648 §5, no padding) form; the runner round-trips both directions and + validates `decoded` against its schema `$def`. Each `avp://` URI case pins parse and format of + `avp:///` (the host is the authority, with an optional port; the repoId is the single + opaque path segment). ## Cryptographic primitive vectors (RFC-anchored) diff --git a/vectors/federation.json b/vectors/federation.json new file mode 100644 index 0000000..b5f918c --- /dev/null +++ b/vectors/federation.json @@ -0,0 +1,59 @@ +{ + "description": "Federation join-handshake tokens (SPEC section 8.1) and avp:// repository URIs (SPEC section 8). Tokens are base64url (RFC 4648 section 5, no padding) over compact JSON. Each token case is a decode oracle: decoding `base64url` MUST yield `decoded`. `canonicalJson` is the recommended minified encoding (members in the order shown, no insignificant whitespace); a conformant encoder SHOULD reproduce it, and `base64url` is its base64url form. The avp-uri cases pin parse and format of avp:///, where the host is the authority (host or host:port) and repoId is the exact opaque id (the id never encodes the host).", + "tokens": [ + { + "name": "invite-request", + "schema": "InviteRequest", + "direction": "joiner -> inviter", + "decoded": { + "v": 1, + "ed25519PublicKey": "ERERERERERERERERERERERERERERERERERERERERERE=", + "x25519PublicKey": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" + }, + "canonicalJson": "{\"v\":1,\"ed25519PublicKey\":\"ERERERERERERERERERERERERERERERERERERERERERE=\",\"x25519PublicKey\":\"IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=\"}", + "base64url": "eyJ2IjoxLCJlZDI1NTE5UHVibGljS2V5IjoiRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRT0iLCJ4MjU1MTlQdWJsaWNLZXkiOiJJaUlpSWlJaUlpSWlJaUlpSWlJaUlpSWlJaUlpSWlJaUlpSWlJaUlpSWlJPSJ9" + }, + { + "name": "repo-locator-full", + "schema": "RepoLocator", + "direction": "inviter -> joiner", + "decoded": { + "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" + }, + "canonicalJson": "{\"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": "eyJ2IjoxLCJob3N0IjoidmF1bHQuZXhhbXBsZSIsInJlcG9JZCI6IjNiMWY5YzY0LTBiMmEtNGYxZS05YzNkLTFhMmIzYzRkNWU2ZiIsInNjaGVtZUlkIjoiWDI1NTE5LUhLREYtU0hBMjU2LUFFU0dDTS12MSIsImtleUVwb2NoIjowLCJpc3N1ZXJKd2tzVXJsIjoiaHR0cHM6Ly9pZHAuZXhhbXBsZS8ud2VsbC1rbm93bi9qd2tzLmpzb24ifQ" + }, + { + "name": "repo-locator-minimal", + "schema": "RepoLocator", + "direction": "inviter -> joiner", + "note": "Optional schemeId/keyEpoch/issuerJwksUrl absent; host carries a port.", + "decoded": { + "v": 1, + "host": "vault.example:8443", + "repoId": "3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f" + }, + "canonicalJson": "{\"v\":1,\"host\":\"vault.example:8443\",\"repoId\":\"3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f\"}", + "base64url": "eyJ2IjoxLCJob3N0IjoidmF1bHQuZXhhbXBsZTo4NDQzIiwicmVwb0lkIjoiM2IxZjljNjQtMGIyYS00ZjFlLTljM2QtMWEyYjNjNGQ1ZTZmIn0" + } + ], + "uris": [ + { + "name": "plain-host", + "uri": "avp://vault.example/3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f", + "host": "vault.example", + "repoId": "3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f" + }, + { + "name": "host-with-port", + "uri": "avp://vault.example:8443/3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f", + "host": "vault.example:8443", + "repoId": "3b1f9c64-0b2a-4f1e-9c3d-1a2b3c4d5e6f" + } + ] +} diff --git a/vectors/index.json b/vectors/index.json index c18786a..57147d0 100644 --- a/vectors/index.json +++ b/vectors/index.json @@ -72,6 +72,16 @@ "expects": "recompute shared secret + KEK; re-wrap equals committed ciphertext; unwrap recovers the data key", "note": "recipientPrivateKeyB64, sharedSecretHex, kekHex are checker aids, NOT part of the wire WrappedKey", "anchors": ["RFC 7748 §6.1 (recipient = Alice, ephemeral = Bob)"] + }, + { + "id": "federation", + "file": "federation.json", + "kind": "deterministic", + "specSection": "8", + "construction": "base64url (no padding) over compact JSON for the join-handshake tokens (InviteRequest, RepoLocator); avp:/// for the repository URI", + "expects": "decoding base64url yields the object (decode oracle); canonical minified encoding round-trips; avp:// URIs parse to {host, repoId} and format back", + "shape": "uses top-level `tokens` and `uris` arrays, not `cases`", + "anchors": ["RFC 4648 §5 (base64url)"] } ] }