Enhancement Proposal

Proposal: Milestone 0 — the walking skeleton (implementation plan)

The implementation plan for Vord's first vertical slice. It records how M0 is built; the why lives in the ADRs it references. M0 is the first slice of the whole implementation arc.

Motivation

Milestone 0 proves the differentiator — stateless edge verification independent of the strongly-consistent core (ADR-0001 rule 1) — end to end in a single process: one auth-code+PKCE login issued by Modgud and verified by Vör, with the verification shown correct while the core is unavailable, under fault injection in simulation. Per the build strategy (architecture §4.3; ADR-0010) this is a vertical slice, not bottom-up: the layers that matter for one real operation, wired thinly, then widened by risk.

Building this slice first de-risks the project's central claim before any breadth (revocation, the authz engine, multi-tenancy, SAML) is added, and it establishes the trait seams and the DST harness everything after it attaches to.

Design

Scope boundary

In scope: minimal OAuth/OIDC authorize + token endpoints (Modgud) → password login ceremony → minimal SSO session → core issuance (the irreducible write) → a signed OIDC JWT → stateless verification (Vör) against published JWKS + a clock, plus the testkit DST harness and the edge-independence property test. Around that spine, the review-hardened day-one seats: the client object of ADR-0038 (M0 registers its one client through the real model — type, hashed secret, exact-match redirect), the budget registry and startup gate of ADR-0039 (the dev profile ships a consistent budget set), EventSink emit points with versioned event schemas (ADR-0022), the interim static admin authorizer behind the Authorizer seam (ADR-0021), a minimal hosted login page (the ceremony half of proposal 0007) with the accessibility gate (proposal 0020), and the observability spine (proposal 0012 — tracing plus the staleness/lag gauges the M2 gate will need).

Explicitly out of M0 (deferred to the risk-ordered widening): production KeyCustody/KMS and rotation, the status-list/NATS revocation propagation path (the status-list format ships at M0 — trivially empty, but parsed and freshness-checked so the verifier contract is final), the Forseti authorization engine, multi-tenancy/realm, SAML, SCIM, MFA, the event-stream projections, and multi-region. Their hooks are respected where retrofit is expensive (see Backward compatibility).

Deliverable

A proof, not a product: a single binary that runs auth-code+PKCE → core issuance → a signed OIDC JWT → stateless edge verification in one process, plus a seed-reproducible DST proof that verification completes correctly with the core unavailable (the property test below), the trait and test scaffold every later milestone attaches to, and the token format (ADR-0003) implemented (v0/unstable) with a conformance vector. It is explicitly not deployable (one key, no revocation propagation, no production core); a usable multi-tenant product is M4.

Workspace and crate scaffold (coarse start, ADR-0013)

A Cargo workspace, coarse, splitting later. Trait boundaries exist from day one even where crate boundaries do not. Cargo.lock committed.

Crate M0 contents
vord-format OIDC-JWT and status-list encode/sign/verify against the specs (specs/vord-token-v0, specs/vord-status-list-v0); dependency-light, spec-first
vord-traits KeyCustody, CredentialVerifier, CredentialStore, SessionStore, Authorizer, EventSink, StatusListPublisher definitions (the keystone — consumers/impls depend here; all seven seams of ADR-0013 even where the M0 concrete is trivial)
vord-core Modgud auth-code+PKCE + token endpoints, the client object (ADR-0038), the Vör verifier, the budget registry + startup gate (ADR-0039), the embedded directory/keystore/session backends, the static admin authorizer
vord-server The binary; wires concretes; hosts the HTTP surface and the minimal hosted login page
testkit Abstract clock; deterministic seed-driven runner; fault injection (core-unavailable); grows the in-sim fakes (ADR-0028) at later milestones
xtask Codegen, conformance-vector run, cargo xtask ci

CI adds cargo build/test/fmt/clippy; the adr-immutability, dco, docs-check, and require-issue checks already exist.

Logical view

M0 collapses the planes into a single process behind the real trait seams: a vertical slice through Modgud (issuance), the embedded core, and Vör (verification), with the CredentialVerifier carrying no core handle so rule 1 is a compile-time fact. The auth-code+PKCE flow is the architecture's §6.1 scenario.

