mirror of
https://github.com/SSLMate/certspotter.git
synced 2025-07-01 10:35:33 +02:00

Specifically, certspotter no longer terminates unless it receives SIGTERM or SIGINT or there is a serious error. Although using cron made sense in the early days of Certificate Transparency, certspotter now needs to run continuously to reliably keep up with the high growth rate of contemporary CT logs, and to gracefully handle the many transient errors that can arise when monitoring CT. Closes: #63 Closes: #37 Closes: #32 (presumably by eliminating $DNS_NAMES and $IP_ADDRESSES) Closes: #21 (with $WATCH_ITEM) Closes: #25
156 lines
4.1 KiB
Go
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/ct"
|
|
"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 ct.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 := stateFile{
|
|
DownloadPosition: &tree,
|
|
VerifiedPosition: &tree,
|
|
VerifiedSTH: &sth,
|
|
LastSuccess: time.Now().UTC(),
|
|
}
|
|
if stateFile.store(filepath.Join(dir, "state.json")); 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"} {
|
|
if err := os.Mkdir(filepath.Join(stateDir, subdir), 0777); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|