carlos / platform Public

settings-2 task 7: a move's held stage installs only §4.1-authorized builds

StageHeld asks HeldAuthorized first: a signed promotion record (its digest
bound at staging), else a verified, ratchet-admitted pointer of the app that
names the build. Anything else is ErrHeldNotAuthorized before a byte is
downloaded; the root relay drops it (and ErrManifestDigest) for good. pub and
root's ratchet reach the root pass; the agent's own held path (heldReleaseReady,
ruling R5) gets the parsed release key and its ratchet, failing closed on a
key that does not parse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9d4f8d0d9b65752907d9f2c3df509b4a09129b87 parent 442ada5
13 files changed, +497 −86
  • internal/host/agent.go +1 −1
  • internal/host/heldstage.go +8 −2
  • internal/host/heldstage_test.go +112 −8
  • internal/host/movespass.go +12 −1
  • internal/host/movesreport_test.go +12 −20
  • internal/host/movesstage.go +10 −2
  • internal/host/movesstage_proof_test.go +9 −6
  • internal/release/heldauth_test.go +184 −0
  • internal/release/heldverify_test.go +3 −3
  • internal/release/materializeheld.go +81 −14
  • internal/release/materializeheld_proof_test.go +26 −10
  • internal/release/materializeheld_test.go +38 −18
  • internal/release/stage_reuse_test.go +1 −1
