certspotter/cmd/common.go

277 lines
8.4 KiB
Go
Raw Normal View History

2016-05-04 20:53:48 +02:00
// Copyright (C) 2016 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.
2016-02-05 03:45:37 +01:00
package cmd
import (
"bytes"
"encoding/json"
2016-02-05 03:45:37 +01:00
"flag"
"fmt"
"log"
"io/ioutil"
2016-02-05 03:45:37 +01:00
"os"
2016-02-05 05:16:25 +01:00
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
2016-02-05 03:45:37 +01:00
"software.sslmate.com/src/certspotter"
"software.sslmate.com/src/certspotter/ct"
2016-02-05 03:45:37 +01:00
)
2016-07-27 02:02:56 +02:00
var batchSize = flag.Int("batch_size", 1000, "Max number of entries to request at per call to get-entries (advanced)")
var numWorkers = flag.Int("num_workers", 2, "Number of concurrent matchers (advanced)")
2016-02-05 03:45:37 +01:00
var script = flag.String("script", "", "Script to execute when a matching certificate is found")
2016-07-27 02:02:56 +02:00
var logsFilename = flag.String("logs", "", "JSON file containing log information")
2016-02-23 00:14:17 +01:00
var underwater = flag.Bool("underwater", false, "Monitor certificates from distrusted CAs instead of trusted CAs")
2016-02-05 05:16:25 +01:00
var noSave = flag.Bool("no_save", false, "Do not save a copy of matching certificates")
2016-02-05 03:45:37 +01:00
var verbose = flag.Bool("verbose", false, "Be verbose")
var allTime = flag.Bool("all_time", false, "Scan certs from all time, not just since last scan")
2016-02-05 05:16:25 +01:00
var stateDir string
2016-02-05 03:45:37 +01:00
var printMutex sync.Mutex
func homedir() string {
2016-02-05 05:16:25 +01:00
home := os.Getenv("HOME")
if home != "" {
return home
}
user, err := user.Current()
if err == nil {
return user.HomeDir
}
panic("Unable to determine home directory")
}
func DefaultStateDir(programName string) string {
return filepath.Join(homedir(), "."+programName)
2016-02-05 05:16:25 +01:00
}
func DefaultConfigDir(programName string) string {
return filepath.Join(homedir(), "."+programName)
}
func LogEntry(info *certspotter.EntryInfo) {
2016-02-05 05:16:25 +01:00
if !*noSave {
var alreadyPresent bool
var err error
2016-05-04 20:49:07 +02:00
alreadyPresent, info.Filename, err = certspotter.WriteCertRepository(filepath.Join(stateDir, "certs"), info.IsPrecert, info.FullChain)
2016-02-05 03:45:37 +01:00
if err != nil {
log.Print(err)
}
if alreadyPresent {
return
}
}
if *script != "" {
if err := info.InvokeHookScript(*script); err != nil {
2016-02-05 03:45:37 +01:00
log.Print(err)
}
} else {
printMutex.Lock()
info.Write(os.Stdout)
2016-02-05 03:45:37 +01:00
fmt.Fprintf(os.Stdout, "\n")
printMutex.Unlock()
}
}
func defangLogUri(logUri string) string {
2016-02-05 05:16:25 +01:00
return strings.Replace(strings.Replace(logUri, "://", "_", 1), "/", "_", -1)
}
2016-02-05 03:45:37 +01:00
func saveEvidence(logUri string, firstSTH *ct.SignedTreeHead, secondSTH *ct.SignedTreeHead, proof ct.ConsistencyProof) (string, string, string, error) {
now := strconv.FormatInt(time.Now().Unix(), 10)
firstFilename := filepath.Join(stateDir, "evidence", defangLogUri(logUri)+".inconsistent."+now+".first")
2016-05-04 20:49:07 +02:00
if err := certspotter.WriteSTHFile(firstFilename, firstSTH); err != nil {
return "", "", "", err
}
secondFilename := filepath.Join(stateDir, "evidence", defangLogUri(logUri)+".inconsistent."+now+".second")
2016-05-04 20:49:07 +02:00
if err := certspotter.WriteSTHFile(secondFilename, secondSTH); err != nil {
return "", "", "", err
}
proofFilename := filepath.Join(stateDir, "evidence", defangLogUri(logUri)+".inconsistent."+now+".proof")
2016-05-04 20:49:07 +02:00
if err := certspotter.WriteProofFile(proofFilename, proof); err != nil {
return "", "", "", err
}
return firstFilename, secondFilename, proofFilename, nil
}
func fileExists (path string) bool {
_, err := os.Lstat(path)
return err == nil
}
func Main(argStateDir string, processCallback certspotter.ProcessCallback) int {
2016-02-05 05:16:25 +01:00
stateDir = argStateDir
2016-02-05 03:45:37 +01:00
2016-05-04 20:49:07 +02:00
var logs []certspotter.LogInfo
2016-02-05 05:16:25 +01:00
if *logsFilename != "" {
logsJson, err := ioutil.ReadFile(*logsFilename)
2016-02-05 05:16:25 +01:00
if err != nil {
fmt.Fprintf(os.Stderr, "%s: Error reading logs file: %s: %s\n", os.Args[0], *logsFilename, err)
return 1
2016-02-05 05:16:25 +01:00
}
2016-05-04 20:49:07 +02:00
var logFileObj certspotter.LogInfoFile
if err := json.Unmarshal(logsJson, &logFileObj); err != nil {
2016-02-18 01:03:49 +01:00
fmt.Fprintf(os.Stderr, "%s: Error decoding logs file: %s: %s\n", os.Args[0], *logsFilename, err)
return 1
2016-02-05 05:16:25 +01:00
}
2016-02-18 01:03:49 +01:00
logs = logFileObj.Logs
} else if *underwater {
2016-05-04 20:49:07 +02:00
logs = certspotter.UnderwaterLogs
2016-02-05 05:16:25 +01:00
} else {
2016-05-04 20:49:07 +02:00
logs = certspotter.DefaultLogs
2016-02-05 03:45:37 +01:00
}
firstRun := !fileExists(filepath.Join(stateDir, "once"))
2016-02-05 05:16:25 +01:00
if err := os.Mkdir(stateDir, 0777); err != nil && !os.IsExist(err) {
fmt.Fprintf(os.Stderr, "%s: Error creating state directory: %s: %s\n", os.Args[0], stateDir, err)
return 1
2016-02-05 03:45:37 +01:00
}
for _, subdir := range []string{"certs", "sths", "evidence"} {
2016-02-05 05:16:25 +01:00
path := filepath.Join(stateDir, subdir)
if err := os.Mkdir(path, 0777); err != nil && !os.IsExist(err) {
fmt.Fprintf(os.Stderr, "%s: Error creating state directory: %s: %s\n", os.Args[0], path, err)
return 1
2016-02-05 03:45:37 +01:00
}
}
/*
* Exit code bits:
2016-02-22 23:58:11 +01:00
* 1 = initialization/configuration/system error
* 2 = usage error
* 4 = error communicating with log
* 8 = log misbehavior
*/
2016-02-05 05:16:25 +01:00
exitCode := 0
2016-02-18 01:03:49 +01:00
for _, logInfo := range logs {
logUri := logInfo.FullURI()
2016-02-22 23:58:11 +01:00
log.SetPrefix(os.Args[0] + ": " + logUri + ": ")
2016-02-18 01:03:49 +01:00
logKey, err := logInfo.ParsedPublicKey()
if err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Bad public key: %s\n", err)
exitCode |= 1
continue
2016-02-18 01:03:49 +01:00
}
stateFilename := filepath.Join(stateDir, "sths", defangLogUri(logUri))
2016-05-04 20:49:07 +02:00
prevSTH, err := certspotter.ReadSTHFile(stateFilename)
2016-02-05 05:16:25 +01:00
if err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Error reading state file: %s: %s\n", stateFilename, err)
exitCode |= 1
continue
2016-02-05 05:16:25 +01:00
}
2016-05-04 20:49:07 +02:00
opts := certspotter.ScannerOptions{
BatchSize: *batchSize,
NumWorkers: *numWorkers,
Quiet: !*verbose,
2016-02-05 05:16:25 +01:00
}
2016-05-04 20:49:07 +02:00
scanner := certspotter.NewScanner(logUri, logKey, &opts)
2016-02-05 05:16:25 +01:00
latestSTH, err := scanner.GetSTH()
2016-02-05 05:16:25 +01:00
if err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Error retrieving STH from log: %s\n", err)
exitCode |= 4
2016-02-05 05:16:25 +01:00
continue
}
2016-02-22 23:58:11 +01:00
if *verbose {
if *allTime {
log.Printf("Scanning all %d entries in the log because -all_time option specified", latestSTH.TreeSize)
} else if prevSTH != nil {
log.Printf("Existing log; scanning %d new entries since previous scan (previous size %d, previous root hash = %x)", latestSTH.TreeSize-prevSTH.TreeSize, prevSTH.TreeSize, prevSTH.SHA256RootHash)
} else if firstRun {
log.Printf("First run of Cert Spotter; not scanning %d existing entries because -all_time option not specified", latestSTH.TreeSize)
2016-02-22 23:58:11 +01:00
} else {
log.Printf("New log; scanning all %d entries in the log", latestSTH.TreeSize)
2016-02-22 23:58:11 +01:00
}
}
var startIndex uint64
if *allTime {
startIndex = 0
} else if prevSTH != nil {
startIndex = prevSTH.TreeSize
} else if firstRun {
startIndex = latestSTH.TreeSize
} else {
startIndex = 0
}
if latestSTH.TreeSize > startIndex {
2016-05-04 20:49:07 +02:00
var treeBuilder *certspotter.MerkleTreeBuilder
if prevSTH != nil {
2016-02-18 19:23:07 +01:00
var valid bool
var err error
var proof ct.ConsistencyProof
valid, treeBuilder, proof, err = scanner.CheckConsistency(prevSTH, latestSTH)
if err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Error fetching consistency proof: %s\n", err)
exitCode |= 4
continue
}
if !valid {
firstFilename, secondFilename, proofFilename, err := saveEvidence(logUri, prevSTH, latestSTH, proof)
if err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Consistency proof failed - the log has misbehaved! Saving evidence of misbehavior failed: %s\n", err)
} else {
log.Printf("Consistency proof failed - the log has misbehaved! Evidence of misbehavior has been saved to '%s' and '%s' (with proof in '%s').\n", firstFilename, secondFilename, proofFilename)
}
exitCode |= 8
continue
}
} else {
2016-05-04 20:49:07 +02:00
treeBuilder = &certspotter.MerkleTreeBuilder{}
}
if err := scanner.Scan(int64(startIndex), int64(latestSTH.TreeSize), processCallback, treeBuilder); err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Error scanning log: %s\n", err)
exitCode |= 4
2016-02-05 05:16:25 +01:00
continue
}
rootHash := treeBuilder.Finish()
if !bytes.Equal(rootHash, latestSTH.SHA256RootHash[:]) {
2016-02-22 23:58:11 +01:00
log.Printf("Validation of log entries failed - calculated tree root (%x) does not match signed tree root (%s). If this error persists for an extended period, it should be construed as misbehavior by the log.\n", rootHash, latestSTH.SHA256RootHash)
exitCode |= 8
continue
}
2016-02-05 05:16:25 +01:00
}
2016-02-22 23:58:11 +01:00
if *verbose {
log.Printf("final log size = %d, final root hash = %x", latestSTH.TreeSize, latestSTH.SHA256RootHash)
}
2016-05-04 20:49:07 +02:00
if err := certspotter.WriteSTHFile(stateFilename, latestSTH); err != nil {
2016-02-22 23:58:11 +01:00
log.Printf("Error writing state file: %s: %s\n", stateFilename, err)
exitCode |= 1
continue
2016-02-05 05:16:25 +01:00
}
2016-02-05 03:45:37 +01:00
}
2016-02-05 05:16:25 +01:00
if firstRun {
if err := ioutil.WriteFile(filepath.Join(stateDir, "once"), []byte{}, 0666); err != nil {
log.Printf("Error writing once file: %s\n", err)
exitCode |= 1
}
}
return exitCode
2016-02-05 03:45:37 +01:00
}