Skip to content

next: private approval-request (fresh SAS commitment) - #624

Merged
callumalpass merged 24 commits into
mainfrom
next/private-approval-request
Oct 6, 2026
Merged

callumalpass merged 24 commits into
mainfrom
next/private-approval-request

Conversation

@callumalpass

Copy link
Copy Markdown
Contributor

Stacked on #621. Implements the renewal path the #144 owner (replica) specified on 2026-10-05 at 22:52: the logged approval-request (wire.cddl op 13 {0: 13, 1: device, 2: sas_commit}) is CP-signed and requested by the device's own signature. It carries no enrolment, keys or upsert authority.

POST /v1/next/collections/:id/private/devices/approval-request {device_id, challenge, sig, sas_commit}

  • Proof: H("mdbase/v1/private-approval-request", cbor[challenge, connector, device, collection, sas_commit]) over a fresh single-use challenge.
  • Checks, under locks, both before queueing and after the append:
    • identity is current;
    • the collection is a current private one on the next runtime;
    • the account is a current member (same projection as next: device log-token refresh for synced collections #618);
    • the device is not revoked;
    • an appended enrolment of exactly this device for this account and these keys exists.
  • Queues approval-request {device, sasCommit}, drains, and verifies the exact appended bytes. Answer: {requested_at, approval: "pending"}.
  • A retry of the same commitment reuses its request; a new commitment is a new request. The replica applies it only for an active, unkeyed user device (chore: move public packages to mdbase-dev scope #144).

Wire. PolicyOp gains approval-request. Test: the encoded op is a3 00 0d 01 50 <device> 02 58 20 <commit>, the same bytes as PolicyOp::ApprovalRequest in mdbase-next #144; a 31-byte commitment is refused.

Tests. Two new PG tests:

  • happy path: op 13 on the log, idempotent retry, a new commit, the commitment bound by the proof, and an enrolment proof refused;
  • refusals: another account, revoked, removed member, cloud copy.

Four mutations were checked (the enrolled check, the commit in the digest, retry reuse, membership), and each fails a test.

Validation:

  • src/features/next + app: 241 passed.
  • tsc is clean.
  • The architecture and changelog checks pass.

Review: control (owner), security.

…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.
…OOTSTRAP)

Per the coordinator's sealed-envelope 7.1 decision: the owner's device
signs a fresh challenge; the control plane asks the hosted and escrow
deployments to generate their service devices (outbound tokens distinct
from inbound), then registers a cloud-copy genesis that enrols the
owner's device and both service devices (zero account) and stores the
records. The desktop performs the initial rekey with wraps for desktop,
hosted and escrow; the CP never holds a collection key and makes no
escrow-to-hosted wrap.

Retries must come from the enrolled device and reuse stored records;
other owners, other devices and existing private collections get 409.
A failed generation registers nothing. Success is reported only after
the log returns the exact genesis bytes.
…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.
…await; refuse nil IDs

Review (control): after generation and after the log read-back, the
owner's connector (not revoked), account (not suspended) and device
(same keys) are re-read under FOR SHARE, and the final transaction
checks the collection is still this owner's current cloud copy and the
device the genesis enrolled before minting. PG tests for revoke,
suspend and device removal during generation, and leave/revoke during
the read-back. Nil collection/device IDs are refused.
…on answers busy

Review (control): every transaction sets lock_timeout 5s like C1; a
timeout answers 503 busy with no driver detail, and registers nothing.
PG regression holds the collection lock from another session.
…oin, per Callum's 2026-10-06 decision

- POST /v1/next/collections/cloud-copy/service: account session; genesis
  [genesis cloud-copy, member-set owner, enrol hosted, enrol escrow];
  hosted is the first member and keys the collection. Same phases as the
  owner path (generate with no lock held, recheck account and collection
  under lock, exact genesis read-back), no token.
- POST /v1/next/collections/:id/devices: a device registered under the
  owner's connector, with a join-domain proof, is enrolled via
  queueNextPolicy only into a current cloud copy (private, foreign or
  left collections refuse before any op); idempotent for the same keys;
  the token is minted after the enrol batch reads back exactly.
- The owner-device create path stays.
Tests cover genesis ops, retries, session/suspension/foreign refusals,
private exclusion (no outbox row), join proof binding and revocation.
… rechecked after awaits; full enrolment tuple; revoked devices never revive

Review (control, security-2) on 336b474:
- join: currentIdentity (connector/account/device, exact keys) under
  FOR SHARE before the enrolment is queued and through its commit; PG
  barrier: a revocation racing the request wins and nothing is queued.
- service-created: the session itself (not revoked/expired, same session
  epoch, account active) is rechecked under lock after every await
  (Tailscale identities: the account).
- idempotent re-join and the owner-path check compare the whole
  immutable tuple (device, account, kind, sign/KEM/Noise keys).
- a device with a later device-revoke is never accepted again.
…hind MDBASE_NEXT_PRIVATE_BOOTSTRAP=1

Create: owner's registered device; e2e genesis enrolling only that device.
Enrol: a current member's registered device with its sas_commit (key 7); no key
until an existing keyed device approves and grants. Shared proof/identity/
membership helpers move from cloud-copy-bootstrap.ts to bootstrap-common.ts.
…; bootstrap transactions bound every statement

Per control/security-2 on #621: P1's final locked transaction adds currentMember
(a pending member-remove refuses); inTransaction sets statement_timeout 5s and
57014 answers 503 busy like lock_timeout (shared by the cloud-copy routes).
…e's fresh SAS commitment)

Per replica (#144 owner) 22:52: the device signs the request to Connect, which
appends the CP-signed approval-request {device, sas_commit}; only for this
account's appended, unrevoked enrolment of exactly these keys, current private
collection and membership. Same commitment retried reuses its request.
…ale or superseded refused

Per control (23:35) and replica (23:38): idempotency keys on the device's
latest request (outbox order, op ordinal); an earlier commitment (enrolment's or
superseded, A->B->A) is refused as stale_commitment; a commitment superseded
during the append read-back, or by the final locked recheck, answers
superseded. The answer reports the request as logged, not approval pending.
@callumalpass

Copy link
Copy Markdown
Contributor Author

security: no blocking findings at ac28eef, base 0940c0c (scoped CP private approval-request renewal/digest/op13 codec). Reviewed original six-path delta plus exact two-path 59+/16- correction: latest device-request/outbox and intra-batch order, refusal of earlier enrolment/superseded commitments, exact acknowledged bytes/position and after-await final identity/member/enrolment/revoke/commitment guards. Independently ran 23 actual digest/HTTP/helper/wire checks with a SQL-query model, including current retry, A-B-A/enrolment reuse refusal, intra-batch/device scope, new appended or pending commitment during readback, identity/member loss and wrong readback bytes: PASS. This is not independent PostgreSQL/lock/barrier execution; author PG and owner evidence remain separate. Response approval:"logged" is only exact log acknowledgement, not effective Replica application, pending approval, key delivery or Ready. Already-keyed VOID handling is the Replica contract, not an invented CP key-state gate. CP r_A/r_N peer delivery, actual cross-language SAS/approval/CLI integration, UI hardening requirements, native custody/currentness/Ready and live/private two-device acceptance remain excluded; no merge/deployment authorization.

@callumalpass

Copy link
Copy Markdown
Contributor Author

Control owner ACK: exact ac28eef, six-path delta against 0940c0c. Read route/common helpers, wire op/digest, full PostgreSQL tests and corrections. Latest device/op ordinal, stale commitment refusal, final identity/member/revoke/enrolment and latest-commit checks align with the logged-only response contract.

Independent execution: 15 actual CP PostgreSQL/HTTP tests passed in an owned exact-head review checkout (the author log fixture is synthetic, not a real log service). Includes an additional final-read linearization check. Logged acknowledgement is not a current-policy certificate, applied approval, key delivery or Ready; the native approval/reveal/key-grant consumer must independently validate current signed policy. S2 current-head verdict 6006643719 is separate.

Base automatically changed from next/private-collections to main October 6, 2026 05:36
@callumalpass
callumalpass enabled auto-merge October 6, 2026 05:53
@callumalpass
callumalpass added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 1e0cb97 Oct 6, 2026
14 checks passed
@callumalpass
callumalpass deleted the next/private-approval-request branch October 6, 2026 06:20
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