From 9558efc9552b0be315fbc81142ea6a1cab2ad7c5 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 17 Feb 2016 16:03:49 -0800 Subject: [PATCH] Verify STH signatures --- cmd/common.go | 44 +++++++++------------ logs.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ scanner.go | 18 ++++++++- 3 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 logs.go diff --git a/cmd/common.go b/cmd/common.go index 50c3f0b..faa4ae8 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -7,7 +7,7 @@ import ( "os" "bytes" "os/user" - "bufio" + "encoding/json" "sync" "strings" "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 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 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 verbose = flag.Bool("verbose", false, "Be verbose") 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 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 { return os.Geteuid() == 0 } @@ -97,7 +85,7 @@ func defangLogUri (logUri string) string { func Main (argStateDir string, processCallback ctwatch.ProcessCallback) { stateDir = argStateDir - var logs []string + var logs []ctwatch.LogInfo if *logsFilename != "" { logFile, err := os.Open(*logsFilename) if err != nil { @@ -105,16 +93,16 @@ func Main (argStateDir string, processCallback ctwatch.ProcessCallback) { os.Exit(3) } defer logFile.Close() - scanner := bufio.NewScanner(logFile) - for scanner.Scan() { - logs = append(logs, scanner.Text()) - } - if err := scanner.Err(); err != nil { - fmt.Fprintf(os.Stderr, "%s: Error reading logs file: %s: %s\n", os.Args[0], *logsFilename, err) + var logFileObj ctwatch.LogInfoFile + if err := json.NewDecoder(logFile).Decode(&logFileObj); err != nil { + fmt.Fprintf(os.Stderr, "%s: Error decoding logs file: %s: %s\n", os.Args[0], *logsFilename, err) os.Exit(3) } + logs = logFileObj.Logs + } else if *underwater { + logs = ctwatch.UnderwaterLogs } else { - logs = defaultLogs + logs = ctwatch.DefaultLogs } if err := os.Mkdir(stateDir, 0777); err != nil && !os.IsExist(err) { @@ -131,7 +119,13 @@ func Main (argStateDir string, processCallback ctwatch.ProcessCallback) { 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)) prevSTH, err := ctwatch.ReadStateFile(stateFilename) if err != nil { @@ -146,7 +140,7 @@ func Main (argStateDir string, processCallback ctwatch.ProcessCallback) { ParallelFetch: *parallelFetch, Quiet: !*verbose, } - scanner := ctwatch.NewScanner(logUri, logClient, opts) + scanner := ctwatch.NewScanner(logUri, logKey, logClient, opts) latestSTH, err := scanner.GetSTH() if err != nil { diff --git a/logs.go b/logs.go new file mode 100644 index 0000000..b03b08b --- /dev/null +++ b/logs.go @@ -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 +} diff --git a/scanner.go b/scanner.go index 614ef75..338a92e 100644 --- a/scanner.go +++ b/scanner.go @@ -7,6 +7,8 @@ import ( "sync" "sync/atomic" "time" + "crypto" + "errors" "github.com/google/certificate-transparency/go" "github.com/google/certificate-transparency/go/client" @@ -44,6 +46,9 @@ type Scanner struct { // Base URI of CT log LogUri string + // Public key of the log + publicKey crypto.PublicKey + // Client used to talk to the CT log instance logClient *client.LogClient @@ -168,7 +173,15 @@ func (s *Scanner) GetSTH() (*ct.SignedTreeHead, error) { if err != nil { 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 } @@ -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 // 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 scanner.LogUri = logUri + scanner.publicKey = publicKey scanner.logClient = client scanner.opts = opts return &scanner