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  }