diff --git a/internal/host/agent.go b/internal/host/agent.go
index c3afd0949..fabde926a 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4206,7 +4206,7 @@ func runAdoptWithEnsureSystemUnits(args []string, buildClaims ClaimFactory, ensu
// release adoption, on -stage-only and on the hourly pass (its floor).
// Reads nothing and writes nothing unless a request exists, and none
// exist while CARLOS_MOVES_PHASE1 is off.
- heldStagePass(ctx, dataDir(), release.OptDir(), reg, store, BoxLabel())
+ heldStagePass(ctx, dataDir(), release.OptDir(), pub, pointerRatchet(true, release.OptDir(), dataDir(), os.Getenv), reg, store, BoxLabel())
// Step 4: the advisories. The two capability advisories that fall out of
// READING manifests (declared-but-ungranted, and capabilities on an
diff --git a/internal/host/heldstage.go b/internal/host/heldstage.go
index c7173cd8d..a7abb7f52 100644
--- a/internal/host/heldstage.go
+++ b/internal/host/heldstage.go
@@ -2,6 +2,7 @@ package host
import (
"context"
+ "crypto/ed25519"
"encoding/json"
"errors"
"fmt"
@@ -194,9 +195,14 @@ func heldStageClaim(obs hostclaim.Observed, req release.HeldStageRequest) error
// D3-held: each request passes D3 as adoption would, through a fresh gate
// (this is a fresh root process, as for the adoption pass before it) whose
// bake directory is the healthy history that pass reads.
-func heldStagePass(ctx context.Context, dataDir, optDir string, reg *registry.Registry, store release.Store, box string) {
+//
+// Settings spec rev 11 §4.3a: each request stages only a build §4.1
+// authorizes, a signed promotion record or a pointer of the app verified
+// under pub and admitted by r, root's pointer digest ratchet (the one
+// adoption uses). Anything else is dropped before a byte is downloaded.
+func heldStagePass(ctx context.Context, dataDir, optDir string, pub ed25519.PublicKey, r *release.DigestRatchet, reg *registry.Registry, store release.Store, box string) {
proof := release.NewProofGate(release.ProofPolicyPath, dataDir)
- if err := release.ServeHeldStageRequests(ctx, store, optDir, dataDir, proof, heldStageAuthority(reg, store, box, dataDir)); err != nil {
+ if err := release.ServeHeldStageRequests(ctx, store, pub, r, optDir, dataDir, proof, heldStageAuthority(reg, store, box, dataDir)); err != nil {
log.Printf("adopt: held stage requests: %v", err)
}
retireEndedHeldRefusals(ctx, store, dataDir, box)
diff --git a/internal/host/heldstage_test.go b/internal/host/heldstage_test.go
index 88aa68140..7dc1d46ab 100644
--- a/internal/host/heldstage_test.go
+++ b/internal/host/heldstage_test.go
@@ -1,7 +1,9 @@
package host
import (
+ "bytes"
"context"
+ "crypto/ed25519"
"encoding/json"
"errors"
"os"
@@ -26,6 +28,28 @@ const (
hsBox = "uk-box"
)
+// hsPriv signs the held-stage tests' pointers and promotion records, and
+// hsPub verifies them: the release key a held stage is handed.
+var (
+ hsPriv = ed25519.NewKeyFromSeed(bytes.Repeat([]byte{9}, ed25519.SeedSize))
+ hsPub = hsPriv.Public().(ed25519.PublicKey)
+)
+
+// hsRatchet is a fresh, unarmed pointer digest ratchet: today's admission.
+func hsRatchet(t *testing.T) *release.DigestRatchet {
+ return release.NewDigestRatchet(t.TempDir(), nil, false)
+}
+
+// promoteHeld promotes bab/app's version to canary/x with hsPriv, writing
+// its signed promotion record: a held stage installs only a build §4.1
+// authorizes (settings spec rev 11 §4.3a).
+func promoteHeld(t *testing.T, s release.Store, app, version string) {
+ t.Helper()
+ if err := release.Promote(context.Background(), s, hsPriv, "bab", app, "canary/x", version); err != nil {
+ t.Fatal(err)
+ }
+}
+
// hsClaim writes host's claim: held by bab/titogo, op as given.
func hsClaim(t *testing.T, s release.Store, op hostclaim.Op) {
t.Helper()
@@ -239,7 +263,7 @@ func (c *hsCountingStore) Put(ctx context.Context, key string, body []byte, ifMa
func TestHeldStagePassIsANoOpWithoutRequests(t *testing.T) {
dd, opt := t.TempDir(), t.TempDir()
s := &hsCountingStore{Store: release.NewDirStore(t.TempDir())}
- heldStagePass(context.Background(), dd, opt, testRegistry(t), s, hsBox)
+ heldStagePass(context.Background(), dd, opt, hsPub, hsRatchet(t), testRegistry(t), s, hsBox)
if s.n != 0 {
t.Fatalf("the pass made %d store calls", s.n)
}
@@ -257,10 +281,7 @@ func TestHeldStagePassServesAnAuthorizedRequest(t *testing.T) {
s := release.NewDirStore(t.TempDir())
hsClaim(t, s, movingOp())
hsReport(t, s, hsOp, "v7")
- m := release.Manifest{App: "titogo", Version: "v7", Kind: "binary", Artifacts: map[string]release.Artifact{runtime.GOOS + "-" + runtime.GOARCH: {}}}
- if err := release.Ship(ctx, s, "bab", m, map[string][]byte{runtime.GOOS + "-" + runtime.GOARCH: []byte("pinned")}); err != nil {
- t.Fatal(err)
- }
+ shipV7(t, s)
dd, opt := t.TempDir(), t.TempDir()
reqDir := release.HeldStageDir(dd)
if err := os.MkdirAll(reqDir, 0o755); err != nil {
@@ -277,7 +298,7 @@ func TestHeldStagePassServesAnAuthorizedRequest(t *testing.T) {
if err := os.WriteFile(filepath.Join(reqDir, stale.Host+".json"), raw, 0o644); err != nil {
t.Fatal(err)
}
- heldStagePass(ctx, dd, opt, testRegistry(t), s, hsBox)
+ heldStagePass(ctx, dd, opt, hsPub, hsRatchet(t), testRegistry(t), s, hsBox)
if got, err := release.LiveVersion(opt, hsHost); err != nil || got != "v7" {
t.Fatalf("LiveVersion = %q, %v", got, err)
}
@@ -293,7 +314,8 @@ func TestHeldStagePassServesAnAuthorizedRequest(t *testing.T) {
}
}
-// shipV7 ships bab/titogo v7 for this platform.
+// shipV7 ships bab/titogo v7 for this platform and promotes it to
+// canary/x, so a held stage of it is authorized.
func shipV7(t *testing.T, s release.Store) {
t.Helper()
plat := runtime.GOOS + "-" + runtime.GOARCH
@@ -301,6 +323,7 @@ func shipV7(t *testing.T, s release.Store) {
if err := release.Ship(context.Background(), s, "bab", m, map[string][]byte{plat: []byte("pinned")}); err != nil {
t.Fatal(err)
}
+ promoteHeld(t, s, "titogo", "v7")
}
// TestHeldPublicationIsGuardedByTheAuthority: the authority that admitted a
@@ -356,7 +379,7 @@ func TestHeldPublicationIsGuardedByTheAuthority(t *testing.T) {
t.Fatalf("authority before staging: %v", err)
}
rt := registry.Route{Host: req.Host, Account: req.Account, App: req.App, HoldOp: req.Op}
- if _, err := release.StageHeld(ctx, s, opt, rt, req.Version); err != nil {
+ if _, err := release.StageHeld(ctx, s, hsPub, hsRatchet(t), opt, rt, req.Version); err != nil {
t.Fatalf("StageHeld: %v", err)
}
tc.during(t, reg, s)
@@ -453,3 +476,84 @@ func TestHeldPublicationRechecksInsideTheFreeze(t *testing.T) {
}
})
}
+
+// shipUnpromoted ships bab/app's version for this platform and promotes it
+// nowhere: no promotion record and no pointer authorizes it.
+func shipUnpromoted(t *testing.T, s release.Store, app, version string) {
+ t.Helper()
+ plat := runtime.GOOS + "-" + runtime.GOARCH
+ m := release.Manifest{App: app, Version: version, Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}}
+ if err := release.Ship(context.Background(), s, "bab", m, map[string][]byte{plat: []byte("pinned")}); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// Settings spec rev 11 §4.3a, root's side: a request the move's authority
+// accepts, for a build no promotion record and no pointer authorizes, is
+// dropped before anything is downloaded, and live/<host> is never written.
+func TestHeldStagePassDropsAnUnauthorizedBuild(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ hsClaim(t, s, movingOp())
+ hsReport(t, s, hsOp, "v7")
+ shipUnpromoted(t, s, "titogo", "v7")
+ dd, opt := t.TempDir(), t.TempDir()
+ reqDir := release.HeldStageDir(dd)
+ if err := os.MkdirAll(reqDir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ raw, _ := json.Marshal(hsRequest())
+ if err := os.WriteFile(filepath.Join(reqDir, hsHost+".json"), raw, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ heldStagePass(ctx, dd, opt, hsPub, hsRatchet(t), testRegistry(t), s, hsBox)
+ if got, _ := release.LiveVersion(opt, hsHost); got != "" {
+ t.Fatalf("an unauthorized build went live: %q", got)
+ }
+ if _, err := os.Stat(filepath.Join(opt, "versions")); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("an unauthorized build was staged: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(reqDir, hsHost+".json")); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("the refused request was kept for a retry: %v", err)
+ }
+}
+
+// Settings spec rev 11 §4.3a, the agent's own path (ruling R5): T7 stages
+// only a build §4.1 authorizes. A shipped, never-promoted build, or any
+// build when the release key did not parse (a nil pub), fails T7 with
+// nothing staged and nothing relayed to the root pass.
+func TestHeldReleaseReadyRefusesAnUnauthorizedBuild(t *testing.T) {
+ ctx := context.Background()
+ for name, tc := range map[string]struct {
+ promote bool
+ pub ed25519.PublicKey
+ }{
+ "never promoted": {false, hsPub},
+ "no release key": {true, nil},
+ } {
+ t.Run(name, func(t *testing.T) {
+ h := newPhase1Harness(t)
+ rec := h.start(t, "manual")
+ shipUnpromoted(t, h.s, "app", "v7")
+ if tc.promote {
+ promoteHeld(t, h.s, "app", "v7")
+ }
+ h.pass(t)
+ data, opt := t.TempDir(), t.TempDir()
+ relayed := false
+ ready := heldReleaseReady(testRegistry(t), opt, data, "uk-box", tc.pub, hsRatchet(t), offProofGate(t), func(string, string, error) { relayed = true })
+ if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed {
+ t.Fatalf("T7 = %+v, want %q", c, t7Failed)
+ }
+ if _, err := os.Stat(filepath.Join(opt, "versions")); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("an unauthorized build was staged: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(release.HeldStageDir(data), p1Host+".json")); !errors.Is(err, os.ErrNotExist) || relayed {
+ t.Fatalf("an unauthorized build was relayed to the root pass (%v, %v)", err, relayed)
+ }
+ if v, _ := release.LiveVersion(opt, p1Host); v != "" {
+ t.Fatalf("live = %q, want no live link", v)
+ }
+ })
+ }
+}
diff --git a/internal/host/movespass.go b/internal/host/movespass.go
index 12f4ba48b..f744420b5 100644
--- a/internal/host/movespass.go
+++ b/internal/host/movespass.go
@@ -349,7 +349,18 @@ func armPhase1(cfg *moveCfg, w *movesWorker, hib activator.Config, dataDir strin
// D3-held: the held path's own gate, for the life of the worker, so
// its miss cache holds between passes; its bake directory is the
// healthy history adoption reads.
- cfg.ReleaseReady = heldReleaseReady(reg, cfg.OptDir, dataDir, cfg.Box,
+ //
+ // Settings spec rev 11 §4.3a: the held stage installs only a build a
+ // promotion record or a pointer verified under the release key, and
+ // admitted by the agent's pointer digest ratchet, authorizes. A key
+ // that does not parse leaves pub nil, and every held stage is
+ // refused: fail closed.
+ pub, perr := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
+ if perr != nil {
+ log.Printf("moves: held stages are refused: %v", perr)
+ pub = nil
+ }
+ cfg.ReleaseReady = heldReleaseReady(reg, cfg.OptDir, dataDir, cfg.Box, pub, pointerRatchet(false, cfg.OptDir, dataDir, getenv),
release.NewProofGate(release.ProofPolicyPath, dataDir), stageRequester(dataDir))
if svc != nil {
// S1 asks the instance itself, woken through the activator.
diff --git a/internal/host/movesreport_test.go b/internal/host/movesreport_test.go
index 49f0109c5..b1c5ccc7a 100644
--- a/internal/host/movesreport_test.go
+++ b/internal/host/movesreport_test.go
@@ -2,6 +2,7 @@ package host
import (
"context"
+ "crypto/ed25519"
"encoding/binary"
"encoding/json"
"errors"
@@ -11,7 +12,6 @@ import (
"net/http/httptest"
"os"
"path/filepath"
- "runtime"
"strings"
"syscall"
"testing"
@@ -258,23 +258,19 @@ func TestPreparingRelaysHeldStageOnAHardenedBox(t *testing.T) {
ctx := context.Background()
h := newPhase1Harness(t)
h.start(t, "manual")
- plat := runtime.GOOS + "-" + runtime.GOARCH
- if err := release.Ship(ctx, h.s, "bab", release.Manifest{App: "app", Version: "v7", Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}},
- map[string][]byte{plat: []byte("pinned")}); err != nil {
- t.Fatal(err)
- }
+ shipPinnedV7(t, h.s)
h.pass(t) // the source report names Release v7
reg := testRegistry(t)
targetData, targetOpt := t.TempDir(), t.TempDir()
prev := heldMaterialize
t.Cleanup(func() { heldMaterialize = prev })
- heldMaterialize = func(context.Context, release.Store, string, registry.Route, string, *release.ProofGate, func(func() error) error) (*release.Manifest, error) {
+ heldMaterialize = func(context.Context, release.Store, ed25519.PublicKey, *release.DigestRatchet, string, registry.Route, string, *release.ProofGate, func(func() error) error) (*release.Manifest, error) {
return nil, fmt.Errorf("%w: %w", release.ErrStageBlocked, &os.PathError{Op: "mkdir", Path: "/opt/carlos/versions", Err: syscall.EROFS})
}
tc := h.targetCfg()
tc.DataDir = targetData
- tc.ReleaseReady = heldReleaseReady(reg, targetOpt, targetData, "uk-box", offProofGate(t), stageRequester(targetData))
+ tc.ReleaseReady = heldReleaseReady(reg, targetOpt, targetData, "uk-box", hsPub, hsRatchet(t), offProofGate(t), stageRequester(targetData))
h.targetPass(t, tc)
if f := failing(h.targetReport(t)); f[moves.CheckRelease] != t7Waiting {
t.Fatalf("T7 = %q, want %q", f[moves.CheckRelease], t7Waiting)
@@ -293,7 +289,7 @@ func TestPreparingRelaysHeldStageOnAHardenedBox(t *testing.T) {
// The root pass, with the relay's own authority, materializes it.
heldMaterialize = prev
- if err := release.ServeHeldStageRequests(ctx, h.s, targetOpt, targetData, offProofGate(t), heldStageAuthority(reg, h.s, "uk-box", targetData)); err != nil {
+ if err := release.ServeHeldStageRequests(ctx, h.s, hsPub, hsRatchet(t), targetOpt, targetData, offProofGate(t), heldStageAuthority(reg, h.s, "uk-box", targetData)); err != nil {
t.Fatal(err)
}
if v, err := release.LiveVersion(targetOpt, p1Host); err != nil || v != "v7" {
@@ -313,7 +309,7 @@ func TestPreparingRelaysHeldStageOnAHardenedBox(t *testing.T) {
if err := writeHeldStageRequest(targetData, bad); err != nil {
t.Fatal(err)
}
- if err := release.ServeHeldStageRequests(ctx, h.s, opt, targetData, offProofGate(t), heldStageAuthority(reg, h.s, "uk-box", targetData)); err != nil {
+ if err := release.ServeHeldStageRequests(ctx, h.s, hsPub, hsRatchet(t), opt, targetData, offProofGate(t), heldStageAuthority(reg, h.s, "uk-box", targetData)); err != nil {
t.Fatal(err)
}
if v, _ := release.LiveVersion(opt, p1Host); v != "" {
@@ -412,15 +408,11 @@ func TestHeldReleaseReadyOnAWritableBox(t *testing.T) {
ctx := context.Background()
h := newPhase1Harness(t)
rec := h.start(t, "manual")
- plat := runtime.GOOS + "-" + runtime.GOARCH
- if err := release.Ship(ctx, h.s, "bab", release.Manifest{App: "app", Version: "v7", Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}},
- map[string][]byte{plat: []byte("pinned")}); err != nil {
- t.Fatal(err)
- }
+ shipPinnedV7(t, h.s)
h.pass(t)
reg := testRegistry(t)
data, opt := t.TempDir(), t.TempDir()
- ready := heldReleaseReady(reg, opt, data, "uk-box", offProofGate(t), nil)
+ ready := heldReleaseReady(reg, opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil)
if c := ready(ctx, h.s, rec, "v7"); !c.OK {
t.Fatalf("T7 = %+v", c)
}
@@ -431,12 +423,12 @@ func TestHeldReleaseReadyOnAWritableBox(t *testing.T) {
// publication's authority refuses, nothing is published.
prev := heldMaterialize
t.Cleanup(func() { heldMaterialize = prev })
- heldMaterialize = func(ctx context.Context, s release.Store, optDir string, rt registry.Route, v string, proof *release.ProofGate, guard func(func() error) error) (*release.Manifest, error) {
+ heldMaterialize = func(ctx context.Context, s release.Store, pub ed25519.PublicKey, r *release.DigestRatchet, optDir string, rt registry.Route, v string, proof *release.ProofGate, guard func(func() error) error) (*release.Manifest, error) {
rewriteClaimOp(t, h.s, func(op *hostclaim.Op) { op.ID = p1ID2 })
- return prev(ctx, s, optDir, rt, v, proof, guard)
+ return prev(ctx, s, pub, r, optDir, rt, v, proof, guard)
}
opt2 := t.TempDir()
- if c := heldReleaseReady(reg, opt2, data, "uk-box", offProofGate(t), nil)(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed {
+ if c := heldReleaseReady(reg, opt2, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil)(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed {
t.Fatalf("T7 when the op changed during staging = %+v", c)
}
if v, _ := release.LiveVersion(opt2, p1Host); v != "" {
@@ -445,7 +437,7 @@ func TestHeldReleaseReadyOnAWritableBox(t *testing.T) {
// Under another op from the start: refused before anything is staged.
heldMaterialize = prev
opt3 := t.TempDir()
- if c := heldReleaseReady(reg, opt3, data, "uk-box", offProofGate(t), nil)(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed {
+ if c := heldReleaseReady(reg, opt3, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil)(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed {
t.Fatalf("T7 under another op = %+v", c)
}
if _, err := os.Stat(filepath.Join(opt3, "versions")); !errors.Is(err, os.ErrNotExist) {
diff --git a/internal/host/movesstage.go b/internal/host/movesstage.go
index cd053b20d..38c994733 100644
--- a/internal/host/movesstage.go
+++ b/internal/host/movesstage.go
@@ -2,6 +2,7 @@ package host
import (
"context"
+ "crypto/ed25519"
"encoding/json"
"errors"
"log"
@@ -44,7 +45,14 @@ var heldMaterialize = release.MaterializeHeld
// never agentProofGate: a wait it records for a held host would sit on the
// adoption waiting list, which the fast loop sweeps, for a host adoption
// never visits.
-func heldReleaseReady(reg *registry.Registry, optDir, dataDir, box string, proof *release.ProofGate, requestStage func(host, ver string, err error)) func(context.Context, release.Store, provision.Record, string) moves.Check {
+//
+// pub and r are §4.1's evidence (settings spec rev 11 §4.3a): the stage
+// installs only a build a promotion record or a pointer of the app,
+// verified under pub and admitted by r (the agent's pointer digest
+// ratchet), authorizes. Anything else is release.ErrHeldNotAuthorized,
+// before anything is downloaded or relayed, and T7 fails (t7Failed). A nil
+// pub verifies nothing, so every held stage is refused.
+func heldReleaseReady(reg *registry.Registry, optDir, dataDir, box string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate, requestStage func(host, ver string, err error)) func(context.Context, release.Store, provision.Record, string) moves.Check {
return func(ctx context.Context, s release.Store, rec provision.Record, version string) moves.Check {
req := release.HeldStageRequest{Account: rec.Account, App: rec.App, Host: rec.Host, Version: version, Op: rec.Move.ID, Box: box}
auth := heldStageAuthority(reg, s, box, dataDir)
@@ -72,7 +80,7 @@ func heldReleaseReady(reg *registry.Registry, optDir, dataDir, box string, proof
if c, done := verified(); done {
return c
}
- _, err := heldMaterialize(ctx, s, optDir, rt, version, proof, func(publish func() error) error { return auth(req, publish) })
+ _, err := heldMaterialize(ctx, s, pub, r, optDir, rt, version, proof, func(publish func() error) error { return auth(req, publish) })
switch {
case errors.Is(err, release.ErrHeldNotProven):
logMoveStage(rec.Host, version, err)
diff --git a/internal/host/movesstage_proof_test.go b/internal/host/movesstage_proof_test.go
index ec532547b..e6c7b5614 100644
--- a/internal/host/movesstage_proof_test.go
+++ b/internal/host/movesstage_proof_test.go
@@ -72,6 +72,8 @@ func (b *heldProofBox) prove(t *testing.T, s release.Store, app string, body []b
}
}
+// shipPinnedV7 ships bab/app v7 for this platform and promotes it to
+// canary/x, so a held stage of it is authorized.
func shipPinnedV7(t *testing.T, s release.Store) {
t.Helper()
plat := runtime.GOOS + "-" + runtime.GOARCH
@@ -79,6 +81,7 @@ func shipPinnedV7(t *testing.T, s release.Store) {
map[string][]byte{plat: []byte("pinned")}); err != nil {
t.Fatal(err)
}
+ promoteHeld(t, s, "app", "v7")
}
// D3-held, through the target's report: on a box whose policy lists the
@@ -99,7 +102,7 @@ func TestHeldReleaseReadyAppliesTheProofGate(t *testing.T) {
data, opt := t.TempDir(), t.TempDir()
tc := h.targetCfg()
tc.DataDir = data
- tc.ReleaseReady = heldReleaseReady(testRegistry(t), opt, data, "uk-box", box.gate(), stageRequester(data))
+ tc.ReleaseReady = heldReleaseReady(testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), box.gate(), stageRequester(data))
h.targetPass(t, tc)
if f := failing(h.targetReport(t)); f[moves.CheckRelease] != t7NotProven {
t.Fatalf("T7 = %q, want %q", f[moves.CheckRelease], t7NotProven)
@@ -124,7 +127,7 @@ func TestHeldReleaseReadyAppliesTheProofGate(t *testing.T) {
box.require(t, "app")
box.prove(t, h.s, "app", []byte("pinned"))
opt := t.TempDir()
- if c := heldReleaseReady(testRegistry(t), opt, t.TempDir(), "uk-box", box.gate(), nil)(ctx, h.s, rec, "v7"); !c.OK {
+ if c := heldReleaseReady(testRegistry(t), opt, t.TempDir(), "uk-box", hsPub, hsRatchet(t), box.gate(), nil)(ctx, h.s, rec, "v7"); !c.OK {
t.Fatalf("T7 = %+v", c)
}
if v, _ := release.LiveVersion(opt, p1Host); v != "v7" {
@@ -140,7 +143,7 @@ func TestHeldReleaseReadyAppliesTheProofGate(t *testing.T) {
box := newHeldProofBox(t)
box.require(t, "other")
opt := t.TempDir()
- if c := heldReleaseReady(testRegistry(t), opt, t.TempDir(), "uk-box", box.gate(), nil)(ctx, h.s, rec, "v7"); !c.OK {
+ if c := heldReleaseReady(testRegistry(t), opt, t.TempDir(), "uk-box", hsPub, hsRatchet(t), box.gate(), nil)(ctx, h.s, rec, "v7"); !c.OK {
t.Fatalf("T7 = %+v", c)
}
if v, _ := release.LiveVersion(opt, p1Host); v != "v7" {
@@ -265,7 +268,7 @@ func TestT7RechecksAnExistingLink(t *testing.T) {
box.prove(t, h.s, "app", []byte("pinned"))
reg, data, opt := testRegistry(t), t.TempDir(), t.TempDir()
t7 := func() moves.Check {
- return heldReleaseReady(reg, opt, data, "uk-box", box.gate(), nil)(ctx, h.s, rec, "v7")
+ return heldReleaseReady(reg, opt, data, "uk-box", hsPub, hsRatchet(t), box.gate(), nil)(ctx, h.s, rec, "v7")
}
if c := t7(); !c.OK {
t.Fatalf("T7 = %+v", c)
@@ -323,7 +326,7 @@ func TestT7WaitsOutAPublicationsRollback(t *testing.T) {
}
prev := heldMaterialize
t.Cleanup(func() { heldMaterialize = prev })
- heldMaterialize = func(context.Context, release.Store, string, registry.Route, string, *release.ProofGate, func(func() error) error) (*release.Manifest, error) {
+ heldMaterialize = func(context.Context, release.Store, ed25519.PublicKey, *release.DigestRatchet, string, registry.Route, string, *release.ProofGate, func(func() error) error) (*release.Manifest, error) {
return nil, fmt.Errorf("%w: read-only", release.ErrStageBlocked)
}
swapped, rolledBack := make(chan struct{}), make(chan time.Time, 1)
@@ -343,7 +346,7 @@ func TestT7WaitsOutAPublicationsRollback(t *testing.T) {
}
}()
<-swapped
- c := heldReleaseReady(reg, opt, data, "uk-box", box.gate(), stageRequester(data))(ctx, h.s, rec, "v7")
+ c := heldReleaseReady(reg, opt, data, "uk-box", hsPub, hsRatchet(t), box.gate(), stageRequester(data))(ctx, h.s, rec, "v7")
answered := time.Now()
if c.OK {
t.Fatalf("T7 passed on a link the publication rolled back: %+v", c)
diff --git a/internal/release/heldauth_test.go b/internal/release/heldauth_test.go
new file mode 100644
index 000000000..6f63d229b
--- /dev/null
+++ b/internal/release/heldauth_test.go
@@ -0,0 +1,184 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func heldAuthRoute(host string) registry.Route {
+ return registry.Route{Host: host, Account: "bab", App: "km", HoldOp: "op-1"}
+}
+
+// nr is a fresh, unarmed ratchet: today's admission.
+func nr(t *testing.T) *DigestRatchet { return NewDigestRatchet(t.TempDir(), nil, false) }
+
+// Astra I1 #2: a pointer the box's ratchet refuses is not held-stage evidence.
+func TestHeldStageAppliesTheDigestRatchet(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { // a legacy, digestless pointer
+ t.Fatal(err)
+ }
+ if err := s.Delete(ctx, PromotionRecordKey("bab", "km", "v1")); err != nil { // so the pointer is the only evidence
+ t.Fatal(err)
+ }
+ armed := NewDigestRatchet(t.TempDir(), nil, false)
+ if err := armed.Admit("bab", &ChannelPointer{App: "km", Channel: "canary/x", Version: "v0", Digest: ManifestDigest([]byte("m"))}); err != nil {
+ t.Fatal(err) // this box has seen canary/x with digests
+ }
+ if _, err := StageHeld(ctx, s, pub, armed, opt, heldAuthRoute("a.example.com"), "v1"); !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("a ratcheted channel's legacy pointer authorized a held stage: %v", err)
+ }
+ required := NewDigestRatchet(t.TempDir(), nil, true)
+ if _, err := StageHeld(ctx, s, pub, required, opt, heldAuthRoute("a.example.com"), "v1"); !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("CARLOS_REQUIRE_POINTER_DIGEST did not apply to a held stage: %v", err)
+ }
+ if _, err := StageHeld(ctx, s, pub, nil, opt, heldAuthRoute("a.example.com"), "v1"); !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("a nil ratchet admitted pointer evidence: %v", err)
+ }
+}
+
+func TestHeldStageRefusesABuildNothingAuthorizes(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ _, pub := testKeys(t)
+ opt := t.TempDir()
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("shipped, never promoted"))
+ if _, err := StageHeld(ctx, s, pub, nr(t), opt, heldAuthRoute("a.example.com"), "v1"); !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("StageHeld = %v; want ErrHeldNotAuthorized", err)
+ }
+ if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil {
+ t.Fatal("an unauthorized build was staged")
+ }
+}
+
+func TestHeldStageAcceptsARecordedBuildOutOfHistory(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ for i := 0; i < historyMax+2; i++ {
+ v := "v" + string(rune('a'+i))
+ shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if _, err := StageHeld(ctx, s, pub, nr(t), opt, heldAuthRoute("a.example.com"), "va"); err != nil {
+ t.Fatalf("a recorded build out of the pointer's history: %v", err)
+ }
+}
+
+func TestHeldStageAcceptsAPointerNamedBuildWithoutARecord(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Delete(ctx, PromotionRecordKey("bab", "km", "v1")); err != nil { // promoted before records
+ t.Fatal(err)
+ }
+ if _, err := StageHeld(ctx, s, pub, nr(t), opt, heldAuthRoute("a.example.com"), "v1"); err != nil {
+ t.Fatalf("a build the signed pointer names: %v", err)
+ }
+}
+
+func TestHeldStageBindsTheRecordDigest(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("promoted"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ saved, _, _ := s.Get(ctx, PromotionRecordKey("bab", "km", "v1"))
+ for i := 0; i <= historyMax; i++ { // push v1 out of the pointer, so the record is the only evidence
+ v := "w" + string(rune('a'+i))
+ shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
+ 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("nobody promoted these"))
+ if _, err := s.Put(ctx, PromotionRecordKey("bab", "km", "v1"), saved, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := StageHeld(ctx, s, pub, nr(t), opt, heldAuthRoute("a.example.com"), "v1"); !errors.Is(err, ErrManifestDigest) && !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("a reused name under a replayed record = %v", err)
+ }
+ if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil {
+ t.Fatal("the reused name's bytes were staged")
+ }
+}
+
+// Under a promotion record the held stage binds the record's digest (spec
+// §4.6): a copy staged while the canonical manifest cannot be read is
+// refused at staging, never published and never overwritten, where pointer
+// trust would have used it as staged and left it to D3's bind.
+func TestHeldStageRecordRefusesAStaleCopyItCannotCheck(t *testing.T) {
+ ctx, s, opt := heldFixture(t)
+ stale := []byte("not the shipped bytes")
+ down := stageStaleCopyKept(t, ctx, s, opt, "bab", "console", "v1", stale)
+ _, err := MaterializeHeld(ctx, down, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), openGuard)
+ if err == nil || errors.Is(err, ErrStageBlocked) {
+ t.Fatalf("MaterializeHeld = %v; want the record-bound stage to refuse the copy", err)
+ }
+ gone(t, LiveLink(opt, heldTestHost))
+ if b, rerr := os.ReadFile(VersionPath(opt, "bab", "v1", "console")); rerr != nil || string(b) != string(stale) {
+ t.Fatalf("staged v1 = %q, %v; want it untouched", b, rerr)
+ }
+}
+
+// §4.1 refusals are final: the root relay removes a request for a build
+// nothing authorizes, or whose manifest does not hash to the digest its
+// record signs, writes no ack and never repoints live/<host>.
+func TestServeHeldStageRequestsDropsUnauthorizedBuilds(t *testing.T) {
+ for name, setup := range map[string]func(t *testing.T, ctx context.Context, s Store) string{
+ "nothing authorizes it": func(t *testing.T, _ context.Context, s Store) string {
+ shipRelease(t, s, "bab", "console", "v3", adoptPlatform, []byte("shipped, never promoted"))
+ return "v3"
+ },
+ "its manifest is not the recorded one": func(t *testing.T, ctx context.Context, s Store) string {
+ raw, etag, err := s.Get(ctx, ManifestKey("bab", "console", "v1"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ManifestKey("bab", "console", "v1"), append(raw, '\n'), etag); err != nil {
+ t.Fatal(err)
+ }
+ return "v1"
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ ctx, s, opt := heldFixture(t)
+ req := goodHeldRequest()
+ req.Version = setup(t, ctx, s)
+ dd := t.TempDir()
+ path := writeHeldRequest(t, dd, req.Host+".json", req)
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), passAuth(nil)); err != nil {
+ t.Fatal(err)
+ }
+ gone(t, path)
+ gone(t, LiveLink(opt, heldTestHost))
+ gone(t, filepath.Join(HeldStagedDir(dd), heldTestHost+".json"))
+ if _, err := os.Stat(VersionPath(opt, "bab", req.Version, "console")); err == nil {
+ t.Fatalf("%s was staged", req.Version)
+ }
+ })
+ }
+}
diff --git a/internal/release/heldverify_test.go b/internal/release/heldverify_test.go
index 5d076faf9..62d214438 100644
--- a/internal/release/heldverify_test.go
+++ b/internal/release/heldverify_test.go
@@ -83,7 +83,7 @@ func TestVerifyHeldLiveWaitsOutAFailedPublication(t *testing.T) {
during = nil
}
}
- _, err := MaterializeHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", e.gate, lock.guard)
+ _, err := MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, lock.guard)
if !errors.Is(err, ErrHeldNotProven) || e.live() != "v0" {
t.Fatalf("the publication: %v, live %q; want ErrHeldNotProven and v0", err, e.live())
}
@@ -116,7 +116,7 @@ func TestVerifyHeldLiveRefusesTheSwappedBytes(t *testing.T) {
}
during = VerifyHeldLive(e.ctx, e.store, e.opt, heldRoute(), "v1", agent, passLock)
}
- if _, err := MaterializeHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", e.gate, openGuard); !errors.Is(err, ErrHeldNotProven) {
+ if _, err := MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, openGuard); !errors.Is(err, ErrHeldNotProven) {
t.Fatalf("the publication: %v", err)
}
if !errors.Is(during, ErrHeldNotProven) {
@@ -132,7 +132,7 @@ func TestVerifyHeldLiveRechecksAnExistingLink(t *testing.T) {
e := newHeldProofEnv(t)
e.require(t, "console")
e.prove(t, e.pinnedSHA)
- if _, err := MaterializeHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", e.gate, openGuard); err != nil || e.live() != "v1" {
+ if _, err := MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, openGuard); err != nil || e.live() != "v1" {
t.Fatalf("publish v1: %v, live %q", err, e.live())
}
verify := func() error {
diff --git a/internal/release/materializeheld.go b/internal/release/materializeheld.go
index c36d618a0..01c3c1f50 100644
--- a/internal/release/materializeheld.go
+++ b/internal/release/materializeheld.go
@@ -2,11 +2,13 @@ package release
import (
"context"
+ "crypto/ed25519"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
+ "slices"
"strings"
"time"
@@ -42,14 +44,64 @@ var ErrHeldClaimWired = errors.New("release: held rows are not staged while CARL
// Returned before anything is published, so live/<host> is untouched.
var ErrHeldNotProven = errors.New("release: the held release is not proven for this box")
+// ErrHeldNotAuthorized is a held stage of a build §4.1 does not authorize:
+// no promotion record, and no verified pointer of the app names it (settings
+// spec rev 11 §4.3a). The request is dropped, never retried.
+var ErrHeldNotAuthorized = errors.New("release: the held release is not authorized: no promotion record and no signed pointer names it")
+
+// HeldAuthorized is §4.1 for a move's held stage. want is the digest staging
+// must bind: the record's, else the naming pointer entry's ("" for an entry
+// signed before digests, today's pointer trust).
+func HeldAuthorized(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, acct, app, version string) (string, error) {
+ rec, err := ReadPromotionRecord(ctx, s, pub, acct, app, version)
+ if err == nil {
+ return rec.ManifestDigest, nil
+ }
+ if !errors.Is(err, ErrNoPromotionRecord) {
+ return "", err
+ }
+ prefix := ChannelsPrefix(acct, app)
+ keys, err := s.List(ctx, prefix)
+ if err != nil {
+ return "", err
+ }
+ for _, k := range keys {
+ name, ok := strings.CutSuffix(strings.TrimPrefix(k, prefix), ".json")
+ if !ok || name == "" {
+ continue
+ }
+ p, err := Channel(ctx, s, pub, acct, app, name)
+ if err != nil {
+ continue
+ }
+ // The box's ratchet and CARLOS_REQUIRE_POINTER_DIGEST apply here as
+ // they do to adoption (§4.6): a pointer the ratchet refuses is not
+ // evidence. A nil ratchet would admit everything, so it is refused.
+ if r == nil {
+ return "", fmt.Errorf("%w: no pointer digest ratchet", ErrHeldNotAuthorized)
+ }
+ if rerr := r.Admit(acct, p); rerr != nil {
+ continue
+ }
+ if p.Version == version || slices.Contains(p.History, version) {
+ return p.DigestOf(version), nil
+ }
+ }
+ return "", fmt.Errorf("%w: %s/%s at %s", ErrHeldNotAuthorized, acct, app, version)
+}
+
var (
errHeldNoGuard = errors.New("release: a held publication needs its authority")
errHeldNoProofGate = errors.New("release: a held stage needs its proof gate")
)
-// materializeHeldStage is MaterializeHeld's staging step, ensureStaged, as
-// a variable so a test can make it fail the way a read-only mount does.
-var materializeHeldStage = ensureStaged
+// materializeHeldStage is MaterializeHeld's staging step, ensureStagedDigest
+// bound to the digest HeldAuthorized returned (want "" is ensureStaged
+// exactly), as a variable so a test can make it fail the way a read-only
+// mount does.
+var materializeHeldStage = func(ctx context.Context, s Store, optDir, acct, app, ver, host, want string) (*Manifest, error) {
+ return ensureStagedDigest(ctx, s, optDir, acct, app, ver, host, want, stageCheck{})
+}
// heldBeforeSwap runs inside the publication between bind and the link
// swap. A test seam: the window a replaced staged file lands in.
@@ -132,7 +184,11 @@ func VerifyHeldLive(ctx context.Context, s Store, optDir string, rt registry.Rou
// downloaded nor relayed to a root pass, and bind before publishing. A
// refusal is ErrHeldNotProven and live/<host> is untouched. A nil proof is
// refused, as a nil guard is.
-func MaterializeHeld(ctx context.Context, s Store, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error) (*Manifest, error) {
+//
+// pub and r are §4.1's evidence for the stage (StageHeld): a build no
+// signed promotion record and no ratchet-admitted pointer of the app
+// authorizes is ErrHeldNotAuthorized, before anything is downloaded.
+func MaterializeHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error) (*Manifest, error) {
if err := heldStageable(rt, version); err != nil {
return nil, err
}
@@ -147,7 +203,7 @@ func MaterializeHeld(ctx context.Context, s Store, optDir string, rt registry.Ro
if err != nil {
return nil, err
}
- m, err := StageHeld(ctx, s, optDir, rt, version)
+ m, err := StageHeld(ctx, s, pub, r, optDir, rt, version)
if err != nil {
return nil, err
}
@@ -161,11 +217,20 @@ func MaterializeHeld(ctx context.Context, s Store, optDir string, rt registry.Ro
// published: the version lands in the version store and live/<host> is
// left alone. It refuses while CARLOS_HOST_CLAIM is on, and a refused write
// is ErrStageBlocked wrapping the cause.
-func StageHeld(ctx context.Context, s Store, optDir string, rt registry.Route, version string) (*Manifest, error) {
+//
+// It stages only a build §4.1 authorizes (settings spec rev 11 §4.3a):
+// HeldAuthorized, under pub and the box's ratchet r, answers first, and the
+// staging binds the digest it returns. A build nothing authorizes is
+// ErrHeldNotAuthorized, and nothing is downloaded.
+func StageHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string) (*Manifest, error) {
if err := heldStageable(rt, version); err != nil {
return nil, err
}
- m, err := materializeHeldStage(ctx, s, optDir, RouteAccount(rt), rt.App, version, rt.Host)
+ want, err := HeldAuthorized(ctx, s, pub, r, RouteAccount(rt), rt.App, version)
+ if err != nil {
+ return nil, err
+ }
+ m, err := materializeHeldStage(ctx, s, optDir, RouteAccount(rt), rt.App, version, rt.Host, want)
if err != nil {
if StageBlocked(err) {
return nil, fmt.Errorf("%w: %w", ErrStageBlocked, err)
@@ -432,8 +497,10 @@ const heldStageRequestMax = 4096
// non-empty op and box), asks authorize, stages, asks authorize again with
// the publication as its then, writes the HeldStaged ack and removes the
// request. A request that fails validation, that authorize refuses at
-// either point with ErrStaleHeldStage, or that D3 refuses (proof, as
-// MaterializeHeld applies it: ErrHeldNotProven) is removed with a log line,
+// either point with ErrStaleHeldStage, that D3 refuses (proof, as
+// MaterializeHeld applies it: ErrHeldNotProven), or that §4.1 does not
+// authorize (ErrHeldNotAuthorized, or a build that does not hash to its
+// authorizing digest: ErrManifestDigest) is removed with a log line,
// and live/<host> is never repointed for it. A request whose staging, ack or
// publication fails any other way is kept for the next pass. With no
// request directory it reads nothing and creates nothing.
@@ -445,7 +512,7 @@ const heldStageRequestMax = 4096
// The directory is agent-owned and this runs as root, so every read, write
// and removal goes through instancefs: no symlink is followed at any
// component, and nothing but a regular file is read.
-func ServeHeldStageRequests(ctx context.Context, s Store, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error) error {
+func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error) error {
reqDir := HeldStageDir(dataDir)
ents, err := os.ReadDir(reqDir)
if errors.Is(err, os.ErrNotExist) {
@@ -459,7 +526,7 @@ func ServeHeldStageRequests(ctx context.Context, s Store, optDir, dataDir string
if strings.HasPrefix(name, ".") || !strings.HasSuffix(name, ".json") {
continue // a writer's temp file, or not a request
}
- if err := serveHeldStageRequest(ctx, s, optDir, dataDir, reqDir, name, proof, authorize); err != nil {
+ if err := serveHeldStageRequest(ctx, s, pub, r, optDir, dataDir, reqDir, name, proof, authorize); err != nil {
return err
}
}
@@ -468,7 +535,7 @@ func ServeHeldStageRequests(ctx context.Context, s Store, optDir, dataDir string
// serveHeldStageRequest serves one request file. It returns an error only
// when this build cannot do the work at all (instancefs unsupported).
-func serveHeldStageRequest(ctx context.Context, s Store, optDir, dataDir, reqDir, name string, proof *ProofGate, authorize func(HeldStageRequest, func() error) error) error {
+func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir, reqDir, name string, proof *ProofGate, authorize func(HeldStageRequest, func() error) error) error {
drop := func(format string, args ...any) error {
fmt.Printf(" held stage request %q: "+format+"; removed\n", append([]any{name}, args...)...)
if err := instancefs.RemoveBeneath(reqDir, name); err != nil {
@@ -498,11 +565,11 @@ func serveHeldStageRequest(ctx context.Context, s Store, optDir, dataDir, reqDir
}
rt := registry.Route{Host: req.Host, Account: req.Account, App: req.App, HoldOp: req.Op}
guard := func(publish func() error) error { return authorize(req, publish) }
- if _, err := MaterializeHeld(ctx, s, optDir, rt, req.Version, proof, guard); err != nil {
+ if _, err := MaterializeHeld(ctx, s, pub, r, optDir, rt, req.Version, proof, guard); err != nil {
if errors.Is(err, ErrStaleHeldStage) {
return drop("refused at publication: %v", err)
}
- if errors.Is(err, ErrHeldNotProven) {
+ if errors.Is(err, ErrHeldNotProven) || errors.Is(err, ErrHeldNotAuthorized) || errors.Is(err, ErrManifestDigest) {
return drop("refused: %v", err)
}
fmt.Printf(" %s: held stage of %s for %s: %v; kept for the next pass\n", req.Host, req.Version, req.Op, err)
diff --git a/internal/release/materializeheld_proof_test.go b/internal/release/materializeheld_proof_test.go
index e5a71a059..1c7fdbd61 100644
--- a/internal/release/materializeheld_proof_test.go
+++ b/internal/release/materializeheld_proof_test.go
@@ -78,6 +78,22 @@ func (e *heldProofEnv) prove(t *testing.T, sha string) {
}
}
+// staleCopyDown is stageStaleCopyKept for v1: a copy of other bytes staged
+// while v1's canonical manifest cannot be read, so only D3's bind can catch
+// it. A held stage reaches bind on such a copy only on pointer trust (want
+// "", a build promoted before records, settings spec §4.1): under v1's
+// promotion record the stage binds the record's digest and refuses the copy
+// before bind (TestHeldStageRecordRefusesAStaleCopyItCannotCheck). So v1's
+// record is removed first, and the digestless canary/x pointer is the
+// stage's evidence.
+func (e *heldProofEnv) staleCopyDown(t *testing.T, stale []byte) Store {
+ t.Helper()
+ if err := e.store.Delete(e.ctx, PromotionRecordKey("bab", "console", "v1")); err != nil {
+ t.Fatal(err)
+ }
+ return stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "v1", stale)
+}
+
func (e *heldProofEnv) live() string { v, _ := LiveVersion(e.opt, heldTestHost); return v }
// materialize runs MaterializeHeld with a guard that records whether it
@@ -91,7 +107,7 @@ func (e *heldProofEnv) materialize(t *testing.T, s Store) (error, bool, string)
out bytes.Buffer
)
withCapturedStdout(t, &out, func() {
- _, err = MaterializeHeld(e.ctx, s, e.opt, heldRoute(), "v1", e.gate, func(publish func() error) error {
+ _, err = MaterializeHeld(e.ctx, s, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, func(publish func() error) error {
guarded = true
return publish()
})
@@ -215,7 +231,7 @@ func TestHeldStageProofBindingRefusesOtherStagedBytes(t *testing.T) {
e.require(t, "console")
e.prove(t, e.pinnedSHA)
stale := []byte("not the shipped bytes")
- down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "v1", stale)
+ down := e.staleCopyDown(t, stale)
// bind runs inside the publication (fix round 1), so the guard is asked
// and the refusal comes from within it.
err, _, out := e.materialize(t, down)
@@ -234,7 +250,7 @@ func TestHeldStageProofBindingRefusesOtherStagedBytes(t *testing.T) {
// PublishHeld called on its own (after StageHeld) runs the same check.
func TestHeldStageProofPublishHeldChecksToo(t *testing.T) {
e := newHeldProofEnv(t)
- if _, err := StageHeld(e.ctx, e.store, e.opt, heldRoute(), "v1"); err != nil {
+ if _, err := StageHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1"); err != nil {
t.Fatal(err)
}
e.require(t, "console")
@@ -251,7 +267,7 @@ func TestHeldStageProofPublishHeldChecksToo(t *testing.T) {
// A held stage with no gate cannot publish: D3 is not optional here.
func TestHeldStageProofNeedsAGate(t *testing.T) {
e := newHeldProofEnv(t)
- if _, err := MaterializeHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", nil, openGuard); err == nil {
+ if _, err := MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", nil, openGuard); err == nil {
t.Fatal("MaterializeHeld published with no proof gate")
}
if err := PublishHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", nil, openGuard); err == nil {
@@ -274,7 +290,7 @@ func TestServeHeldStageRequestsAppliesTheProofGate(t *testing.T) {
path := writeHeldRequest(t, dd, heldTestHost+".json", goodHeldRequest())
var out bytes.Buffer
withCapturedStdout(t, &out, func() {
- if err := ServeHeldStageRequests(e.ctx, e.store, e.opt, dd, e.gate, passAuth(nil)); err != nil {
+ if err := ServeHeldStageRequests(e.ctx, e.store, heldPub, nr(t), e.opt, dd, e.gate, passAuth(nil)); err != nil {
t.Fatalf("ServeHeldStageRequests: %v", err)
}
})
@@ -291,7 +307,7 @@ func TestServeHeldStageRequestsAppliesTheProofGate(t *testing.T) {
e.gate = NewProofGate(e.policy, e.bake) // a fresh root pass: no cached miss
writeHeldRequest(t, dd, heldTestHost+".json", goodHeldRequest())
withCapturedStdout(t, &out, func() {
- if err := ServeHeldStageRequests(e.ctx, e.store, e.opt, dd, e.gate, passAuth(nil)); err != nil {
+ if err := ServeHeldStageRequests(e.ctx, e.store, heldPub, nr(t), e.opt, dd, e.gate, passAuth(nil)); err != nil {
t.Fatalf("ServeHeldStageRequests: %v", err)
}
})
@@ -308,7 +324,7 @@ func (e *heldProofEnv) materializeWith(t *testing.T, guard func(publish func() e
out bytes.Buffer
)
withCapturedStdout(t, &out, func() {
- _, err = MaterializeHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", e.gate, guard)
+ _, err = MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, guard)
})
return err, out.String()
}
@@ -376,7 +392,7 @@ func TestHeldStageProofRefusalCarriesTheMove(t *testing.T) {
e := newHeldProofEnv(t)
e.require(t, "console")
e.prove(t, e.pinnedSHA)
- down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "v1", []byte("not the shipped bytes"))
+ down := e.staleCopyDown(t, []byte("not the shipped bytes"))
e.materialize(t, down)
r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost)
if r == nil || r.MoveOp != heldTestOp || r.Kind != RefusalNotProven {
@@ -401,7 +417,7 @@ func TestHeldStageProofLeavesAnAdoptionRefusal(t *testing.T) {
t.Fatal(err)
}
before, _ := os.ReadFile(filepath.Join(RefusalDir(e.bake), heldTestHost+".json"))
- down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "v1", []byte("not the shipped bytes"))
+ down := e.staleCopyDown(t, []byte("not the shipped bytes"))
err, _, _ := e.materialize(t, down)
if !errors.Is(err, ErrHeldNotProven) {
t.Fatalf("MaterializeHeld: %v, want ErrHeldNotProven", err)
@@ -421,7 +437,7 @@ func TestHeldStageProofSuccessRetiresTheMovesRefusal(t *testing.T) {
e := newHeldProofEnv(t)
e.require(t, "console")
e.prove(t, e.pinnedSHA)
- down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "v1", []byte("not the shipped bytes"))
+ down := e.staleCopyDown(t, []byte("not the shipped bytes"))
e.materialize(t, down)
if r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost); r == nil {
t.Fatal("no refusal to retire")
diff --git a/internal/release/materializeheld_test.go b/internal/release/materializeheld_test.go
index 25c7e45df..30f4fde86 100644
--- a/internal/release/materializeheld_test.go
+++ b/internal/release/materializeheld_test.go
@@ -1,7 +1,9 @@
package release
import (
+ "bytes"
"context"
+ "crypto/ed25519"
"encoding/json"
"errors"
"fmt"
@@ -19,16 +21,27 @@ const (
heldTestOp = "mv_abcdefghijklmnopqrstuvwxyz"
)
+// heldPriv signs every held fixture's pointers and promotion records, and
+// heldPub verifies them: the release key a held stage is handed.
+var (
+ heldPriv = ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))
+ heldPub = heldPriv.Public().(ed25519.PublicKey)
+)
+
// heldFixture ships two builds of bab/console and moves the stable channel
// to the newer one: the pinned release (v1) is deliberately NOT the head.
+// v1 was promoted to canary/x before that, so it has a promotion record and
+// a held stage of it is §4.1-authorized (settings spec rev 11 §4.3a).
func heldFixture(t *testing.T) (context.Context, Store, string) {
t.Helper()
ctx := context.Background()
s := NewDirStore(t.TempDir())
- priv, _ := testKeys(t)
shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("pinned build"))
shipRelease(t, s, "bab", "console", "v2", adoptPlatform, []byte("channel head"))
- if err := Promote(ctx, s, priv, "bab", "console", "stable", "v2"); err != nil {
+ if err := Promote(ctx, s, heldPriv, "bab", "console", "canary/x", "v1"); err != nil {
+ t.Fatalf("Promote: %v", err)
+ }
+ if err := Promote(ctx, s, heldPriv, "bab", "console", "stable", "v2"); err != nil {
t.Fatalf("Promote: %v", err)
}
return ctx, s, t.TempDir()
@@ -42,7 +55,7 @@ func heldRoute() registry.Route {
// pinned release, never the channel head, and LiveVersion reads it back.
func TestMaterializeHeldStagesThePinnedVersion(t *testing.T) {
ctx, s, opt := heldFixture(t)
- m, err := MaterializeHeld(ctx, s, opt, heldRoute(), "v1", offGate(t), openGuard)
+ m, err := MaterializeHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), openGuard)
if err != nil {
t.Fatalf("MaterializeHeld: %v", err)
}
@@ -67,10 +80,10 @@ func TestMaterializeHeldStageBlocked(t *testing.T) {
ctx, s, opt := heldFixture(t)
orig := materializeHeldStage
t.Cleanup(func() { materializeHeldStage = orig })
- materializeHeldStage = func(context.Context, Store, string, string, string, string, string) (*Manifest, error) {
+ materializeHeldStage = func(context.Context, Store, string, string, string, string, string, string) (*Manifest, error) {
return nil, &os.PathError{Op: "mkdir", Path: filepath.Join(opt, "versions"), Err: errno}
}
- _, err := MaterializeHeld(ctx, s, opt, heldRoute(), "v1", offGate(t), openGuard)
+ _, err := MaterializeHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), openGuard)
if !errors.Is(err, ErrStageBlocked) || !errors.Is(err, errno) {
t.Fatalf("MaterializeHeld: %v, want ErrStageBlocked wrapping %v", err, errno)
}
@@ -91,7 +104,7 @@ func TestMaterializeHeldStageBlocked(t *testing.T) {
func TestMaterializeHeldRefusesWhileHostClaimIsWired(t *testing.T) {
ctx, s, opt := heldFixture(t)
t.Setenv("CARLOS_HOST_CLAIM", "1")
- if _, err := MaterializeHeld(ctx, s, opt, heldRoute(), "v1", offGate(t), openGuard); !errors.Is(err, ErrHeldClaimWired) {
+ if _, err := MaterializeHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), openGuard); !errors.Is(err, ErrHeldClaimWired) {
t.Fatalf("MaterializeHeld: %v, want ErrHeldClaimWired", err)
}
if ents, _ := os.ReadDir(opt); len(ents) != 0 {
@@ -154,7 +167,7 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
rec := &heldRecordingStore{Store: s}
dd := t.TempDir()
called := false
- if err := ServeHeldStageRequests(ctx, rec, opt, dd, offGate(t), passAuth(func(HeldStageRequest) { called = true })); err != nil {
+ if err := ServeHeldStageRequests(ctx, rec, heldPub, nr(t), opt, dd, offGate(t), passAuth(func(HeldStageRequest) { called = true })); err != nil {
t.Fatalf("ServeHeldStageRequests: %v", err)
}
if called || len(rec.calls) != 0 {
@@ -180,7 +193,7 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
}
return runThen(then)
}
- if err := ServeHeldStageRequests(ctx, s, opt, dd, offGate(t), authorize); err != nil {
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), authorize); err != nil {
t.Fatalf("ServeHeldStageRequests: %v", err)
}
gone(t, path)
@@ -209,7 +222,7 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
tc.mutate(&req)
path := writeHeldRequest(t, dd, tc.file, req)
called := false
- if err := ServeHeldStageRequests(ctx, rec, opt, dd, offGate(t), passAuth(func(HeldStageRequest) { called = true })); err != nil {
+ if err := ServeHeldStageRequests(ctx, rec, heldPub, nr(t), opt, dd, offGate(t), passAuth(func(HeldStageRequest) { called = true })); err != nil {
t.Fatalf("ServeHeldStageRequests: %v", err)
}
gone(t, path)
@@ -232,7 +245,7 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
if err := os.WriteFile(path, []byte("{not json"), 0o644); err != nil {
t.Fatal(err)
}
- if err := ServeHeldStageRequests(ctx, s, opt, dd, offGate(t), passAuth(nil)); err != nil {
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), passAuth(nil)); err != nil {
t.Fatal(err)
}
gone(t, path)
@@ -254,7 +267,7 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
t.Fatal(err)
}
called := false
- if err := ServeHeldStageRequests(ctx, s, opt, dd, offGate(t), passAuth(func(HeldStageRequest) { called = true })); err != nil {
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), passAuth(func(HeldStageRequest) { called = true })); err != nil {
t.Fatal(err)
}
gone(t, link)
@@ -271,8 +284,15 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
dd := t.TempDir()
req := goodHeldRequest()
req.Version = "v9" // never shipped
+ // ...but authorized (a signed record), so the stage fails after
+ // §4.1 lets it through, not at it: an unauthorized build is
+ // dropped (TestServeHeldStageRequestsDropsUnauthorizedBuilds).
+ if err := RootSigner(heldPriv).WritePromotionRecord(ctx, s, PromotionRecord{Account: "bab", App: "console", Version: "v9",
+ ManifestDigest: ManifestDigest([]byte("v9")), Channel: "canary/x"}); err != nil {
+ t.Fatal(err)
+ }
path := writeHeldRequest(t, dd, req.Host+".json", req)
- if err := ServeHeldStageRequests(ctx, s, opt, dd, offGate(t), passAuth(nil)); err != nil {
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), passAuth(nil)); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(path); err != nil {
@@ -287,7 +307,7 @@ func TestServeHeldStageRequestsRefusesStaleAndTraversal(t *testing.T) {
req := goodHeldRequest()
path := writeHeldRequest(t, dd, req.Host+".json", req)
var seen []HeldStageRequest
- if err := ServeHeldStageRequests(ctx, s, opt, dd, offGate(t), passAuth(func(r HeldStageRequest) { seen = append(seen, r) })); err != nil {
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), passAuth(func(r HeldStageRequest) { seen = append(seen, r) })); err != nil {
t.Fatalf("ServeHeldStageRequests: %v", err)
}
if len(seen) != 2 || seen[0] != req || seen[1] != req {
@@ -334,7 +354,7 @@ func passAuth(note func(HeldStageRequest)) func(HeldStageRequest, func() error)
// refused after staging and before anything is published.
func TestMaterializeHeldNeedsAGuard(t *testing.T) {
ctx, s, opt := heldFixture(t)
- if _, err := MaterializeHeld(ctx, s, opt, heldRoute(), "v1", offGate(t), nil); err == nil {
+ if _, err := MaterializeHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), nil); err == nil {
t.Fatal("MaterializeHeld published with no guard")
}
gone(t, LiveLink(opt, heldTestHost))
@@ -349,8 +369,8 @@ func TestHeldPublicationRechecksItsAuthority(t *testing.T) {
orig := materializeHeldStage
t.Cleanup(func() { materializeHeldStage = orig })
staged := false
- materializeHeldStage = func(ctx context.Context, s Store, optDir, acct, app, ver, host string) (*Manifest, error) {
- m, err := orig(ctx, s, optDir, acct, app, ver, host)
+ materializeHeldStage = func(ctx context.Context, s Store, optDir, acct, app, ver, host, want string) (*Manifest, error) {
+ m, err := orig(ctx, s, optDir, acct, app, ver, host, want)
staged = true // the move is replaced while this runs
return m, err
}
@@ -361,7 +381,7 @@ func TestHeldPublicationRechecksItsAuthority(t *testing.T) {
}
return runThen(then)
}
- _, err := MaterializeHeld(ctx, s, opt, heldRoute(), "v1", offGate(t), func(publish func() error) error {
+ _, err := MaterializeHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), func(publish func() error) error {
return authorize(goodHeldRequest(), func() error { published = true; return publish() })
})
if !errors.Is(err, ErrStaleHeldStage) || published {
@@ -372,7 +392,7 @@ func TestHeldPublicationRechecksItsAuthority(t *testing.T) {
staged = false
dd := t.TempDir()
path := writeHeldRequest(t, dd, heldTestHost+".json", goodHeldRequest())
- if err := ServeHeldStageRequests(ctx, s, opt, dd, offGate(t), authorize); err != nil {
+ if err := ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, dd, offGate(t), authorize); err != nil {
t.Fatal(err)
}
gone(t, path)
diff --git a/internal/release/stage_reuse_test.go b/internal/release/stage_reuse_test.go
index 3d45f7fa2..f0c60c6e2 100644
--- a/internal/release/stage_reuse_test.go
+++ b/internal/release/stage_reuse_test.go
@@ -302,7 +302,7 @@ func TestStageHeldNeverRestagesADifferingCopy(t *testing.T) {
if err := StageArtifact(opt, "bab", "v1", "console", []byte("old v1")); err != nil {
t.Fatal(err)
}
- m, err := StageHeld(ctx, s, opt, heldRoute(), "v1")
+ m, err := StageHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1")
if err == nil || m != nil {
t.Fatalf("StageHeld = %+v, %v; want a refusal", m, err)
}