| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "os" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | // replayPointer writes back a saved pointer, the attacker's move in #253. |
| 13 | func replayPointer(t *testing.T, s Store, ch string, saved []byte) { |
| 14 | t.Helper() |
| 15 | ctx := context.Background() |
| 16 | _, etag, err := s.Get(ctx, ChannelKey("bab", "km", ch)) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | if _, err := s.Put(ctx, ChannelKey("bab", "km", ch), saved, etag); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | // Spec §8: a reused version name under a replayed digest-bearing pointer is |
| 26 | // refused before staging. |
| 27 | func TestAReusedNameUnderAReplayedPointerIsRefusedBeforeStaging(t *testing.T) { |
| 28 | s := NewDirStore(t.TempDir()) |
| 29 | ctx := context.Background() |
| 30 | priv, pub := testKeys(t) |
| 31 | opt := t.TempDir() |
| 32 | reg := testRegistry(t) |
| 33 | markerOn(t, s, priv, "bab") |
| 34 | |
| 35 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("the bytes that were promoted")) |
| 36 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | saved, _, _ := s.Get(ctx, ChannelKey("bab", "km", "canary/x")) |
| 40 | shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| 41 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("bytes nobody promoted")) |
| 48 | replayPointer(t, s, "canary/x", saved) |
| 49 | |
| 50 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 51 | var out bytes.Buffer |
| 52 | withCapturedStdout(t, &out, func() { |
| 53 | if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { |
| 54 | t.Fatalf("Pass: %v", err) |
| 55 | } |
| 56 | }) |
| 57 | if v, _ := LiveVersion(opt, "a.example.com"); v != "" { |
| 58 | t.Fatalf("live on %q: the replayed pointer installed bytes nobody promoted", v) |
| 59 | } |
| 60 | if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil { |
| 61 | t.Fatal("the reused name's artifact was staged") |
| 62 | } |
| 63 | if !strings.Contains(out.String(), "pointer signed") { |
| 64 | t.Fatalf("no refusal line:\n%s", out.String()) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // A staged copy that is not the signed build is never pointed live. Nothing |
| 69 | // refers to this one, so zdd-4's reuse rule restages it, from the manifest |
| 70 | // that passed the digest; the referenced case is refused (below). |
| 71 | func TestAStagedCopyThatIsNotTheSignedBuildIsNeverPointedLive(t *testing.T) { |
| 72 | s := NewDirStore(t.TempDir()) |
| 73 | ctx := context.Background() |
| 74 | priv, pub := testKeys(t) |
| 75 | opt := t.TempDir() |
| 76 | reg := testRegistry(t) |
| 77 | markerOn(t, s, priv, "bab") |
| 78 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("signed")) |
| 79 | if err := StageArtifact(opt, "bab", "v1", "km", []byte("left over from a pruned v1")); err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 86 | var out bytes.Buffer |
| 87 | withCapturedStdout(t, &out, func() { _ = Pass(ctx, reg, s, pub, opt, nil) }) |
| 88 | if b, _ := os.ReadFile(VersionPath(opt, "bab", "v1", "km")); string(b) != "signed" { |
| 89 | t.Fatalf("staged v1 = %q: a copy that is not the signed build stayed staged", b) |
| 90 | } |
| 91 | if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { |
| 92 | t.Fatalf("live %q, want v1 restaged from the signed manifest", v) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | // getLog is a Store that remembers every Get key in order, and can answer |
| 97 | // every read of swapKey after the first with other bytes: the swap a re-read |
| 98 | // between the digest check and the download would take. |
| 99 | type getLog struct { |
| 100 | Store |
| 101 | keys *[]string |
| 102 | swapKey string |
| 103 | swapTo []byte |
| 104 | } |
| 105 | |
| 106 | func (g getLog) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 107 | seen := indexOf(*g.keys, key) >= 0 |
| 108 | *g.keys = append(*g.keys, key) |
| 109 | if seen && key == g.swapKey && g.swapTo != nil { |
| 110 | _, etag, err := g.Store.Get(ctx, key) |
| 111 | return g.swapTo, etag, err |
| 112 | } |
| 113 | return g.Store.Get(ctx, key) |
| 114 | } |
| 115 | |
| 116 | func indexOf(keys []string, key string) int { |
| 117 | for i, k := range keys { |
| 118 | if k == key { |
| 119 | return i |
| 120 | } |
| 121 | } |
| 122 | return -1 |
| 123 | } |
| 124 | |
| 125 | // newDigestReuseFixture is stage_reuse_test.go's reuseFixture with the digest |
| 126 | // marker on from the start: h1 serves v1, a stale "old v2" is staged, and |
| 127 | // canary/x is promoted to a v2 shipped as "new v2", under a pointer that |
| 128 | // signs v2's manifest digest. |
| 129 | func newDigestReuseFixture(t *testing.T) reuseFixture { |
| 130 | t.Helper() |
| 131 | ctx := context.Background() |
| 132 | f := reuseFixture{s: NewDirStore(t.TempDir()), opt: t.TempDir(), reg: testRegistry(t)} |
| 133 | f.priv, f.pub = testKeys(t) |
| 134 | markerOn(t, f.s, f.priv, "bab") |
| 135 | shipRelease(t, f.s, "bab", "myapp", "v1", adoptPlatform, []byte("v1")) |
| 136 | if err := Promote(ctx, f.s, f.priv, "bab", "myapp", "canary/x", "v1"); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | addRoute(t, f.reg, "h1.example.com", "myapp", "canary/x", false) |
| 140 | if err := Pass(ctx, f.reg, f.s, f.pub, f.opt, func(string) error { return nil }); err != nil { |
| 141 | t.Fatal(err) |
| 142 | } |
| 143 | if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" { |
| 144 | t.Fatalf("setup: live = %q, want v1", v) |
| 145 | } |
| 146 | if err := StageArtifact(f.opt, "bab", "v2", "myapp", []byte("old v2")); err != nil { |
| 147 | t.Fatal(err) |
| 148 | } |
| 149 | shipRelease(t, f.s, "bab", "myapp", "v2", adoptPlatform, []byte("new v2")) |
| 150 | if err := Promote(ctx, f.s, f.priv, "bab", "myapp", "canary/x", "v2"); err != nil { |
| 151 | t.Fatal(err) |
| 152 | } |
| 153 | ptr, err := Channel(ctx, f.s, f.pub, "bab", "myapp", "canary/x") |
| 154 | if err != nil || ptr.Digest == "" || ptr.Digest != manifestDigestOf(t, f.s, "bab", "myapp", "v2") { |
| 155 | t.Fatalf("setup: pointer %+v, %v; want it to sign v2's manifest digest", ptr, err) |
| 156 | } |
| 157 | return f |
| 158 | } |
| 159 | |
| 160 | // Composition with zdd-4 (spec §4.2 + §4.6): a digest-bound pointer whose |
| 161 | // version is staged with other bytes that something on the box still runs is |
| 162 | // refused. The referenced copy is never overwritten and h1 stays where it is. |
| 163 | func TestDigestBoundReuseOfAReferencedStaleCopyIsRefused(t *testing.T) { |
| 164 | f := newDigestReuseFixture(t) |
| 165 | if err := PointLive(f.opt, "h2.example.com", "bab", "v2", "myapp"); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | var keys []string |
| 169 | g := f |
| 170 | g.s = getLog{Store: f.s, keys: &keys} |
| 171 | f.assertRefused(t, g.pass(t), ErrVersionReused.Error()) |
| 172 | if i := indexOf(keys, ArtifactKey("bab", "myapp", "v2", adoptPlatform)); i >= 0 { |
| 173 | t.Fatalf("v2's artifact was downloaded for a refused restage: %v", keys) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | // The unreferenced case under a digest-bound pointer: restaged, and only |
| 178 | // after the manifest passed the digest. The manifest is read ONCE: the store |
| 179 | // answers any second read with a different, valid manifest for v2, which a |
| 180 | // re-read between the check and the download would stage from. |
| 181 | func TestDigestBoundUnreferencedStaleCopyIsRestagedFromTheCheckedManifest(t *testing.T) { |
| 182 | f := newDigestReuseFixture(t) |
| 183 | canonical, _, err := f.s.Get(context.Background(), ManifestKey("bab", "myapp", "v2")) |
| 184 | if err != nil { |
| 185 | t.Fatal(err) |
| 186 | } |
| 187 | other := putRawRelease(t, NewDirStore(t.TempDir()), "bab", "myapp", "v2", []byte("bytes nobody signed"), "") |
| 188 | var keys []string |
| 189 | g := f |
| 190 | g.s = getLog{Store: f.s, keys: &keys, swapKey: ManifestKey("bab", "myapp", "v2"), swapTo: other} |
| 191 | out := g.pass(t) |
| 192 | if got := f.stagedV2(t); got != "new v2" { |
| 193 | t.Fatalf("staged v2 = %q, want the signed build:\n%s", got, out) |
| 194 | } |
| 195 | if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v2" { |
| 196 | t.Fatalf("h1 live = %q, want v2:\n%s", v, out) |
| 197 | } |
| 198 | if !strings.Contains(out, "restaging") { |
| 199 | t.Fatalf("pass printed %q; want a line saying the copy was restaged", out) |
| 200 | } |
| 201 | mi := indexOf(keys, ManifestKey("bab", "myapp", "v2")) |
| 202 | ai := indexOf(keys, ArtifactKey("bab", "myapp", "v2", adoptPlatform)) |
| 203 | if mi < 0 || ai < 0 || mi > ai { |
| 204 | t.Fatalf("manifest read at %d, artifact at %d: the digest check must come first: %v", mi, ai, keys) |
| 205 | } |
| 206 | sib, err := os.ReadFile(StagedManifestPath(f.opt, "bab", "v2", "myapp")) |
| 207 | if err != nil || !bytes.Equal(sib, canonical) { |
| 208 | t.Fatalf("sibling = %s, %v; want the checked canonical bytes", sib, err) |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | // The same unreferenced stale copy, but the bucket's manifest no longer |
| 213 | // hashes to the digest the pointer signed: refused before anything is |
| 214 | // downloaded, and the stale copy is left exactly as it was. |
| 215 | func TestDigestBoundStagingRefusesAManifestThatNoLongerHashesToTheDigest(t *testing.T) { |
| 216 | f := newDigestReuseFixture(t) |
| 217 | ctx := context.Background() |
| 218 | raw, etag, err := f.s.Get(ctx, ManifestKey("bab", "myapp", "v2")) |
| 219 | if err != nil { |
| 220 | t.Fatal(err) |
| 221 | } |
| 222 | if _, err := f.s.Put(ctx, ManifestKey("bab", "myapp", "v2"), append(raw, '\n'), etag); err != nil { |
| 223 | t.Fatal(err) |
| 224 | } |
| 225 | var keys []string |
| 226 | g := f |
| 227 | g.s = getLog{Store: f.s, keys: &keys} |
| 228 | out := g.pass(t) |
| 229 | f.assertRefused(t, out, "pointer signed") |
| 230 | if strings.Contains(out, "restaging") { |
| 231 | t.Fatalf("the pass restaged under a manifest that failed its digest:\n%s", out) |
| 232 | } |
| 233 | if i := indexOf(keys, ArtifactKey("bab", "myapp", "v2", adoptPlatform)); i >= 0 { |
| 234 | t.Fatalf("v2's artifact was downloaded under a manifest that failed its digest: %v", keys) |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // A copy this pass verified against one manifest is not proof against |
| 239 | // another. a1 follows a channel promoted before the marker (no digest) and |
| 240 | // reuses the staged v2 against the manifest the bucket answers first; b1 |
| 241 | // follows a channel whose pointer signs a different v2 manifest. b1 must not |
| 242 | // reuse that copy on the strength of the pass's "already verified" memory: |
| 243 | // it hashes the copy against its own checked manifest, finds it differs, and |
| 244 | // since a1 now runs it, refuses. |
| 245 | func TestADigestBoundHostNeverTrustsACopyVerifiedAgainstAnotherManifest(t *testing.T) { |
| 246 | s := NewDirStore(t.TempDir()) |
| 247 | ctx := context.Background() |
| 248 | priv, pub := testKeys(t) |
| 249 | opt := t.TempDir() |
| 250 | reg := testRegistry(t) |
| 251 | shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("signed v2")) |
| 252 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/a", "v2"); err != nil { |
| 253 | t.Fatal(err) |
| 254 | } |
| 255 | markerOn(t, s, priv, "bab") |
| 256 | if err := Promote(ctx, s, priv, "bab", "myapp", "canary/b", "v2"); err != nil { |
| 257 | t.Fatal(err) |
| 258 | } |
| 259 | if err := StageArtifact(opt, "bab", "v2", "myapp", []byte("unsigned v2")); err != nil { |
| 260 | t.Fatal(err) |
| 261 | } |
| 262 | // The bucket answers the first read of v2's manifest with one for the |
| 263 | // staged "unsigned v2" bytes, and every later read with the signed one. |
| 264 | unsigned := putRawRelease(t, NewDirStore(t.TempDir()), "bab", "myapp", "v2", []byte("unsigned v2"), "") |
| 265 | flip := &flipStore{Store: s, key: ManifestKey("bab", "myapp", "v2"), first: unsigned} |
| 266 | addRoute(t, reg, "a1.example.com", "myapp", "canary/a", false) |
| 267 | addRoute(t, reg, "b1.example.com", "myapp", "canary/b", false) |
| 268 | var out bytes.Buffer |
| 269 | withCapturedStdout(t, &out, func() { |
| 270 | if err := Pass(ctx, reg, flip, pub, opt, func(string) error { return nil }); err != nil { |
| 271 | t.Fatalf("Pass: %v", err) |
| 272 | } |
| 273 | }) |
| 274 | if v, _ := LiveVersion(opt, "a1.example.com"); v != "v2" { |
| 275 | t.Fatalf("setup: a1 live = %q, want v2 (it has no digest to check):\n%s", v, out.String()) |
| 276 | } |
| 277 | if v, _ := LiveVersion(opt, "b1.example.com"); v != "" { |
| 278 | t.Fatalf("b1 live on %q: a digest-bound host reused a copy verified against another manifest:\n%s", v, out.String()) |
| 279 | } |
| 280 | if !strings.Contains(out.String(), ErrVersionReused.Error()) { |
| 281 | t.Fatalf("no refusal line for b1:\n%s", out.String()) |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | // flipStore answers the first Get of key with first, and the real object |
| 286 | // after that. |
| 287 | type flipStore struct { |
| 288 | Store |
| 289 | key string |
| 290 | first []byte |
| 291 | done bool |
| 292 | } |
| 293 | |
| 294 | func (f *flipStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 295 | if key == f.key && !f.done { |
| 296 | f.done = true |
| 297 | _, etag, err := f.Store.Get(ctx, key) |
| 298 | return f.first, etag, err |
| 299 | } |
| 300 | return f.Store.Get(ctx, key) |
| 301 | } |
| 302 | |
| 303 | func TestADigestBearingPointerAdoptsTheSignedBuild(t *testing.T) { |
| 304 | s := NewDirStore(t.TempDir()) |
| 305 | ctx := context.Background() |
| 306 | priv, pub := testKeys(t) |
| 307 | opt := t.TempDir() |
| 308 | reg := testRegistry(t) |
| 309 | markerOn(t, s, priv, "bab") |
| 310 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| 311 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 312 | t.Fatal(err) |
| 313 | } |
| 314 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 315 | if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { |
| 316 | t.Fatal(err) |
| 317 | } |
| 318 | if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { |
| 319 | t.Fatalf("live %q, want v1", v) |
| 320 | } |
| 321 | sib, err := os.ReadFile(StagedManifestPath(opt, "bab", "v1", "km")) |
| 322 | if err != nil || ManifestDigest(sib) != manifestDigestOf(t, s, "bab", "km", "v1") { |
| 323 | t.Fatalf("the staged sibling is not the checked canonical bytes: %v", err) |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | // A rollback's head is an old history entry: its digest moved with it. |
| 328 | func TestARolledBackHeadIsCheckedAgainstItsDigest(t *testing.T) { |
| 329 | s := NewDirStore(t.TempDir()) |
| 330 | ctx := context.Background() |
| 331 | priv, pub := testKeys(t) |
| 332 | opt := t.TempDir() |
| 333 | reg := testRegistry(t) |
| 334 | markerOn(t, s, priv, "bab") |
| 335 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| 336 | shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| 337 | for _, v := range []string{"v1", "v2"} { |
| 338 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil { |
| 339 | t.Fatal(err) |
| 340 | } |
| 341 | } |
| 342 | if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil { |
| 343 | t.Fatal(err) |
| 344 | } |
| 345 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("other bytes, same name")) |
| 346 | if err := Rollback(ctx, s, priv, "bab", "km", "canary/x"); err != nil { |
| 347 | t.Fatal(err) |
| 348 | } |
| 349 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 350 | withCapturedStdout(t, &bytes.Buffer{}, func() { _ = Pass(ctx, reg, s, pub, opt, nil) }) |
| 351 | if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" { |
| 352 | t.Fatal("a rollback onto a reused name installed its new bytes") |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | func TestTheRatchetRefusesALegacyPointerInThePass(t *testing.T) { |
| 357 | s := NewDirStore(t.TempDir()) |
| 358 | ctx := context.Background() |
| 359 | priv, pub := testKeys(t) |
| 360 | opt := t.TempDir() |
| 361 | reg := testRegistry(t) |
| 362 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| 363 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 364 | t.Fatal(err) |
| 365 | } |
| 366 | legacy, _, _ := s.Get(ctx, ChannelKey("bab", "km", "canary/x")) |
| 367 | markerOn(t, s, priv, "bab") |
| 368 | shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| 369 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil { |
| 370 | t.Fatal(err) |
| 371 | } |
| 372 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 373 | r := NewDigestRatchet(t.TempDir(), nil, false) |
| 374 | if err := Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(r)); err != nil { |
| 375 | t.Fatal(err) |
| 376 | } |
| 377 | replayPointer(t, s, "canary/x", legacy) |
| 378 | var out bytes.Buffer |
| 379 | withCapturedStdout(t, &out, func() { _ = Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(r)) }) |
| 380 | if v, _ := LiveVersion(opt, "a.example.com"); v != "v2" { |
| 381 | t.Fatalf("live %q: the box took a legacy pointer after seeing digests", v) |
| 382 | } |
| 383 | if !strings.Contains(out.String(), "channel pointer refused") { |
| 384 | t.Fatalf("no refusal line:\n%s", out.String()) |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | // The default is today: no marker, no ratchet, legacy pointers adopt. |
| 389 | func TestALegacyPointerAdoptsAsBefore(t *testing.T) { |
| 390 | s := NewDirStore(t.TempDir()) |
| 391 | ctx := context.Background() |
| 392 | priv, pub := testKeys(t) |
| 393 | opt := t.TempDir() |
| 394 | reg := testRegistry(t) |
| 395 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| 396 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 397 | t.Fatal(err) |
| 398 | } |
| 399 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 400 | if err := Pass(ctx, reg, s, pub, opt, nil, WithDigestRatchet(NewDigestRatchet(t.TempDir(), nil, false))); err != nil { |
| 401 | t.Fatal(err) |
| 402 | } |
| 403 | if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { |
| 404 | t.Fatalf("live %q, want v1", v) |
| 405 | } |
| 406 | } |
| 407 | |
| 408 | // P10: the live version's capability sibling is bound to the digest too. A |
| 409 | // bucket manifest that no longer hashes to the pointer's digest is not |
| 410 | // written beside the running binary. |
| 411 | func TestBackfillOfTheLiveSiblingIsBoundToTheDigest(t *testing.T) { |
| 412 | s := NewDirStore(t.TempDir()) |
| 413 | ctx := context.Background() |
| 414 | priv, pub := testKeys(t) |
| 415 | opt := t.TempDir() |
| 416 | reg := testRegistry(t) |
| 417 | markerOn(t, s, priv, "bab") |
| 418 | shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| 419 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 420 | t.Fatal(err) |
| 421 | } |
| 422 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 423 | if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { |
| 424 | t.Fatal(err) |
| 425 | } |
| 426 | if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { |
| 427 | t.Fatalf("live %q, want v1", v) |
| 428 | } |
| 429 | sib := StagedManifestPath(opt, "bab", "v1", "km") |
| 430 | if err := os.Remove(sib); err != nil { |
| 431 | t.Fatal(err) |
| 432 | } |
| 433 | raw, etag, err := s.Get(ctx, ManifestKey("bab", "km", "v1")) |
| 434 | if err != nil { |
| 435 | t.Fatal(err) |
| 436 | } |
| 437 | if _, err := s.Put(ctx, ManifestKey("bab", "km", "v1"), append(raw, '\n'), etag); err != nil { |
| 438 | t.Fatal(err) |
| 439 | } |
| 440 | var out bytes.Buffer |
| 441 | withCapturedStdout(t, &out, func() { |
| 442 | if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { |
| 443 | t.Fatalf("Pass: %v", err) |
| 444 | } |
| 445 | }) |
| 446 | if _, err := os.Stat(sib); err == nil { |
| 447 | t.Fatal("a sibling that does not hash to the pointer's digest was written beside the live version") |
| 448 | } |
| 449 | if !strings.Contains(out.String(), "pointer signed") { |
| 450 | t.Fatalf("no refusal line:\n%s", out.String()) |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | // A matching staged copy reused under a digest-bound pointer keeps exactly the |
| 455 | // checked manifest bytes beside it (spec §4.2, §4.6), even when the sibling |
| 456 | // already there reads: here an older binary's lossy re-marshal. |
| 457 | func TestDigestBoundReuseReplacesAReadableSiblingThatIsNotTheCheckedBytes(t *testing.T) { |
| 458 | s := NewDirStore(t.TempDir()) |
| 459 | ctx := context.Background() |
| 460 | priv, pub := testKeys(t) |
| 461 | opt := t.TempDir() |
| 462 | reg := testRegistry(t) |
| 463 | markerOn(t, s, priv, "bab") |
| 464 | canonical := putRawRelease(t, s, "bab", "km", "v1", []byte("one"), `"from_a_newer_shipper":true,`) |
| 465 | if err := StageArtifact(opt, "bab", "v1", "km", []byte("one")); err != nil { |
| 466 | t.Fatal(err) |
| 467 | } |
| 468 | m, err := GetManifest(ctx, s, "bab", "km", "v1") |
| 469 | if err != nil { |
| 470 | t.Fatal(err) |
| 471 | } |
| 472 | lossy, err := json.MarshalIndent(m, "", " ") |
| 473 | if err != nil { |
| 474 | t.Fatal(err) |
| 475 | } |
| 476 | if err := StageManifest(opt, "bab", "v1", "km", lossy); err != nil { |
| 477 | t.Fatal(err) |
| 478 | } |
| 479 | if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil { |
| 480 | t.Fatal(err) |
| 481 | } |
| 482 | addRoute(t, reg, "a.example.com", "km", "canary/x", false) |
| 483 | if err := Pass(ctx, reg, s, pub, opt, nil); err != nil { |
| 484 | t.Fatal(err) |
| 485 | } |
| 486 | if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" { |
| 487 | t.Fatalf("live %q, want v1", v) |
| 488 | } |
| 489 | sib, err := os.ReadFile(StagedManifestPath(opt, "bab", "v1", "km")) |
| 490 | if err != nil || !bytes.Equal(sib, canonical) { |
| 491 | t.Fatalf("sibling = %s, %v; want the checked canonical bytes %s", sib, err, canonical) |
| 492 | } |
| 493 | } |
| 494 | |