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 }