carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/hostactors"
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostfloor"
17 "github.com/carlosframework/platform/internal/release"
18)
19
20// signedWrite changes the roster by a DIRECT SIGNED WRITE: mutate edits a
21// copy, Revision moves by one, and the result is signed with the root key
22// and CAS-written, bypassing Directory.Replace's membership rules. The L5
23// changes (a floor change after publication, a home region change, a
24// same-reference re-enrolment, a census-key rotation) are refused by the
25// normal writers, so each is simulated this way and named as such.
26func (f *bindingRosterFixture) signedWrite(mutate func(*hostactors.Roster)) {
27 f.t.Helper()
28 o := f.read()
29 next := hostactors.Clone(o.Roster)
30 mutate(&next)
31 next.Revision++
32 wire, err := release.SignRootEnvelope(f.signer, next)
33 if err != nil {
34 f.t.Fatalf("signedWrite: %v", err)
35 }
36 if _, err := f.store.Put(f.t.Context(), hostactors.Key, wire, o.ETag); err != nil {
37 f.t.Fatalf("signedWrite: %v", err)
38 }
39}
40
41// republishFloor replaces the published floor, remotely (the signed floor
42// and its authorization, overwritten by DIRECT WRITE: hostfloor.Publish is
43// create-only) and locally (the box's floor policy), and returns its digest.
44func (f *bindingRosterFixture) republishFloor(floor hostfloor.Floor) string {
45 f.t.Helper()
46 ctx := f.t.Context()
47 raw, auth, err := hostfloor.Authorize(f.signer, bindingFixtureBucket, floor)
48 if err != nil {
49 f.t.Fatal(err)
50 }
51 for key, body := range map[string][]byte{hostfloor.AuthKey: auth, hostfloor.Key: raw} {
52 _, etag, err := f.store.Get(ctx, key)
53 if err != nil {
54 f.t.Fatal(err)
55 }
56 if _, err := f.store.Put(ctx, key, body, etag); err != nil {
57 f.t.Fatal(err)
58 }
59 }
60 policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: bindingFixtureBucket, Mode: "enforced", Floor: raw, Authorization: auth})
61 if err := os.WriteFile(filepath.Join(f.floorDir, "policy.json"), policy, 0600); err != nil {
62 f.t.Fatal(err)
63 }
64 return hostfloor.Digest(raw)
65}
66
67// L5 (roster binding): a change to a binding fact other than quarantine
68// moves the box binding and refuses installed state. Each change is a
69// direct signed write (see signedWrite): the published floor (rewritten
70// locally and remotely so productionClaimRoster.Actor still passes its floor
71// check and only the binding differs), the home region, a same-reference
72// re-enrolment, and a census-key rotation. After each, the installation made
73// at B0 is refused at root's wake (state registration) and the B0 agent's
74// new install is refused at root's inventory write; the bound identity
75// comparison refuses the pinned actor.
76func TestClaimBindingFloorAndHomeChange(t *testing.T) {
77 for _, change := range []string{"floor", "home region", "re-enrolment", "census key"} {
78 t.Run(change, func(t *testing.T) {
79 ctx := t.Context()
80 w := newAssemblyWorld(t)
81 w.mustStart()
82 b0 := w.fresh()
83 agent := w.agent(b0)
84 rec, route := w.newHost("a")
85 if err := agent.install(rec, route); err != nil {
86 t.Fatal(err)
87 }
88 switch change {
89 case "floor":
90 digest := w.roster.republishFloor(hostfloor.Floor{Min: 1, MinRelease: "v0.1.1"})
91 w.roster.signedWrite(func(r *hostactors.Roster) { r.FloorSHA256 = digest })
92 case "home region":
93 w.roster.signedWrite(func(r *hostactors.Roster) { r.HomeRegion = "uk" })
94 case "re-enrolment":
95 w.roster.signedWrite(func(r *hostactors.Roster) {
96 b := r.Boxes["box-1"]
97 b.Enrollment = strings.Repeat("f", 32)
98 r.Boxes["box-1"] = b
99 })
100 case "census key":
101 rotated := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
102 w.roster.signedWrite(func(r *hostactors.Roster) {
103 b := r.Boxes["box-1"]
104 b.CensusPublicKey = rotated.Public().(ed25519.PublicKey)
105 r.Boxes["box-1"] = b
106 })
107 }
108 b1 := w.fresh()
109 if !hostactors.ValidBinding(b1.Binding) || b1.Binding == b0.Binding {
110 t.Fatalf("%s did not move the binding", change)
111 }
112 if change == "floor" {
113 only := b1
114 only.Binding = b0.Binding
115 if !hostbox.SameBinding(only, b0) {
116 t.Fatalf("a floor change moved more than the binding: %+v", b1)
117 }
118 }
119 if err := agent.registerState(rec.Host, route.DBPath, b1); !errors.Is(err, hostbox.ErrTypedInventory) {
120 t.Fatalf("the B0 installation at wake after the %s change: %v", change, err)
121 }
122 recB, routeB := w.newHost("b")
123 if err := agent.install(recB, routeB); err == nil {
124 t.Fatalf("root accepted a B0 install after the %s change", change)
125 }
126 if err := claimBoundIdentity(w.owners.base.Identity.Verify, b0)(ctx); err == nil {
127 t.Fatalf("the bound identity accepted the B0 actor after the %s change", change)
128 }
129 })
130 }
131}
132