| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/base64" |
| 6 | "encoding/json" |
| 7 | "errors" |
| 8 | "math" |
| 9 | "reflect" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/maintenanceguard" |
| 15 | ) |
| 16 | |
| 17 | func workerTestRef(b byte) maintenanceguard.Reference { |
| 18 | return maintenanceguard.Reference{ID: base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{b}, 32)), Digest: strings.Repeat("ab", 32)} |
| 19 | } |
| 20 | func workerTestHead() WorkerHead { |
| 21 | return WorkerHead{Schema: 1, Kind: "maintenance-worker", Deployment: "https://console.example.com", Account: "bds", Box: "annie-welding", PhysicalIdentity: strings.Repeat("cd", 32), Generation: 1, Revision: 1, TokenHash: strings.Repeat("ef", 32), Status: "active", Placement: "eu", Candidate: workerTestRef(1), Open: []maintenanceguard.Reference{workerTestRef(2)}} |
| 22 | } |
| 23 | func workerTestAssignment() WorkerAssignmentState { |
| 24 | return WorkerAssignmentState{Schema: 1, Kind: "maintenance-assignment", Identity: WorkerAssignmentIdentity{Assignment: workerTestRef(2), Deployment: "https://console.example.com", Box: "annie-welding", Generation: 1, Account: "bds", App: "home", Host: "home.example.com", Operation: workerTestRef(3).ID, Request: workerTestRef(4).ID, Attempt: workerTestRef(5).ID}, Revision: 1, Expires: 2000000000} |
| 25 | } |
| 26 | func TestWorkerControlRoundTripAndTypeSeparation(t *testing.T) { |
| 27 | priv, pub := testKeys(t) |
| 28 | head := workerTestHead() |
| 29 | head.Generation = math.MaxInt64 |
| 30 | head.Revision = math.MaxInt64 |
| 31 | wire, err := SignWorkerHead(RootSigner(priv), head) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | got, err := VerifyWorkerHead(pub, wire) |
| 36 | if err != nil || !reflect.DeepEqual(got, head) { |
| 37 | t.Fatalf("head round trip: %v", err) |
| 38 | } |
| 39 | if _, err = VerifyWorkerAssignment(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 40 | t.Fatal("head accepted as assignment") |
| 41 | } |
| 42 | state := workerTestAssignment() |
| 43 | refs := []*maintenanceguard.Reference{&head.Candidate, &head.Open[0]} |
| 44 | state.Preflight = refs[0] |
| 45 | state.Execution = refs[1] |
| 46 | state.Disabled = true |
| 47 | wire, err = SignWorkerAssignment(RootSigner(priv), state) |
| 48 | if err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | gotState, err := VerifyWorkerAssignment(pub, wire) |
| 52 | if err != nil || !reflect.DeepEqual(gotState, state) { |
| 53 | t.Fatalf("assignment round trip: %v", err) |
| 54 | } |
| 55 | if _, err = VerifyWorkerHead(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 56 | t.Fatal("assignment accepted as head") |
| 57 | } |
| 58 | _, otherPub := testKeys(t) |
| 59 | if _, err = VerifyWorkerAssignment(otherPub, wire); !errors.Is(err, ErrWorkerControl) { |
| 60 | t.Fatal("wrong root accepted") |
| 61 | } |
| 62 | if _, err = VerifyWorkerAssignment(nil, wire); !errors.Is(err, ErrWorkerControl) { |
| 63 | t.Fatal("missing root accepted") |
| 64 | } |
| 65 | wire[len(wire)/2] ^= 1 |
| 66 | if _, err = VerifyWorkerAssignment(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 67 | t.Fatal("tampered wire accepted") |
| 68 | } |
| 69 | } |
| 70 | func TestWorkerControlRefusesNonRootSigner(t *testing.T) { |
| 71 | _, _, scoped := scopedFixture(t, []string{"canary"}, time.Hour) |
| 72 | for _, sg := range []Signer{{}, scoped} { |
| 73 | if _, err := SignWorkerHead(sg, workerTestHead()); !errors.Is(err, ErrWorkerControl) { |
| 74 | t.Fatal("head signed without root") |
| 75 | } |
| 76 | if _, err := SignWorkerAssignment(sg, workerTestAssignment()); !errors.Is(err, ErrWorkerControl) { |
| 77 | t.Fatal("assignment signed without root") |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | func TestWorkerControlRefusesSignedMalformedPayload(t *testing.T) { |
| 82 | priv, pub := testKeys(t) |
| 83 | headRaw, _ := json.Marshal(workerTestHead()) |
| 84 | stateRaw, _ := json.Marshal(workerTestAssignment()) |
| 85 | for name, mutate := range map[string]func(string) string{ |
| 86 | "duplicate": func(s string) string { return strings.Replace(s, `"schema":1`, `"schema":1,"schema":1`, 1) }, |
| 87 | "alias": func(s string) string { return strings.Replace(s, `"schema":1`, `"Schema":1`, 1) }, |
| 88 | "unknown": func(s string) string { return strings.Replace(s, `"schema":1`, `"schema":1,"extension":true`, 1) }, |
| 89 | "float": func(s string) string { return strings.Replace(s, `"schema":1`, `"schema":1.0`, 1) }, |
| 90 | "overflow": func(s string) string { return strings.Replace(s, `"revision":1`, `"revision":9223372036854775808`, 1) }, |
| 91 | "trailing": func(s string) string { return s + ` {}` }, |
| 92 | "escaped-alias": func(s string) string { return strings.Replace(s, `"schema":1`, `"\u0073chema":1`, 1) }, |
| 93 | "invalid-utf8": func(s string) string { return strings.Replace(s, "console", string([]byte{255}), 1) }, |
| 94 | "surrogate": func(s string) string { return strings.Replace(s, "console", `\ud800`, 1) }, |
| 95 | } { |
| 96 | t.Run(name, func(t *testing.T) { |
| 97 | for _, raw := range [][]byte{headRaw, stateRaw} { |
| 98 | wire, err := signRootPayload(RootSigner(priv), []byte(mutate(string(raw)))) |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | if _, err = VerifyWorkerHead(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 103 | t.Fatal("malformed head accepted") |
| 104 | } |
| 105 | if _, err = VerifyWorkerAssignment(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 106 | t.Fatal("malformed assignment accepted") |
| 107 | } |
| 108 | } |
| 109 | }) |
| 110 | } |
| 111 | for _, raw := range []string{ |
| 112 | strings.Replace(string(stateRaw), `"disabled":false,`, "", 1), |
| 113 | strings.TrimSuffix(string(stateRaw), "}") + `,"execution":null}`, |
| 114 | strings.Replace(string(headRaw), `"open":[`+mustWorkerJSON(t, workerTestRef(2))+`]`, `"open":null`, 1), |
| 115 | } { |
| 116 | wire, err := signRootPayload(RootSigner(priv), []byte(raw)) |
| 117 | if err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if _, err = VerifyWorkerHead(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 121 | t.Fatal("missing/null field accepted as head") |
| 122 | } |
| 123 | if _, err = VerifyWorkerAssignment(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 124 | t.Fatal("missing/null field accepted as assignment") |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | func mustWorkerJSON(t *testing.T, v any) string { |
| 129 | t.Helper() |
| 130 | raw, err := json.Marshal(v) |
| 131 | if err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | return string(raw) |
| 135 | } |
| 136 | func TestWorkerControlRefusesInvalidHeadScope(t *testing.T) { |
| 137 | priv, pub := testKeys(t) |
| 138 | for name, change := range map[string]func(*WorkerHead){ |
| 139 | "http": func(h *WorkerHead) { h.Deployment = "http://console.example.com" }, |
| 140 | "path": func(h *WorkerHead) { h.Deployment += "/" }, |
| 141 | "user": func(h *WorkerHead) { h.Deployment = "https://owner@console.example.com" }, |
| 142 | "box-path": func(h *WorkerHead) { h.Box = "../other" }, |
| 143 | "zero-generation": func(h *WorkerHead) { h.Generation = 0 }, |
| 144 | "zero-revision": func(h *WorkerHead) { h.Revision = 0 }, |
| 145 | "token": func(h *WorkerHead) { h.TokenHash = "bad" }, |
| 146 | "physical-identity": func(h *WorkerHead) { h.PhysicalIdentity = "bad" }, |
| 147 | "unknown-state": func(h *WorkerHead) { h.Status = "enabled" }, |
| 148 | "duplicate-assignment": func(h *WorkerHead) { h.Open = append(h.Open, h.Open[0]) }, |
| 149 | "overflow-open": func(h *WorkerHead) { |
| 150 | for i := byte(3); i < 11; i++ { |
| 151 | h.Open = append(h.Open, workerTestRef(i)) |
| 152 | } |
| 153 | }, |
| 154 | "detached-open": func(h *WorkerHead) { h.Status = "detached" }, |
| 155 | "null-open": func(h *WorkerHead) { h.Open = nil }, |
| 156 | } { |
| 157 | t.Run(name, func(t *testing.T) { |
| 158 | h := workerTestHead() |
| 159 | change(&h) |
| 160 | if _, err := SignWorkerHead(RootSigner(priv), h); !errors.Is(err, ErrWorkerControl) { |
| 161 | t.Fatal("invalid head signed") |
| 162 | } |
| 163 | wire, err := signRootPayload(RootSigner(priv), []byte(mustWorkerJSON(t, h))) |
| 164 | if err != nil { |
| 165 | t.Fatal(err) |
| 166 | } |
| 167 | if _, err = VerifyWorkerHead(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 168 | t.Fatal("invalid signed head accepted") |
| 169 | } |
| 170 | }) |
| 171 | } |
| 172 | } |
| 173 | func TestWorkerControlRejectsWrapperAmbiguityAndOversize(t *testing.T) { |
| 174 | priv, pub := testKeys(t) |
| 175 | wire, err := SignWorkerHead(RootSigner(priv), workerTestHead()) |
| 176 | if err != nil { |
| 177 | t.Fatal(err) |
| 178 | } |
| 179 | var compact bytes.Buffer |
| 180 | if err = json.Compact(&compact, wire); err != nil { |
| 181 | t.Fatal(err) |
| 182 | } |
| 183 | s := compact.String() |
| 184 | for _, raw := range []string{ |
| 185 | strings.Replace(s, `"payload":`, `"Payload":`, 1), |
| 186 | strings.TrimSuffix(s, "}") + `,"grant":null}`, |
| 187 | strings.TrimSuffix(s, "}") + `,"extra":0}`, |
| 188 | s + strings.Repeat(" ", MaxWorkerControlBytes), |
| 189 | `null`, |
| 190 | } { |
| 191 | if _, err := VerifyWorkerHead(pub, []byte(raw)); !errors.Is(err, ErrWorkerControl) { |
| 192 | t.Fatal("malformed wrapper accepted") |
| 193 | } |
| 194 | } |
| 195 | big, err := signRootPayload(RootSigner(priv), bytes.Repeat([]byte(" "), MaxWorkerControlBytes)) |
| 196 | if err != nil { |
| 197 | t.Fatal(err) |
| 198 | } |
| 199 | if _, err = VerifyWorkerHead(pub, big); !errors.Is(err, ErrWorkerControl) { |
| 200 | t.Fatal("oversize payload accepted") |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | func TestWorkerControlRefusesInvalidAssignmentScope(t *testing.T) { |
| 205 | priv, pub := testKeys(t) |
| 206 | for name, change := range map[string]func(*WorkerAssignmentState){ |
| 207 | "kind": func(s *WorkerAssignmentState) { s.Kind = "maintenance-worker" }, |
| 208 | "schema": func(s *WorkerAssignmentState) { s.Schema = 2 }, |
| 209 | "assignment-digest": func(s *WorkerAssignmentState) { s.Identity.Assignment.Digest = "bad" }, |
| 210 | "deployment": func(s *WorkerAssignmentState) { s.Identity.Deployment = "https://console.example.com/#fragment" }, |
| 211 | "box": func(s *WorkerAssignmentState) { s.Identity.Box = "../other" }, |
| 212 | "generation": func(s *WorkerAssignmentState) { s.Identity.Generation = 0 }, |
| 213 | "account": func(s *WorkerAssignmentState) { s.Identity.Account = "../bds" }, |
| 214 | "app": func(s *WorkerAssignmentState) { s.Identity.App = "../home" }, |
| 215 | "host": func(s *WorkerAssignmentState) { s.Identity.Host = "https://home.example.com" }, |
| 216 | "operation": func(s *WorkerAssignmentState) { s.Identity.Operation = "bad" }, |
| 217 | "request": func(s *WorkerAssignmentState) { s.Identity.Request = "bad" }, |
| 218 | "attempt": func(s *WorkerAssignmentState) { s.Identity.Attempt = "bad" }, |
| 219 | "revision": func(s *WorkerAssignmentState) { s.Revision = 0 }, |
| 220 | "expiry": func(s *WorkerAssignmentState) { s.Expires = 0 }, |
| 221 | "preflight": func(s *WorkerAssignmentState) { s.Preflight = &maintenanceguard.Reference{} }, |
| 222 | "compatibility": func(s *WorkerAssignmentState) { s.Compatibility = &maintenanceguard.Reference{} }, |
| 223 | "execution": func(s *WorkerAssignmentState) { s.Execution = &maintenanceguard.Reference{} }, |
| 224 | "resume": func(s *WorkerAssignmentState) { s.Resume = &maintenanceguard.Reference{} }, |
| 225 | } { |
| 226 | t.Run(name, func(t *testing.T) { |
| 227 | state := workerTestAssignment() |
| 228 | change(&state) |
| 229 | if _, err := SignWorkerAssignment(RootSigner(priv), state); !errors.Is(err, ErrWorkerControl) { |
| 230 | t.Fatal("invalid assignment signed") |
| 231 | } |
| 232 | wire, err := signRootPayload(RootSigner(priv), []byte(mustWorkerJSON(t, state))) |
| 233 | if err != nil { |
| 234 | t.Fatal(err) |
| 235 | } |
| 236 | if _, err = VerifyWorkerAssignment(pub, wire); !errors.Is(err, ErrWorkerControl) { |
| 237 | t.Fatal("invalid signed assignment accepted") |
| 238 | } |
| 239 | }) |
| 240 | } |
| 241 | } |
| 242 | |