certspotter/cmd/state.go

221 lines
6.5 KiB
Go

// Copyright (C) 2017 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 cmd
import (
"bytes"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"software.sslmate.com/src/certspotter/ct"
"software.sslmate.com/src/certspotter/loglist"
)
type State struct {
path string
}
func legacySTHFilename(logInfo *loglist.Log) string {
return strings.Replace(strings.Replace(logInfo.URL, "://", "_", 1), "/", "_", -1)
}
func readVersionFile(statePath string) (int, error) {
versionFilePath := filepath.Join(statePath, "version")
versionBytes, err := ioutil.ReadFile(versionFilePath)
if err == nil {
version, err := strconv.Atoi(string(bytes.TrimSpace(versionBytes)))
if err != nil {
return -1, fmt.Errorf("%s: contains invalid integer: %s", versionFilePath, err)
}
if version < 0 {
return -1, fmt.Errorf("%s: contains negative integer", versionFilePath)
}
return version, nil
} else if os.IsNotExist(err) {
if fileExists(filepath.Join(statePath, "sths")) {
// Original version of certspotter had no version file.
// Infer version 0 if "sths" directory is present.
return 0, nil
}
return -1, nil
} else {
return -1, fmt.Errorf("%s: %s", versionFilePath, err)
}
}
func writeVersionFile(statePath string) error {
version := 1
versionString := fmt.Sprintf("%d\n", version)
versionFilePath := filepath.Join(statePath, "version")
if err := ioutil.WriteFile(versionFilePath, []byte(versionString), 0666); err != nil {
return fmt.Errorf("%s: %s\n", versionFilePath, err)
}
return nil
}
func makeStateDir(statePath string) error {
if err := os.Mkdir(statePath, 0777); err != nil && !os.IsExist(err) {
return fmt.Errorf("%s: %s", statePath, err)
}
for _, subdir := range []string{"certs", "logs"} {
path := filepath.Join(statePath, subdir)
if err := os.Mkdir(path, 0777); err != nil && !os.IsExist(err) {
return fmt.Errorf("%s: %s", path, err)
}
}
return nil
}
func OpenState(statePath string) (*State, error) {
version, err := readVersionFile(statePath)
if err != nil {
return nil, fmt.Errorf("Error reading version file: %s", err)
}
if version < 1 {
if err := makeStateDir(statePath); err != nil {
return nil, fmt.Errorf("Error creating state directory: %s", err)
}
if version == 0 {
log.Printf("Migrating state directory (%s) to new layout...", statePath)
if err := os.Rename(filepath.Join(statePath, "sths"), filepath.Join(statePath, "legacy_sths")); err != nil {
return nil, fmt.Errorf("Error migrating STHs directory: %s", err)
}
for _, subdir := range []string{"evidence", "legacy_sths"} {
os.Remove(filepath.Join(statePath, subdir))
}
if err := ioutil.WriteFile(filepath.Join(statePath, "once"), []byte{}, 0666); err != nil {
return nil, fmt.Errorf("Error creating once file: %s", err)
}
}
if err := writeVersionFile(statePath); err != nil {
return nil, fmt.Errorf("Error writing version file: %s", err)
}
} else if version > 1 {
return nil, fmt.Errorf("%s was created by a newer version of Cert Spotter; please remove this directory or upgrade Cert Spotter", statePath)
}
return &State{path: statePath}, nil
}
func (state *State) IsFirstRun() bool {
return !fileExists(filepath.Join(state.path, "once"))
}
func (state *State) WriteOnceFile() error {
if err := ioutil.WriteFile(filepath.Join(state.path, "once"), []byte{}, 0666); err != nil {
return fmt.Errorf("Error writing once file: %s", err)
}
return nil
}
func (state *State) SaveCert(isPrecert bool, certs [][]byte) (bool, string, error) {
if len(certs) == 0 {
return false, "", fmt.Errorf("Cannot write an empty certificate chain")
}
fingerprint := sha256hex(certs[0])
prefixPath := filepath.Join(state.path, "certs", fingerprint[0:2])
var filenameSuffix string
if isPrecert {
filenameSuffix = ".precert.pem"
} else {
filenameSuffix = ".cert.pem"
}
if err := os.Mkdir(prefixPath, 0777); err != nil && !os.IsExist(err) {
return false, "", fmt.Errorf("Failed to create prefix directory %s: %s", prefixPath, err)
}
path := filepath.Join(prefixPath, fingerprint+filenameSuffix)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
if os.IsExist(err) {
return true, path, nil
} else {
return false, path, fmt.Errorf("Failed to open %s for writing: %s", path, err)
}
}
for _, cert := range certs {
if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
file.Close()
return false, path, fmt.Errorf("Error writing to %s: %s", path, err)
}
}
if err := file.Close(); err != nil {
return false, path, fmt.Errorf("Error writing to %s: %s", path, err)
}
return false, path, nil
}
func (state *State) OpenLogState(logInfo *loglist.Log) (*LogState, error) {
return OpenLogState(filepath.Join(state.path, "logs", base64.RawURLEncoding.EncodeToString(logInfo.LogID[:])))
}
func (state *State) GetLegacySTH(logInfo *loglist.Log) (*ct.SignedTreeHead, error) {
sth, err := readSTHFile(filepath.Join(state.path, "legacy_sths", legacySTHFilename(logInfo)))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
} else {
return nil, err
}
}
return sth, nil
}
func (state *State) RemoveLegacySTH(logInfo *loglist.Log) error {
err := os.Remove(filepath.Join(state.path, "legacy_sths", legacySTHFilename(logInfo)))
os.Remove(filepath.Join(state.path, "legacy_sths"))
return err
}
func (state *State) LockFilename() string {
return filepath.Join(state.path, "lock")
}
func (state *State) Lock() (bool, error) {
file, err := os.OpenFile(state.LockFilename(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
if os.IsExist(err) {
return false, nil
} else {
return false, err
}
}
if _, err := fmt.Fprintf(file, "%d\n", os.Getpid()); err != nil {
file.Close()
os.Remove(state.LockFilename())
return false, err
}
if err := file.Close(); err != nil {
os.Remove(state.LockFilename())
return false, err
}
return true, nil
}
func (state *State) Unlock() error {
return os.Remove(state.LockFilename())
}
func (state *State) LockingPid() int {
pidBytes, err := ioutil.ReadFile(state.LockFilename())
if err != nil {
return 0
}
pid, err := strconv.Atoi(string(bytes.TrimSpace(pidBytes)))
if err != nil {
return 0
}
return pid
}