certspotter/scanner.go

325 lines
8.9 KiB
Go
Raw Permalink 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-06-22 19:32:42 +02:00
//
// This file contains code from https://github.com/google/certificate-transparency/tree/master/go
// See ct/AUTHORS and ct/LICENSE for copyright and license information.
2016-05-04 20:53:48 +02:00
2016-05-04 20:49:07 +02:00
package certspotter
2016-02-05 03:45:37 +01:00
import (
// "container/list"
2016-11-26 05:13:17 +01:00
"bytes"
"crypto"
"errors"
2016-02-05 03:45:37 +01:00
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"software.sslmate.com/src/certspotter/ct"
"software.sslmate.com/src/certspotter/ct/client"
2016-02-05 03:45:37 +01:00
)
type ProcessCallback func(*Scanner, *ct.LogEntry)
2016-02-05 03:45:37 +01:00
2016-02-22 23:11:47 +01:00
const (
FETCH_RETRIES = 10
2016-02-22 23:11:47 +01:00
FETCH_RETRY_WAIT = 1
)
2016-02-05 03:45:37 +01:00
// ScannerOptions holds configuration options for the Scanner
type ScannerOptions struct {
// Number of entries to request in one batch from the Log
BatchSize int
// Number of concurrent proecssors to run
2016-02-05 03:45:37 +01:00
NumWorkers int
// Don't print any status messages to stdout
Quiet bool
}
// Creates a new ScannerOptions struct with sensible defaults
func DefaultScannerOptions() *ScannerOptions {
return &ScannerOptions{
BatchSize: 1000,
NumWorkers: 1,
Quiet: false,
2016-02-05 03:45:37 +01:00
}
}
// Scanner is a tool to scan all the entries in a CT Log.
type Scanner struct {
2016-02-05 05:16:25 +01:00
// Base URI of CT log
LogUri string
2016-02-05 05:16:25 +01:00
2016-02-18 01:03:49 +01:00
// Public key of the log
publicKey crypto.PublicKey
2017-01-08 19:17:00 +01:00
LogId []byte
2016-02-18 01:03:49 +01:00
2016-02-05 03:45:37 +01:00
// Client used to talk to the CT log instance
logClient *client.LogClient
2016-02-05 03:45:37 +01:00
// Configuration options for this Scanner instance
opts ScannerOptions
2016-02-05 03:45:37 +01:00
// Stats
certsProcessed int64
2016-02-05 03:45:37 +01:00
}
// fetchRange represents a range of certs to fetch from a CT log
type fetchRange struct {
start int64
end int64
}
// Worker function to process certs.
// Accepts ct.LogEntries over the |entries| channel, and invokes processCert on them.
2016-02-05 03:45:37 +01:00
// Returns true over the |done| channel when the |entries| channel is closed.
func (s *Scanner) processerJob(id int, entries <-chan ct.LogEntry, processCert ProcessCallback, wg *sync.WaitGroup) {
for entry := range entries {
atomic.AddInt64(&s.certsProcessed, 1)
processCert(s, &entry)
2016-02-05 03:45:37 +01:00
}
wg.Done()
}
func (s *Scanner) fetch(r fetchRange, entries chan<- ct.LogEntry, tree *CollapsedMerkleTree) error {
success := false
2016-02-22 23:11:47 +01:00
retries := FETCH_RETRIES
retryWait := FETCH_RETRY_WAIT
for !success {
s.Log(fmt.Sprintf("Fetching entries %d to %d", r.start, r.end))
logEntries, err := s.logClient.GetEntries(r.start, r.end)
if err != nil {
2016-02-22 23:11:47 +01:00
if retries == 0 {
s.Warn(fmt.Sprintf("Problem fetching entries %d to %d from log: %s", r.start, r.end, err.Error()))
return err
} else {
s.Log(fmt.Sprintf("Problem fetching entries %d to %d from log (will retry): %s", r.start, r.end, err.Error()))
time.Sleep(time.Duration(retryWait) * time.Second)
retries--
retryWait *= 2
continue
}
}
2016-02-22 23:11:47 +01:00
retries = FETCH_RETRIES
retryWait = FETCH_RETRY_WAIT
for _, logEntry := range logEntries {
if tree != nil {
tree.Add(hashLeaf(logEntry.LeafBytes))
}
logEntry.Index = r.start
entries <- logEntry
r.start++
}
if r.start > r.end {
// Only complete if we actually got all the leaves we were
// expecting -- Logs MAY return fewer than the number of
// leaves requested.
success = true
}
}
2016-02-22 23:11:47 +01:00
return nil
}
2016-02-05 03:45:37 +01:00
// Worker function for fetcher jobs.
// Accepts cert ranges to fetch over the |ranges| channel, and if the fetch is
// successful sends the individual LeafInputs out into the
// |entries| channel for the processors to chew on.
2016-02-05 03:45:37 +01:00
// Will retry failed attempts to retrieve ranges indefinitely.
// Sends true over the |done| channel when the |ranges| channel is closed.
2016-02-22 23:11:47 +01:00
/* disabled becuase error handling is broken
func (s *Scanner) fetcherJob(id int, ranges <-chan fetchRange, entries chan<- ct.LogEntry, wg *sync.WaitGroup) {
2016-02-05 03:45:37 +01:00
for r := range ranges {
s.fetch(r, entries, nil)
2016-02-05 03:45:37 +01:00
}
wg.Done()
}
2016-02-22 23:11:47 +01:00
*/
2016-02-05 03:45:37 +01:00
// Returns the smaller of |a| and |b|
func min(a int64, b int64) int64 {
if a < b {
return a
} else {
return b
}
}
// Returns the larger of |a| and |b|
func max(a int64, b int64) int64 {
if a > b {
return a
} else {
return b
}
}
// Pretty prints the passed in number of |seconds| into a more human readable
// string.
func humanTime(seconds int) string {
nanos := time.Duration(seconds) * time.Second
hours := int(nanos / (time.Hour))
nanos %= time.Hour
minutes := int(nanos / time.Minute)
nanos %= time.Minute
seconds = int(nanos / time.Second)
s := ""
if hours > 0 {
s += fmt.Sprintf("%d hours ", hours)
}
if minutes > 0 {
s += fmt.Sprintf("%d minutes ", minutes)
}
if seconds > 0 {
s += fmt.Sprintf("%d seconds ", seconds)
}
return s
}
func (s Scanner) Log(msg string) {
if !s.opts.Quiet {
log.Print(msg)
2016-02-05 03:45:37 +01:00
}
}
func (s Scanner) Warn(msg string) {
log.Print(msg)
2016-02-05 03:45:37 +01:00
}
func (s *Scanner) GetSTH() (*ct.SignedTreeHead, error) {
2016-02-05 03:45:37 +01:00
latestSth, err := s.logClient.GetSTH()
if err != nil {
return nil, err
2016-02-05 03:45:37 +01:00
}
2016-02-18 01:03:49 +01:00
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())
}
}
copy(latestSth.LogID[:], s.LogId)
return latestSth, nil
2016-02-05 03:45:37 +01:00
}
func (s *Scanner) CheckConsistency(first *ct.SignedTreeHead, second *ct.SignedTreeHead) (bool, error) {
if first.TreeSize < second.TreeSize {
proof, err := s.logClient.GetConsistencyProof(int64(first.TreeSize), int64(second.TreeSize))
if err != nil {
return false, err
}
return VerifyConsistencyProof(proof, first, second), nil
} else if first.TreeSize > second.TreeSize {
proof, err := s.logClient.GetConsistencyProof(int64(second.TreeSize), int64(first.TreeSize))
if err != nil {
return false, err
}
return VerifyConsistencyProof(proof, second, first), nil
} else {
// There is no need to ask the server for a consistency proof if the trees
// are the same size, and the DigiCert log returns a 400 error if we try.
return bytes.Equal(first.SHA256RootHash[:], second.SHA256RootHash[:]), nil
}
}
func (s *Scanner) MakeCollapsedMerkleTree(sth *ct.SignedTreeHead) (*CollapsedMerkleTree, error) {
2016-11-26 05:13:17 +01:00
if sth.TreeSize == 0 {
return &CollapsedMerkleTree{}, nil
2016-11-26 05:13:17 +01:00
}
2017-01-08 19:17:00 +01:00
entries, err := s.logClient.GetEntries(int64(sth.TreeSize-1), int64(sth.TreeSize-1))
2016-11-26 05:13:17 +01:00
if err != nil {
return nil, err
}
if len(entries) == 0 {
2017-01-08 19:17:00 +01:00
return nil, fmt.Errorf("Log did not return entry %d", sth.TreeSize-1)
2016-11-26 05:13:17 +01:00
}
leafHash := hashLeaf(entries[0].LeafBytes)
var tree *CollapsedMerkleTree
2016-11-26 05:13:17 +01:00
if sth.TreeSize > 1 {
auditPath, _, err := s.logClient.GetAuditProof(leafHash, sth.TreeSize)
if err != nil {
return nil, err
}
reverseHashes(auditPath)
2017-01-08 19:17:00 +01:00
tree, err = NewCollapsedMerkleTree(auditPath, sth.TreeSize-1)
if err != nil {
return nil, fmt.Errorf("Error returned bad audit proof for %x to %d", leafHash, sth.TreeSize)
}
2016-11-26 05:13:17 +01:00
} else {
tree = EmptyCollapsedMerkleTree()
2016-11-26 05:13:17 +01:00
}
tree.Add(leafHash)
if !bytes.Equal(tree.CalculateRoot(), sth.SHA256RootHash[:]) {
2016-11-26 05:13:17 +01:00
return nil, fmt.Errorf("Calculated root hash does not match signed tree head at size %d", sth.TreeSize)
}
return tree, nil
2016-11-26 05:13:17 +01:00
}
func (s *Scanner) Scan(startIndex int64, endIndex int64, processCert ProcessCallback, tree *CollapsedMerkleTree) error {
s.Log("Starting scan...")
2016-02-05 03:45:37 +01:00
s.certsProcessed = 0
startTime := time.Now()
/* TODO: only launch ticker goroutine if in verbose mode; kill the goroutine when the scanner finishes
ticker := time.NewTicker(time.Second)
2016-02-05 03:45:37 +01:00
go func() {
for range ticker.C {
throughput := float64(s.certsProcessed) / time.Since(startTime).Seconds()
remainingCerts := int64(endIndex) - int64(startIndex) - s.certsProcessed
remainingSeconds := int(float64(remainingCerts) / throughput)
remainingString := humanTime(remainingSeconds)
s.Log(fmt.Sprintf("Processed: %d certs (to index %d). Throughput: %3.2f ETA: %s", s.certsProcessed,
2016-02-05 03:45:37 +01:00
startIndex+int64(s.certsProcessed), throughput, remainingString))
}
}()
*/
2016-02-05 03:45:37 +01:00
// Start processor workers
jobs := make(chan ct.LogEntry, 100)
var processorWG sync.WaitGroup
for w := 0; w < s.opts.NumWorkers; w++ {
processorWG.Add(1)
go s.processerJob(w, jobs, processCert, &processorWG)
}
for start := startIndex; start < int64(endIndex); {
end := min(start+int64(s.opts.BatchSize), int64(endIndex)) - 1
if err := s.fetch(fetchRange{start, end}, jobs, tree); err != nil {
2016-02-22 23:11:47 +01:00
return err
}
start = end + 1
}
2016-02-05 03:45:37 +01:00
close(jobs)
processorWG.Wait()
2016-02-05 03:45:37 +01:00
s.Log(fmt.Sprintf("Completed %d certs in %s", s.certsProcessed, humanTime(int(time.Since(startTime).Seconds()))))
return nil
}
// Creates a new Scanner instance using |client| to talk to the log, and taking
// configuration options from |opts|.
func NewScanner(logUri string, logId []byte, publicKey crypto.PublicKey, opts *ScannerOptions) *Scanner {
2016-02-05 03:45:37 +01:00
var scanner Scanner
2016-02-05 05:16:25 +01:00
scanner.LogUri = logUri
scanner.LogId = logId
2016-02-18 01:03:49 +01:00
scanner.publicKey = publicKey
2016-02-18 19:23:07 +01:00
scanner.logClient = client.New(logUri)
scanner.opts = *opts
2016-02-05 03:45:37 +01:00
return &scanner
}