diff options
Diffstat (limited to 'cmd_snapshot.go')
-rw-r--r-- | cmd_snapshot.go | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/cmd_snapshot.go b/cmd_snapshot.go new file mode 100644 index 0000000..5a9c50e --- /dev/null +++ b/cmd_snapshot.go @@ -0,0 +1,163 @@ +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 +} |