Issuance — the auth-code + PKCE flow (ADR-0014)

  1. Authorize — Modgud validates the request against the registered client (exact-match redirect, allowed grant — ADR-0038) with a PKCE challenge, runs the password login ceremony behind the multi-step-shaped AuthenticationMethod seam (ADR-0015 — challenge/response-shaped now so WebAuthn and federation widen it without a trait break), establishes a minimal session with a sid, auth_time, and assurance context (ADR-0016), and returns a code.
  2. Issue — on the token request (PKCE verifier), Modgud performs the irreducible core write and mints a signed OIDC JWT (EdDSA, embedded keystore, kid-addressed from the first token — ADR-0023), stamping vfmt, sid, amr (password), acr, and the status-list reference. This write is the issuance point, and it emits the login event through the EventSink (ADR-0022) — rich (IP, client, method), versioned, projectionless for now.
  3. Verify — Vör checks the JWT against the published JWKS, the clock, and the (trivially-empty at M0, but signed, counter-carrying, freshness-checked) status list, returning a decision without any core read.

Directory and issuance model (ADR-0011)

A principal record (immutable internal id, mutable identifiers, the password hash — Argon2id with the cost-before-auth posture of proposal 0006, and the per-principal data-key reference of ADR-0034) plus a unique realm-scoped, blind-indexed identifier entry (HMAC(realm-index-key, identifier) — ADR-0011 decision 3), written atomically; single-tenant/single-realm at M0, but the realm column and the index key shape are final so multi-tenant (M4) is a widening, not a reshape. The meta:version consistency fence (ADR-0004) is reserved (returned, not yet enforced).

Token and status-list formats (ADR-0003)

The external OIDC JWT (EdDSA primary) implements specs/vord-token-v0: required kid with per-kid algorithm binding, vfmt, sid, amr/acr, and the status-list reference — the full v0 register, so no later milestone adds a claim verifiers must already know. The status list implements specs/vord-status-list-v0 (counter + iat + the revocation sections, all empty at M0). M0 lands the first conformance vectors in specs/conformance/; it does not redesign the formats. Both stay v0/unstable until a second independent verifier validates them.

DST harness + property tests — attach at M0 (ADR-0010)

testkit provides an abstract clock and a single-threaded, seed-reproducible runner. The headline property:

  • Edge-independence (bounded autonomy) — with the core made unavailable, verification of an issued, unexpired token completes correctly for as long as the cached projections are within their freshness budgets; an expired token correctly fails; and past the status-list max-age the edge fails closed (ADR-0010, ADR-0032) — the DST scenario advances simulated time across the boundary and asserts both phases. A seed that finds a verification path reaching for the core is committed as a permanent regression.

Supporting assertions: a token verifies against the published JWKS; an unknown format version is rejected; a replayed older status list is rejected while an equal-counter re-fetch is accepted (ADR-0032); the CredentialVerifier signature carries no core handle (asserted structurally at the trait seam — it graduates to a crate-graph compile-time fact at the M4 Vör split, ADR-0013).

Test tiers

The project-wide testing strategy is ADR-0028; M0 stands up its first tier.

  • Tier 0 — DST correctness (testkit), from M0, no containers. Production logic is written against abstract time and the trait seams, run in a seed-reproducible simulator (madsim). The core-unavailable fault is injected here.
  • Tier 1 — integration against real backends, from M1+. Real KMS, the Zanzibar engine, a Wyrd metadata deployment, and NATS run under containers for (non-deterministic) integration tests. None of this exists in M0.

CI

cargo fmt --check, cargo clippy -D warnings, cargo build, cargo test (the DST property tests run here on madsim), and cargo xtask conformance (the token-format reader against specs/conformance/). DCO sign-off and cargo-deny (the permissive-license allowlist + RUSTSEC advisories) enforce ADR-0008 in CI from the first commit.

Alternatives considered

  • Bottom-up (directory and protocols first): rejected — a vertical slice proves the differentiator sooner.
  • Fine-grained crates from day one: rejected (ADR-0013) — a combined vord-core now, split later.
  • Full OIDC surface in M0: deferred — auth-code+PKCE is enough to drive the slice end to end; client-credentials/refresh/the rest widen later.

