Verify STH signatures

This commit is contained in:
Andrew Ayer 2016-02-17 16:03:49 -08:00
parent 4b304fd192
commit 9558efc955
3 changed files with 141 additions and 27 deletions

View File

@ -7,7 +7,7 @@ import (
"os" "os"
"bytes" "bytes"
"os/user" "os/user"
"bufio" "encoding/json"
"sync" "sync"
"strings" "strings"
"path/filepath" "path/filepath"
@ -20,7 +20,8 @@ var batchSize = flag.Int("batch_size", 1000, "Max number of entries to request a
var numWorkers = flag.Int("num_workers", 2, "Number of concurrent matchers") var numWorkers = flag.Int("num_workers", 2, "Number of concurrent matchers")
var parallelFetch = flag.Int("parallel_fetch", 2, "Number of concurrent GetEntries fetches") var parallelFetch = flag.Int("parallel_fetch", 2, "Number of concurrent GetEntries fetches")
var script = flag.String("script", "", "Script to execute when a matching certificate is found") var script = flag.String("script", "", "Script to execute when a matching certificate is found")
var logsFilename = flag.String("logs", "", "File containing log URLs") var logsFilename = flag.String("logs", "", "JSON file containing log URLs")
var underwater = flag.Bool("underwater", false, "Monitor certificates from distrusted CAs")
var noSave = flag.Bool("no_save", false, "Do not save a copy of matching certificates") var noSave = flag.Bool("no_save", false, "Do not save a copy of matching certificates")
var verbose = flag.Bool("verbose", false, "Be verbose") 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") var allTime = flag.Bool("all_time", false, "Scan certs from all time, not just since last scan")
@ -28,19 +29,6 @@ var stateDir string
var printMutex sync.Mutex var printMutex sync.Mutex
var defaultLogs = []string{
"https://log.certly.io",
"https://ct1.digicert-ct.com/log",
"https://ct.googleapis.com/aviator",
"https://ct.googleapis.com/pilot",
"https://ct.googleapis.com/rocketeer",
"https://ct.izenpe.com",
"https://ct.ws.symantec.com",
"https://vega.ws.symantec.com",
"https://ctlog.api.venafi.com",
"https://ct.wosign.com",
}
func isRoot () bool { func isRoot () bool {
return os.Geteuid() == 0 return os.Geteuid() == 0
} }
@ -97,7 +85,7 @@ func defangLogUri (logUri string) string {
func Main (argStateDir string, processCallback ctwatch.ProcessCallback) { func Main (argStateDir string, processCallback ctwatch.ProcessCallback) {
stateDir = argStateDir stateDir = argStateDir
var logs []string var logs []ctwatch.LogInfo
if *logsFilename != "" { if *logsFilename != "" {
logFile, err := os.Open(*logsFilename) logFile, err := os.Open(*logsFilename)
if err != nil { if err != nil {
@ -105,16 +93,16 @@ func Main (argStateDir string, processCallback ctwatch.ProcessCallback) {
os.Exit(3) os.Exit(3)
} }
defer logFile.Close() defer logFile.Close()
scanner := bufio.NewScanner(logFile) var logFileObj ctwatch.LogInfoFile
for scanner.Scan() { if err := json.NewDecoder(logFile).Decode(&logFileObj); err != nil {
logs = append(logs, scanner.Text()) fmt.Fprintf(os.Stderr, "%s: Error decoding logs file: %s: %s\n", os.Args[0], *logsFilename, err)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "%s: Error reading logs file: %s: %s\n", os.Args[0], *logsFilename, err)
os.Exit(3) os.Exit(3)
} }
logs = logFileObj.Logs
} else if *underwater {
logs = ctwatch.UnderwaterLogs
} else { } else {
logs = defaultLogs logs = ctwatch.DefaultLogs
} }
if err := os.Mkdir(stateDir, 0777); err != nil && !os.IsExist(err) { if err := os.Mkdir(stateDir, 0777); err != nil && !os.IsExist(err) {
@ -131,7 +119,13 @@ func Main (argStateDir string, processCallback ctwatch.ProcessCallback) {
exitCode := 0 exitCode := 0
for _, logUri := range logs { for _, logInfo := range logs {
logUri := logInfo.FullURI()
logKey, err := logInfo.ParsedPublicKey()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s: Bad public key: %s\n", os.Args[0], logUri, err)
os.Exit(3)
}
stateFilename := filepath.Join(stateDir, "sths", defangLogUri(logUri)) stateFilename := filepath.Join(stateDir, "sths", defangLogUri(logUri))
prevSTH, err := ctwatch.ReadStateFile(stateFilename) prevSTH, err := ctwatch.ReadStateFile(stateFilename)
if err != nil { if err != nil {
@ -146,7 +140,7 @@ func Main (argStateDir string, processCallback ctwatch.ProcessCallback) {
ParallelFetch: *parallelFetch, ParallelFetch: *parallelFetch,
Quiet: !*verbose, Quiet: !*verbose,
} }
scanner := ctwatch.NewScanner(logUri, logClient, opts) scanner := ctwatch.NewScanner(logUri, logKey, logClient, opts)
latestSTH, err := scanner.GetSTH() latestSTH, err := scanner.GetSTH()
if err != nil { if err != nil {

106
logs.go Normal file
View File

@ -0,0 +1,106 @@
package ctwatch
import (
"encoding/base64"
"crypto"
"crypto/x509"
)
type LogInfoFile struct {
Logs []LogInfo `json:"logs"`
}
type LogInfo struct {
Description string `json:"description"`
Key []byte `json:"key"`
Url string `json:"url"`
MMD int `json:"maximum_merge_delay"`
}
func (info *LogInfo) FullURI () string {
return "https://" + info.Url
}
func (info *LogInfo) ParsedPublicKey () (crypto.PublicKey, error) {
return x509.ParsePKIXPublicKey(info.Key)
}
var DefaultLogs = []LogInfo{
{
Description: "Google 'Pilot' log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA=="),
Url: "ct.googleapis.com/pilot",
MMD: 86400,
},
{
Description: "Google 'Aviator' log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q=="),
Url: "ct.googleapis.com/aviator",
MMD: 86400,
},
{
Description: "DigiCert Log Server",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A=="),
Url: "ct1.digicert-ct.com/log",
MMD: 86400,
},
{
Description: "Google 'Rocketeer' log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg=="),
Url: "ct.googleapis.com/rocketeer",
MMD: 86400,
},
{
Description: "Certly.IO log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECyPLhWKYYUgEc+tUXfPQB4wtGS2MNvXrjwFCCnyYJifBtd2Sk7Cu+Js9DNhMTh35FftHaHu6ZrclnNBKwmbbSA=="),
Url: "log.certly.io",
MMD: 86400,
},
{
Description: "Izenpe log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ2Q5DC3cUBj4IQCiDu0s6j51up+TZAkAEcQRF6tczw90rLWXkJMAW7jr9yc92bIKgV8vDXU4lDeZHvYHduDuvg=="),
Url: "ct.izenpe.com",
MMD: 86400,
},
{
Description: "Symantec log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg=="),
Url: "ct.ws.symantec.com",
MMD: 86400,
},
{
Description: "Venafi log",
Key: mustDecodeBase64("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolpIHxdSlTXLo1s6H1OCdpSj/4DyHDc8wLG9wVmLqy1lk9fz4ATVmm+/1iN2Nk8jmctUKK2MFUtlWXZBSpym97M7frGlSaQXUWyA3CqQUEuIJOmlEjKTBEiQAvpfDjCHjlV2Be4qTM6jamkJbiWtgnYPhJL6ONaGTiSPm7Byy57iaz/hbckldSOIoRhYBiMzeNoA0DiRZ9KmfSeXZ1rB8y8X5urSW+iBzf2SaOfzBvDpcoTuAaWx2DPazoOl28fP1hZ+kHUYvxbcMjttjauCFx+JII0dmuZNIwjfeG/GBb9frpSX219k1O4Wi6OEbHEr8at/XQ0y7gTikOxBn/s5wQIDAQAB"),
Url: "ctlog.api.venafi.com",
MMD: 86400,
},
{
Description: "Symantec 'Vega' log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6pWeAv/u8TNtS4e8zf0ZF2L/lNPQWQc/Ai0ckP7IRzA78d0NuBEMXR2G3avTK0Zm+25ltzv9WWis36b4ztIYTQ=="),
Url: "vega.ws.symantec.com",
MMD: 86400,
},
{
Description: "WoSign CT Log Server",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1+wvK3VPN7yjQ7qLZWY8fWrlDCqmwuUm/gx9TnzwOrzi0yLcAdAfbkOcXG6DrZwV9sSNYLUdu6NiaX7rp6oBmw=="),
Url: "ct.wosign.com",
MMD: 86400,
},
}
// Logs which monitor certs from distrusted roots
var UnderwaterLogs = []LogInfo{
{
Description: "Google 'Submariner' log",
Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOfifIGLUV1Voou9JLfA5LZreRLSUMOCeeic8q3Dw0fpRkGMWV0Gtq20fgHQweQJeLVmEByQj9p81uIW4QkWkTw=="),
Url: "ct.googleapis.com/submariner",
MMD: 86400,
},
}
func mustDecodeBase64 (str string) []byte {
bytes, err := base64.StdEncoding.DecodeString(str)
if err != nil {
panic("MustDecodeBase64: " + err.Error())
}
return bytes
}

View File

@ -7,6 +7,8 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"crypto"
"errors"
"github.com/google/certificate-transparency/go" "github.com/google/certificate-transparency/go"
"github.com/google/certificate-transparency/go/client" "github.com/google/certificate-transparency/go/client"
@ -44,6 +46,9 @@ type Scanner struct {
// Base URI of CT log // Base URI of CT log
LogUri string LogUri string
// Public key of the log
publicKey crypto.PublicKey
// Client used to talk to the CT log instance // Client used to talk to the CT log instance
logClient *client.LogClient logClient *client.LogClient
@ -168,7 +173,15 @@ func (s *Scanner) GetSTH() (*ct.SignedTreeHead, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: Verify STH signature if s.publicKey != nil {
verifier, err := ct.NewSignatureVerifier(s.publicKey)
if err != nil {
return nil, err
}
if err := verifier.VerifySTHSignature(*latestSth); err != nil {
return nil, errors.New("STH signature is invalid: " + err.Error())
}
}
return latestSth, nil return latestSth, nil
} }
@ -255,9 +268,10 @@ func (s *Scanner) Scan(startIndex int64, endIndex int64, processCert ProcessCall
// Creates a new Scanner instance using |client| to talk to the log, and taking // Creates a new Scanner instance using |client| to talk to the log, and taking
// configuration options from |opts|. // configuration options from |opts|.
func NewScanner(logUri string, client *client.LogClient, opts ScannerOptions) *Scanner { func NewScanner(logUri string, publicKey crypto.PublicKey, client *client.LogClient, opts ScannerOptions) *Scanner {
var scanner Scanner var scanner Scanner
scanner.LogUri = logUri scanner.LogUri = logUri
scanner.publicKey = publicKey
scanner.logClient = client scanner.logClient = client
scanner.opts = opts scanner.opts = opts
return &scanner return &scanner