carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
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.
13func 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.
27func 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).
71func 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.
99type getLog struct {
100 Store
101 keys *[]string
102 swapKey string
103 swapTo []byte
104}
105
106func (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
116func 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.
129func 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.
163func 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.
181func 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.
215func 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.
245func 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.
287type flipStore struct {
288 Store
289 key string
290 first []byte
291 done bool
292}
293
294func (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
303func 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.
328func 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
356func 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.
389func 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.
411func 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.
457func 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