carlos / platform Public

host: the root pass mints dev proofs and records healthy builds (console defences D3)

proofRoot runs after release.Pass in RunAdopt: it records every named-unit route's live build that meets observe's conditions in the healthy history, mints a proof on a proving box (no refusal names it, none in the bucket; a create-only conflict reads as already minted), leaves a proof-required/<app> marker whenever it reads a require entry, and on the hourly pass logs that the requirement is off where the marker exists. A policy that does not parse mints nothing and is logged every pass. The root pass's restart callback carries recordSettled, which now records only when the unit's ExecPath is the live link and the running bytes are the live link's (Ruling 8), and passOpts carries the proof gate on dataDir().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d70e93c9632503d52749d63c7ed36646885c3a22 parent a45afef
6 files changed, +617 −8
  • internal/host/agent.go +32 −4
  • internal/host/agent_test.go +3 −3
  • internal/host/proofobserve.go +23 −0
  • internal/host/proofobserve_test.go +51 −1
  • internal/host/proofroot.go +203 −0
  • internal/host/proofroot_test.go +305 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 036fd7b46..d938b6289 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -3345,8 +3345,12 @@ func systemctlStartUnit(unit string) error {
// staging, the repoint, the restart and the version stamp. A restart arm
// wired only into the agent would therefore be a restart arm the consoles
// never reach.
-func rootRestartRoute(reg *registry.Registry, optDir string) func(host string) error {
- return makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv, nil), nil)
+//
+// settled is checkedUnitRestart's hook for a named unit whose settle check
+// passed (D3: recordSettled, which adds the build to the healthy history);
+// nil records nothing.
+func rootRestartRoute(reg *registry.Registry, optDir string, settled func(rt registry.Route)) func(host string) error {
+ return makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv, settled), nil)
}
// bounceFn adapts an activator to makeRestartRoute's bounce parameter,
@@ -3639,7 +3643,12 @@ func RunAdopt(args []string) {
// -unit` refuses to stamp — so config sync sees no unit-named route by
// construction. It is handed the same callback anyway because there should
// be ONE restart dispatch on this pass, not two that could drift.
- restart := rootRestartRoute(reg, release.OptDir())
+ //
+ // settled: D1's settle passing records the build in the healthy history
+ // (D3, proofobserve.go). Root is the process that can hash the unit's
+ // executable.
+ restart := rootRestartRoute(reg, release.OptDir(),
+ recordSettled(systemdUnits{}, procExePath, release.OptDir(), dataDir(), time.Now))
// The restart-request half, and the ONE place it dispatches from —
// including for exec-backed routes, whose children belong to the agent:
// rootRestartRoute's try-restart arm reaches a carlos-app@ unit, and an
@@ -3765,7 +3774,12 @@ func RunAdopt(args []string) {
// a relayed stage is exactly the request that performs such a restart.
passOpts := []release.PassOption{release.WithCapabilities(caps.Ops{}), release.BakeDir(dataDir()),
release.WithStartUnit(makeStartRoute(reg, systemctlStartUnit)),
- release.WithRollbackRestart(rollbackUnitRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv))}
+ release.WithRollbackRestart(rollbackUnitRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv)),
+ // D3's gate: a fresh gate per root pass, so the policy file is read
+ // at the start of this pass (BeginPass) like every other. Its bake
+ // directory is BakeDir's above: the healthy history it reads is the
+ // one this pass and recordSettled write.
+ release.WithProofGate(release.NewProofGate(release.ProofPolicyPath, dataDir()))}
if !*stageOnly {
passOpts = append(passOpts, release.NagExpiredCanaries(os.Stdout))
}
@@ -3773,6 +3787,20 @@ func RunAdopt(args []string) {
log.Fatalf("adopt: %v", err)
}
+ // D3 (proofroot.go): record live builds as healthy, mint on a proving
+ // box, and on the hourly pass say when a requirement is off. After the
+ // pass, so it looks at what this pass left live.
+ proofRoot{
+ obs: productionProofObserver(release.OptDir()),
+ reg: reg,
+ store: store,
+ bakeDir: dataDir(),
+ policyPath: release.ProofPolicyPath,
+ readKey: readProvingKey,
+ hourly: !*stageOnly,
+ logf: log.Printf,
+ }.run(ctx)
+
// Step 4: the advisories. The two capability advisories that fall out of
// READING manifests (declared-but-ungranted, and capabilities on an
// exec-backed route) are printed by capabilityPass above, where the
diff --git a/internal/host/agent_test.go b/internal/host/agent_test.go
index 5ffdde92b..c5bb22dfa 100644
--- a/internal/host/agent_test.go
+++ b/internal/host/agent_test.go
@@ -715,7 +715,7 @@ func TestRootRestartRouteCarriesTheNamedUnitArm(t *testing.T) {
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
- if err := rootRestartRoute(reg, t.TempDir())("console.carloku.com"); err != nil {
+ if err := rootRestartRoute(reg, t.TempDir(), nil)("console.carloku.com"); err != nil {
t.Fatalf("root restart: %v", err)
}
got, err := os.ReadFile(callLog)
@@ -836,7 +836,7 @@ func TestAdoptPassRefusesToStampAVersionAWrongUnitDidNotStart(t *testing.T) {
log.SetOutput(&journal)
t.Cleanup(func() { log.SetOutput(os.Stderr) })
- restart := rootRestartRoute(reg, optDir)
+ restart := rootRestartRoute(reg, optDir, nil)
if err := release.Pass(ctx, reg, s, pub, optDir, restart); err != nil {
t.Fatalf("Pass: %v", err)
}
@@ -963,7 +963,7 @@ func TestAdoptPassStartsTheNamedUnitAgainAfterAFailedRestart(t *testing.T) {
log.SetOutput(&journal)
t.Cleanup(func() { log.SetOutput(os.Stderr) })
- if err := release.Pass(ctx, reg, s, pub, optDir, rootRestartRoute(reg, optDir),
+ if err := release.Pass(ctx, reg, s, pub, optDir, rootRestartRoute(reg, optDir, nil),
release.WithStartUnit(makeStartRoute(reg, systemctlStartUnit))); err != nil {
t.Fatalf("Pass: %v", err)
}
diff --git a/internal/host/proofobserve.go b/internal/host/proofobserve.go
index fdad3fbcf..b190f9f12 100644
--- a/internal/host/proofobserve.go
+++ b/internal/host/proofobserve.go
@@ -92,6 +92,14 @@ func (o proofObserver) observe(rt registry.Route, after time.Duration) (liveBuil
// the executable that just passed the settle check and add it to the app's
// healthy history. Every failure is a log line; a missed record costs only a
// proof wait later.
+//
+// It holds the process to observe's two equalities (Ruling 8): the unit's
+// ExecPath is the route's live link, and the bytes of /proc/<MainPID>/exe
+// are the live link's. This Show is a second look after the settle, and in
+// that moment the unit can have restarted, or its new process not yet exec'd
+// the binary; without both checks such a process's bytes could enter the
+// history. A build missed here is recorded later by the root pass's "live for
+// after" rule.
func recordSettled(sys unitSystem, procExe func(pid int) string, optDir, bakeDir string, now func() time.Time) func(rt registry.Route) {
return func(rt registry.Route) {
p, err := sys.Show(rt.Unit)
@@ -99,11 +107,26 @@ func recordSettled(sys unitSystem, procExe func(pid int) string, optDir, bakeDir
log.Printf("adopt: %s: settled, but cannot read %s's process for the healthy history (%v)", rt.Host, rt.Unit, err)
return
}
+ link := release.LiveLink(optDir, rt.Host)
+ if p.ExecPath != link {
+ log.Printf("adopt: %s: settled, but %s now runs %s, not %s; not recorded as healthy", rt.Host, rt.Unit, p.ExecPath, link)
+ return
+ }
sha, err := release.FileSHA256(procExe(p.MainPID))
if err != nil {
log.Printf("adopt: %s: settled, but hashing its executable failed: %v", rt.Host, err)
return
}
+ live, err := release.FileSHA256(link)
+ if err != nil {
+ log.Printf("adopt: %s: settled, but hashing the live link's build failed: %v", rt.Host, err)
+ return
+ }
+ if sha != live {
+ log.Printf("adopt: %s: settled, but the running executable (sha256 %s) is not the live link's build (sha256 %s); not recorded as healthy",
+ rt.Host, sha, live)
+ return
+ }
ver, _ := release.LiveVersion(optDir, rt.Host)
if err := release.RecordHealthy(bakeDir, release.RouteAccount(rt), rt.App,
release.HealthyBuild{SHA256: sha, Version: ver, How: "settle", At: now().UTC()}); err != nil {
diff --git a/internal/host/proofobserve_test.go b/internal/host/proofobserve_test.go
index 2b7bce228..90dbb46ae 100644
--- a/internal/host/proofobserve_test.go
+++ b/internal/host/proofobserve_test.go
@@ -119,7 +119,7 @@ func TestSettledRestartRecordsTheRunningExe(t *testing.T) {
rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service", Addr: ln.Addr().String()}
f := &fakeUnits{shows: []unitProps{
{ActiveState: "active", InvocationID: "old", ExecPath: filepath.Join(opt, "live", observedHost)},
- {ActiveState: "active", InvocationID: "new", MainPID: 7},
+ {ActiveState: "active", InvocationID: "new", MainPID: 7, ExecPath: filepath.Join(opt, "live", observedHost)},
}, exe: map[int]string{7: staged}}
settled := recordSettled(f, func(int) string { return staged }, opt, bake, time.Now)
if err := checkedUnitRestart(f, opt, func(string) string { return "" }, settled)(rt); err != nil {
@@ -132,6 +132,56 @@ func TestSettledRestartRecordsTheRunningExe(t *testing.T) {
}
}
+// Ruling 8: the settled hook records only what observe would. Its own Show
+// must name the live link as the unit's executable, and the bytes it hashes
+// must be the live link's; otherwise a process restarted, or not yet exec'd,
+// in the moment after the settle could enter the history.
+func TestRecordSettledRecordsNothingForAnotherExe(t *testing.T) {
+ for name, c := range map[string]struct {
+ execPath func(opt string) string
+ procExe func(t *testing.T, opt string) string
+ }{
+ "Show names another executable": {
+ execPath: func(opt string) string { return release.LiveLink(opt, "console.example.com") },
+ procExe: func(t *testing.T, opt string) string { return filepath.Join(opt, "versions", "new", "console") },
+ },
+ "the running bytes are not the live link's": {
+ execPath: func(opt string) string { return release.LiveLink(opt, observedHost) },
+ procExe: func(t *testing.T, opt string) string {
+ other := filepath.Join(t.TempDir(), "console")
+ if err := os.WriteFile(other, []byte("a different build"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ return other
+ },
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ opt, bake := t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "new")
+ exe := c.procExe(t, opt)
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ f := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "new", MainPID: 7,
+ ExecPath: c.execPath(opt)}}}
+ recordSettled(f, func(int) string { return exe }, opt, bake, time.Now)(rt)
+ if h := release.ReadHealthy(bake, "bab", "console"); len(h) != 0 {
+ t.Fatalf("recorded %+v for a process that is not the live link's build", h)
+ }
+ })
+ }
+ // The control: the same call with both equalities holding records.
+ opt, bake := t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "new")
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ f := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "new", MainPID: 7,
+ ExecPath: release.LiveLink(opt, observedHost)}}}
+ staged := filepath.Join(opt, "versions", "new", "console")
+ recordSettled(f, func(int) string { return staged }, opt, bake, time.Now)(rt)
+ if h := release.ReadHealthy(bake, "bab", "console"); len(h) != 1 || h[0].How != "settle" {
+ t.Fatalf("history = %+v, want the settled build", h)
+ }
+}
+
func TestUnsettledRestartRecordsNothing(t *testing.T) {
opt, bake := t.TempDir(), t.TempDir()
liveTo(t, opt, observedHost, "new")
diff --git a/internal/host/proofroot.go b/internal/host/proofroot.go
new file mode 100644
index 000000000..b0b72c1ab
--- /dev/null
+++ b/internal/host/proofroot.go
@@ -0,0 +1,203 @@
+// The root pass's D3 work (spec 2026-10-06-console-release-defences-design.md
+// §4.3, §4.5): after release.Pass, look at every named-unit route's live build.
+//
+// - A build meeting observe's conditions (bound, active in one invocation
+// for the policy's after, hashed from its running executable, ready)
+// enters the app's healthy history, on every box, policy file or not.
+// That is what puts the build already live when a requirement is switched
+// on into the history without a restart.
+// - On a proving box (a prove block lists the app) the same build is
+// minted a proof, unless a refusal record names it or the bucket already
+// has one. Minting needs the root-only key, which is why it is here.
+// - A pass that reads a require entry leaves a proof-required/<app> marker
+// in the bake directory. On the hourly pass, an app with that marker,
+// healthy history and no require entry gets one line saying the
+// requirement is off: the break-glass trail. A box that never required a
+// proof has no marker and stays quiet.
+//
+// A policy file that does not parse is closed (spec §4.5): nothing is
+// minted, no marker is touched, and the pass says so every time. The live
+// build is still recorded as healthy at the default after, because that
+// record is evidence about the running bytes, not a reading of the policy.
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type proofRoot struct {
+ obs proofObserver
+ reg *registry.Registry
+ store release.Store
+ bakeDir string
+ policyPath string
+ readKey func(path string) (ed25519.PrivateKey, error)
+ hourly bool // carlos-adopt.service, not -stage-only
+ logf func(format string, a ...any)
+}
+
+func (r proofRoot) run(ctx context.Context) {
+ pol, perr := release.LoadProofPolicy(r.policyPath)
+ if perr != nil {
+ r.logf("proof: %v; nothing is minted until it parses", perr)
+ }
+ routes, err := r.reg.List()
+ if err != nil {
+ r.logf("proof: list routes: %v", err)
+ return
+ }
+ after := pol.ProveAfter()
+ type acctApp struct{ acct, app string }
+ seen := map[acctApp]bool{}
+ for _, rt := range routes {
+ if !release.Adoptable(rt) || rt.Unit == "" {
+ continue
+ }
+ acct := release.RouteAccount(rt)
+ seen[acctApp{acct, rt.App}] = true
+ b, err := r.obs.observe(rt, after)
+ if err != nil {
+ if pol.Proves(rt.App) {
+ r.logf("proof: %s: not minting yet: %v", rt.Host, err)
+ }
+ continue
+ }
+ if err := release.RecordHealthy(r.bakeDir, acct, rt.App, release.HealthyBuild{
+ SHA256: b.SHA256, Version: b.Version, How: "live", At: r.obs.now().UTC()}); err != nil {
+ r.logf("proof: %s: recording the healthy build: %v", rt.Host, err)
+ }
+ if perr == nil && pol.Proves(rt.App) {
+ r.mint(ctx, pol, rt, acct, b)
+ }
+ }
+ if perr != nil {
+ return
+ }
+ // The marker says this box has required a proof for the app at some
+ // point. Written on every pass that reads a require entry, so a
+ // requirement switched on and off between two hourly passes still leaves
+ // its trail.
+ if pol != nil {
+ for _, e := range pol.Require {
+ r.markRequired(e.App)
+ }
+ }
+ if !r.hourly {
+ return
+ }
+ keys := make([]acctApp, 0, len(seen))
+ for k := range seen {
+ keys = append(keys, k)
+ }
+ sort.Slice(keys, func(i, j int) bool { return keys[i].acct+"/"+keys[i].app < keys[j].acct+"/"+keys[j].app })
+ for _, k := range keys {
+ // Only where the marker exists is "off" news: a box that never
+ // required a proof (Tito, a self-hoster) has nothing switched off.
+ if len(pol.Requires(k.app)) > 0 {
+ continue
+ }
+ if _, err := os.Stat(r.requiredMarker(k.app)); err != nil {
+ continue
+ }
+ if n := len(release.ReadHealthy(r.bakeDir, k.acct, k.app)); n > 0 {
+ r.logf("proof: requirement is off for %s on this box: %s has no require entry for it (%d healthy builds on record)",
+ k.app, r.policyPath, n)
+ }
+ }
+}
+
+func (r proofRoot) requiredMarker(app string) string {
+ return filepath.Join(r.bakeDir, "proof-required", app)
+}
+
+func (r proofRoot) markRequired(app string) {
+ m := r.requiredMarker(app)
+ if _, err := os.Stat(m); err == nil {
+ return
+ }
+ if err := os.MkdirAll(filepath.Dir(m), 0o755); err != nil {
+ r.logf("proof: writing the requirement marker %s: %v", m, err)
+ return
+ }
+ if err := os.WriteFile(m, nil, 0o644); err != nil {
+ r.logf("proof: writing the requirement marker %s: %v", m, err)
+ }
+}
+
+func (r proofRoot) mint(ctx context.Context, pol *release.ProofPolicy, rt registry.Route, acct string, b liveBuild) {
+ refusals, err := release.ListRefusals(r.bakeDir)
+ if err != nil {
+ r.logf("proof: %s: reading refusal records: %v; not minting", rt.Host, err)
+ return
+ }
+ for _, ref := range refusals {
+ if ref.SHA256 == b.SHA256 {
+ r.logf("proof: %s: not minting %s: this box refused it (%s)", rt.Host, b.Version, ref.Reason)
+ return
+ }
+ }
+ // Spec §4.3's last condition, and the reason a re-mint is never tried:
+ // PutProof is create-only, and a second proof for one build would carry
+ // a different proven_at and conflict.
+ prover := pol.Prove.Prover
+ _, err = release.GetProof(ctx, r.store, acct, rt.App, b.SHA256, prover)
+ switch {
+ case err == nil:
+ return // already proven
+ case !errors.Is(err, os.ErrNotExist):
+ r.logf("proof: %s: reading the bucket for an existing proof: %v; not minting", rt.Host, err)
+ return
+ }
+ key, err := r.readKey(pol.Prove.KeyFile)
+ if err != nil {
+ r.logf("proof: %s: %v; not minting", rt.Host, err)
+ return
+ }
+ p, err := release.SignProof(key, release.Proof{App: rt.App, Platform: release.AdoptPlatform(), SHA256: b.SHA256,
+ Version: b.Version, Prover: prover, ActiveSince: b.ActiveSince.UTC().Truncate(time.Second),
+ ProvenAt: r.obs.now().UTC().Truncate(time.Second)})
+ if err != nil {
+ r.logf("proof: %s: signing: %v", rt.Host, err)
+ return
+ }
+ if err := release.PutProof(ctx, r.store, acct, p); err != nil {
+ // A concurrent pass (the stage relay and the hourly timer can overlap
+ // when the pass lock is unavailable) minted it between the check above
+ // and this write. Its proof stands: already minted, not a failure.
+ if _, gerr := release.GetProof(ctx, r.store, acct, rt.App, b.SHA256, prover); gerr == nil {
+ r.logf("proof: %s: %s %s was already minted by another pass", rt.Host, rt.App, b.Version)
+ return
+ }
+ r.logf("proof: %s: writing the proof: %v (the agent asks again in 2 minutes)", rt.Host, err)
+ return
+ }
+ r.logf("proof: minted %s %s (sha256 %s) as %s, live since %s", rt.App, b.Version, b.SHA256, prover,
+ b.ActiveSince.UTC().Format(time.RFC3339))
+}
+
+// readProvingKey reads the root-only proving key, refusing one any other user
+// can read (spec §4.2: the key is root 0600).
+func readProvingKey(path string) (ed25519.PrivateKey, error) {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return nil, fmt.Errorf("proving key: %w", err)
+ }
+ if perm := fi.Mode().Perm(); perm&0o077 != 0 {
+ return nil, fmt.Errorf("proving key %s is mode %04o; it must be readable by root only (0600)", path, perm)
+ }
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("proving key: %w", err)
+ }
+ return release.ParseSigningKey(string(b), path)
+}
diff --git a/internal/host/proofroot_test.go b/internal/host/proofroot_test.go
new file mode 100644
index 000000000..6557a29d5
--- /dev/null
+++ b/internal/host/proofroot_test.go
@@ -0,0 +1,305 @@
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type proofRootEnv struct {
+ root proofRoot
+ store release.Store
+ bake string
+ keyFile string
+ pub ed25519.PublicKey
+ pubB64 string
+ sha string
+ now time.Time
+ logs []string
+}
+
+func provePolicy(keyFile string) string {
+ return fmt.Sprintf(`{"schema":1,"prove":{"apps":["console"],"prover":"dev","after":"15m","key_file":%q}}`, keyFile)
+}
+
+// newProofRootEnv: one named-unit console route, bound, running its live
+// build for 20 minutes, ready. policy builds the policy file from the key
+// file's path; nil writes no policy file.
+func newProofRootEnv(t *testing.T, policy func(keyFile string) string) *proofRootEnv {
+ t.Helper()
+ opt, bake, etc := t.TempDir(), t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "v1")
+ reg := restartTestRegistry(t)
+ if err := reg.Upsert(registry.Route{Host: observedHost, Kind: "instance", App: "console",
+ Channel: "canary/console", Unit: "console.service", Status: "ready"}); err != nil {
+ t.Fatal(err)
+ }
+ seed, pubB64, err := release.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ pub, err := release.ParsePublicKey(pubB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ keyFile := filepath.Join(etc, "proving.key")
+ if err := os.WriteFile(keyFile, []byte(seed+"\n"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ policyPath := filepath.Join(etc, "proof-policy.json")
+ if policy != nil {
+ if err := os.WriteFile(policyPath, []byte(policy(keyFile)), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ staged := filepath.Join(opt, "versions", "v1", "console")
+ sha, err := release.FileSHA256(staged)
+ if err != nil {
+ t.Fatal(err)
+ }
+ e := &proofRootEnv{store: release.NewDirStore(t.TempDir()), bake: bake, keyFile: keyFile, pub: pub, pubB64: pubB64,
+ sha: sha, now: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)}
+ units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "i1", MainPID: 7,
+ ExecPath: release.LiveLink(opt, observedHost), ActiveEnterTimestamp: e.now.Add(-20 * time.Minute)}}}
+ e.root = proofRoot{
+ obs: proofObserver{sys: units, procExe: func(int) string { return staged }, optDir: opt,
+ now: func() time.Time { return e.now }, ready: func(registry.Route) func() error { return nil }},
+ reg: reg, store: e.store, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey,
+ logf: func(f string, a ...any) { e.logs = append(e.logs, fmt.Sprintf(f, a...)) },
+ }
+ return e
+}
+
+func (e *proofRootEnv) proof(t *testing.T) *release.Proof {
+ t.Helper()
+ p, err := release.GetProof(context.Background(), e.store, "bab", "console", e.sha, "dev")
+ if err != nil {
+ return nil
+ }
+ return p
+}
+
+// signed is a proof for the env's live build, signed with its proving key.
+func (e *proofRootEnv) signed(t *testing.T, provenAt time.Time) *release.Proof {
+ t.Helper()
+ seed, err := os.ReadFile(e.keyFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := release.ParseSigningKey(string(seed), "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ p, err := release.SignProof(key, release.Proof{App: "console", Platform: release.AdoptPlatform(), SHA256: e.sha,
+ Version: "v1", Prover: "dev", ActiveSince: e.now.Add(-time.Hour), ProvenAt: provenAt})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return p
+}
+
+func TestProofRootMintsWhenEveryConditionHolds(t *testing.T) {
+ e := newProofRootEnv(t, provePolicy)
+ e.root.run(context.Background())
+ p := e.proof(t)
+ if p == nil {
+ t.Fatalf("no proof minted; logs: %v", e.logs)
+ }
+ if err := p.Check(e.pub, "console", release.AdoptPlatform(), e.sha, "dev"); err != nil {
+ t.Fatalf("minted proof: %v", err)
+ }
+ if p.Version != "v1" || !p.ActiveSince.Equal(e.now.Add(-20*time.Minute)) || !p.ProvenAt.Equal(e.now) {
+ t.Fatalf("proof = %+v", p)
+ }
+}
+
+func TestProofRootDoesNotMintARefusedBuild(t *testing.T) {
+ e := newProofRootEnv(t, provePolicy)
+ if err := release.WriteRefusal(e.bake, release.Refusal{Host: observedHost, App: "console", Version: "v1", SHA256: e.sha}); err != nil {
+ t.Fatal(err)
+ }
+ e.root.run(context.Background())
+ if e.proof(t) != nil {
+ t.Fatal("minted a proof for a build a refusal record names")
+ }
+}
+
+func TestProofRootNeverOverwritesAProof(t *testing.T) {
+ e := newProofRootEnv(t, provePolicy)
+ first := e.signed(t, e.now.Add(-time.Minute))
+ if err := release.PutProof(context.Background(), e.store, "bab", first); err != nil {
+ t.Fatal(err)
+ }
+ e.root.run(context.Background())
+ if p := e.proof(t); p == nil || !p.ProvenAt.Equal(first.ProvenAt) {
+ t.Fatalf("proof = %+v, want the first one untouched", p)
+ }
+}
+
+// racedStore hides one key from the first Get, as if another pass minted it
+// between this pass's existence check and its write.
+type racedStore struct {
+ release.Store
+ hidden string
+ gets int
+}
+
+func (s *racedStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == s.hidden {
+ s.gets++
+ if s.gets == 1 {
+ return nil, "", fmt.Errorf("raced: %s: %w", key, os.ErrNotExist)
+ }
+ }
+ return s.Store.Get(ctx, key)
+}
+
+// PutProof is create-only: a second mint of one build carries a different
+// proven_at and conflicts. That conflict means "already minted", not an error.
+func TestProofRootTreatsAMintRaceAsAlreadyMinted(t *testing.T) {
+ e := newProofRootEnv(t, provePolicy)
+ first := e.signed(t, e.now.Add(-time.Minute))
+ if err := release.PutProof(context.Background(), e.store, "bab", first); err != nil {
+ t.Fatal(err)
+ }
+ e.root.store = &racedStore{Store: e.store, hidden: release.ProofKey("bab", "console", e.sha, "dev")}
+ e.root.run(context.Background())
+ logs := strings.Join(e.logs, "\n")
+ if strings.Contains(logs, "writing the proof") {
+ t.Fatalf("a mint race was logged as a write failure: %v", e.logs)
+ }
+ if strings.Contains(logs, "proof: minted") {
+ t.Fatalf("a mint race was logged as this pass's mint: %v", e.logs)
+ }
+ if p := e.proof(t); p == nil || !p.ProvenAt.Equal(first.ProvenAt) {
+ t.Fatalf("proof = %+v, want the first one untouched", p)
+ }
+}
+
+func TestProofRootRefusesAKeyOthersCanRead(t *testing.T) {
+ e := newProofRootEnv(t, provePolicy)
+ if err := os.Chmod(e.keyFile, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ e.root.run(context.Background())
+ if e.proof(t) != nil {
+ t.Fatal("minted with a world-readable key")
+ }
+ if !strings.Contains(strings.Join(e.logs, "\n"), "readable by root only") {
+ t.Fatalf("logs = %v, want the key-mode refusal", e.logs)
+ }
+}
+
+// Task 2's carry: a policy file that does not parse is closed. A proving box
+// with a typo mints nothing, and says so on every pass.
+func TestProofRootBrokenPolicyMintsNothingAndSaysSoEveryPass(t *testing.T) {
+ e := newProofRootEnv(t, func(keyFile string) string {
+ return strings.Replace(provePolicy(keyFile), `"schema":1`, `"schema":1,"requier":[]`, 1)
+ })
+ for pass := 0; pass < 2; pass++ {
+ e.logs = nil
+ e.root.hourly = pass == 1
+ e.root.run(context.Background())
+ if e.proof(t) != nil {
+ t.Fatalf("pass %d minted under a policy that does not parse", pass)
+ }
+ if !strings.Contains(strings.Join(e.logs, "\n"), "nothing is minted until it parses") {
+ t.Fatalf("pass %d logs = %v, want the policy error", pass, e.logs)
+ }
+ }
+}
+
+type noReadStore struct {
+ release.Store
+ t *testing.T
+}
+
+func (s noReadStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ s.t.Errorf("bucket GET %s with no policy file", key)
+ return s.Store.Get(ctx, key)
+}
+
+// Review focus 3's source: with no policy file at all, the build already
+// live enters the healthy history, and no proof is read or minted.
+func TestProofRootRecordsTheLiveBuildAsHealthy(t *testing.T) {
+ e := newProofRootEnv(t, nil)
+ e.root.store = noReadStore{Store: e.store, t: t}
+ e.root.run(context.Background())
+ h := release.ReadHealthy(e.bake, "bab", "console")
+ if len(h) != 1 || h[0].SHA256 != e.sha || h[0].How != "live" || h[0].Version != "v1" {
+ t.Fatalf("history = %+v, want the live build", h)
+ }
+ if e.proof(t) != nil {
+ t.Fatal("a proof was minted with no policy file")
+ }
+}
+
+// A box that never had the requirement (Tito, a self-hoster) has nothing
+// switched off and must stay quiet; one that had it and lost it logs hourly.
+func TestProofRootHourlyLineOnlyWhereTheRequirementWasOnce(t *testing.T) {
+ e := newProofRootEnv(t, func(string) string { return `{"schema":1}` })
+ e.root.hourly = true
+ e.root.run(context.Background())
+ if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") {
+ t.Fatalf("a box that never required a proof logged that the requirement is off: %v", e.logs)
+ }
+}
+
+func TestProofRootHourlyLineWhenRequirementIsOff(t *testing.T) {
+ e := newProofRootEnv(t, func(string) string { return `{"schema":1}` })
+ marker := filepath.Join(e.root.bakeDir, "proof-required", "console")
+ if err := os.MkdirAll(filepath.Dir(marker), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(marker, nil, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ e.root.hourly = true
+ e.root.run(context.Background())
+ if !strings.Contains(strings.Join(e.logs, "\n"), "requirement is off for console") {
+ t.Fatalf("logs = %v, want the hourly requirement-off line", e.logs)
+ }
+ e.logs = nil
+ e.root.hourly = false
+ e.root.run(context.Background())
+ if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") {
+ t.Fatalf("a stage-only pass printed the hourly line: %v", e.logs)
+ }
+}
+
+// The marker is written by any root pass that reads a require entry, not
+// only the hourly one, so a requirement switched on and off between two
+// hourly passes still leaves its trail. Deleting the whole file (the other
+// break glass) is "off" too.
+func TestProofRootRequireEntryLeavesTheMarker(t *testing.T) {
+ e := newProofRootEnv(t, nil)
+ if err := os.WriteFile(e.root.policyPath, []byte(fmt.Sprintf(
+ `{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, e.pubB64)), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ marker := filepath.Join(e.bake, "proof-required", "console")
+ e.root.run(context.Background())
+ if _, err := os.Stat(marker); err != nil {
+ t.Fatalf("a stage-only pass that read a require entry left no marker: %v", err)
+ }
+ if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") {
+ t.Fatalf("logged the requirement off while it is on: %v", e.logs)
+ }
+ if err := os.Remove(e.root.policyPath); err != nil {
+ t.Fatal(err)
+ }
+ e.logs = nil
+ e.root.hourly = true
+ e.root.run(context.Background())
+ if !strings.Contains(strings.Join(e.logs, "\n"), "requirement is off for console") {
+ t.Fatalf("logs = %v, want the hourly requirement-off line once the file is gone", e.logs)
+ }
+}