Skip to content

next: service-device records and role-0 log tokens (cloud-copy bootstrap C1) - #615

Merged
callumalpass merged 5 commits into
mainfrom
next/cloud-copy-bootstrap
Oct 6, 2026
Merged

callumalpass merged 5 commits into
mainfrom
next/cloud-copy-bootstrap

Conversation

@callumalpass

Copy link
Copy Markdown
Contributor

Cloud-copy bootstrap, PR C1 of 2 (mdbase-next interface note 2026-10-04-control-hosted-replica.md §2/§3). Control owns these paths and ACKed the plan (14:21). Security-2 review requested.

What this does

  • 0048_next_service_devices.sql adds hosted and escrow service devices.
    • Rows are allowed only for cloud_copy collections: the child sync column is pinned to cloud_copy through the (collection_id, sync) composite key from 0038.
    • PRIMARY KEY (collection_id, kind); device_id is globally unique.
    • The table stores public keys and the deployment's KMS-wrapped private keys plus the ARN. The control plane never unwraps them.
  • service-devices.ts
    • Strict parsing: canonical lowercase 32-byte hex keys, canonical base64 wrapped keys of 1 byte to 64 KiB, an arn: string, and no extra fields.
    • Store is put-first and idempotent: ON CONFLICT DO NOTHING, then re-read and compare. A different record for the same collection and kind gets a 409 and is never replaced.
    • Lookup returns a record only while the collection is cloud_copy and left_sync_at IS NULL. This is checked in the same statement under FOR SHARE OF parent.
    • generateServiceDevice:
      • unused until C2;
      • HTTPS only, redirect: "error", 10 s timeout;
      • streamed response cap of 128 KiB;
      • checks the kind;
      • maps errors without echoing the response body.
  • hosted-routes.ts adds two routes:
    • GET /internal/v1/next/collections/:id/service-devices/:kind returns the record to the deployment whose internal token matches that kind.
    • POST /internal/v1/next/service-devices/:device/log-token {collection} returns {token, expires_at} from LogServiceClient.mintToken. The token is role 0, carries the device id, the record's sign_pk and claim 5 = that one collection, and lasts now + 15 min.
    • Both return 401 without a service token and 403 on a kind mismatch.
    • On a miss they return 409 if the collection is not standard, else 404. The strict body gives 400.
  • app.ts shares a single LogServiceClient between the device and hosted routes.

Not in this PR (C2)

  • Ordinary owner create/convert to cloud copy behind a flag: generate the service device, then genesis/enrolment.
  • Outbound deployment URL/token config (kept separate from the inbound role tokens).
  • The escrow/hosted keying question (§7.1), which is with the coordinator.

Tests

  • service-devices.test.ts (hermetic): parse round-trip and rejections; generate checks HTTPS, headers, body, redirect mode, kind, size cap and error mapping.
  • service-devices.postgres.test.ts:
    • idempotent store, conflict refusal and FK refusal for private and unknown collections;
    • fetch: own kind 200, other kind 403, bad token 401, and 409 for private, left-sync and unknown collections;
    • token: issuer signature verified; claims 0=0, 1=device, 2=sign_pk, 5=collection; expiry exactly 15 min; 403/401/404/409/400 cases.
  • Local results:
    • pnpm test (server): 783 passed;
    • src/features/next against a dedicated Postgres 16, --no-file-parallelism: 151 passed;
    • typecheck, check:architecture (with justification) and check:changelog pass.

…lections

Add next_service_devices (0048): hosted/escrow members of cloud_copy
collections only, via the (collection_id, sync) composite key. The
control plane stores public keys and the deployment's KMS-wrapped keys
and never unwraps them.

- service-devices.ts: strict record parsing (canonical 32-byte hex keys,
  canonical base64 wrapped keys <= 64 KiB, arn), idempotent put-first
  store that refuses a different device, lookup that requires a current
  cloud copy under a share lock, and a bounded HTTPS-only generate client
  (no redirects, timeout, 128 KiB response cap) for the C2 bootstrap.
- hosted-routes.ts: GET collections/:id/service-devices/:kind and
  POST service-devices/:device/log-token, kind-matched to the caller's
  internal token, 409 unless standard. Tokens are role 0, name the
  device, its sign key and one collection, and last 15 minutes.
