oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1# Directory roster HTTP and signing integration
2
3Status: design-only; review record in Home discussion 34. Explicit Fable design
4approval is required before implementation.
5Scope: Home discussion 34, stacked on directory runtime 171f49a, shared roster
6667dc82 and producer 32fb49c. The approved roster contract (discussion 28) and
7producer storage contract (discussion 33) remain authoritative. This design adds
8runtime wiring and an HTTP adapter; it adds no operator mutation or provisioning
9transport and makes no deployment/readiness claim.
10
11## Source audit and boundaries
12
13`runtime.go` currently merges sessions, handoff and directorydata.Schema only for
14the directory role. The credential role alone owns `Runtime.signer` and
15HOME_ASSERTION_KEY. Directory configuration holds credential public verification
16keys and its own sealer; `newDirectory` rejects a credential private signer.
17`directoryRouter` puts sessions middleware around the browser routes.
18`directoryFeatures` already supplies the compiled app registry and validated
19team-origin policy. Reuse that registry and policy when constructing the producer;
20do not duplicate per-team rows in environment configuration.
21
22The published client builds GET directory+/roster with `url.Values.Encode`, a
23Bearer token, Accept application/json and Accept-Encoding identity. It refuses
24redirects, compression, bodies over 64 KiB and Content-Type values other than
25exact application/json. It checks signatures before using page fields. Page-zero
26CursorHash is SHA256(empty), not an empty string. There is no epoch query or 304
27shortcut. Wire members use person_ref. The pending Docs cache lease amendment
28changes no Page/SignPage contract; this adapter remains compatible with 667dc82.
29
30## Runtime and schema
31
32Merge directorydata.RosterSchema immediately after directorydata.Schema in the
33directory-only schema. Credential and legacy schemas must not gain these tables
34or membership triggers. Applying this migration leaves generation, consumers,
35tokens and receipts empty. Repeated startup never fills them.
36
37Introduce HOME_ROSTER_MODE with absent/disabled and serve values. In directory
38mode, absent/disabled plus an absent HOME_ROSTER_KEY means a disabled endpoint.
39The directory browser routes and boot remain available; every request that passes
40the roster host/method guards receives the same fixed 503 before inspecting any
41token, query or database. There is no standalone roster fallback.
42
43Serve requires HOME_ROSTER_KEY. Unknown mode, key supplied while disabled, missing
44serve key or invalid key refuses startup with one fixed configuration error.
45Credential and legacy roles reject either roster environment variable when set,
46including an explicit disabled value. Direct Runtime constructors enforce the
47same mode/key invariants; LoadRuntime is not the only guard.
48
49Represent roster configuration separately, for example a private rosterRuntime
50field with mode, signer and derived key ID. Keep Runtime.signer credential-only
51and preserve newDirectory's existing rejection of it. Directory now deliberately
52owns one new roster-only private key; update role comments, validation and tests
53that currently equate all private signing keys with credential authority.
54
55HOME_ROSTER_KEY is canonical standard-base64 of the existing family.MarshalKeypair
56format, bounded to 4096 encoded bytes, with no alternate file or second encoding
57input. Decode strictly and verify the encoding round trip. Validate the closed
58keypair JSON before calling family.UnmarshalKeypair: exactly sign_priv_d and
59box_priv_d, canonical 64-character lowercase hexadecimal strings, no duplicate or
60unknown fields. Require P256 signing scalar 0<D<N and valid box scalar; derived
61public keys come from the existing library. The box half is unused by roster.
62Derive roster key ID as lowercase hex SHA256(SignPub()), matching the shared
6364-byte atom bound. Do not take the key ID from request input.
64
65Reject a roster signing public key equal to any configured credential assertion
66verification key. The independently audited operator provisioning key must also
67be distinct; it is not loaded by this producer and its separation is a writer/
68provisioning gate. Never derive a roster private key from HOME_DEK, pepper, token,
69credential key or startup randomness. Startup does not publish a verification key
70or manufacture a signed operator record. Key rotation requires the separately
71approved operator record/key rollout before changing the active signing key.
72
73## Handler seam and dispatch
74
75Use an unexported rosterHandler with the concrete validated roster runtime,
76configured directory origin, clock seam and a small read-source interface:
77Read(context.Context, directorydata.RosterRequest, time.Time)
78(roster.Page, int, error). Production injects NewRosterSource using the existing
79store's DB/sealer/app policy. Tests may inject a counting source or real store.
80Disabled mode does not construct or invoke a source or signer.
81
82Dispatch decoded URL.Path == /roster to this adapter before directory browser
83session/chooser middleware. The adapter owns all methods for that exact route,
84so chi's default method response cannot bypass its headers. Cookies neither
85authorize the request nor cause Set-Cookie, sign-in redirects or session writes.
86All other routes retain the existing directory router and guards. The roster
87adapter does not reuse browser CSRF or assertion-consumer middleware.
88
89At adapter entry set Cache-Control: no-store, no-transform, Content-Type: application/json and
90X-Content-Type-Options: nosniff. No CORS permission, compression, ETag, Last-Modified,
91Location or browser cookie is emitted. Refusal precedence is:
92
931. r.Host must equal the configured directory host, including any configured
94 port, exactly. No trailing-dot, alternate-host or forwarded-header alias.
95 Wrong host returns fixed 404, before source access.
962. Method must be GET; HEAD, POST, OPTIONS and all others return fixed 405 with
97 Allow: GET. Nothing follows a redirect to turn a POST into a roster read.
983. Disabled runtime returns fixed 503 for every token/query variation.
994. Validate request syntax below; malformed query/path/body returns fixed 400.
1005. Validate one exact bearer header; malformed/missing/duplicate returns fixed401.
1016. Call the source once with a current server time. Nonpositive server time
102 returns fixed503 here, before the source's own input check.
103
104Require URL.EscapedPath() exactly /roster, no absolute request-target, no fragment,
105and no nonempty GET body: ContentLength must be zero and TransferEncoding empty.
106Do not read an attacker-sized body. The configured directory origin plus the
107request path/query must fit 4096 bytes; bound RequestURI too. A valid first query
108contains exactly team and fetch. A continuation additionally contains one
109nonempty cursor. Query field order is unrestricted. Reject duplicate/unknown or
110empty fields, extra separators, semicolons, percent encodings, plus encodings and
111padding. Parse literal bounded name=value pairs; do not use a helper that silently
112discards malformed parameters. Team is 1..64 ASCII alphanumerics, fetch passes
113roster.ValidChallenge, and cursor is strict canonical raw-base64url at most
114roster.CursorLimit (2048 bytes), checked by decoding and round trip. The producer
115validates the sealed cursor contents again. Neither team nor cursor selects a
116network destination.
117
118Count Authorization values across case-insensitive header names; require exactly
119one value equal to Bearer + one ASCII space + a canonical43-character token.
120Reject comma lists, repeated lines, tabs, extra spaces, alternate schemes and
121padded/noncanonical values. Use roster.ValidChallenge before calling the source.
122No browser session, Origin, Referer or query token substitutes for this header.
123Accept and Accept-Encoding need no special value from the caller: the adapter
124always returns the one JSON representation without compression. The no-transform
125directive is required by the CARLOS edge compressor as well as the app adapter:
126without it, application/json responses can be gzipped for Accept-Encoding: gzip,
127including small error responses when the upstream length is unknown.
128
129The application adapter never redirects. The existing outer Go ServeMux can
130canonicalize unrelated malformed paths before the application sees them; the
131shared client never follows these redirects. Tests through the real outer mux
132must show no alternate path produces a trusted page, and canonical /roster never
133redirects for any method/query/error. No change to global router normalization is
134needed to serve the fixed canonical endpoint.
135
136## Response and error boundary
137
138Give source.Read a request-derived context capped at five seconds, and obtain
139now immediately before it. No network call or operator write occurs under that
140read transaction. On nil error, accept only status200 with type page or status409
141with type stale-cursor. Sign with roster.SignPage using the validated roster key
142and derived ID, and buffer the complete returned bytes before writing headers.
143Check len <= roster.BodyLimit as a final boundary. Sign/marshal/status failure
144returns the fixed unsigned503; never emit an unsigned Page or a partial signed
145response. Do not duplicate the shared envelope/signature or JSON-pointer logic.
146
147A source failure maps only its fixed401/404/400/503 error/status pairs. Unexpected
148pairs become fixed503. Keep the approved source's exact mapping, including401
149for a missing token-to-registry join; do not infer a more revealing cause.
150Authenticated epoch/generation change is the signed409 shape, with current scope,
151fetch, request-cursor hash, generation and epoch and all page-only fields absent.
152It is never treated as an empty successful roster or a new generation approval.
153
154Unsigned errors contain only a fixed machine code: bad_request, unauthorized,
155not_found, method_not_allowed or unavailable in an error JSON member. No supplied
156value or underlying error is serialized. No response includes a team name, email,
157origin selected from request input or token diagnostic. All errors keep no-store
158and exact application/json, although the client still refuses unsigned errors.
159No raw URLs, bodies, cursor, token, label, subject or key enters logs, traces,
160metrics or status messages. The handler itself needs no logging. Transport and
161platform access-log behavior must be checked before any live enablement; the
162application's no-log claim does not attest to an unaudited proxy configuration.
163
164## Authority and deployment gates
165
166In serve mode an otherwise authenticated, scoped request with absent generation
167returns503 through the source;
168it must not mint a generation, owner, membership, token or receipt. Invalid or
169unknown tokens remain401 and foreign/unknown teams remain identical404. Merely
170installing the key does not initialize or approve authority. Runtime health may
171report process health as before; it is not a claim that roster provisioning is
172complete or that Docs TeamMode is ready.
173
174The future writer must authenticate independent operator authority before its
175transaction, verify the exact scope/expected generation/record sequence, assert
176foreign_keys ON, and atomically write state and a successful receipt. It must
177follow the producer storage contract's token-slot and epoch-trigger rules.
178This slice adds no such writer, route, command, migration seed or environment
179import. No first visitor or roster bearer can supply it. Existing-db bootstrap
180and restore remain behind the supported fenced maintenance path (platform117);
181a genuinely fresh first-boot path needs separate audit. Without either approved
182path there is no live generation/consumer initialization and no general team
183roster launch. Test SQL and test operator signatures prove behavior only.
184
185## Implementation acceptance
186
187Run the complete Home gate, focused producer/handler race tests, actual CI and
188Fable whole-code review on a frozen candidate before parent integration. Tests
189must use actual runtime/schema/router paths as well as the injected handler:
190
191- Directory schema contains roster tables/triggers; credential/legacy schemas do
192 not. Empty and repeated startup leave authority tables empty. Disabled runtime
193 returns identical503 across token/query inputs without DB/signing/session work.
194- Mode/key matrix covers both environment loader and direct constructors, role
195 separation, malformed/private-key scalar/JSON cases, credential key reuse and
196 no startup generation. Existing Home handoff/chooser browser gates still pass.
197- Exact Host, all wrong methods, duplicate/mixed-case Authorization maps, comma/
198 whitespace forms, absent or noncanonical challenges, hostile team/cursor,
199 duplicate/unknown/escaped query keys, empty cursor, oversized target and GET
200 body all refuse before any source call. Errors leak none of the test corpus.
201- Use real directory fixture transactions and the actual shared Cache client
202 against the handler through an injected same-origin TLS test transport. Signed
203 200 with zero, one and more than100 members verifies; exact empty array stays
204 explicit. Rotation, disable and foreign/unknown team failures remain bounded.
205- Between pages mutate membership or generation; verify signed409 and no cache
206 publication/lease extension. Wrong key/context/scope/signature cannot become
207 standing. A missing generation never signs a successful page. No epoch/304
208 response path exists. Two consumers and two teams stay isolated.
209- Buffer/sign failures return unsigned503 with no partial Page. Every adapter
210 result has no-store, no-transform/exact JSON and no Location, encoding, validator or cookie.
211 The outer mux/real server retains these for canonical /roster, and no redirect
212 is followed by the shared client.
213- Seed names, tokens and cursor plaintext in fixtures and scan DB/WAL plus
214 captured application logs. Negative mutations removing bearer/mode/Host
215 checks, bypassing SignPage, or dropping signed409 expose failing tests.
216
217No browser UI is introduced by this endpoint. A deployed two-team transport
218canary is a later root-owned gate once an audited provisioning path exists; this
219design and local fixtures do not authorize live seeding or mark the suite ready.
220