aboutsummaryrefslogtreecommitdiff
path: root/cmd_snapshot.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd_snapshot.go')
-rw-r--r--cmd_snapshot.go163
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
+}