aboutsummaryrefslogtreecommitdiff
path: root/snapshot.go
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@rgdd.se>2023-03-17 10:08:42 +0100
committerRasmus Dahlberg <rasmus@rgdd.se>2023-03-17 10:08:42 +0100
commit4e217298a1cb52a20617a9f77c63fba6363b8633 (patch)
treeedd7225c9497c09c9e322fc0e4d83447e24c38fd /snapshot.go
parent16b0369780183b6f429b571ced21a8341da28971 (diff)
Add drafty snapshot command
Diffstat (limited to 'snapshot.go')
-rw-r--r--snapshot.go126
1 files changed, 126 insertions, 0 deletions
diff --git a/snapshot.go b/snapshot.go
new file mode 100644
index 0000000..fb17056
--- /dev/null
+++ b/snapshot.go
@@ -0,0 +1,126 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+
+ "git.cs.kau.se/rasmoste/ct-sans/internal/merkle"
+ "git.cs.kau.se/rasmoste/ct-sans/internal/utils"
+ 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
+ }
+
+ fmt.Fprintf(os.Stderr, "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
+ }
+
+ fmt.Fprintf(os.Stderr, "INFO: updating signed tree heads\n")
+ for _, log := range utils.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: "wip"})
+ 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)
+ }
+
+ fmt.Fprintf(os.Stderr, "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)
+ }
+
+ fmt.Fprintf(os.Stderr, "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),
+ utils.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)
+ }
+ fmt.Fprintf(os.Stderr, "INFO: updated %s to tree size %d\n", *log.Description, nextSTH.TreeSize)
+ }
+
+ return nil
+}