github.com/google/trillian-examples@v0.0.0-20240520080811-0d40d35cef0e/binary_transparency/firmware/cmd/ft_monitor/impl/ft_monitor.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 impl is the implementation of the Firmware Transparency monitor.
    16  // The monitor follows the growth of the Firmware Transparency log server,
    17  // inspects new firmware metadata as it appears, and prints out a short
    18  // summary.
    19  //
    20  // TODO(al): Extend monitor to verify claims.
    21  package impl
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"crypto/sha512"
    27  	"encoding/json"
    28  	"errors"
    29  	"fmt"
    30  	"net/url"
    31  	"os"
    32  	"regexp"
    33  	"time"
    34  
    35  	"github.com/golang/glog"
    36  	"github.com/google/trillian-examples/binary_transparency/firmware/api"
    37  	"github.com/google/trillian-examples/binary_transparency/firmware/internal/client"
    38  	"github.com/google/trillian-examples/binary_transparency/firmware/internal/crypto"
    39  	"golang.org/x/mod/sumdb/note"
    40  )
    41  
    42  // MatchFunc is the signature of a function which can be called by the monitor
    43  // to signal when it's found a keyword match in a logged entry.
    44  type MatchFunc func(index uint64, fw api.FirmwareMetadata)
    45  
    46  // MonitorOpts encapsulates options for running the monitor.
    47  type MonitorOpts struct {
    48  	LogURL         string
    49  	LogSigVerifier note.Verifier
    50  	PollInterval   time.Duration
    51  	Keyword        string
    52  	Matched        MatchFunc
    53  	Annotate       bool
    54  	StateFile      string
    55  }
    56  
    57  // Main runs the monitor until the context is canceled.
    58  func Main(ctx context.Context, opts MonitorOpts) error {
    59  	if len(opts.LogURL) == 0 {
    60  		return errors.New("log URL is required")
    61  	}
    62  	if len(opts.StateFile) == 0 {
    63  		return errors.New("state file is required")
    64  	}
    65  
    66  	ftURL, err := url.Parse(opts.LogURL)
    67  	if err != nil {
    68  		return fmt.Errorf("failed to parse FT log URL: %w", err)
    69  	}
    70  
    71  	// Parse the input keywords as regular expression
    72  	matcher := regexp.MustCompile(opts.Keyword)
    73  
    74  	c := client.ReadonlyClient{
    75  		LogURL:         ftURL,
    76  		LogSigVerifier: opts.LogSigVerifier,
    77  	}
    78  
    79  	// Initialize the checkpoint from persisted state.
    80  	var latestCP api.LogCheckpoint
    81  	if state, err := os.ReadFile(opts.StateFile); err != nil {
    82  		if !errors.Is(err, os.ErrNotExist) {
    83  			return fmt.Errorf("failed to read state: %w", err)
    84  		}
    85  		// This could fail here unless a force flag is provided, for better security.
    86  		glog.Warningf("State file %q did not exist; first log checkpoint will be trusted implicitly", opts.StateFile)
    87  	} else {
    88  		cp, err := api.ParseCheckpoint(state, opts.LogSigVerifier)
    89  		if err != nil {
    90  			return fmt.Errorf("failed to open state: %w", err)
    91  		}
    92  		latestCP = *cp
    93  	}
    94  	head := latestCP.Size
    95  	follow := client.NewLogFollower(c)
    96  
    97  	glog.Infof("Monitoring FT log (%q) starting from index %d", opts.LogURL, head)
    98  	cpc, cperrc := follow.Checkpoints(ctx, opts.PollInterval, latestCP)
    99  	ec, eerrc := follow.Entries(ctx, cpc, head)
   100  
   101  	for {
   102  		var entry client.LogEntry
   103  		select {
   104  		case err = <-cperrc:
   105  			return err
   106  		case err = <-eerrc:
   107  			return err
   108  		case <-ctx.Done():
   109  			return ctx.Err()
   110  		case entry = <-ec:
   111  		}
   112  
   113  		if err := processEntry(entry, c, opts, matcher); err != nil {
   114  			// TODO(mhutchinson): Consider a flag that causes processing errors to hard-fail.
   115  			glog.Warningf("Warning processing entry at index %d: %q", entry.Index, err)
   116  		}
   117  
   118  		if entry.Index == entry.Root.Size-1 {
   119  			// If we have processed all leaves in the current checkpoint, then persist this checkpoint
   120  			// so that we don't repeat work on startup.
   121  			if err := os.WriteFile(opts.StateFile, entry.Root.Envelope, 0o755); err != nil {
   122  				glog.Errorf("os.WriteFile(): %v", err)
   123  			}
   124  		}
   125  	}
   126  }
   127  
   128  func processEntry(entry client.LogEntry, c client.ReadonlyClient, opts MonitorOpts, matcher *regexp.Regexp) error {
   129  	stmt := entry.Value
   130  	if stmt.Type != api.FirmwareMetadataType {
   131  		// Only analyze firmware statements in the monitor.
   132  		return nil
   133  	}
   134  
   135  	// Parse the firmware metadata:
   136  	var meta api.FirmwareMetadata
   137  	if err := json.Unmarshal(stmt.Statement, &meta); err != nil {
   138  		return fmt.Errorf("unable to decode FW Metadata from Statement %q", err)
   139  	}
   140  
   141  	glog.Infof("Found firmware (@%d): %s", entry.Index, meta)
   142  
   143  	// Fetch the Image from FT Personality
   144  	image, err := c.GetFirmwareImage(meta.FirmwareImageSHA512)
   145  	if err != nil {
   146  		return fmt.Errorf("unable to GetFirmwareImage for Firmware with Hash 0x%x , reason %q", meta.FirmwareImageSHA512, err)
   147  	}
   148  	// Verify Image Hash from log Manifest matches the actual image hash
   149  	h := sha512.Sum512(image)
   150  	if !bytes.Equal(h[:], meta.FirmwareImageSHA512) {
   151  		return fmt.Errorf("downloaded image does not match SHA512 in metadata (%x != %x)", h[:], meta.FirmwareImageSHA512)
   152  	}
   153  	glog.V(1).Infof("Image Hash Verified for image at leaf index %d", entry.Index)
   154  
   155  	// Search for specific keywords inside firmware image
   156  	malwareDetected := matcher.Match(image)
   157  
   158  	if malwareDetected {
   159  		opts.Matched(entry.Index, meta)
   160  	}
   161  	if opts.Annotate {
   162  		ms := api.MalwareStatement{
   163  			FirmwareID: api.FirmwareID{
   164  				LogIndex:            entry.Index,
   165  				FirmwareImageSHA512: h[:],
   166  			},
   167  			Good: !malwareDetected,
   168  		}
   169  		glog.V(1).Infof("Annotating %s", ms)
   170  		js, err := createStatementJSON(ms)
   171  		if err != nil {
   172  			return fmt.Errorf("failed to create annotation: %q", err)
   173  		}
   174  		sc := client.SubmitClient{
   175  			ReadonlyClient: &c,
   176  		}
   177  		if err := sc.PublishAnnotationMalware(js); err != nil {
   178  			return fmt.Errorf("failed to publish annotation: %q", err)
   179  		}
   180  	}
   181  	return nil
   182  }
   183  
   184  func createStatementJSON(m api.MalwareStatement) ([]byte, error) {
   185  	js, err := json.Marshal(m)
   186  	if err != nil {
   187  		return nil, fmt.Errorf("failed to marshal metadata: %w", err)
   188  	}
   189  	sig, err := crypto.AnnotatorMalware.SignMessage(api.MalwareStatementType, js)
   190  	if err != nil {
   191  		return nil, fmt.Errorf("failed to generate signature: %w", err)
   192  	}
   193  
   194  	statement := api.SignedStatement{
   195  		Type:      api.MalwareStatementType,
   196  		Statement: js,
   197  		Signature: sig,
   198  	}
   199  
   200  	return json.Marshal(statement)
   201  }