carlos / platform Public

settings-1 task 10: resign with operator-confirmed digests, and the digest report

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d36e4cf23e2dff68c574c1f541156aca58f6f227 parent 9da43d1
4 files changed, +408 −1
  • internal/release/claim_app_writes.go +14 −1
  • internal/release/digests.go +57 −0
  • internal/release/resign.go +133 −0
  • internal/release/resign_test.go +204 −0
diff --git a/internal/release/claim_app_writes.go b/internal/release/claim_app_writes.go
index 9fd02a729..41edc3114 100644
--- a/internal/release/claim_app_writes.go
+++ b/internal/release/claim_app_writes.go
@@ -1,6 +1,9 @@
package release
-import "context"
+import (
+ "context"
+ "errors"
+)
// The signed-pointer and ship entry points below run as one app mutation:
// on an enabled console store the whole read, authorize and conditional
@@ -41,3 +44,13 @@ func (sg Signer) RollbackAuthorized(ctx context.Context, s Store, acct, app, cha
return sg.claimRawRollbackAuthorized(ctx, s, acct, app, channel, authorize)
})
}
+
+// Resign is claimRawResign under one app mutation.
+func (sg Signer) Resign(ctx context.Context, s Store, plan *ResignPlan, actor string) error {
+ if plan == nil {
+ return errors.New("resign: no plan to sign")
+ }
+ return WithAppMutation(ctx, s, plan.Account, plan.App, "release.Resign", func(ctx context.Context) error {
+ return sg.claimRawResign(ctx, s, plan, actor)
+ })
+}
diff --git a/internal/release/digests.go b/internal/release/digests.go
index d13f827d5..040856b2b 100644
--- a/internal/release/digests.go
+++ b/internal/release/digests.go
@@ -2,9 +2,12 @@ package release
import (
"context"
+ "crypto/ed25519"
"crypto/sha256"
"errors"
"fmt"
+ "strings"
+ "time"
)
// ManifestDigest is the sha256 of a manifest's canonical bytes: exactly what
@@ -125,3 +128,57 @@ type ResignNeededError struct{ App, Channel, Version string }
func (e *ResignNeededError) Error() string {
return fmt.Sprintf(TextRollbackResign, e.App, e.Channel, e.Version, e.App, e.Channel)
}
+
+// ChannelDigestState is one channel's line in the straggler report (spec
+// §4.6 "Watch for stragglers").
+type ChannelDigestState struct {
+ Channel string `json:"channel"`
+ Version string `json:"version"`
+ PromotedAt time.Time `json:"promoted_at"`
+ HeadDigest bool `json:"head_digest"`
+ HistoryMissing int `json:"history_missing"`
+ Straggler bool `json:"straggler"`
+}
+
+// DigestStateOf is a verified pointer's line. A straggler is a pointer
+// without digests signed after the marker went on: an old writer is still in
+// use.
+func DigestStateOf(p *ChannelPointer, marker *DigestMarker) ChannelDigestState {
+ st := ChannelDigestState{Channel: p.Channel, Version: p.Version, PromotedAt: p.PromotedAt, HeadDigest: p.CarriesDigests()}
+ hd := alignedHistoryDigests(p)
+ for _, d := range hd {
+ if d == "" {
+ st.HistoryMissing++
+ }
+ }
+ st.Straggler = marker != nil && !st.HeadDigest && p.PromotedAt.After(marker.At)
+ return st
+}
+
+// DigestReport is every channel of one app, plus the account's marker. A
+// pointer that does not verify is skipped here: `carlos channels` already
+// shows it as an error row.
+func DigestReport(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) ([]ChannelDigestState, *DigestMarker, error) {
+ marker, err := ReadDigestMarker(ctx, s, pub, acct)
+ if err != nil {
+ return nil, nil, err
+ }
+ prefix := ChannelsPrefix(acct, app)
+ keys, err := s.List(ctx, prefix)
+ if err != nil {
+ return nil, nil, err
+ }
+ var out []ChannelDigestState
+ 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
+ }
+ out = append(out, DigestStateOf(p, marker))
+ }
+ return out, marker, nil
+}
diff --git a/internal/release/resign.go b/internal/release/resign.go
new file mode 100644
index 000000000..3df884f2d
--- /dev/null
+++ b/internal/release/resign.go
@@ -0,0 +1,133 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+)
+
+// ErrResignChanged is a resign whose channel moved after its plan was shown.
+var ErrResignChanged = errors.New("release: the channel moved since the resign plan was made")
+
+// ResignPlan is what `carlos channels resign` shows the operator, and the
+// only thing Resign signs (spec §4.6). Computed[0] is the head, Computed[i+1]
+// History[i]: true where the digest was read from the bucket now rather than
+// copied from the signed pointer, which is the part the operator confirms.
+type ResignPlan struct {
+ Account, App, Channel string
+ Version string
+ Digest string
+ History []string
+ HistoryDigests []string
+ Computed []bool
+ etag string
+}
+
+// PlanResign reads the channel's verified pointer and fills in a digest for
+// every entry it can: a digest the pointer already signs is copied, and any
+// other is the bucket's current manifest digest. A history entry whose
+// manifest is gone stays "", so rolling back onto it stays refused. A head
+// whose manifest is gone cannot be resigned.
+func (sg Signer) PlanResign(ctx context.Context, s Store, acct, app, channel string) (*ResignPlan, error) {
+ if !ValidAccount(acct) || !ValidApp(app) || !ValidChannel(channel) {
+ return nil, fmt.Errorf("resign: invalid %s/%s/%s", acct, app, channel)
+ }
+ if err := sg.allows(channel, time.Now().UTC()); err != nil {
+ return nil, fmt.Errorf("resign: %w", err)
+ }
+ if err := resignMarker(ctx, s, sg, acct); err != nil {
+ return nil, err
+ }
+ key := ChannelKey(acct, app, channel)
+ cur, etag, err := GetQualified(ctx, s, key)
+ if err != nil {
+ return nil, fmt.Errorf("resign: %w", err)
+ }
+ old, err := verifyPointer(sg.verifyPub(), cur, acct, app, channel)
+ if err != nil {
+ return nil, fmt.Errorf("resign: existing pointer at %s: %w", key, err)
+ }
+ plan := &ResignPlan{Account: acct, App: app, Channel: channel, Version: old.Version,
+ History: old.History, HistoryDigests: alignedHistoryDigests(old),
+ Computed: make([]bool, len(old.History)+1), etag: etag}
+ plan.Digest = old.Digest
+ if plan.Digest == "" {
+ _, raw, err := GetManifestRaw(ctx, s, acct, app, old.Version)
+ if err != nil {
+ return nil, fmt.Errorf(TextResignUnreadable, old.Version)
+ }
+ plan.Digest, plan.Computed[0] = ManifestDigest(raw), true
+ }
+ for i, v := range old.History {
+ if plan.HistoryDigests[i] != "" {
+ continue
+ }
+ if _, raw, err := GetManifestRaw(ctx, s, acct, app, v); err == nil {
+ plan.HistoryDigests[i], plan.Computed[i+1] = ManifestDigest(raw), true
+ }
+ }
+ return plan, nil
+}
+
+// claimRawResign signs the plan's pointer: the same version, history, audit
+// trail and fast-track waiver, with the plan's digests, conditional on the
+// pointer being exactly the one the plan was made from. Resign is this under
+// the app mutation.
+func (sg Signer) claimRawResign(ctx context.Context, s Store, plan *ResignPlan, actor string) error {
+ if plan == nil {
+ return errors.New("resign: no plan to sign")
+ }
+ // Checked again at signing: the marker may have been turned off since
+ // the plan was shown.
+ if err := resignMarker(ctx, s, sg, plan.Account); err != nil {
+ return err
+ }
+ key := ChannelKey(plan.Account, plan.App, plan.Channel)
+ cur, etag, err := GetQualified(ctx, s, key)
+ if err != nil {
+ return fmt.Errorf("resign: %w", err)
+ }
+ if etag != plan.etag {
+ return fmt.Errorf("%w: %s", ErrResignChanged, fmt.Sprintf(TextResignChanged, plan.Channel))
+ }
+ old, err := verifyPointer(sg.verifyPub(), cur, plan.Account, plan.App, plan.Channel)
+ if err != nil {
+ return fmt.Errorf("resign: existing pointer at %s: %w", key, err)
+ }
+ next := *old
+ next.Digest, next.HistoryDigests = plan.Digest, plan.HistoryDigests
+ if len(next.HistoryDigests) == 0 {
+ next.HistoryDigests = nil
+ }
+ if old.FastTrack && sg.scope != nil {
+ return fmt.Errorf("resign: %s carries a fast-track waiver, which only the root key may sign", plan.Channel)
+ }
+ wrapper, err := signPointer(sg, &next)
+ if err != nil {
+ return err
+ }
+ if _, err := s.Put(ctx, key, wrapper, etag); err != nil {
+ if errors.Is(err, ErrCASConflict) {
+ return fmt.Errorf("%w: %s", ErrResignChanged, fmt.Sprintf(TextResignChanged, plan.Channel))
+ }
+ return err
+ }
+ return nil
+}
+
+// resignMarker is resign's gate (Astra I3 #4, spec §4.6 and §8): the
+// account's emission marker must exist and verify against the root, the
+// operator's statement that every writer is upgraded. Without it a resigned
+// pointer would arm the boxes' ratchets while old writers still write
+// pointers without digests, and every such write would then be refused.
+func resignMarker(ctx context.Context, s Store, sg Signer, acct string) error {
+ m, err := ReadDigestMarker(ctx, s, sg.verifyPub(), acct)
+ if err != nil {
+ return fmt.Errorf(TextMarkerUnreadable, "resign", acct, err)
+ }
+ if m == nil {
+ return fmt.Errorf(TextResignNoMarker, acct, acct)
+ }
+ return nil
+}
diff --git a/internal/release/resign_test.go b/internal/release/resign_test.go
new file mode 100644
index 000000000..9ec390d6a
--- /dev/null
+++ b/internal/release/resign_test.go
@@ -0,0 +1,204 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestResignSignsTheDigestsTheOperatorSaw(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ for _, v := range []string{"v1", "v2"} {
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ markerOn(t, s, priv, "bab") // every writer upgraded; the old pointer still has no digests
+ sg := RootSigner(priv)
+ plan, err := sg.PlanResign(ctx, s, "bab", "km", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if plan.Digest != manifestDigestOf(t, s, "bab", "km", "v2") || plan.HistoryDigests[0] != manifestDigestOf(t, s, "bab", "km", "v1") ||
+ !plan.Computed[0] || !plan.Computed[1] {
+ t.Fatalf("plan = %+v", plan)
+ }
+ if p := readPtr(t, s, pub, "stable"); p.Digest != "" {
+ t.Fatal("PlanResign wrote something")
+ }
+ if err := sg.Resign(ctx, s, plan, "ops@example.com"); err != nil {
+ t.Fatal(err)
+ }
+ p := readPtr(t, s, pub, "stable")
+ if p.Version != "v2" || p.Digest != plan.Digest || p.HistoryDigests[0] != plan.HistoryDigests[0] {
+ t.Fatalf("resigned pointer = %+v", p)
+ }
+ // Once resigned, the rollback the spec refused is allowed.
+ if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err != nil {
+ t.Fatalf("rollback after resign: %v", err)
+ }
+}
+
+func TestResignCopiesSignedDigestsAndLeavesGoneBuildsEmpty(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, _ := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ shipRelease(t, s, "bab", "km", "v3", adoptPlatform, []byte("three"))
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
+ t.Fatal(err)
+ }
+ markerOn(t, s, priv, "bab")
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v3"); err != nil {
+ t.Fatal(err)
+ }
+ if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ plan, err := RootSigner(priv).PlanResign(ctx, s, "bab", "km", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // head v3 signed, v2 inherited "" (computed now), v1 gone.
+ if plan.Computed[0] || !plan.Computed[1] || plan.HistoryDigests[1] != "" || plan.Computed[2] {
+ t.Fatalf("plan = %+v", plan)
+ }
+}
+
+func TestResignRefusesAChannelThatMoved(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, _ := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ markerOn(t, s, priv, "bab")
+ sg := RootSigner(priv)
+ plan, err := sg.PlanResign(ctx, s, "bab", "km", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
+ t.Fatal(err)
+ }
+ if err := sg.Resign(ctx, s, plan, "ops"); !errors.Is(err, ErrResignChanged) {
+ t.Fatalf("resign over a moved channel = %v, want ErrResignChanged", err)
+ }
+}
+
+func TestResignRefusesAHeadWithNoManifest(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, _ := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ markerOn(t, s, priv, "bab")
+ if _, err := RootSigner(priv).PlanResign(ctx, s, "bab", "km", "stable"); err == nil || err.Error() != fmt.Sprintf(TextResignUnreadable, "v1") {
+ t.Fatalf("a head with no manifest was planned: %v", err)
+ }
+}
+
+// Astra I3 #4: resign arms every box's ratchet for the channel, so it needs
+// the verified marker (every writer upgraded), at plan time and at signing.
+func TestResignRequiresTheVerifiedMarker(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ sg := RootSigner(priv)
+ if _, err := sg.PlanResign(ctx, s, "bab", "km", "stable"); err == nil || err.Error() != fmt.Sprintf(TextResignNoMarker, "bab", "bab") {
+ t.Fatalf("PlanResign with no marker = %v, want the no-marker refusal", err)
+ }
+ markerOn(t, s, priv, "bab")
+ plan, err := sg.PlanResign(ctx, s, "bab", "km", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := RemoveDigestMarker(ctx, s, "bab"); err != nil { // turned off between plan and confirmation
+ t.Fatal(err)
+ }
+ if err := sg.Resign(ctx, s, plan, "ops"); err == nil || err.Error() != fmt.Sprintf(TextResignNoMarker, "bab", "bab") {
+ t.Fatalf("Resign with the marker gone = %v, want the no-marker refusal", err)
+ }
+ foreign, _ := testKeys(t)
+ markerOn(t, s, foreign, "bab") // present, but not root-signed
+ if err := sg.Resign(ctx, s, plan, "ops"); err == nil || !strings.Contains(err.Error(), "marker") {
+ t.Fatalf("Resign with an unverifiable marker = %v", err)
+ }
+ if p := readPtr(t, s, pub, "stable"); p.Digest != "" {
+ t.Fatal("a refused resign wrote digests")
+ }
+}
+
+func TestResignRefusesANilPlan(t *testing.T) {
+ priv, _ := testKeys(t)
+ if err := RootSigner(priv).Resign(context.Background(), NewDirStore(t.TempDir()), nil, "ops"); err == nil {
+ t.Fatal("a nil plan was signed")
+ }
+}
+
+func TestDigestStateOf(t *testing.T) {
+ at := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC)
+ m := &DigestMarker{Schema: 1, Account: "bab", At: at}
+ legacyBefore := &ChannelPointer{Channel: "a", Version: "v1", PromotedAt: at.Add(-time.Hour), History: []string{"v0"}}
+ legacyAfter := &ChannelPointer{Channel: "b", Version: "v1", PromotedAt: at.Add(time.Hour)}
+ signed := &ChannelPointer{Channel: "c", Version: "v2", Digest: dA, PromotedAt: at.Add(time.Hour), History: []string{"v1"}, HistoryDigests: []string{""}}
+ if st := DigestStateOf(legacyBefore, m); st.HeadDigest || st.Straggler || st.HistoryMissing != 1 {
+ t.Fatalf("before the marker: %+v", st)
+ }
+ if st := DigestStateOf(legacyAfter, m); !st.Straggler {
+ t.Fatalf("written after the marker without digests: %+v", st)
+ }
+ if st := DigestStateOf(legacyAfter, nil); st.Straggler {
+ t.Fatal("no marker, no stragglers")
+ }
+ if st := DigestStateOf(signed, m); !st.HeadDigest || st.Straggler || st.HistoryMissing != 1 {
+ t.Fatalf("signed: %+v", st)
+ }
+}
+
+func TestDigestReportListsEveryChannel(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "beta", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ markerOn(t, s, priv, "bab")
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ rows, marker, err := DigestReport(ctx, s, pub, "bab", "km")
+ if err != nil || marker == nil || len(rows) != 2 {
+ t.Fatalf("report = %+v, %+v, %v", rows, marker, err)
+ }
+ got := map[string]bool{}
+ for _, r := range rows {
+ got[r.Channel] = r.HeadDigest
+ }
+ if got["beta"] || !got["canary/x"] {
+ t.Fatalf("report = %+v", rows)
+ }
+}