Initial commit
This commit is contained in:
commit
a418a3686d
|
@ -0,0 +1,3 @@
|
||||||
|
*/*
|
||||||
|
!.gitignore
|
||||||
|
!*.go
|
|
@ -0,0 +1,83 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"src.agwa.name/ctwatch"
|
||||||
|
"github.com/google/certificate-transparency/go"
|
||||||
|
"github.com/google/certificate-transparency/go/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
var batchSize = flag.Int("batch_size", 1000, "Max number of entries to request at per call to get-entries")
|
||||||
|
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 repo = flag.String("repo", "", "Directory of scanned certificates")
|
||||||
|
var verbose = flag.Bool("verbose", false, "Be verbose")
|
||||||
|
|
||||||
|
var printMutex sync.Mutex
|
||||||
|
|
||||||
|
func logCallback (entry *ct.LogEntry) {
|
||||||
|
if *repo != "" {
|
||||||
|
alreadyPresent, err := ctwatch.WriteCertRepository(*repo, entry)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
if alreadyPresent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *script != "" {
|
||||||
|
if err := ctwatch.InvokeHookScript(*script, entry); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printMutex.Lock()
|
||||||
|
ctwatch.DumpLogEntry(os.Stdout, entry)
|
||||||
|
fmt.Fprintf(os.Stdout, "\n")
|
||||||
|
printMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main(logUri string, stateFile string, matcher ctwatch.Matcher) {
|
||||||
|
startIndex, err := ctwatch.ReadStateFile(stateFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: Error reading state file: %s: %s\n", os.Args[0], stateFile, err)
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("LOG_URI", logUri)
|
||||||
|
|
||||||
|
logClient := client.New(logUri)
|
||||||
|
opts := ctwatch.ScannerOptions{
|
||||||
|
Matcher: matcher,
|
||||||
|
BatchSize: *batchSize,
|
||||||
|
NumWorkers: *numWorkers,
|
||||||
|
ParallelFetch: *parallelFetch,
|
||||||
|
Quiet: !*verbose,
|
||||||
|
}
|
||||||
|
scanner := ctwatch.NewScanner(logClient, opts)
|
||||||
|
|
||||||
|
endIndex, err := scanner.TreeSize()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: Error contacting log: %s: %s\n", os.Args[0], logUri, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if startIndex != -1 {
|
||||||
|
if err := scanner.Scan(startIndex, endIndex, logCallback); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: Error scanning log: %s: %s\n", os.Args[0], logUri, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctwatch.WriteStateFile(stateFile, endIndex); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: Error writing state file: %s: %s\n", os.Args[0], stateFile, err)
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"bufio"
|
||||||
|
|
||||||
|
"src.agwa.name/ctwatch"
|
||||||
|
"src.agwa.name/ctwatch/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
if flag.NArg() < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s [flags] log_uri state_file [domain ...]\n", os.Args[0])
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
logUri := flag.Arg(0)
|
||||||
|
stateFile := flag.Arg(1)
|
||||||
|
|
||||||
|
var domains []string
|
||||||
|
if flag.NArg() == 3 && flag.Arg(2) == "-" {
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
domains = append(domains, scanner.Text())
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading standard input: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
domains = flag.Args()[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var matcher ctwatch.Matcher
|
||||||
|
if len(domains) == 0 {
|
||||||
|
matcher = ctwatch.MatchAll{}
|
||||||
|
} else {
|
||||||
|
matcher = ctwatch.NewDomainMatcher(domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Main(logUri, stateFile, matcher)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/certificate-transparency/go"
|
||||||
|
"github.com/google/certificate-transparency/go/x509"
|
||||||
|
|
||||||
|
"src.agwa.name/ctwatch/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sha1Matcher struct { }
|
||||||
|
|
||||||
|
func (m sha1Matcher) CertificateMatches(c *x509.Certificate) bool {
|
||||||
|
return c.NotBefore.After(time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)) &&
|
||||||
|
(c.SignatureAlgorithm == x509.SHA1WithRSA ||
|
||||||
|
c.SignatureAlgorithm == x509.MD5WithRSA ||
|
||||||
|
c.SignatureAlgorithm == x509.MD2WithRSA ||
|
||||||
|
c.SignatureAlgorithm == x509.DSAWithSHA1 ||
|
||||||
|
c.SignatureAlgorithm == x509.ECDSAWithSHA1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m sha1Matcher) PrecertificateMatches(pc *ct.Precertificate) bool {
|
||||||
|
return m.CertificateMatches(&pc.TBSCertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
if flag.NArg() != 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s [flags] log_uri state_file\n", os.Args[0])
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
logUri := flag.Arg(0)
|
||||||
|
stateFile := flag.Arg(1)
|
||||||
|
|
||||||
|
cmd.Main(logUri, stateFile, &sha1Matcher{})
|
||||||
|
}
|
|
@ -0,0 +1,267 @@
|
||||||
|
package ctwatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
|
"github.com/google/certificate-transparency/go"
|
||||||
|
"github.com/google/certificate-transparency/go/x509"
|
||||||
|
"github.com/google/certificate-transparency/go/x509/pkix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadStateFile (path string) (int64, error) {
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex, err := strconv.ParseInt(strings.TrimSpace(string(content)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return startIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteStateFile (path string, endIndex int64) error {
|
||||||
|
return ioutil.WriteFile(path, []byte(strconv.FormatInt(endIndex, 10) + "\n"), 0666)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendDnArray (buf *bytes.Buffer, code string, values []string) {
|
||||||
|
for _, value := range values {
|
||||||
|
if buf.Len() != 0 {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
buf.WriteString(code)
|
||||||
|
buf.WriteString("=")
|
||||||
|
buf.WriteString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendDnValue (buf *bytes.Buffer, code string, value string) {
|
||||||
|
if value != "" {
|
||||||
|
appendDnArray(buf, code, []string{value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDN (name pkix.Name) (string) {
|
||||||
|
// C=US, ST=UT, L=Salt Lake City, O=The USERTRUST Network, OU=http://www.usertrust.com, CN=UTN-USERFirst-Hardware
|
||||||
|
var buf bytes.Buffer
|
||||||
|
appendDnArray(&buf, "C", name.Country)
|
||||||
|
appendDnArray(&buf, "ST", name.Province)
|
||||||
|
appendDnArray(&buf, "L", name.Locality)
|
||||||
|
appendDnArray(&buf, "O", name.Organization)
|
||||||
|
appendDnArray(&buf, "OU", name.OrganizationalUnit)
|
||||||
|
appendDnValue(&buf, "CN", name.CommonName)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func allDNSNames (cert *x509.Certificate) []string {
|
||||||
|
dnsNames := []string{}
|
||||||
|
|
||||||
|
if cert.Subject.CommonName != "" {
|
||||||
|
dnsNames = append(dnsNames, cert.Subject.CommonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dnsName := range cert.DNSNames {
|
||||||
|
if dnsName != cert.Subject.CommonName {
|
||||||
|
dnsNames = append(dnsNames, dnsName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dnsNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoot (chain []ct.ASN1Cert) *x509.Certificate {
|
||||||
|
if len(chain) > 0 {
|
||||||
|
root, err := x509.ParseCertificate(chain[len(chain)-1])
|
||||||
|
if err == nil {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
log.Printf("Failed to parse root certificate: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSubjectOrganization (cert *x509.Certificate) string {
|
||||||
|
if cert != nil && len(cert.Subject.Organization) > 0 {
|
||||||
|
return cert.Subject.Organization[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSerial (serial *big.Int) string {
|
||||||
|
if serial != nil {
|
||||||
|
return fmt.Sprintf("%x", serial)
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256hex (data []byte) string {
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRaw (entry *ct.LogEntry) []byte {
|
||||||
|
if entry.Precert != nil {
|
||||||
|
return entry.Precert.Raw
|
||||||
|
} else if entry.X509Cert != nil {
|
||||||
|
return entry.X509Cert.Raw
|
||||||
|
} else {
|
||||||
|
panic("getRaw: entry is neither precert nor x509")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type certInfo struct {
|
||||||
|
IsPrecert bool
|
||||||
|
RootOrg string
|
||||||
|
SubjectDn string
|
||||||
|
IssuerDn string
|
||||||
|
DnsNames []string
|
||||||
|
Serial string
|
||||||
|
PubkeyHash string
|
||||||
|
Fingerprint string
|
||||||
|
NotBefore time.Time
|
||||||
|
NotAfter time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCertInfo (entry *ct.LogEntry) certInfo {
|
||||||
|
var isPrecert bool
|
||||||
|
var cert *x509.Certificate
|
||||||
|
|
||||||
|
if entry.Precert != nil {
|
||||||
|
isPrecert = true
|
||||||
|
cert = &entry.Precert.TBSCertificate
|
||||||
|
} else if entry.X509Cert != nil {
|
||||||
|
isPrecert = false
|
||||||
|
cert = entry.X509Cert
|
||||||
|
} else {
|
||||||
|
panic("makeCertInfo: entry is neither precert nor x509")
|
||||||
|
}
|
||||||
|
return certInfo {
|
||||||
|
IsPrecert: isPrecert,
|
||||||
|
RootOrg: getSubjectOrganization(getRoot(entry.Chain)),
|
||||||
|
SubjectDn: formatDN(cert.Subject),
|
||||||
|
IssuerDn: formatDN(cert.Issuer),
|
||||||
|
DnsNames: allDNSNames(cert),
|
||||||
|
Serial: formatSerial(cert.SerialNumber),
|
||||||
|
PubkeyHash: sha256hex(cert.RawSubjectPublicKeyInfo),
|
||||||
|
Fingerprint: sha256hex(getRaw(entry)),
|
||||||
|
NotBefore: cert.NotBefore,
|
||||||
|
NotAfter: cert.NotAfter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *certInfo) TypeString () string {
|
||||||
|
if info.IsPrecert {
|
||||||
|
return "precert"
|
||||||
|
} else {
|
||||||
|
return "cert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *certInfo) TypeFriendlyString () string {
|
||||||
|
if info.IsPrecert {
|
||||||
|
return "Pre-certificate"
|
||||||
|
} else {
|
||||||
|
return "Certificate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DumpLogEntry (out io.Writer, entry *ct.LogEntry) {
|
||||||
|
info := makeCertInfo(entry)
|
||||||
|
|
||||||
|
fmt.Fprintf(out, "%d:\n", entry.Index)
|
||||||
|
fmt.Fprintf(out, "\t Type = %s\n", info.TypeFriendlyString())
|
||||||
|
fmt.Fprintf(out, "\t DNS Names = %v\n", info.DnsNames)
|
||||||
|
fmt.Fprintf(out, "\t Pubkey = %s\n", info.PubkeyHash)
|
||||||
|
fmt.Fprintf(out, "\t Fingerprint = %s\n", info.Fingerprint)
|
||||||
|
fmt.Fprintf(out, "\t Subject = %s\n", info.SubjectDn)
|
||||||
|
fmt.Fprintf(out, "\t Issuer = %s\n", info.IssuerDn)
|
||||||
|
fmt.Fprintf(out, "\tRoot Operator = %s\n", info.RootOrg)
|
||||||
|
fmt.Fprintf(out, "\t Serial = %s\n", info.Serial)
|
||||||
|
fmt.Fprintf(out, "\t Not Before = %s\n", info.NotBefore)
|
||||||
|
fmt.Fprintf(out, "\t Not After = %s\n", info.NotAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InvokeHookScript (command string, entry *ct.LogEntry) error {
|
||||||
|
info := makeCertInfo(entry)
|
||||||
|
|
||||||
|
cmd := exec.Command(command)
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"LOG_INDEX=" + strconv.FormatInt(entry.Index, 10),
|
||||||
|
"CERT_TYPE=" + info.TypeString(),
|
||||||
|
"SUBJECT_DN=" + info.SubjectDn,
|
||||||
|
"ISSUER_DN=" + info.IssuerDn,
|
||||||
|
"DNS_NAMES=" + strings.Join(info.DnsNames, ","),
|
||||||
|
"SERIAL=" + info.Serial,
|
||||||
|
"PUBKEY_HASH=" + info.PubkeyHash,
|
||||||
|
"FINGERPRINT=" + info.Fingerprint,
|
||||||
|
"NOT_BEFORE=" + strconv.FormatInt(info.NotBefore.Unix(), 10),
|
||||||
|
"NOT_AFTER=" + strconv.FormatInt(info.NotAfter.Unix(), 10))
|
||||||
|
stderrBuffer := bytes.Buffer{}
|
||||||
|
cmd.Stderr = &stderrBuffer
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if _, isExitError := err.(*exec.ExitError); isExitError {
|
||||||
|
fmt.Errorf("Script failed: %s: %s", command, strings.TrimSpace(stderrBuffer.String()))
|
||||||
|
} else {
|
||||||
|
fmt.Errorf("Failed to execute script: %s: %s", command, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteCertRepository (repoPath string, entry *ct.LogEntry) (bool, error) {
|
||||||
|
fingerprint := sha256hex(getRaw(entry))
|
||||||
|
prefixPath := filepath.Join(repoPath, fingerprint[0:2])
|
||||||
|
var filenameSuffix string
|
||||||
|
if entry.Precert != nil {
|
||||||
|
filenameSuffix = ".precert.pem"
|
||||||
|
} else if entry.X509Cert != nil {
|
||||||
|
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, nil
|
||||||
|
} else {
|
||||||
|
return false, fmt.Errorf("Failed to open %s for writing: %s", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: getRaw(entry)}); err != nil {
|
||||||
|
file.Close()
|
||||||
|
return false, fmt.Errorf("Error writing to %s: %s", path, err)
|
||||||
|
}
|
||||||
|
for _, chainCert := range entry.Chain {
|
||||||
|
if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: chainCert}); err != nil {
|
||||||
|
file.Close()
|
||||||
|
return false, fmt.Errorf("Error writing to %s: %s", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return false, fmt.Errorf("Error writing to %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
|
@ -0,0 +1,370 @@
|
||||||
|
package ctwatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/certificate-transparency/go"
|
||||||
|
"github.com/google/certificate-transparency/go/client"
|
||||||
|
"github.com/google/certificate-transparency/go/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clients wishing to implement their own Matchers should implement this interface:
|
||||||
|
type Matcher interface {
|
||||||
|
// CertificateMatches is called by the scanner for each X509 Certificate found in the log.
|
||||||
|
// The implementation should return |true| if the passed Certificate is interesting, and |false| otherwise.
|
||||||
|
CertificateMatches(*x509.Certificate) bool
|
||||||
|
|
||||||
|
// PrecertificateMatches is called by the scanner for each CT Precertificate found in the log.
|
||||||
|
// The implementation should return |true| if the passed Precertificate is interesting, and |false| otherwise.
|
||||||
|
PrecertificateMatches(*ct.Precertificate) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAll is a Matcher which will match every possible Certificate and Precertificate.
|
||||||
|
type MatchAll struct{}
|
||||||
|
|
||||||
|
func (m MatchAll) CertificateMatches(_ *x509.Certificate) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MatchAll) PrecertificateMatches(_ *ct.Precertificate) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainMatcher struct {
|
||||||
|
domains []string
|
||||||
|
domainSuffixes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainMatcher (domains []string) DomainMatcher {
|
||||||
|
m := DomainMatcher{}
|
||||||
|
for _, domain := range domains {
|
||||||
|
m.domains = append(m.domains, strings.ToLower(domain))
|
||||||
|
m.domainSuffixes = append(m.domainSuffixes, "." + strings.ToLower(domain))
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DomainMatcher) dnsNameMatches (dnsName string) bool {
|
||||||
|
dnsNameLower := strings.ToLower(dnsName)
|
||||||
|
for _, domain := range m.domains {
|
||||||
|
if dnsNameLower == domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, domainSuffix := range m.domainSuffixes {
|
||||||
|
if strings.HasSuffix(dnsNameLower, domainSuffix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DomainMatcher) CertificateMatches(c *x509.Certificate) bool {
|
||||||
|
if m.dnsNameMatches(c.Subject.CommonName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, dnsName := range c.DNSNames {
|
||||||
|
if m.dnsNameMatches(dnsName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DomainMatcher) PrecertificateMatches(pc *ct.Precertificate) bool {
|
||||||
|
return m.CertificateMatches(&pc.TBSCertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ScannerOptions holds configuration options for the Scanner
|
||||||
|
type ScannerOptions struct {
|
||||||
|
// Custom matcher for x509 Certificates, functor will be called for each
|
||||||
|
// Certificate found during scanning.
|
||||||
|
Matcher Matcher
|
||||||
|
|
||||||
|
// Number of entries to request in one batch from the Log
|
||||||
|
BatchSize int
|
||||||
|
|
||||||
|
// Number of concurrent matchers to run
|
||||||
|
NumWorkers int
|
||||||
|
|
||||||
|
// Number of concurrent fethers to run
|
||||||
|
ParallelFetch int
|
||||||
|
|
||||||
|
// Don't print any status messages to stdout
|
||||||
|
Quiet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new ScannerOptions struct with sensible defaults
|
||||||
|
func DefaultScannerOptions() *ScannerOptions {
|
||||||
|
return &ScannerOptions{
|
||||||
|
Matcher: &MatchAll{},
|
||||||
|
BatchSize: 1000,
|
||||||
|
NumWorkers: 1,
|
||||||
|
ParallelFetch: 1,
|
||||||
|
Quiet: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanner is a tool to scan all the entries in a CT Log.
|
||||||
|
type Scanner struct {
|
||||||
|
// Client used to talk to the CT log instance
|
||||||
|
logClient *client.LogClient
|
||||||
|
|
||||||
|
// Configuration options for this Scanner instance
|
||||||
|
opts ScannerOptions
|
||||||
|
|
||||||
|
// Size of tree at end of scan
|
||||||
|
latestTreeSize int64
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
certsProcessed int64
|
||||||
|
unparsableEntries int64
|
||||||
|
entriesWithNonFatalErrors int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// matcherJob represents the context for an individual matcher job.
|
||||||
|
type matcherJob struct {
|
||||||
|
// The log entry returned by the log server
|
||||||
|
entry ct.LogEntry
|
||||||
|
// The index of the entry containing the LeafInput in the log
|
||||||
|
index int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchRange represents a range of certs to fetch from a CT log
|
||||||
|
type fetchRange struct {
|
||||||
|
start int64
|
||||||
|
end int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes the error returned by either x509.ParseCertificate() or
|
||||||
|
// x509.ParseTBSCertificate() and determines if it's non-fatal or otherwise.
|
||||||
|
// In the case of non-fatal errors, the error will be logged,
|
||||||
|
// entriesWithNonFatalErrors will be incremented, and the return value will be
|
||||||
|
// nil.
|
||||||
|
// Fatal errors will be logged, unparsableEntires will be incremented, and the
|
||||||
|
// fatal error itself will be returned.
|
||||||
|
// When |err| is nil, this method does nothing.
|
||||||
|
func (s *Scanner) handleParseEntryError(err error, entryType ct.LogEntryType, index int64) error {
|
||||||
|
if err == nil {
|
||||||
|
// No error to handle
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch err.(type) {
|
||||||
|
case x509.NonFatalErrors:
|
||||||
|
s.entriesWithNonFatalErrors++
|
||||||
|
// We'll make a note, but continue.
|
||||||
|
s.Warn(fmt.Sprintf("Non-fatal error in %+v at index %d: %s", entryType, index, err.Error()))
|
||||||
|
default:
|
||||||
|
s.unparsableEntries++
|
||||||
|
s.Warn(fmt.Sprintf("Failed to parse in %+v at index %d : %s", entryType, index, err.Error()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processes the given |entry| in the specified log.
|
||||||
|
func (s *Scanner) processEntry(entry ct.LogEntry, foundCert func(*ct.LogEntry)) {
|
||||||
|
atomic.AddInt64(&s.certsProcessed, 1)
|
||||||
|
switch entry.Leaf.TimestampedEntry.EntryType {
|
||||||
|
case ct.X509LogEntryType:
|
||||||
|
cert, err := x509.ParseCertificate(entry.Leaf.TimestampedEntry.X509Entry)
|
||||||
|
if err = s.handleParseEntryError(err, entry.Leaf.TimestampedEntry.EntryType, entry.Index); err != nil {
|
||||||
|
// We hit an unparseable entry, already logged inside handleParseEntryError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.opts.Matcher.CertificateMatches(cert) {
|
||||||
|
entry.X509Cert = cert
|
||||||
|
foundCert(&entry)
|
||||||
|
}
|
||||||
|
case ct.PrecertLogEntryType:
|
||||||
|
c, err := x509.ParseTBSCertificate(entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate)
|
||||||
|
if err = s.handleParseEntryError(err, entry.Leaf.TimestampedEntry.EntryType, entry.Index); err != nil {
|
||||||
|
// We hit an unparseable entry, already logged inside handleParseEntryError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
precert := &ct.Precertificate{
|
||||||
|
Raw: entry.Chain[0],
|
||||||
|
TBSCertificate: *c,
|
||||||
|
IssuerKeyHash: entry.Leaf.TimestampedEntry.PrecertEntry.IssuerKeyHash,
|
||||||
|
}
|
||||||
|
if s.opts.Matcher.PrecertificateMatches(precert) {
|
||||||
|
entry.Precert = precert
|
||||||
|
foundCert(&entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker function to match certs.
|
||||||
|
// Accepts MatcherJobs over the |entries| channel, and processes them.
|
||||||
|
// Returns true over the |done| channel when the |entries| channel is closed.
|
||||||
|
func (s *Scanner) matcherJob(id int, entries <-chan matcherJob, foundCert func(*ct.LogEntry), wg *sync.WaitGroup) {
|
||||||
|
for e := range entries {
|
||||||
|
s.processEntry(e.entry, foundCert)
|
||||||
|
}
|
||||||
|
s.Log(fmt.Sprintf("Matcher %d finished", id))
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (as MatcherJobs) into the
|
||||||
|
// |entries| channel for the matchers to chew on.
|
||||||
|
// Will retry failed attempts to retrieve ranges indefinitely.
|
||||||
|
// Sends true over the |done| channel when the |ranges| channel is closed.
|
||||||
|
func (s *Scanner) fetcherJob(id int, ranges <-chan fetchRange, entries chan<- matcherJob, wg *sync.WaitGroup) {
|
||||||
|
for r := range ranges {
|
||||||
|
success := false
|
||||||
|
// TODO(alcutter): give up after a while:
|
||||||
|
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 {
|
||||||
|
s.Warn(fmt.Sprintf("Problem fetching from log: %s", err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, logEntry := range logEntries {
|
||||||
|
logEntry.Index = r.start
|
||||||
|
entries <- matcherJob{logEntry, r.start}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Log(fmt.Sprintf("Fetcher %d finished", id))
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Scanner) Warn(msg string) {
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) TreeSize() (int64, error) {
|
||||||
|
latestSth, err := s.logClient.GetSTH()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int64(latestSth.TreeSize), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Scan(startIndex int64, endIndex int64, foundCert func(*ct.LogEntry)) error {
|
||||||
|
s.Log("Starting up...\n")
|
||||||
|
|
||||||
|
s.certsProcessed = 0
|
||||||
|
s.unparsableEntries = 0
|
||||||
|
s.entriesWithNonFatalErrors = 0
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
startTime := time.Now()
|
||||||
|
fetches := make(chan fetchRange, 1000)
|
||||||
|
jobs := make(chan matcherJob, 100000)
|
||||||
|
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\n", s.certsProcessed,
|
||||||
|
startIndex+int64(s.certsProcessed), throughput, remainingString))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var ranges list.List
|
||||||
|
for start := startIndex; start < int64(endIndex); {
|
||||||
|
end := min(start+int64(s.opts.BatchSize), int64(endIndex)) - 1
|
||||||
|
ranges.PushBack(fetchRange{start, end})
|
||||||
|
start = end + 1
|
||||||
|
}
|
||||||
|
var fetcherWG sync.WaitGroup
|
||||||
|
var matcherWG sync.WaitGroup
|
||||||
|
// Start matcher workers
|
||||||
|
for w := 0; w < s.opts.NumWorkers; w++ {
|
||||||
|
matcherWG.Add(1)
|
||||||
|
go s.matcherJob(w, jobs, foundCert, &matcherWG)
|
||||||
|
}
|
||||||
|
// Start fetcher workers
|
||||||
|
for w := 0; w < s.opts.ParallelFetch; w++ {
|
||||||
|
fetcherWG.Add(1)
|
||||||
|
go s.fetcherJob(w, fetches, jobs, &fetcherWG)
|
||||||
|
}
|
||||||
|
for r := ranges.Front(); r != nil; r = r.Next() {
|
||||||
|
fetches <- r.Value.(fetchRange)
|
||||||
|
}
|
||||||
|
close(fetches)
|
||||||
|
fetcherWG.Wait()
|
||||||
|
close(jobs)
|
||||||
|
matcherWG.Wait()
|
||||||
|
s.Log(fmt.Sprintf("Completed %d certs in %s", s.certsProcessed, humanTime(int(time.Since(startTime).Seconds()))))
|
||||||
|
s.Log(fmt.Sprintf("%d unparsable entries, %d non-fatal errors", s.unparsableEntries, s.entriesWithNonFatalErrors))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new Scanner instance using |client| to talk to the log, and taking
|
||||||
|
// configuration options from |opts|.
|
||||||
|
func NewScanner(client *client.LogClient, opts ScannerOptions) *Scanner {
|
||||||
|
var scanner Scanner
|
||||||
|
scanner.logClient = client
|
||||||
|
// Set a default match-everything regex if none was provided:
|
||||||
|
if opts.Matcher == nil {
|
||||||
|
opts.Matcher = &MatchAll{}
|
||||||
|
}
|
||||||
|
scanner.opts = opts
|
||||||
|
return &scanner
|
||||||
|
}
|
Loading…
Reference in New Issue