carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostactors"
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostevidence"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21func TestPlan9LegacyJournalNeedsExactRootSignedStop(t *testing.T) {
22 root := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize))
23 signer := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{2}, ed25519.SeedSize))
24 store := release.NewDirStore(t.TempDir())
25 enrollment := strings.Repeat("a", 32)
26 roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 3, RequestLimitNS: int64(hostbox.RequestLimit), Regions: map[string]hostactors.Region{"ie": {}}, Boxes: map[string]hostactors.Box{
27 "box": {CensusPublicKey: signer.Public().(ed25519.PublicKey), Enrollment: enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "ie", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"},
28 }, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
29 raw, err := release.SignRootEnvelope(release.RootSigner(root), roster)
30 if err != nil {
31 t.Fatal(err)
32 }
33 if _, err = store.Put(t.Context(), hostactors.Key, raw, ""); err != nil {
34 t.Fatal(err)
35 }
36 newBatch := func() func(context.Context, hostbox.LaunchInventory, hostbox.StopInventory, hostbox.StopKernelJournal) error {
37 return legacyJournalProof(store, hostactors.Directory{Store: store, PublicKey: root.Public().(ed25519.PublicKey), Bucket: "bucket"})
38 }
39 verify := newBatch()
40 j, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "legacy.db"))
41 if err != nil {
42 t.Fatal(err)
43 }
44 defer j.Close()
45 in := hostbox.LaunchInventory{Target: hostguard.FenceTarget{Host: "app.example", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: enrollment, BootID: "boot", ControllerID: "controller", Op: "operation"}, ControllerPID: 1, ProcessStart: "1", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "session", Device: 1, Inode: 2}}}
46 if err = j.Create(t.Context(), in); err != nil {
47 t.Fatal(err)
48 }
49 journal, err := j.ReadStopKernelJournal(t.Context(), in.Target)
50 if err != nil {
51 t.Fatal(err)
52 }
53 inventory := hostbox.StopInventory{Complete: true, Host: in.Target.Host, Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: enrollment, DirectoryRevision: "1"}
54 if err = verify(t.Context(), in, inventory, journal); err == nil {
55 t.Fatal("unproven legacy accepted")
56 }
57 target := in.Target
58 receipt := StopReceipt{FenceID: "fence", ClaimGen: 1, ObservedBootID: "boot", DirectoryRevision: "1", Binding: "1", CgroupIdentities: []string{"session"}, V: 1, ID: "stop1", Kind: "stopped", Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, OperationID: target.Op, RequestLimitNS: int64(hostbox.RequestLimit), EvidenceAt: time.Now().UTC(), InventorySHA256: digestStop(inventory), LocalJournalSHA256: digestStop(journal)}
59 raw, _ = json.Marshal(receipt)
60 key := "control/hosts/app.example/evidence/operation/box/stop1.json"
61 if _, err = store.Put(t.Context(), key, raw, ""); err != nil {
62 t.Fatal(err)
63 }
64 verify = newBatch()
65 if err = verify(t.Context(), in, inventory, journal); err == nil {
66 t.Fatal("unsigned stop accepted")
67 }
68 if err = hostevidence.Publish(t.Context(), store, signer, key, raw); err != nil {
69 t.Fatal(err)
70 }
71 if err = verify(t.Context(), in, inventory, journal); err == nil {
72 t.Fatal("batch silently refreshed unsigned evidence")
73 }
74 // Two distinct writer histories under one evidence namespace are fetched
75 // and indexed once, rather than rescanning every receipt for each row.
76 second := in
77 second.Target.Session = 2
78 secondJournal := journal
79 secondJournal.Target = second.Target
80 secondReceipt := receipt
81 secondReceipt.ID = "stop2"
82 secondReceipt.WriterSession = 2
83 secondReceipt.LocalJournalSHA256 = digestStop(secondJournal)
84 secondRaw, _ := json.Marshal(secondReceipt)
85 secondKey := "control/hosts/app.example/evidence/operation/box/stop2.json"
86 if err = hostevidence.Publish(t.Context(), store, signer, secondKey, secondRaw); err != nil {
87 t.Fatal(err)
88 }
89 counted := &legacyProofCountStore{Store: store, t: t, gets: map[string]int{}, lists: map[string]int{}}
90 directory := hostactors.Directory{Store: counted, PublicKey: root.Public().(ed25519.PublicKey), Bucket: "bucket"}
91 verify = legacyJournalProof(counted, directory)
92 if err = verify(t.Context(), second, inventory, secondJournal); err != nil {
93 t.Fatal(err)
94 }
95 if err = verify(t.Context(), in, inventory, journal); err != nil {
96 t.Fatal(err)
97 }
98 if counted.gets[hostactors.Key] != 1 || counted.lists["control/hosts/app.example/evidence/operation/box/"] != 1 {
99 t.Fatalf("batch repeated roster/list: %v %v", counted.gets, counted.lists)
100 }
101 for _, key := range []string{key, key + ".sig", secondKey, secondKey + ".sig"} {
102 if counted.gets[key] != 1 {
103 t.Fatalf("receipt fetched %d times: %s", counted.gets[key], key)
104 }
105 }
106 cancelled, cancel := context.WithCancel(t.Context())
107 cancel()
108 if err = verify(cancelled, in, inventory, journal); !errors.Is(err, context.Canceled) {
109 t.Fatalf("cached proof ignored cancellation: %v", err)
110 }
111 if counted.gets[hostactors.Key] != 1 {
112 t.Fatal("canceled cache lookup reread roster")
113 }
114 // In-flight cancellation is sticky in this immutable batch, while a new
115 // startup gets a fresh closure and can retry the failed read.
116 during, stopRead := context.WithCancel(t.Context())
117 counted.onGet = func(ctx context.Context, key string) error {
118 if key == hostactors.Key {
119 stopRead()
120 return ctx.Err()
121 }
122 return nil
123 }
124 interrupted := legacyJournalProof(counted, directory)
125 if err = interrupted(during, in, inventory, journal); !errors.Is(err, context.Canceled) {
126 t.Fatalf("in-flight cancellation: %v", err)
127 }
128 before := counted.gets[hostactors.Key]
129 counted.onGet = nil
130 if err = interrupted(t.Context(), in, inventory, journal); !errors.Is(err, context.Canceled) || counted.gets[hostactors.Key] != before {
131 t.Fatal("failed batch reread roster", err)
132 }
133 if err = legacyJournalProof(counted, directory)(t.Context(), in, inventory, journal); err != nil {
134 t.Fatal("fresh batch cannot retry", err)
135 }
136 changed := journal
137 changed.Completed = hostbox.ProducerCompletion{Count: 1, Digest: strings.Repeat("d", 64)}
138 if err = verify(t.Context(), in, inventory, changed); err == nil {
139 t.Fatal("forged completed counter accepted")
140 }
141 inventory.DirectoryRevision = "2"
142 if err = verify(t.Context(), in, inventory, journal); err == nil {
143 t.Fatal("changed inventory accepted")
144 }
145}
146
147type legacyProofCountStore struct {
148 release.Store
149 t *testing.T
150 gets, lists map[string]int
151 onGet func(context.Context, string) error
152}
153
154func (s *legacyProofCountStore) Get(ctx context.Context, key string) ([]byte, string, error) {
155 s.t.Helper()
156 if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > 30*time.Second {
157 s.t.Fatal("unbounded migration object read")
158 }
159 s.gets[key]++
160 if s.onGet != nil {
161 if err := s.onGet(ctx, key); err != nil {
162 return nil, "", err
163 }
164 }
165 return s.Store.Get(ctx, key)
166}
167func (s *legacyProofCountStore) List(ctx context.Context, prefix string) ([]string, error) {
168 s.t.Helper()
169 if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > 30*time.Second {
170 s.t.Fatal("unbounded migration namespace read")
171 }
172 s.lists[prefix]++
173 return s.Store.List(ctx, prefix)
174}
175