Graduation criteria (definition of done)

  • A relying party completes auth-code+PKCE against a registered client and receives an OIDC JWT carrying the full v0 claim register (kid, vfmt, sid, amr/acr, status reference).
  • The JWT verifies at the edge against the published JWKS + a clock + the (empty, freshness-checked) status list.
  • Verification completes correctly under DST with the core made unavailable — within the projection freshness window, failing closed past it — reproducible from a seed (the bounded edge-independence property).
  • The budget startup gate rejects an inconsistent dev profile (ADR-0039); the abuse-case suite passes; the login page passes the axe-core gate (proposal 0020).
  • fmt/clippy clean; Cargo.lock committed; the vord-format reader accepts the conformance vectors in specs/conformance/ (token and status list, accept and reject).

Suggested PR sequence

  1. Workspace scaffoldvord-traits (all seven seams, including Authorizer, EventSink, StatusListPublisher as definitions), testkit skeleton (abstract clock, seed-driven runner), xtask, CI (cargo xtask ci, cargo-deny, DCO; Actions pinned per proposal 0005).
  2. vord-format, token side — OIDC-JWT encode/sign/verify per specs/vord-token-v0 (kid/alg binding, vfmt, claim register) + first accept/reject conformance vectors.
  3. vord-format, status-list side — container encode/verify per specs/vord-status-list-v0 (counter, iat, revocation sections; persistent high-water-mark check) + vectors.
  4. Embedded storesCredentialStore (principal record with data-key reference + realm-scoped blind identifier index, atomic write) and KeyCustody (software keystore, kid discipline) behind the traits.
  5. Budget registry + startup gate (ADR-0039) with the self-consistent dev profile; an inconsistent profile refuses to start (tested).
  6. Client object + authorize endpoint — ADR-0038 registration (type, hashed secret, exact-match redirect), PKCE, the password ceremony behind the multi-step AuthenticationMethod seam, minimal SessionStore (sid, auth_time, assurance context).
  7. Token endpoint — issuance (the irreducible core write) + signed JWT with the full claim set; EventSink emit points (versioned login/issuance events); the interim static admin Authorizer for the two management calls M0 needs (create tenant, register client).
  8. Vör CredentialVerifier — verify against JWKS + clock + status list with freshness state; no core handle.
  9. vord-server wiring — minimal HTTP surface + the hosted login page (ceremony half of proposal 0007, axe-core gate per proposal 0020) + end-to-end auth-code+PKCE test.
  10. DST bounded edge-independence — core-unavailable fault injection asserting both phases (correct within freshness window, fail-closed past it).
  11. Abuse-case tests (ADR-0028, §13): forged alg:none / wrong-algorithm / missing-kid tokens rejected; unknown vfmt rejected; PKCE-downgrade and redirect-mismatch requests refused (ADR-0038); replayed older status list rejected, equal-counter re-fetch accepted (ADR-0032); the CredentialVerifier carries no core handle (structural assertion). The security spine starts at M0.
  12. Observability spine (proposal 0012, M0 slice) — tracing through the issuance and verification paths plus the projection staleness/lag gauges the M2 gate will read.

Backward compatibility

  • Token and status-list formats: fixed by the v0 specs (ADR-0003; v0/unstable until validated); M0 implements them. No tokens to migrate.
  • Reserved seats honored now because retrofit is expensive: the full v0 claim register — kid/alg-binding, vfmt, sid, amr/acr, the status-list reference (ADR-0003 decision 6, ADR-0044) — plus the meta:version fence (ADR-0004), the versioned EventSink emit-seam (ADR-0022), the realm-scoped blind-index key shape and per-principal data-key reference (ADR-0011, ADR-0034), the session-record fields (ADR-0016), and the trait seams for KMS/Forseti/Wyrd-metadata.
  • API / deployments: none yet, so nothing to stay compatible with.

Open questions

  • redb-or-equivalent embedded directory encoding (identifier-index key shape) — settled during M0 as an implementation detail, recorded in-code and in the architecture doc.
  • Whether the minimal HTTP surface lives in vord-server or warrants a vord-gateway crate at M0 — combined vord-server at M0, split later under the crate-evolution rule (ADR-0013).
  • No open question blocks M0.