package release import ( "bytes" "context" "encoding/json" "os" "strings" "testing" ) // replayPointer writes back a saved pointer, the attacker's move in #253. func replayPointer(t *testing.T, s Store, ch string, saved []byte) { t.Helper() ctx := context.Background() _, etag, err := s.Get(ctx, ChannelKey("bab", "km", ch)) if err != nil { t.Fatal(err) } if _, err := s.Put(ctx, ChannelKey("bab", "km", ch), saved, etag); err != nil { t.Fatal(err) } } // Spec §8: a reused version name under a replayed digest-bearing pointer is // refused before staging. func TestAReusedNameUnderAReplayedPointerIsRefusedBeforeStaging(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) markerOn(t, s, priv, "bab") shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("the bytes that were promoted")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } saved, _, _ := s.Get(ctx, ChannelKey("bab", "km", "canary/x")) shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil { t.Fatal(err) } if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil { t.Fatal(err) } shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("bytes nobody promoted")) replayPointer(t, s, "canary/x", saved) addRoute(t, reg, "a.example.com", "km", "canary/x", false) var out bytes.Buffer withCapturedStdout(t, &out, func() { if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { t.Fatalf("Pass: %v", err) } }) if v, _ := LiveVersion(opt, "a.example.com"); v != "" { t.Fatalf("live on %q: the replayed pointer installed bytes nobody promoted", v) } if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil { t.Fatal("the reused name's artifact was staged") } if !strings.Contains(out.String(), "pointer signed") { t.Fatalf("no refusal line:\n%s", out.String()) } } // A staged copy that is not the signed build is never pointed live. Nothing // refers to this one, so zdd-4's reuse rule restages it, from the manifest // that passed the digest; the referenced case is refused (below). func TestAStagedCopyThatIsNotTheSignedBuildIsNeverPointedLive(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) markerOn(t, s, priv, "bab") shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("signed")) if err := StageArtifact(opt, "bab", "v1", "km", []byte("left over from a pruned v1")); err != nil { t.Fatal(err) } if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) var out bytes.Buffer withCapturedStdout(t, &out, func() { _ = Pass(ctx, reg, s, pub, opt, nil) }) if b, _ := os.ReadFile(VersionPath(opt, "bab", "v1", "km")); string(b) != "signed" { t.Fatalf("staged v1 = %q: a copy that is not the signed build stayed staged", b) } if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { t.Fatalf("live %q, want v1 restaged from the signed manifest", v) } } // getLog is a Store that remembers every Get key in order, and can answer // every read of swapKey after the first with other bytes: the swap a re-read // between the digest check and the download would take. type getLog struct { Store keys *[]string swapKey string swapTo []byte } func (g getLog) Get(ctx context.Context, key string) ([]byte, string, error) { seen := indexOf(*g.keys, key) >= 0 *g.keys = append(*g.keys, key) if seen && key == g.swapKey && g.swapTo != nil { _, etag, err := g.Store.Get(ctx, key) return g.swapTo, etag, err } return g.Store.Get(ctx, key) } func indexOf(keys []string, key string) int { for i, k := range keys { if k == key { return i } } return -1 } // newDigestReuseFixture is stage_reuse_test.go's reuseFixture with the digest // marker on from the start: h1 serves v1, a stale "old v2" is staged, and // canary/x is promoted to a v2 shipped as "new v2", under a pointer that // signs v2's manifest digest. func newDigestReuseFixture(t *testing.T) reuseFixture { t.Helper() ctx := context.Background() f := reuseFixture{s: NewDirStore(t.TempDir()), opt: t.TempDir(), reg: testRegistry(t)} f.priv, f.pub = testKeys(t) markerOn(t, f.s, f.priv, "bab") shipRelease(t, f.s, "bab", "myapp", "v1", adoptPlatform, []byte("v1")) if err := Promote(ctx, f.s, f.priv, "bab", "myapp", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, f.reg, "h1.example.com", "myapp", "canary/x", false) if err := Pass(ctx, f.reg, f.s, f.pub, f.opt, func(string) error { return nil }); err != nil { t.Fatal(err) } if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" { t.Fatalf("setup: live = %q, want v1", v) } if err := StageArtifact(f.opt, "bab", "v2", "myapp", []byte("old v2")); err != nil { t.Fatal(err) } shipRelease(t, f.s, "bab", "myapp", "v2", adoptPlatform, []byte("new v2")) if err := Promote(ctx, f.s, f.priv, "bab", "myapp", "canary/x", "v2"); err != nil { t.Fatal(err) } ptr, err := Channel(ctx, f.s, f.pub, "bab", "myapp", "canary/x") if err != nil || ptr.Digest == "" || ptr.Digest != manifestDigestOf(t, f.s, "bab", "myapp", "v2") { t.Fatalf("setup: pointer %+v, %v; want it to sign v2's manifest digest", ptr, err) } return f } // Composition with zdd-4 (spec §4.2 + §4.6): a digest-bound pointer whose // version is staged with other bytes that something on the box still runs is // refused. The referenced copy is never overwritten and h1 stays where it is. func TestDigestBoundReuseOfAReferencedStaleCopyIsRefused(t *testing.T) { f := newDigestReuseFixture(t) if err := PointLive(f.opt, "h2.example.com", "bab", "v2", "myapp"); err != nil { t.Fatal(err) } var keys []string g := f g.s = getLog{Store: f.s, keys: &keys} f.assertRefused(t, g.pass(t), ErrVersionReused.Error()) if i := indexOf(keys, ArtifactKey("bab", "myapp", "v2", adoptPlatform)); i >= 0 { t.Fatalf("v2's artifact was downloaded for a refused restage: %v", keys) } } // The unreferenced case under a digest-bound pointer: restaged, and only // after the manifest passed the digest. The manifest is read ONCE: the store // answers any second read with a different, valid manifest for v2, which a // re-read between the check and the download would stage from. func TestDigestBoundUnreferencedStaleCopyIsRestagedFromTheCheckedManifest(t *testing.T) { f := newDigestReuseFixture(t) canonical, _, err := f.s.Get(context.Background(), ManifestKey("bab", "myapp", "v2")) if err != nil { t.Fatal(err) } other := putRawRelease(t, NewDirStore(t.TempDir()), "bab", "myapp", "v2", []byte("bytes nobody signed"), "") var keys []string g := f g.s = getLog{Store: f.s, keys: &keys, swapKey: ManifestKey("bab", "myapp", "v2"), swapTo: other} out := g.pass(t) if got := f.stagedV2(t); got != "new v2" { t.Fatalf("staged v2 = %q, want the signed build:\n%s", got, out) } if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v2" { t.Fatalf("h1 live = %q, want v2:\n%s", v, out) } if !strings.Contains(out, "restaging") { t.Fatalf("pass printed %q; want a line saying the copy was restaged", out) } mi := indexOf(keys, ManifestKey("bab", "myapp", "v2")) ai := indexOf(keys, ArtifactKey("bab", "myapp", "v2", adoptPlatform)) if mi < 0 || ai < 0 || mi > ai { t.Fatalf("manifest read at %d, artifact at %d: the digest check must come first: %v", mi, ai, keys) } sib, err := os.ReadFile(StagedManifestPath(f.opt, "bab", "v2", "myapp")) if err != nil || !bytes.Equal(sib, canonical) { t.Fatalf("sibling = %s, %v; want the checked canonical bytes", sib, err) } } // The same unreferenced stale copy, but the bucket's manifest no longer // hashes to the digest the pointer signed: refused before anything is // downloaded, and the stale copy is left exactly as it was. func TestDigestBoundStagingRefusesAManifestThatNoLongerHashesToTheDigest(t *testing.T) { f := newDigestReuseFixture(t) ctx := context.Background() raw, etag, err := f.s.Get(ctx, ManifestKey("bab", "myapp", "v2")) if err != nil { t.Fatal(err) } if _, err := f.s.Put(ctx, ManifestKey("bab", "myapp", "v2"), append(raw, '\n'), etag); err != nil { t.Fatal(err) } var keys []string g := f g.s = getLog{Store: f.s, keys: &keys} out := g.pass(t) f.assertRefused(t, out, "pointer signed") if strings.Contains(out, "restaging") { t.Fatalf("the pass restaged under a manifest that failed its digest:\n%s", out) } if i := indexOf(keys, ArtifactKey("bab", "myapp", "v2", adoptPlatform)); i >= 0 { t.Fatalf("v2's artifact was downloaded under a manifest that failed its digest: %v", keys) } } // A copy this pass verified against one manifest is not proof against // another. a1 follows a channel promoted before the marker (no digest) and // reuses the staged v2 against the manifest the bucket answers first; b1 // follows a channel whose pointer signs a different v2 manifest. b1 must not // reuse that copy on the strength of the pass's "already verified" memory: // it hashes the copy against its own checked manifest, finds it differs, and // since a1 now runs it, refuses. func TestADigestBoundHostNeverTrustsACopyVerifiedAgainstAnotherManifest(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("signed v2")) if err := Promote(ctx, s, priv, "bab", "myapp", "canary/a", "v2"); err != nil { t.Fatal(err) } markerOn(t, s, priv, "bab") if err := Promote(ctx, s, priv, "bab", "myapp", "canary/b", "v2"); err != nil { t.Fatal(err) } if err := StageArtifact(opt, "bab", "v2", "myapp", []byte("unsigned v2")); err != nil { t.Fatal(err) } // The bucket answers the first read of v2's manifest with one for the // staged "unsigned v2" bytes, and every later read with the signed one. unsigned := putRawRelease(t, NewDirStore(t.TempDir()), "bab", "myapp", "v2", []byte("unsigned v2"), "") flip := &flipStore{Store: s, key: ManifestKey("bab", "myapp", "v2"), first: unsigned} addRoute(t, reg, "a1.example.com", "myapp", "canary/a", false) addRoute(t, reg, "b1.example.com", "myapp", "canary/b", false) var out bytes.Buffer withCapturedStdout(t, &out, func() { if err := Pass(ctx, reg, flip, pub, opt, func(string) error { return nil }); err != nil { t.Fatalf("Pass: %v", err) } }) if v, _ := LiveVersion(opt, "a1.example.com"); v != "v2" { t.Fatalf("setup: a1 live = %q, want v2 (it has no digest to check):\n%s", v, out.String()) } if v, _ := LiveVersion(opt, "b1.example.com"); v != "" { t.Fatalf("b1 live on %q: a digest-bound host reused a copy verified against another manifest:\n%s", v, out.String()) } if !strings.Contains(out.String(), ErrVersionReused.Error()) { t.Fatalf("no refusal line for b1:\n%s", out.String()) } } // flipStore answers the first Get of key with first, and the real object // after that. type flipStore struct { Store key string first []byte done bool } func (f *flipStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == f.key && !f.done { f.done = true _, etag, err := f.Store.Get(ctx, key) return f.first, etag, err } return f.Store.Get(ctx, key) } func TestADigestBearingPointerAdoptsTheSignedBuild(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) markerOn(t, s, priv, "bab") shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { t.Fatal(err) } if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { t.Fatalf("live %q, want v1", v) } sib, err := os.ReadFile(StagedManifestPath(opt, "bab", "v1", "km")) if err != nil || ManifestDigest(sib) != manifestDigestOf(t, s, "bab", "km", "v1") { t.Fatalf("the staged sibling is not the checked canonical bytes: %v", err) } } // A rollback's head is an old history entry: its digest moved with it. func TestARolledBackHeadIsCheckedAgainstItsDigest(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) markerOn(t, s, priv, "bab") shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) for _, v := range []string{"v1", "v2"} { if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil { t.Fatal(err) } } if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil { t.Fatal(err) } shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("other bytes, same name")) if err := Rollback(ctx, s, priv, "bab", "km", "canary/x"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) withCapturedStdout(t, &bytes.Buffer{}, func() { _ = Pass(ctx, reg, s, pub, opt, nil) }) if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" { t.Fatal("a rollback onto a reused name installed its new bytes") } } func TestTheRatchetRefusesALegacyPointerInThePass(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } legacy, _, _ := s.Get(ctx, ChannelKey("bab", "km", "canary/x")) markerOn(t, s, priv, "bab") shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) r := NewDigestRatchet(t.TempDir(), nil, false) if err := Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(r)); err != nil { t.Fatal(err) } replayPointer(t, s, "canary/x", legacy) var out bytes.Buffer withCapturedStdout(t, &out, func() { _ = Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(r)) }) if v, _ := LiveVersion(opt, "a.example.com"); v != "v2" { t.Fatalf("live %q: the box took a legacy pointer after seeing digests", v) } if !strings.Contains(out.String(), "channel pointer refused") { t.Fatalf("no refusal line:\n%s", out.String()) } } // The default is today: no marker, no ratchet, legacy pointers adopt. func TestALegacyPointerAdoptsAsBefore(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) if err := Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(NewDigestRatchet(t.TempDir(), nil, false))); err != nil { t.Fatal(err) } if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { t.Fatalf("live %q, want v1", v) } } // P10: the live version's capability sibling is bound to the digest too. A // bucket manifest that no longer hashes to the pointer's digest is not // written beside the running binary. func TestBackfillOfTheLiveSiblingIsBoundToTheDigest(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) markerOn(t, s, priv, "bab") shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { t.Fatal(err) } if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { t.Fatalf("live %q, want v1", v) } sib := StagedManifestPath(opt, "bab", "v1", "km") if err := os.Remove(sib); err != nil { t.Fatal(err) } raw, etag, err := s.Get(ctx, ManifestKey("bab", "km", "v1")) if err != nil { t.Fatal(err) } if _, err := s.Put(ctx, ManifestKey("bab", "km", "v1"), append(raw, '\n'), etag); err != nil { t.Fatal(err) } var out bytes.Buffer withCapturedStdout(t, &out, func() { if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { t.Fatalf("Pass: %v", err) } }) if _, err := os.Stat(sib); err == nil { t.Fatal("a sibling that does not hash to the pointer's digest was written beside the live version") } if !strings.Contains(out.String(), "pointer signed") { t.Fatalf("no refusal line:\n%s", out.String()) } } // A matching staged copy reused under a digest-bound pointer keeps exactly the // checked manifest bytes beside it (spec §4.2, §4.6), even when the sibling // already there reads: here an older binary's lossy re-marshal. func TestDigestBoundReuseReplacesAReadableSiblingThatIsNotTheCheckedBytes(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) markerOn(t, s, priv, "bab") canonical := putRawRelease(t, s, "bab", "km", "v1", []byte("one"), `"from_a_newer_shipper":true,`) if err := StageArtifact(opt, "bab", "v1", "km", []byte("one")); err != nil { t.Fatal(err) } m, err := GetManifest(ctx, s, "bab", "km", "v1") if err != nil { t.Fatal(err) } lossy, err := json.MarshalIndent(m, "", " ") if err != nil { t.Fatal(err) } if err := StageManifest(opt, "bab", "v1", "km", lossy); err != nil { t.Fatal(err) } if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { t.Fatal(err) } addRoute(t, reg, "a.example.com", "km", "canary/x", false) if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { t.Fatal(err) } if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { t.Fatalf("live %q, want v1", v) } sib, err := os.ReadFile(StagedManifestPath(opt, "bab", "v1", "km")) if err != nil || !bytes.Equal(sib, canonical) { t.Fatalf("sibling = %s, %v; want the checked canonical bytes %s", sib, err, canonical) } }