github.com/google/trillian-examples@v0.0.0-20240520080811-0d40d35cef0e/binary_transparency/firmware/integration/ft_integration_test.go (about) 1 // Copyright 2020 Google LLC. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package integration is an integration test for the FT demo. 16 package integration_test 17 18 import ( 19 "context" 20 "flag" 21 "fmt" 22 "net/http" 23 "os" 24 "path/filepath" 25 "testing" 26 "time" 27 28 "github.com/google/trillian-examples/binary_transparency/firmware/api" 29 i_emu "github.com/google/trillian-examples/binary_transparency/firmware/cmd/emulator/dummy/impl" 30 i_flash "github.com/google/trillian-examples/binary_transparency/firmware/cmd/flash_tool/impl" 31 i_monitor "github.com/google/trillian-examples/binary_transparency/firmware/cmd/ft_monitor/impl" 32 i_personality "github.com/google/trillian-examples/binary_transparency/firmware/cmd/ft_personality/impl" 33 i_witness "github.com/google/trillian-examples/binary_transparency/firmware/cmd/ft_witness/impl" 34 i_modify "github.com/google/trillian-examples/binary_transparency/firmware/cmd/hacker/modify_bundle/impl" 35 i_publish "github.com/google/trillian-examples/binary_transparency/firmware/cmd/publisher/impl" 36 "github.com/google/trillian-examples/binary_transparency/firmware/internal/crypto" 37 "golang.org/x/mod/sumdb/note" 38 ) 39 40 const ( 41 PublishTimestamp1 = "2020-11-24 10:00:00+00:00" 42 PublishTimestamp2 = "2020-11-24 10:15:00+00:00" 43 PublishTimestamp3 = "2021-02-16 10:00:00+00:00" 44 PublishMalwareTimestamp = "2020-11-24 10:30:00+00:00" 45 46 GoodFirmware = "../testdata/firmware/dummy_device/example.wasm" 47 HackedFirmware = "../testdata/firmware/dummy_device/hacked.wasm" 48 ) 49 50 var ( 51 trillianAddr = flag.String("trillian", "", "Host:port of Trillian Log RPC server") 52 ) 53 54 func mustGetLogSigVerifier(t *testing.T) note.Verifier { 55 t.Helper() 56 v, err := note.NewVerifier(crypto.TestFTPersonalityPub) 57 if err != nil { 58 t.Fatalf("Failed to create CP verifier: %q", err) 59 } 60 return v 61 } 62 63 func TestFTIntegration(t *testing.T) { 64 if len(*trillianAddr) == 0 { 65 t.Skip("--trillian flag unset, skipping test") 66 } 67 68 tmpDir := t.TempDir() 69 updatePath := filepath.Join(tmpDir, "update.ota") 70 devStoragePath := filepath.Join(tmpDir, "dummy_device") 71 setupDeviceStorage(t, devStoragePath) 72 73 ctx, cancel := testContext(t) 74 defer cancel() 75 76 // TODO(al): make this dynamic 77 pListen := "localhost:43563" 78 pAddr := fmt.Sprintf("http://%s", pListen) 79 80 pErrChan := make(chan error) 81 logSigVerifier := mustGetLogSigVerifier(t) 82 83 go func() { 84 if err := runPersonality(ctx, t, pListen); err != nil { 85 pErrChan <- err 86 } 87 close(pErrChan) 88 }() 89 90 // TODO(al): make this wait until the personality is listening 91 <-time.After(5 * time.Second) 92 93 for _, step := range []struct { 94 desc string 95 step func() error 96 wantErrMsg string 97 }{ 98 { 99 desc: "Log initial firmware", 100 step: func() error { 101 return i_publish.Main(ctx, i_publish.PublishOpts{ 102 LogURL: pAddr, 103 LogSigVerifier: logSigVerifier, 104 DeviceID: "dummy", 105 BinaryPath: GoodFirmware, 106 Timestamp: PublishTimestamp1, 107 Revision: 1, 108 OutputPath: updatePath, 109 }) 110 }, 111 }, { 112 desc: "Force flashing device (init)", 113 step: func() error { 114 return i_flash.Main(ctx, i_flash.FlashOpts{ 115 LogURL: pAddr, 116 LogSigVerifier: logSigVerifier, 117 WitnessURL: "", 118 DeviceID: "dummy", 119 UpdateFile: updatePath, 120 DeviceStorage: devStoragePath, 121 Force: true, 122 }) 123 }, 124 }, { 125 desc: "Boot device with initial firmware", 126 step: func() error { 127 return i_emu.Main(i_emu.EmulatorOpts{ 128 DeviceStorage: devStoragePath, 129 }) 130 }, 131 }, { 132 desc: "Log updated firmware", 133 step: func() error { 134 return i_publish.Main(ctx, i_publish.PublishOpts{ 135 LogURL: pAddr, 136 LogSigVerifier: logSigVerifier, 137 DeviceID: "dummy", 138 BinaryPath: GoodFirmware, 139 Timestamp: PublishTimestamp2, 140 Revision: 2, 141 OutputPath: updatePath, 142 }) 143 }, 144 }, { 145 desc: "Flashing device (update)", 146 step: func() error { 147 return i_flash.Main(ctx, i_flash.FlashOpts{ 148 LogURL: pAddr, 149 LogSigVerifier: logSigVerifier, 150 WitnessURL: "", 151 DeviceID: "dummy", 152 UpdateFile: updatePath, 153 DeviceStorage: devStoragePath, 154 }) 155 }, 156 }, { 157 desc: "Booting updated device", 158 step: func() error { 159 return i_emu.Main(i_emu.EmulatorOpts{ 160 DeviceStorage: devStoragePath, 161 }) 162 }, 163 }, { 164 desc: "Replace FW, boot device", 165 wantErrMsg: "firmware measurement does not match", 166 step: func() error { 167 if err := copyFile(HackedFirmware, filepath.Join(devStoragePath, "firmware.bin")); err != nil { 168 t.Fatalf("Failed to overwrite stored firmware: %q", err) 169 } 170 // Booting this should return an error: 171 return i_emu.Main(i_emu.EmulatorOpts{ 172 DeviceStorage: devStoragePath, 173 }) 174 }, 175 }, { 176 desc: "Replace FW, update hash (but not sign), and boot", 177 wantErrMsg: "failed to verify signature", 178 step: func() error { 179 if err := copyFile(HackedFirmware, filepath.Join(devStoragePath, "firmware.bin")); err != nil { 180 t.Fatalf("Failed to overwrite stored firmware: %q", err) 181 } 182 183 if err := i_modify.Main(i_modify.ModifyBundleOpts{ 184 BinaryPath: HackedFirmware, 185 DeviceID: "dummy", 186 Input: filepath.Join(devStoragePath, "bundle.json"), 187 Output: filepath.Join(devStoragePath, "bundle.json"), 188 }); err != nil { 189 t.Fatalf("Failed to modify bundle: %q", err) 190 } 191 192 // Booting this should return an error: 193 return i_emu.Main(i_emu.EmulatorOpts{ 194 DeviceStorage: devStoragePath, 195 }) 196 }, 197 }, { 198 desc: "Replace FW, update hash, sign manifest, and boot", 199 wantErrMsg: "invalid inclusion proof in bundle", 200 step: func() error { 201 if err := copyFile(HackedFirmware, filepath.Join(devStoragePath, "firmware.bin")); err != nil { 202 t.Fatalf("Failed to overwrite stored firmware: %q", err) 203 } 204 205 if err := i_modify.Main(i_modify.ModifyBundleOpts{ 206 BinaryPath: HackedFirmware, 207 DeviceID: "dummy", 208 Input: filepath.Join(devStoragePath, "bundle.json"), 209 Output: filepath.Join(devStoragePath, "bundle.json"), 210 Sign: true, 211 }); err != nil { 212 t.Fatalf("Failed to modify bundle: %q", err) 213 } 214 215 // Booting this should return an error: 216 return i_emu.Main(i_emu.EmulatorOpts{ 217 DeviceStorage: devStoragePath, 218 }) 219 }, 220 }, { 221 desc: "Log malware, device boots, but monitor sees all!", 222 step: func() error { 223 224 // Start up the monitor: 225 mErrChan := make(chan error, 1) 226 matchedChan := make(chan bool, 1) 227 mCtx, mCancel := context.WithCancel(context.Background()) 228 defer mCancel() 229 go func() { 230 if err := runMonitor(mCtx, t, pAddr, "H4x0r3d", logSigVerifier, func(idx uint64, fw api.FirmwareMetadata) { 231 t.Logf("Found malware firmware @%d", idx) 232 matchedChan <- true 233 }); err != nil && err != context.Canceled { 234 mErrChan <- err 235 } 236 close(mErrChan) 237 }() 238 239 // Log malware fw: 240 if err := i_publish.Main(ctx, i_publish.PublishOpts{ 241 LogURL: pAddr, 242 LogSigVerifier: logSigVerifier, 243 DeviceID: "dummy", 244 BinaryPath: HackedFirmware, 245 Timestamp: PublishMalwareTimestamp, 246 Revision: 1, 247 OutputPath: updatePath, 248 }); err != nil { 249 t.Fatalf("Failed to log malware: %q", err) 250 } 251 252 <-time.After(5 * time.Second) 253 // Now flash the bundle normally, it will install because it's been logged 254 // and so is now discoverable. 255 if err := i_flash.Main(ctx, i_flash.FlashOpts{ 256 LogURL: pAddr, 257 LogSigVerifier: logSigVerifier, 258 WitnessURL: "", 259 DeviceID: "dummy", 260 UpdateFile: updatePath, 261 DeviceStorage: devStoragePath, 262 }); err != nil { 263 t.Fatalf("Failed to flash malware update onto device: %q", err) 264 } 265 266 // Booting should also succeed: 267 if err := i_emu.Main(i_emu.EmulatorOpts{ 268 DeviceStorage: devStoragePath, 269 }); err != nil { 270 t.Fatalf("Failed to boot device with logged malware: %q", err) 271 } 272 273 // Wait and see if the monitor spots the malware 274 select { 275 case <-time.After(30 * time.Second): 276 t.Fatal("Monitor didn't spot logged malware") 277 case err := <-mErrChan: 278 t.Fatalf("Monitor errored: %q", err) 279 case <-matchedChan: 280 // We found it 281 } 282 283 return nil 284 }, 285 }, { 286 desc: "Firmware update with witness verification", 287 step: func() error { 288 // Start up the witness: 289 wHost := "localhost:43565" 290 wAddr := fmt.Sprintf("http://%s", wHost) 291 wCtx, wCancel := context.WithCancel(context.Background()) 292 defer wCancel() 293 go func() { 294 if err := runWitness(wCtx, t, pAddr, wHost, logSigVerifier); err != nil { 295 t.Errorf("Witness error: %q", err) 296 } 297 }() 298 299 // Wait for few seconds before starting the test 300 <-time.After(2 * time.Second) 301 if err := i_publish.Main(ctx, i_publish.PublishOpts{ 302 LogURL: pAddr, 303 LogSigVerifier: logSigVerifier, 304 DeviceID: "dummy", 305 BinaryPath: GoodFirmware, 306 Timestamp: PublishTimestamp3, 307 Revision: 3, 308 OutputPath: updatePath, 309 }); err != nil { 310 t.Fatalf("Failed to publish new bundle: %q", err) 311 } 312 313 // Wait witness to view the device checkpoint 314 <-time.After(5 * time.Second) 315 316 if err := i_flash.Main(ctx, i_flash.FlashOpts{ 317 LogURL: pAddr, 318 LogSigVerifier: logSigVerifier, 319 WitnessURL: wAddr, 320 DeviceID: "dummy", 321 UpdateFile: updatePath, 322 DeviceStorage: devStoragePath, 323 }); err != nil { 324 t.Fatalf("witness verification failed: %q", err) 325 } 326 327 return nil 328 }, 329 }, 330 } { 331 t.Run(step.desc, func(t *testing.T) { 332 wantErr := len(step.wantErrMsg) > 0 333 err := step.step() 334 if wantErr && err == nil { 335 t.Fatal("Want error, got no error") 336 } else if !wantErr && err != nil { 337 t.Fatalf("Want no error, got %q", err) 338 } 339 if err != nil { 340 t.Logf("Got expected error: %q", err) 341 } 342 // TODO(al): output matching 343 }) 344 } 345 } 346 347 func testContext(t *testing.T) (context.Context, func()) { 348 ctx := context.Background() 349 c := func() {} 350 if deadline, ok := t.Deadline(); ok { 351 ctx, c = context.WithDeadline(context.Background(), deadline) 352 } 353 return ctx, c 354 } 355 356 func setupDeviceStorage(t *testing.T, devStoragePath string) { 357 t.Helper() 358 if err := os.MkdirAll(devStoragePath, 0755); err != nil { 359 t.Fatalf("Failed to create device storage dir %q: %q", devStoragePath, err) 360 } 361 } 362 363 func runPersonality(ctx context.Context, t *testing.T, serverAddr string) error { 364 t.Helper() 365 r := t.TempDir() 366 367 signer, err := note.NewSigner(crypto.TestFTPersonalityPriv) 368 if err != nil { 369 return fmt.Errorf("failed to create CP signer: %w", err) 370 } 371 372 if err := i_personality.Main(ctx, i_personality.PersonalityOpts{ 373 ListenAddr: serverAddr, 374 CASFile: filepath.Join(r, "ft-cas.db"), 375 TrillianAddr: *trillianAddr, 376 ConnectTimeout: 10 * time.Second, 377 STHRefresh: time.Second, 378 Signer: signer, 379 }); err != http.ErrServerClosed { 380 return err 381 } 382 return nil 383 } 384 385 func runWitness(ctx context.Context, t *testing.T, persAddr, serverAddr string, logSigVerifier note.Verifier) error { 386 t.Helper() 387 r := t.TempDir() 388 389 err := i_witness.Main(ctx, i_witness.WitnessOpts{ 390 ListenAddr: serverAddr, 391 WSFile: filepath.Join(r, "ft-witness.db"), 392 FtLogURL: persAddr, 393 FtLogSigVerifier: logSigVerifier, 394 PollInterval: 5 * time.Second, 395 }) 396 if err != http.ErrServerClosed { 397 return err 398 } 399 return nil 400 } 401 402 func runMonitor(ctx context.Context, t *testing.T, serverAddr string, pattern string, logSigVerifier note.Verifier, matched i_monitor.MatchFunc) error { 403 t.Helper() 404 405 r := t.TempDir() 406 407 err := i_monitor.Main(ctx, i_monitor.MonitorOpts{ 408 LogURL: serverAddr, 409 LogSigVerifier: logSigVerifier, 410 PollInterval: 1 * time.Second, 411 Keyword: "H4x0r3d", 412 Matched: matched, 413 StateFile: filepath.Join(r, "ft-monitor.state"), 414 }) 415 if err != http.ErrServerClosed { 416 return err 417 } 418 return nil 419 } 420 421 func copyFile(from, to string) error { 422 i, err := os.ReadFile(from) 423 if err != nil { 424 return err 425 } 426 427 return os.WriteFile(to, i, 0644) 428 }