certspotter/monitor/statedir.go
Andrew Ayer 958e7a9efb Avoid relying on STH timestamp during monitoring
Instead use the time at which the STH was observed (which for
FilesystemState is assumed to be the mtime of the STH file).  This is
easier to reason about: we don't have to worry about logs lying about
the time; we don't have to take into account the delay between STH fetch
and healthcheck; we won't raise spurious health checks about logs with
MMDs longer than the healthcheck interval.
2025-05-06 10:41:33 -04:00

156 lines
4.1 KiB
Go

// Copyright (C) 2023 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package monitor
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"software.sslmate.com/src/certspotter/cttypes"
"software.sslmate.com/src/certspotter/merkletree"
"strconv"
"strings"
"time"
)
func readVersion(stateDir string) (int, error) {
path := filepath.Join(stateDir, "version")
fileBytes, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
if fileExists(filepath.Join(stateDir, "evidence")) {
return 0, nil
} else {
return -1, nil
}
} else if err != nil {
return -1, err
}
version, err := strconv.Atoi(strings.TrimSpace(string(fileBytes)))
if err != nil {
return -1, fmt.Errorf("version file %q is malformed: %w", path, err)
}
return version, nil
}
func writeVersion(stateDir string) error {
return writeFile(filepath.Join(stateDir, "version"), []byte{'2', '\n'}, 0666)
}
func migrateLogStateDirV1(dir string) error {
var sth cttypes.SignedTreeHead
var tree merkletree.CollapsedTree
sthPath := filepath.Join(dir, "sth.json")
sthData, err := os.ReadFile(sthPath)
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return err
}
treePath := filepath.Join(dir, "tree.json")
treeData, err := os.ReadFile(treePath)
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return err
}
if err := json.Unmarshal(sthData, &sth); err != nil {
return fmt.Errorf("error unmarshaling %s: %w", sthPath, err)
}
if err := json.Unmarshal(treeData, &tree); err != nil {
return fmt.Errorf("error unmarshaling %s: %w", treePath, err)
}
stateFile := LogState{
DownloadPosition: &tree,
VerifiedPosition: &tree,
VerifiedSTH: &sth,
LastSuccess: time.Now(),
}
if err := writeJSONFile(filepath.Join(dir, "state.json"), stateFile, 0666); err != nil {
return err
}
if err := os.Remove(sthPath); err != nil {
return err
}
if err := os.Remove(treePath); err != nil {
return err
}
return nil
}
func migrateStateDirV1(stateDir string) error {
if lockfile := filepath.Join(stateDir, "lock"); fileExists(lockfile) {
return fmt.Errorf("directory is locked by another instance of certspotter; remove %s if this is not the case", lockfile)
}
if logDirs, err := os.ReadDir(filepath.Join(stateDir, "logs")); err == nil {
for _, logDir := range logDirs {
if strings.HasPrefix(logDir.Name(), ".") || !logDir.IsDir() {
continue
}
if err := migrateLogStateDirV1(filepath.Join(stateDir, "logs", logDir.Name())); err != nil {
return fmt.Errorf("error migrating log state: %w", err)
}
}
} else if !errors.Is(err, fs.ErrNotExist) {
return err
}
if err := writeVersion(stateDir); err != nil {
return err
}
if err := os.Remove(filepath.Join(stateDir, "once")); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
return nil
}
func prepareStateDir(stateDir string) error {
if err := os.Mkdir(stateDir, 0777); err != nil && !errors.Is(err, fs.ErrExist) {
return err
}
if version, err := readVersion(stateDir); err != nil {
return err
} else if version == -1 {
if err := writeVersion(stateDir); err != nil {
return err
}
} else if version == 0 {
return fmt.Errorf("%s was created by a very old version of certspotter; run any version of certspotter after 0.2 and before 0.15.0 to upgrade this directory, or remove it to start from scratch", stateDir)
} else if version == 1 {
if err := migrateStateDirV1(stateDir); err != nil {
return err
}
} else if version > 2 {
return fmt.Errorf("%s was created by a newer version of certspotter; upgrade to the latest version of certspotter or remove this directory to start from scratch", stateDir)
}
for _, subdir := range []string{"certs", "logs", "healthchecks"} {
if err := os.Mkdir(filepath.Join(stateDir, subdir), 0777); err != nil && !errors.Is(err, fs.ErrExist) {
return err
}
}
return nil
}