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) } nextSTH.Version = ct.V1 nextSTH.LogID = id nextSTHBytes, err := json.Marshal(nextSTH) if err != nil { return fmt.Errorf("%s: %v", *log.Description, err) } // 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 }