carlos / platform Public

settings-2 final review: held stages observe every verified pointer and skip digestless entries on digested channels

Astra final A1/A2, rulings R15 and R16. HeldAuthorized now runs every
pointer that verifies through the box's ratchet before the version-name
check, so a digested pointer marks its channel whatever it names (§4.6
"once verified"); the outcome on a non-naming pointer never changes the
answer, and on a naming pointer R13's skip/retry split stands. A naming
entry with no digest on a pointer whose channel carries digests is not
evidence and is skipped. "" is returned only for a pointer of a wholly
digestless channel.

Tests: a digested head over a digestless history entry is refused with
the flag off and on, nothing downloaded; a two-pass replay on one ratchet
is refused; TestHeldAuthorizedAdmitsOnlyNamingPointers now asserts the
mark is written and that an unwritable mark on a non-naming pointer does
not change the answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6f79b01d3b22a4cbfb7abcbd28f897bd28207cdc parent 98e3f16
2 files changed, +156 −13
  • internal/release/heldauth_test.go +130 −5
  • internal/release/materializeheld.go +26 −8
diff --git a/internal/release/heldauth_test.go b/internal/release/heldauth_test.go
index 6776ec750..663eceea6 100644
--- a/internal/release/heldauth_test.go
+++ b/internal/release/heldauth_test.go
@@ -6,6 +6,7 @@ import (
"errors"
"os"
"path/filepath"
+ "strings"
"testing"
"github.com/carlosframework/platform/internal/registry"
@@ -311,9 +312,11 @@ func TestHeldAuthorizedPrefersASignedDigest(t *testing.T) {
}
}
-// A pointer that does not name the build never reaches the ratchet: it is
-// not evidence, so it neither marks its channel nor, when the mark cannot be
-// written, holds up a build another pointer authorizes.
+// Every verified pointer a held stage reads goes through the ratchet (spec
+// §4.6, "once verified"; ruling R15): one that does not name the build is not
+// evidence, but its digests still mark its channel. Its ratchet outcome never
+// changes the answer, so a mark that cannot be written does not hold up a
+// build another pointer authorizes.
func TestHeldAuthorizedAdmitsOnlyNamingPointers(t *testing.T) {
s := NewDirStore(t.TempDir())
ctx := context.Background()
@@ -330,7 +333,129 @@ func TestHeldAuthorizedAdmitsOnlyNamingPointers(t *testing.T) {
if want, err := HeldAuthorized(ctx, s, pub, NewDigestRatchet(dir, nil, false), "bab", "km", "v1"); err != nil || want != "" {
t.Fatalf("HeldAuthorized = %q, %v; want channel b's pointer trust", want, err)
}
- if _, err := os.Stat(markPath(dir, "bab", "km", "a")); err == nil {
- t.Fatal("a pointer that does not name the build was admitted")
+ if _, err := os.Stat(markPath(dir, "bab", "km", "a")); err != nil {
+ t.Fatalf("channel a's verified digests left no mark: %v", err)
}
+ if os.Geteuid() != 0 {
+ ro := filepath.Join(t.TempDir(), "ro")
+ if err := os.MkdirAll(ro, 0o555); err != nil {
+ t.Fatal(err)
+ }
+ if want, err := HeldAuthorized(ctx, s, pub, NewDigestRatchet(ro, nil, false), "bab", "km", "v1"); err != nil || want != "" {
+ t.Fatalf("HeldAuthorized with channel a's mark unwritable = %q, %v; want channel b's pointer trust", want, err)
+ }
+ }
+}
+
+// heldNothingDownloaded fails the test if the held stage read any of
+// version's release objects or left anything in the version store.
+func heldNothingDownloaded(t *testing.T, rec *heldRecordingStore, opt, version string) {
+ t.Helper()
+ rel := ReleasesPrefix("bab", "km") + version + "/"
+ rec.mu.Lock()
+ defer rec.mu.Unlock()
+ for _, c := range rec.calls {
+ if strings.HasPrefix(c, "get "+rel) {
+ t.Fatalf("the held stage downloaded %s", strings.TrimPrefix(c, "get "))
+ }
+ }
+ if _, err := os.Stat(VersionPath(opt, "bab", version, "km")); err == nil {
+ t.Fatalf("%s was staged", version)
+ }
+}
+
+// heldSubstitute replaces version's shipped bytes under the same name.
+func heldSubstitute(t *testing.T, s Store, version string) {
+ t.Helper()
+ if err := DeleteVersion(context.Background(), s, "bab", "km", version); err != nil {
+ t.Fatal(err)
+ }
+ shipRelease(t, s, "bab", "km", version, adoptPlatform, []byte("nobody promoted these"))
+}
+
+// Astra final A1, ruling R16: a pointer whose head signs a digest, naming a
+// legacy history entry that signs none, is no evidence for that entry ("never
+// a rollback target once its channel has any digest", spec §4.6). With no
+// record, its replaced bytes are refused with the flag off and on, and
+// nothing is downloaded.
+func TestHeldStageRefusesADigestlessEntryOnADigestedChannel(t *testing.T) {
+ for name, require := range map[string]bool{"flag off": false, "flag on": true} {
+ t.Run(name, func(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ pointerOnly(t, s, priv, []string{"canary/x"}, nil) // v1, digestless
+ if err := WriteDigestMarker(ctx, s, RootSigner(priv), "bab", "test"); err != nil {
+ t.Fatal(err)
+ }
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("v2"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil {
+ t.Fatal(err)
+ }
+ p, err := Channel(ctx, s, pub, "bab", "km", "canary/x")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if p.Version != "v2" || p.Digest == "" || len(p.History) != 1 || p.History[0] != "v1" || p.DigestOf("v1") != "" {
+ t.Fatalf("pointer = %+v; want a digested head v2 over a digestless history v1", p)
+ }
+ heldSubstitute(t, s, "v1")
+ rec := &heldRecordingStore{Store: s}
+ if _, err := StageHeld(ctx, rec, pub, NewDigestRatchet(t.TempDir(), nil, require), opt, heldAuthRoute("a.example.com"), "v1"); !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("StageHeld of a digestless entry on a digested channel = %v; want ErrHeldNotAuthorized", err)
+ }
+ heldNothingDownloaded(t, rec, opt, "v1")
+ })
+ }
+}
+
+// Astra final A2, ruling R15: a held stage remembers every verified pointer
+// with digests it reads, naming the build or not. Pass 1 stages v1 on channel
+// b's word while channel a's pointer signs digests for v2; pass 2, on the
+// same ratchet, is a saved digestless pointer of channel a replayed over
+// substituted v2 bytes, and is refused.
+func TestHeldStageRemembersEveryVerifiedPointer(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"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("v2"))
+ _, v2raw, err := GetManifestRaw(ctx, s, "bab", "km", "v2")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sign := func(p *ChannelPointer) []byte {
+ t.Helper()
+ w, err := signPointer(RootSigner(priv), p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return w
+ }
+ saved := sign(&ChannelPointer{Account: "bab", App: "km", Channel: "a", Version: "v2"}) // signed before digests
+ put := func(channel string, w []byte) {
+ t.Helper()
+ _, etag, err := s.Get(ctx, ChannelKey("bab", "km", channel))
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ChannelKey("bab", "km", channel), w, etag); err != nil {
+ t.Fatal(err)
+ }
+ }
+ put("a", sign(&ChannelPointer{Account: "bab", App: "km", Channel: "a", Version: "v2", Digest: ManifestDigest(v2raw)}))
+ put("b", sign(&ChannelPointer{Account: "bab", App: "km", Channel: "b", Version: "v1"}))
+ ratchet := NewDigestRatchet(t.TempDir(), nil, false)
+ if _, err := StageHeld(ctx, s, pub, ratchet, opt, heldAuthRoute("a.example.com"), "v1"); err != nil {
+ t.Fatalf("pass 1, v1 on channel b's word: %v", err)
+ }
+ put("a", saved)
+ heldSubstitute(t, s, "v2")
+ rec := &heldRecordingStore{Store: s}
+ if _, err := StageHeld(ctx, rec, pub, ratchet, opt, heldAuthRoute("a.example.com"), "v2"); !errors.Is(err, ErrHeldNotAuthorized) {
+ t.Fatalf("pass 2, a replayed digestless pointer over substituted v2 = %v; want ErrHeldNotAuthorized", err)
+ }
+ heldNothingDownloaded(t, rec, opt, "v2")
}
diff --git a/internal/release/materializeheld.go b/internal/release/materializeheld.go
index cac75e879..9c3ef09ab 100644
--- a/internal/release/materializeheld.go
+++ b/internal/release/materializeheld.go
@@ -52,14 +52,23 @@ var ErrHeldNotAuthorized = errors.New("release: the held release is not authoriz
// HeldAuthorized is §4.1 for a move's held stage. want is the digest staging
// must bind: the record's, else a naming pointer entry's, preferring one that
-// signs a digest ("" only when every admitted naming entry was signed before
-// digests, today's pointer trust).
+// signs a digest ("" only when every admitted naming entry comes from a
+// pointer whose channel carries no digests at all, today's pointer trust).
+//
+// Every pointer that verifies goes through the box's ratchet, whether or not
+// it names the version: §4.6 remembers a channel from the moment the box has
+// verified digests for it, so a digestless replay later is refused (ruling
+// R15). That observation never changes this answer; only a naming pointer's
+// ratchet outcome does.
//
// A pointer that is gone, fails verification, does not name the version, or
-// that the box's ratchet refuses is not evidence and is skipped. A read that
-// fails any other way, or a ratchet that cannot tell (the mark could not be
-// persisted or read), is returned as is: the answer is unknown, not no, so
-// the root relay keeps the request for the next pass.
+// that the box's ratchet refuses is not evidence and is skipped. So is a
+// naming entry with no digest on a pointer whose channel carries digests: a
+// legacy build is never a target once its channel has any digest (§4.6,
+// ruling R16). A read that fails any other way, or a ratchet that cannot
+// tell about a naming pointer (the mark could not be persisted or read), is
+// returned as is: the answer is unknown, not no, so the root relay keeps the
+// request for the next pass.
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 {
@@ -73,7 +82,7 @@ func HeldAuthorized(ctx context.Context, s Store, pub ed25519.PublicKey, r *Dige
if err != nil {
return "", err
}
- named := false // some admitted pointer names the version, digest or not
+ named := false // some admitted pointer of a digestless channel names the version
for _, k := range keys {
name, ok := strings.CutSuffix(strings.TrimPrefix(k, prefix), ".json")
if !ok || name == "" {
@@ -90,6 +99,12 @@ func HeldAuthorized(ctx context.Context, s Store, pub ed25519.PublicKey, r *Dige
if err != nil {
continue // a pointer that does not verify is not evidence
}
+ // Observed first, so a verified pointer with digests marks its channel
+ // whatever it names (§4.6 "once verified"). On a pointer that does not
+ // name the version the outcome is dropped: a refusal or an unpersisted
+ // mark there is no evidence either way, and the ratchet's in-memory
+ // mark keeps that channel refusing digestless pointers regardless.
+ rerr := r.Admit(acct, p)
if p.Version != version && !slices.Contains(p.History, version) {
continue
}
@@ -99,7 +114,7 @@ func HeldAuthorized(ctx context.Context, s Store, pub ed25519.PublicKey, r *Dige
if r == nil {
return "", fmt.Errorf("%w: no pointer digest ratchet", ErrHeldNotAuthorized)
}
- if rerr := r.Admit(acct, p); rerr != nil {
+ if rerr != nil {
if errors.Is(rerr, ErrPointerWithoutDigests) && !errors.Is(rerr, errRatchetMarkUnreadable) {
continue // the ratchet's verdict: this pointer is refused
}
@@ -108,6 +123,9 @@ func HeldAuthorized(ctx context.Context, s Store, pub ed25519.PublicKey, r *Dige
if d := p.DigestOf(version); d != "" {
return d, nil
}
+ if p.ChannelHasDigests() {
+ continue // a digestless entry on a digested channel is not evidence
+ }
named = true
}
if named {