edge: the store door grants a box its platform pointer and artifact
Two exact shapes under control/system/ and bootstrap/router/, no listing, no writes. Without the pointer grant a door-backed fleet box would never see a platform promote.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QCUCpfxbCyEAVQ2m51dH5
2 files changed,
+48
−4
internal/edge/storedoor.go+30 −2internal/edge/storedoor_test.go+18 −2
diff --git a/internal/edge/storedoor.go b/internal/edge/storedoor.go| index ad3a0d460..b2a623761 100644 |
| --- a/internal/edge/storedoor.go |
| +++ b/internal/edge/storedoor.go |
| @@ -352,8 +352,14 @@ func canonicalPrefix(raw string) (string, bool) { |
| // aclRead is the read half of F3, prefix-string exact against the |
| // authenticated fleet's own identity: |
| // |
| -// control/serial the convergence poll |
| -// bootstrap/carlos the sanctioned self-update path (F7) |
| +// control/serial the convergence poll |
| +// bootstrap/carlos the sanctioned self-update path (F7); |
| +// stays until update-platform-binary.sh |
| +// is retired (spec §10) |
| +// control/system/<c>/<ch>.json its platform channel pointer (system |
| +// update spec §3) — see systemReadKey |
| +// bootstrap/router/<v>/<asset> a mirrored platform artifact — see |
| +// systemReadKey |
| // accounts/<sqid>/** its owning account's record — the manifest lives at |
| // accounts/<sqid>/account.json (domains.AccountManifestKey), |
| // NOT accounts/<sqid>.json: a box that cannot read it |
| @@ -388,6 +394,9 @@ func aclRead(sqid, fleet, key string) bool { |
| case "control/serial", "bootstrap/carlos": |
| return true |
| } |
| + if systemReadKey(key) { |
| + return true |
| + } |
| if strings.HasPrefix(key, "domains/") { |
| return true |
| } |
| @@ -396,6 +405,25 @@ func aclRead(sqid, fleet, key string) bool { |
| strings.HasPrefix(key, "fleets/"+fleet+"/") |
| } |
| +// systemReadKey is the two shapes the system-update spec (§3) grants every |
| +// fleet box: its platform channel pointer and a mirrored platform artifact. |
| +// Shapes, not prefixes — exactly three segments each, so nothing else that |
| +// ever lands under control/ or bootstrap/ is readable by a box. Listing is |
| +// not granted (aclDir is unchanged), and canonicalKey has already refused |
| +// any "." or ".." segment before this runs. |
| +func systemReadKey(key string) bool { |
| + if rest, ok := strings.CutPrefix(key, "control/system/"); ok { |
| + comp, file, ok := strings.Cut(rest, "/") |
| + return ok && comp != "" && file != "" && !strings.Contains(file, "/") && |
| + strings.HasSuffix(file, ".json") && len(file) > len(".json") |
| + } |
| + if rest, ok := strings.CutPrefix(key, "bootstrap/router/"); ok { |
| + ver, asset, ok := strings.Cut(rest, "/") |
| + return ok && ver != "" && asset != "" && !strings.Contains(asset, "/") |
| + } |
| + return false |
| +} |
| + |
| // aclDir is aclRead for a LISTING's child prefixes (ListDirs), which are |
| // directories rather than objects and so cannot be judged by it: a returned |
| // prefix is in scope exactly when it lies inside one of this tenant's own |
diff --git a/internal/edge/storedoor_test.go b/internal/edge/storedoor_test.go| index cfb98cf8f..979c11fa6 100644 |
| --- a/internal/edge/storedoor_test.go |
| +++ b/internal/edge/storedoor_test.go |
| @@ -82,6 +82,19 @@ func TestStoreDoorACLMatrix(t *testing.T) { |
| {"control/serial2", opRead, false, "the allowlist is exact, not a prefix"}, |
| {"control/", opRead, false, "no listing-shaped key stands in for the allowlist"}, |
| {"bootstrap/carlos.sig", opRead, false, "exact key only"}, |
| + {"control/system/router/stable.json", opRead, true, "the platform channel pointer (system update spec §3)"}, |
| + {"control/system/router/canary.json", opRead, true, "…and the canary one"}, |
| + {"control/system/router/stable.json", opWrite, false, "a box never writes a pointer"}, |
| + {"control/system/router", opRead, false, "the pointer shape is exactly three segments ending .json"}, |
| + {"control/system/router/stable.json/x", opRead, false, "…not four"}, |
| + {"control/system/router/stable.txt", opRead, false, "…and .json"}, |
| + {"bootstrap/router/v0.18.0/carlos-linux-arm64", opRead, true, "a mirrored platform artifact"}, |
| + {"bootstrap/router/v0.18.0/carlos-linux-arm64", opWrite, false, "a box must never publish the platform binary"}, |
| + {"bootstrap/router/v0.18.0", opRead, false, "the artifact shape is exactly three segments"}, |
| + {"bootstrap/router/v0.18.0/a/b", opRead, false, "…not four"}, |
| + {"bootstrap/router.sig", opRead, false, "nothing else under bootstrap/"}, |
| + {"bootstrap/router/../../x", opRead, false, "traversal is refused by canonicalKey before the ACL"}, |
| + {"control/system/../serial", opRead, false, "…on both prefixes"}, |
| // --- own account estate --- |
| // The account manifest is accounts/<sqid>/account.json |
| @@ -367,8 +380,10 @@ func TestStoreDoorGetServesOwnEstate(t *testing.T) { |
| etag := seedObject(t, store, ownKey, `{"version":"v1"}`) |
| seedObject(t, store, "control/serial", "42") |
| seedObject(t, store, "bootstrap/carlos", "ELF...") |
| + seedObject(t, store, "control/system/router/stable.json", `{"payload":"x"}`) |
| + seedObject(t, store, "bootstrap/router/v1/carlos-linux-arm64", "ELF") |
| - for _, key := range []string{ownKey, "control/serial", "bootstrap/carlos"} { |
| + for _, key := range []string{ownKey, "control/serial", "bootstrap/carlos", "control/system/router/stable.json", "bootstrap/router/v1/carlos-linux-arm64"} { |
| w := doorDo(d, "GET", "/store/o/"+key, "", nil) |
| if w.Code != http.StatusOK { |
| t.Fatalf("GET %s = %d, want 200", key, w.Code) |
| @@ -517,7 +532,8 @@ func TestStoreDoorRefusalsAreByteIdentical(t *testing.T) { |
| // control/certs/**, control/edge-mesh-key and the housekeeper's own lease |
| // object are NOT in aclRead's allowlist (compare the doc comment above |
| // aclRead — control/serial, bootstrap/carlos, domains/, accounts/<sqid>/, |
| -// apps/<sqid>/, fleets/<fleet>/, and nothing else), so no fleet box can ever |
| +// apps/<sqid>/, fleets/<fleet>/, control/system/<c>/<ch>.json, |
| +// bootstrap/router/<v>/<asset>, and nothing else), so no fleet box can ever |
| // read a private key, an ACME account key, a challenge token or the |
| // housekeeper's coordination lock through this door, however it authenticates. |
| // |