package main import ( "bytes" "context" "crypto/sha256" "crypto/x509" "encoding/json" "errors" "fmt" logger "log" "net/http" "os" "time" "git.cs.kau.se/rasmoste/ct-sans/internal/merkle" ct "github.com/google/certificate-transparency-go" "github.com/google/certificate-transparency-go/client" "github.com/google/certificate-transparency-go/jsonclient" "gitlab.torproject.org/rgdd/ct/pkg/metadata" ) func snapshot(opts options) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := os.MkdirAll(opts.Directory, os.ModePerm); err != nil { return err } logger.Printf("INFO: updating metadata file\n") source := metadata.NewHTTPSource(metadata.HTTPSourceOptions{Name: "google"}) msg, sig, md, err := source.Load(ctx) if err != nil { return err } if err := os.WriteFile(fmt.Sprintf("%s/%s", opts.Directory, opts.metadataFile), msg, 0644); err != nil { return err } if err := os.WriteFile(fmt.Sprintf("%s/%s", opts.Directory, opts.metadataSignatureFile), sig, 0644); err != nil { return err } timestamp := []byte(fmt.Sprintf("%d", time.Now().Unix())) if err := os.WriteFile(fmt.Sprintf("%s/%s", opts.Directory, opts.metadataTimestampFile), timestamp, 0644); err != nil { return err } logger.Printf("INFO: updating signed tree heads\n") for _, log := range logs(md) { id, _ := log.Key.ID() der, _ := x509.MarshalPKIXPublicKey(log.Key) dir := fmt.Sprintf("%s/%x", opts.logDirectory, id) sthFile := fmt.Sprintf("%s/%s", dir, opts.sthFile) if err := os.MkdirAll(dir, os.ModePerm); err != nil { return fmt.Errorf("%s: %v", log.Description, err) } // Fetch next STH cli, err := client.New(string(log.URL), &http.Client{}, jsonclient.Options{PublicKeyDER: der, UserAgent: opts.HTTPAgent}) if err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } nextSTH, err := cli.GetSTH(ctx) if err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } nextSTHBytes, err := json.Marshal(nextSTH) if err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } // // It's a bit ugly that ct.SignedTreeHead contains fields that // are not populated. Doesn't cause any prolems here, however. // // Bootstrap log if we don't have any STH yet if _, err := os.Stat(sthFile); err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("%s: %v", *log.Description, err) } if err := os.WriteFile(sthFile, nextSTHBytes, 0644); err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } logger.Printf("INFO: bootstrapped %s at tree size %d\n", *log.Description, nextSTH.TreeSize) continue } // Otherwise: update an existing STH currSTHBytes, err := os.ReadFile(sthFile) if err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } var currSTH ct.SignedTreeHead if err := json.Unmarshal(currSTHBytes, &currSTH); err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } if nextSTH.TreeSize < currSTH.TreeSize { return fmt.Errorf("%s: next tree size is smaller: %s", *log.Description, nextSTHBytes) } if nextSTH.TreeSize == currSTH.TreeSize { if !bytes.Equal(nextSTH.SHA256RootHash[:], currSTH.SHA256RootHash[:]) { return fmt.Errorf("%s: split-view: %s", *log.Description, nextSTHBytes) } logger.Printf("INFO: %s is already up-to-date at size %d\n", *log.Description, nextSTH.TreeSize) continue } hashes, err := cli.GetSTHConsistency(ctx, currSTH.TreeSize, nextSTH.TreeSize) if err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } if err := merkle.VerifyConsistency(currSTH.TreeSize, nextSTH.TreeSize, [sha256.Size]byte(currSTH.SHA256RootHash), [sha256.Size]byte(nextSTH.SHA256RootHash), proof(hashes)); err != nil { return fmt.Errorf("%s: inconsistent tree: %v", *log.Description, err) } if err := os.WriteFile(sthFile, nextSTHBytes, 0644); err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } logger.Printf("INFO: updated %s to tree size %d\n", *log.Description, nextSTH.TreeSize) } return nil } // logs select logs that count towards CT-compliance checks. Logs that don't // have a description are skipped after printing a warning. func logs(md metadata.Metadata) (logs []metadata.Log) { for _, operators := range md.Operators { for _, log := range operators.Logs { if log.Description == nil { fmt.Fprintf(os.Stderr, "WARNING: skipping log without description") continue } if log.State == nil { continue // skip logs with unknown states } if log.State.Name == metadata.LogStatePending { continue // pending logs do not count towards CT-compliance } if log.State.Name == metadata.LogStateRetired { continue // retired logs are not necessarily reachable } if log.State.Name == metadata.LogStateRejected { continue // rejected logs do not count towards CT-compliance } logs = append(logs, log) } } return } // proof formats hashes so that they can be passed to the merkle package func proof(hashes [][]byte) (p [][sha256.Size]byte) { for _, hash := range hashes { var h [sha256.Size]byte copy(h[:], hash) p = append(p, h) } return }