carlos / platform Public

settings-1 task 7: adoption checks the signed manifest digest before staging

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 345aa02ec01908ee43ef0deb5aa85e5d4d799c82 parent af49b40
2 files changed, +333 −12
  • internal/release/adopt.go +95 −12
  • internal/release/adoptdigest_test.go +238 −0
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 82dc9f422..1b7ee9884 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -42,6 +42,9 @@ type passConfig struct {
// forward adoption ran under, never a fresh one that could be a
// successor's.
selected map[string]hostwrite.Work
+ // ratchet is #253's per-box ratchet (WithDigestRatchet); nil admits
+ // every verified pointer, as before.
+ ratchet *DigestRatchet
// nag is where the canary-TTL nag (spec §5) is printed; nil (the
// default — every call site that does not pass NagExpiredCanaries)
// means the nag never fires at all, not even to a discarded writer.
@@ -280,6 +283,14 @@ func WithProofGate(g *ProofGate) PassOption {
return func(c *passConfig) { c.proof = g }
}
+// WithDigestRatchet arms #253's per-box ratchet for this pass (spec §4.6):
+// a pointer without digests is refused on a channel this box has seen with
+// them, or everywhere under CARLOS_REQUIRE_POINTER_DIGEST. Root and the
+// agent each pass their own (internal/host pointerRatchet).
+func WithDigestRatchet(r *DigestRatchet) PassOption {
+ return func(c *passConfig) { c.ratchet = r }
+}
+
// stageBlocked reports whether err is the version store refusing a write to
// THIS uid, rather than any of the many other ways staging fails. errors.Is
// unwraps the *os.PathError that mkdir/openat return.
@@ -440,6 +451,13 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
}
continue
}
+ // Any error from Admit refuses the pointer, ErrRatchetUnpersisted
+ // included, not only ErrPointerWithoutDigests.
+ if rerr := cfg.ratchet.Admit(k.acct, ptr); rerr != nil {
+ pointers[k] = nil // refused: the group keeps what it has
+ fmt.Printf(" %s/%s/%s: channel pointer refused: %v\n", k.acct, k.app, k.channel, rerr)
+ continue
+ }
pointers[k] = ptr
}
@@ -996,7 +1014,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry,
// This call never fails the pass: an absent or unreadable sibling is the
// safe leave-alone state, so a repeated failure here is only a log line.
if cur != "" {
- backfillManifest(ctx, s, optDir, RouteAccount(rt), rt.App, cur, rt.Host)
+ backfillManifest(ctx, s, optDir, RouteAccount(rt), rt.App, cur, rt.Host, ptr.DigestOf(cur))
}
if cur == ver {
cfg.proof.clear(rt.Host) // live on its head: nothing left to wait for
@@ -1090,7 +1108,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry,
if !admitted {
return
}
- m, err := ensureStaged(ctx, s, optDir, RouteAccount(rt), rt.App, ver, rt.Host)
+ m, err := ensureStagedDigest(ctx, s, optDir, RouteAccount(rt), rt.App, ver, rt.Host, ptr.DigestOf(ver))
if err != nil {
fmt.Printf(" %s: %v\n", rt.Host, err)
// A version this uid cannot write is the one staging failure a
@@ -1688,6 +1706,55 @@ func ensureStaged(ctx context.Context, s Store, optDir, acct, app, ver, host str
return m, nil
}
+// ensureStagedDigest is ensureStaged bound to a signed manifest digest (#253,
+// spec §4.6). want "" is ensureStaged exactly: a pointer from before digests,
+// or one written with the marker off. Otherwise the canonical manifest must
+// hash to want BEFORE anything is downloaded, and the manifest that passed
+// is the one staging uses: it is never read a second time, so a swap between
+// the check and the download cannot slip through. A copy already staged under
+// the name must be that manifest's artifact, or it is refused: a staged
+// directory is only a name.
+func ensureStagedDigest(ctx context.Context, s Store, optDir, acct, app, ver, host, want string) (*Manifest, error) {
+ if want == "" {
+ return ensureStaged(ctx, s, optDir, acct, app, ver, host)
+ }
+ m, raw, err := CheckManifestDigest(ctx, s, acct, app, ver, want)
+ if err != nil {
+ return nil, fmt.Errorf("manifest for %s/%s: %w", app, ver, err)
+ }
+ art, ok := m.Artifacts[adoptPlatform]
+ if !ok {
+ return nil, fmt.Errorf("release %s has no %s build; skipping", ver, adoptPlatform)
+ }
+ path := VersionPath(optDir, acct, ver, app)
+ if _, err := os.Stat(path); err == nil {
+ sum, err := FileSHA256(path)
+ if err != nil {
+ return nil, fmt.Errorf("hash the staged copy of %s: %w", ver, err)
+ }
+ if sum != art.SHA256 {
+ return nil, fmt.Errorf("%w: the staged copy of %s is not the build its pointer signed; not pointed live", ErrManifestDigest, ver)
+ }
+ } else {
+ body, _, err := GetQualified(ctx, s, ArtifactKey(acct, app, ver, art.File))
+ if err != nil {
+ return nil, fmt.Errorf("download %s: %w", ver, err)
+ }
+ if got := etagOf(body); got != art.SHA256 {
+ return nil, fmt.Errorf("download %s: sha256 mismatch (refusing to stage)", ver)
+ }
+ if err := StageArtifact(optDir, acct, ver, app, body); err != nil {
+ return nil, err
+ }
+ }
+ // The sibling is the checked canonical bytes. Best-effort, as in
+ // ensureStaged.
+ if err := StageManifest(optDir, acct, ver, app, raw); err != nil {
+ fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err)
+ }
+ return m, nil
+}
+
// backfillManifest is adoptHost's per-pass repair for a LIVE version's
// staged sibling (2026-08-03 review, C1). ensureStaged's own back-fill only
// fires from the call inside adoptHost that sits behind the cur == ver
@@ -1701,19 +1768,35 @@ func ensureStaged(ctx context.Context, s Store, optDir, acct, app, ver, host str
// converge pass treats it as "unknown", never as "no capabilities"), so a
// repeated failure here is only ever worth a log line, host-prefixed like
// every other line adoptHost prints.
-func backfillManifest(ctx context.Context, s Store, optDir, acct, app, ver, host string) {
+//
+// want is the digest the authorizing pointer signs for ver ("" when it signs
+// none, which is today's behaviour exactly). With one, the bucket manifest
+// must hash to it (CheckManifestDigest) and the checked canonical bytes are
+// what is staged; a mismatch is a log line and no sibling, the same
+// leave-alone state as any other failure here (#253, spec §4.6).
+func backfillManifest(ctx context.Context, s Store, optDir, acct, app, ver, host, want string) {
if _, err := os.Stat(StagedManifestPath(optDir, acct, ver, app)); err == nil {
return
}
- m, err := GetManifest(ctx, s, acct, app, ver)
- if err != nil {
- fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err)
- return
- }
- raw, err := json.MarshalIndent(m, "", " ")
- if err != nil {
- fmt.Printf(" %s: marshal staged manifest for %s: %v\n", host, ver, err)
- return
+ var raw []byte
+ if want != "" {
+ _, checked, err := CheckManifestDigest(ctx, s, acct, app, ver, want)
+ if err != nil {
+ fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err)
+ return
+ }
+ raw = checked
+ } else {
+ m, err := GetManifest(ctx, s, acct, app, ver)
+ if err != nil {
+ fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err)
+ return
+ }
+ raw, err = json.MarshalIndent(m, "", " ")
+ if err != nil {
+ fmt.Printf(" %s: marshal staged manifest for %s: %v\n", host, ver, err)
+ return
+ }
}
if err := StageManifest(optDir, acct, ver, app, raw); err != nil {
fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err)
diff --git a/internal/release/adoptdigest_test.go b/internal/release/adoptdigest_test.go
new file mode 100644
index 000000000..6a1db4bd6
--- /dev/null
+++ b/internal/release/adoptdigest_test.go
@@ -0,0 +1,238 @@
+package release
+
+import (
+ "bytes"
+ "context"
+ "os"
+ "strings"
+ "testing"
+)
+
+// replayPointer writes back a saved pointer, the attacker's move in #253.
+func replayPointer(t *testing.T, s Store, ch string, saved []byte) {
+ t.Helper()
+ ctx := context.Background()
+ _, etag, err := s.Get(ctx, ChannelKey("bab", "km", ch))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ChannelKey("bab", "km", ch), saved, etag); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// Spec §8: a reused version name under a replayed digest-bearing pointer is
+// refused before staging.
+func TestAReusedNameUnderAReplayedPointerIsRefusedBeforeStaging(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ markerOn(t, s, priv, "bab")
+
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("the bytes that were promoted"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ saved, _, _ := s.Get(ctx, ChannelKey("bab", "km", "canary/x"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil {
+ t.Fatal(err)
+ }
+ if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("bytes nobody promoted"))
+ replayPointer(t, s, "canary/x", saved)
+
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() {
+ if err := Pass(ctx, reg, s, pub, opt, nil); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ })
+ if v, _ := LiveVersion(opt, "a.example.com"); v != "" {
+ t.Fatalf("live on %q: the replayed pointer installed bytes nobody promoted", v)
+ }
+ if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil {
+ t.Fatal("the reused name's artifact was staged")
+ }
+ if !strings.Contains(out.String(), "pointer signed") {
+ t.Fatalf("no refusal line:\n%s", out.String())
+ }
+}
+
+func TestAStagedCopyThatIsNotTheSignedBuildIsRefused(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("signed"))
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("left over from a pruned v1")); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() { _ = Pass(ctx, reg, s, pub, opt, nil) })
+ if v, _ := LiveVersion(opt, "a.example.com"); v != "" {
+ t.Fatalf("live on %q over a staged copy that is not the signed build", v)
+ }
+}
+
+func TestADigestBearingPointerAdoptsTheSignedBuild(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ if err := Pass(ctx, reg, s, pub, opt, nil); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
+ t.Fatalf("live %q, want v1", v)
+ }
+ sib, err := os.ReadFile(StagedManifestPath(opt, "bab", "v1", "km"))
+ if err != nil || ManifestDigest(sib) != manifestDigestOf(t, s, "bab", "km", "v1") {
+ t.Fatalf("the staged sibling is not the checked canonical bytes: %v", err)
+ }
+}
+
+// A rollback's head is an old history entry: its digest moved with it.
+func TestARolledBackHeadIsCheckedAgainstItsDigest(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ for _, v := range []string{"v1", "v2"} {
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("other bytes, same name"))
+ if err := Rollback(ctx, s, priv, "bab", "km", "canary/x"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ withCapturedStdout(t, &bytes.Buffer{}, func() { _ = Pass(ctx, reg, s, pub, opt, nil) })
+ if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" {
+ t.Fatal("a rollback onto a reused name installed its new bytes")
+ }
+}
+
+func TestTheRatchetRefusesALegacyPointerInThePass(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ legacy, _, _ := s.Get(ctx, ChannelKey("bab", "km", "canary/x"))
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ r := NewDigestRatchet(t.TempDir(), nil, false)
+ if err := Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(r)); err != nil {
+ t.Fatal(err)
+ }
+ replayPointer(t, s, "canary/x", legacy)
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() { _ = Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(r)) })
+ if v, _ := LiveVersion(opt, "a.example.com"); v != "v2" {
+ t.Fatalf("live %q: the box took a legacy pointer after seeing digests", v)
+ }
+ if !strings.Contains(out.String(), "channel pointer refused") {
+ t.Fatalf("no refusal line:\n%s", out.String())
+ }
+}
+
+// The default is today: no marker, no ratchet, legacy pointers adopt.
+func TestALegacyPointerAdoptsAsBefore(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ if err := Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(NewDigestRatchet(t.TempDir(), nil, false))); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
+ t.Fatalf("live %q, want v1", v)
+ }
+}
+
+// P10: the live version's capability sibling is bound to the digest too. A
+// bucket manifest that no longer hashes to the pointer's digest is not
+// written beside the running binary.
+func TestBackfillOfTheLiveSiblingIsBoundToTheDigest(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "a.example.com", "km", "canary/x", false)
+ if err := Pass(ctx, reg, s, pub, opt, nil); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
+ t.Fatalf("live %q, want v1", v)
+ }
+ sib := StagedManifestPath(opt, "bab", "v1", "km")
+ if err := os.Remove(sib); err != nil {
+ t.Fatal(err)
+ }
+ raw, etag, err := s.Get(ctx, ManifestKey("bab", "km", "v1"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ManifestKey("bab", "km", "v1"), append(raw, '\n'), etag); err != nil {
+ t.Fatal(err)
+ }
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() {
+ if err := Pass(ctx, reg, s, pub, opt, nil); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ })
+ if _, err := os.Stat(sib); err == nil {
+ t.Fatal("a sibling that does not hash to the pointer's digest was written beside the live version")
+ }
+ if !strings.Contains(out.String(), "pointer signed") {
+ t.Fatalf("no refusal line:\n%s", out.String())
+ }
+}