| index f1b9695a8..d9fd0122c 100644 |
| --- a/internal/host/relocationreleases_test.go |
| +++ b/internal/host/relocationreleases_test.go |
| @@ -30,16 +30,30 @@ type releaseWorld struct { |
| } |
| |
| func newReleaseWorld(t *testing.T) releaseWorld { |
| + t.Helper() |
| + return newReleaseWorldWith(t, nil) |
| +} |
| + |
| +// newReleaseWorldWith is newReleaseWorld with the roster's boxes changed |
| +// by roster before it is signed (nil leaves them). |
| +func newReleaseWorldWith(t *testing.T, roster func(map[string]hostactors.Box)) releaseWorld { |
| t.Helper() |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| root := ed25519.NewKeyFromSeed(make([]byte, 32)) |
| boxKey := ed25519.NewKeyFromSeed([]byte(strings.Repeat("b", 32))) |
| en := strings.Repeat("a", 32) |
| - roster := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{"box": {Enrollment: en, BoxID: "box", Label: "box", Namespace: "platform", Region: "ie", Pool: "default", State: "active", |
| - Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: boxKey.Public().(ed25519.PublicKey)}}, |
| + box := func(name, enrollment string, key ed25519.PrivateKey) hostactors.Box { |
| + return hostactors.Box{Enrollment: enrollment, BoxID: name, Label: name, Namespace: "platform", Region: "ie", Pool: "default", State: "active", |
| + Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-" + name}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: key.Public().(ed25519.PublicKey)} |
| + } |
| + boxes := map[string]hostactors.Box{"box": box("box", en, boxKey), "other": box("other", strings.Repeat("c", 32), otherBoxKey)} |
| + if roster != nil { |
| + roster(boxes) |
| + } |
| + signed := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: boxes, |
| Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| - wire, err := release.SignRootEnvelope(release.RootSigner(root), roster) |
| + wire, err := release.SignRootEnvelope(release.RootSigner(root), signed) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -62,16 +76,33 @@ func newReleaseWorld(t *testing.T) releaseWorld { |
| obs: hostclaim.Observed{Host: host, Claim: &claim, ETag: tag}} |
| } |
| |
| +// otherBoxKey is another roster box's census key. |
| +var otherBoxKey = ed25519.NewKeyFromSeed([]byte(strings.Repeat("o", 32))) |
| + |
| // receipt publishes root's signed full stop of the claim's session. |
| func (w releaseWorld) receipt(t *testing.T) (string, string) { |
| + t.Helper() |
| + return w.receiptWith(t, nil, w.boxKey) |
| +} |
| + |
| +// receiptWith publishes the receipt changed by mutate, signed by signer |
| +// (nil: the body alone, unsigned). |
| +func (w releaseWorld) receiptWith(t *testing.T, mutate func(*StopReceipt), signer ed25519.PrivateKey) (string, string) { |
| t.Helper() |
| c := w.obs.Claim |
| r := StopReceipt{V: 1, ID: "stop-1", Kind: "stopped", Host: c.Host, Incarnation: c.Holder.Incarnation, InstallationStamp: c.Holder.Stamp, WriterSession: c.Holder.Session, OperationID: c.Op.ID, |
| Phase: hostclaim.PhaseReleasing, ObligationID: RelocationReleaseObligation, Box: "box", Enrollment: w.en, Region: "ie", EvidenceAt: time.Now(), RequestLimitNS: int64(hostbox.RequestLimit), ClaimGen: 1, |
| FenceID: "fence", InventorySHA256: strings.Repeat("e", 64), LocalJournalSHA256: strings.Repeat("f", 64), ProducerDrainDigest: strings.Repeat("a", 64), CgroupIdentities: []string{"root-role"}} |
| + if mutate != nil { |
| + mutate(&r) |
| + } |
| body, _ := json.Marshal(r) |
| key := stopEvidenceKey(c.Host, c.Op.ID, "box", r.ID) |
| - if err := hostevidence.Publish(context.Background(), w.s, w.boxKey, key, body); err != nil { |
| + if signer == nil { |
| + if _, err := w.s.Put(context.Background(), key, body, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if err := hostevidence.Publish(context.Background(), w.s, signer, key, body); err != nil { |
| t.Fatal(err) |
| } |
| sum := sha256.Sum256(body) |
| @@ -98,7 +129,7 @@ func TestReleaseObligationQueuesOnceAndDischargesWithRootsReceipt(t *testing.T) |
| } |
| key, digest := w.receipt(t) |
| rel := hostclaim.Release{ReleasedBy: "box", Enrollment: w.en, Generation: "", Session: 7, Receipt: key, ReceiptSHA256: digest} |
| - if err := (StopReceiptReleases{}).VerifyRelease(ctx, *queued.Claim, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| + if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, *queued.Claim, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| t.Fatalf("a release counted before its receipt discharged the obligation: %v", err) |
| } |
| done, err := o.Complete(ctx, queued, key, digest) |
| @@ -108,7 +139,7 @@ func TestReleaseObligationQueuesOnceAndDischargesWithRootsReceipt(t *testing.T) |
| if len(done.Claim.Obligations) != 0 || done.Claim.Op.Journal["release_stop_receipt"] != key { |
| t.Fatalf("not discharged: %+v %v", done.Claim.Obligations, done.Claim.Op.Journal) |
| } |
| - if err := (StopReceiptReleases{}).VerifyRelease(ctx, *done.Claim, rel); err != nil { |
| + if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, *done.Claim, rel); err != nil { |
| t.Fatalf("the discharged release: %v", err) |
| } |
| // A replay: the discharged release queues nothing and completes again. |
| @@ -125,7 +156,7 @@ func TestReleaseObligationQueuesOnceAndDischargesWithRootsReceipt(t *testing.T) |
| } { |
| other := rel |
| change(&other) |
| - if err := (StopReceiptReleases{}).VerifyRelease(ctx, *done.Claim, other); !errors.Is(err, ErrClaimStopReceipt) { |
| + if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, *done.Claim, other); !errors.Is(err, ErrClaimStopReceipt) { |
| t.Fatalf("%s counted: %v", name, err) |
| } |
| } |
| @@ -133,7 +164,7 @@ func TestReleaseObligationQueuesOnceAndDischargesWithRootsReceipt(t *testing.T) |
| // releasing: a failed target's release in probe-failed never counts. |
| failed := *done.Claim |
| failed.Op.Phase = hostclaim.PhaseProbeFailed |
| - if err := (StopReceiptReleases{}).VerifyRelease(ctx, failed, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| + if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, failed, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| t.Fatalf("a probe-failed release counted: %v", err) |
| } |
| } |
| @@ -165,3 +196,105 @@ func TestReleaseObligationRefusesAnotherRelease(t *testing.T) { |
| t.Fatalf("no store: %v", err) |
| } |
| } |
| + |
| +// discharged is the world's claim as Complete leaves it: the receipt's key |
| +// and digest journaled and no obligation owed. |
| +func (w releaseWorld) discharged(key, digest string) hostclaim.Claim { |
| + c := *w.obs.Claim |
| + c.Op.Journal = map[string]string{"release_stop_receipt": key, "release_stop_sha256": digest} |
| + c.Obligations = nil |
| + return c |
| +} |
| + |
| +// The pin's verifier consumes root's signed receipt itself (Plan 11 Task |
| +// E), never the journaled digest alone: every receipt main's Complete |
| +// would refuse is refused at the pin, though the claim journals it as |
| +// discharged. (The deleted branch verifier's signature tests, against |
| +// main's receipt.) |
| +func TestStopReceiptReleasesVerifiesRootsSignedReceiptAtThePin(t *testing.T) { |
| + ctx := context.Background() |
| + verify := func(t *testing.T, w releaseWorld, key, digest string, dir hostactors.Directory) error { |
| + t.Helper() |
| + rel := hostclaim.Release{ReleasedBy: "box", Enrollment: w.en, Session: 7, Receipt: key, ReceiptSHA256: digest} |
| + return (StopReceiptReleases{Receipts: RelocationReleaseReceipts{Store: w.s, Directory: dir}}).VerifyRelease(ctx, w.discharged(key, digest), rel) |
| + } |
| + w := newReleaseWorld(t) |
| + key, digest := w.receipt(t) |
| + if err := verify(t, w, key, digest, w.dir); err != nil { |
| + t.Fatalf("root's signed receipt: %v", err) |
| + } |
| + if now, err := hostclaim.Read(ctx, w.s, w.obs.Host); err != nil || now.ETag != w.obs.ETag { |
| + t.Fatalf("verifying wrote the claim: %v", err) |
| + } |
| + t.Run("an unsigned receipt", func(t *testing.T) { |
| + w := newReleaseWorld(t) |
| + key, digest := w.receiptWith(t, nil, nil) |
| + if err := verify(t, w, key, digest, w.dir); err == nil { |
| + t.Fatal("an unsigned receipt counted") |
| + } |
| + }) |
| + t.Run("signed by another box's key", func(t *testing.T) { |
| + w := newReleaseWorld(t) |
| + key, digest := w.receiptWith(t, nil, otherBoxKey) |
| + if err := verify(t, w, key, digest, w.dir); err == nil { |
| + t.Fatal("another box's signature counted") |
| + } |
| + }) |
| + t.Run("a roster entry of another enrollment", func(t *testing.T) { |
| + w := newReleaseWorldWith(t, func(b map[string]hostactors.Box) { |
| + box := b["box"] |
| + box.Enrollment = strings.Repeat("d", 32) |
| + b["box"] = box |
| + }) |
| + key, digest := w.receipt(t) |
| + if err := verify(t, w, key, digest, w.dir); !errors.Is(err, ErrClaimStopIdentity) { |
| + t.Fatalf("another enrollment's key counted: %v", err) |
| + } |
| + }) |
| + t.Run("a box the roster does not name", func(t *testing.T) { |
| + w := newReleaseWorldWith(t, func(b map[string]hostactors.Box) { delete(b, "box") }) |
| + key, digest := w.receipt(t) |
| + if err := verify(t, w, key, digest, w.dir); !errors.Is(err, ErrClaimStopIdentity) { |
| + t.Fatalf("a box missing from the roster counted: %v", err) |
| + } |
| + }) |
| + t.Run("no roster", func(t *testing.T) { |
| + w := newReleaseWorld(t) |
| + key, digest := w.receipt(t) |
| + if err := verify(t, w, key, digest, hostactors.Directory{Store: w.s, Bucket: "bucket"}); err == nil { |
| + t.Fatal("a release counted without a roster to name its key") |
| + } |
| + }) |
| + t.Run("a digest that is not the bytes", func(t *testing.T) { |
| + w := newReleaseWorld(t) |
| + key, _ := w.receipt(t) |
| + if err := verify(t, w, key, strings.Repeat("0", 64), w.dir); !errors.Is(err, ErrClaimStopReceipt) { |
| + t.Fatalf("another digest counted: %v", err) |
| + } |
| + }) |
| + for name, mutate := range map[string]func(*StopReceipt){ |
| + "another session": func(r *StopReceipt) { r.WriterSession++ }, |
| + "another installation stamp": func(r *StopReceipt) { r.InstallationStamp++ }, |
| + "another operation's phase": func(r *StopReceipt) { r.Phase = hostclaim.PhaseProbeFailed }, |
| + "a retirement's stop": func(r *StopReceipt) { r.ObligationID = "park-local" }, |
| + "another enrollment": func(r *StopReceipt) { r.Enrollment = strings.Repeat("f", 32) }, |
| + "no stopped cgroups": func(r *StopReceipt) { r.CgroupIdentities = nil }, |
| + "a malformed producer digest": func(r *StopReceipt) { r.ProducerDrainDigest = "drain" }, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + w := newReleaseWorld(t) |
| + key, digest := w.receiptWith(t, mutate, w.boxKey) |
| + if err := verify(t, w, key, digest, w.dir); !errors.Is(err, ErrClaimStopReceipt) { |
| + t.Fatalf("accepted: %v", err) |
| + } |
| + }) |
| + } |
| + t.Run("no receipts store", func(t *testing.T) { |
| + w := newReleaseWorld(t) |
| + key, digest := w.receipt(t) |
| + rel := hostclaim.Release{ReleasedBy: "box", Enrollment: w.en, Session: 7, Receipt: key, ReceiptSHA256: digest} |
| + if err := (StopReceiptReleases{}).VerifyRelease(ctx, w.discharged(key, digest), rel); !errors.Is(err, ErrClaimStopUnavailable) { |
| + t.Fatalf("verified with no store: %v", err) |
| + } |
| + }) |
| +} |