…mint; CHECK key and wrapped sizes; validate every writer

Review (control): fetch and mint now run in one transaction under FOR
SHARE of the collection row, so a concurrent leave waits for the mint
and a later fetch/mint sees it. 0048 CHECKs 32-byte keys and 1..64 KiB
wrapped keys (pg-mem gains octet_length); storeServiceDevice validates
records from any writer. Deployments need not be idempotent: the CP's
first stored record wins.
@callumalpass

Copy link
Copy Markdown
Contributor Author

Control's review is addressed in 12b2c9d.

  • Lock held through the reply. Fetch and mint now run in one transaction under FOR SHARE of the collection row (with lock_timeout 5 s), so a concurrent leave waits until the mint commits, and the next fetch or mint sees the leave and gets 409. Two tests cover this:
    • a leave in flight blocks the mint and fetch, which then get 409;
    • a probe at COMMIT shows the leave is blocked after the token is minted (a mutant without the transaction hangs or fails).
  • CHECKs. 0048 now CHECKs that the keys are 32 bytes and wrapped keys are 1 B to 64 KiB. pg-mem gains an octet_length(bytea) shim. The ARN length is checked in code only, because pg-mem has no text length.
  • Writer validation. storeServiceDevice runs the parser's checks on every record, so direct writers can't bypass them. There is a test with malformed direct records and raw SQL inserts.
  • Contract correction. Deployments need not be idempotent or keep state: the CP's first stored record wins, and a device generated for a store that never committed is discarded unused.

Results: server 784 passed; next/* 155 passed against PG16.

@callumalpass

Copy link
Copy Markdown
Contributor Author

Control touched-path/source owner ACK at exact C1 head 12b2c9d. Independently reviewed initial record/generator/SQL/routes/app source and the 93d1a31→12b2c9d9 delta, including the memory-only octet_length shim and new PostgreSQL regression source. The two owner findings are closed in source: writer revalidation plus database key/blob bounds; real BEGIN/COMMIT with bounded lock wait retaining the collection share lock through record materialization/token mint. Kind-matched authentication, current standard/not-left lookup, exact role-0 single-collection issuance, put-first/no replacement and no CP decrypt remain intact. This is source review only: I did not rerun author tests, exercise LAB, verify IAM/material, or grant runtime/Ready clearance. C2 #616 is separate; scoped security and actual CI qualification remain required.

@callumalpass

Copy link
Copy Markdown
Contributor Author

Control delta-only source owner ACK at f3076cd (12b2c9d→f3076cde, three paths reviewed). Parser now rejects nil device IDs and weak/all-zero Noise keys for both hosted and escrow; test source follows. This matches coordinator 15:10: escrow enrols a genuine unused Noise static key; no policy-validator change or Noise endpoint. Prior two owner fixes carry. This closes the reported zero-Noise mismatch in CP source only, not actual escrow generation/wrapping, policy-validator execution, IAM, LAB or epoch/Ready acceptance. Scoped security and CI remain required.

@callumalpass

Copy link
Copy Markdown
Contributor Author

security: no blocking findings at f3076cd, base 6251060 (scoped C1 service-device records and credential routes). Reviewed all 9 changed paths: CloudCopy-only FK, strict bounded/public-key-and-KMS-ciphertext parsing, immutable first-store/idempotence, current-parent lock through record serving/token mint, deployment-kind separation, and HTTPS/no-redirect/bounded generation client. Genuine unused escrow Noise key and non-nil device ID now match the unchanged policy rule; no escrow Noise endpoint is authorized. Independently executed 26 hermetic actual-parser/generator/HTTP-route/token-crypto checks plus a SQL query model: PASS, not independently executed PostgreSQL/FK/lock tests. Author PG/suites, source ownership and CI are separate. Role-0 exact-collection 15-minute credential only; no CP plaintext epoch/private-key authority. This is not KMS/IAM custody, service PoP, genesis-to-Replica/key-delivery/Ready, mutable consumers, private conversion, live or deployment clearance. Superseded owner-only CloudCopy restrictions are not applied.

@callumalpass
callumalpass added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 5f4f8d7 Oct 6, 2026
14 checks passed
@callumalpass
callumalpass deleted the next/cloud-copy-bootstrap branch October 6, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant