Initial BygoneSSL support

This commit is contained in:
Ian Foster 2018-07-04 19:03:57 -07:00
parent ca1acc7d77
commit e5fd2e9efc
3 changed files with 92 additions and 30 deletions

16
README
View File

@ -81,6 +81,8 @@ COMMAND LINE FLAGS
Default: use the logs trusted by Chromium. Default: use the logs trusted by Chromium.
-state_dir PATH -state_dir PATH
Directory for storing state. Default: ~/.certspotter Directory for storing state. Default: ~/.certspotter
-bygonessl
Only print certificates which predate domain registration and live into it (requires 'issued_before' option in watchlist)
-verbose -verbose
Be verbose. Be verbose.
@ -130,3 +132,17 @@ Cert Spotter is not just a log monitor, but also a log auditor which
checks that the log is obeying its append-only property. A future checks that the log is obeying its append-only property. A future
release of Cert Spotter will support gossiping with other log monitors release of Cert Spotter will support gossiping with other log monitors
to ensure the log is presenting a single view. to ensure the log is presenting a single view.
BygoneSSL
Cert Spotter can also notify users of bygone SSL certificates, which are SSL
certificates that outlived their prior domain owner's registration into the
next owners registration. To detect these certificates add an issued_before
argument to each domain in the watchlist followed by the date the domain was
registered in t he following format YYYY-MM-DD. For example:
example.com issued_before:2014-05-02
The optional -bygonessl flag will cause Cert Spotter to only match bygone SSL
certificates.

View File

@ -17,6 +17,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"golang.org/x/net/idna" "golang.org/x/net/idna"
@ -50,35 +51,71 @@ func trimTrailingDots(value string) string {
var stateDir = flag.String("state_dir", defaultStateDir(), "Directory for storing state") var stateDir = flag.String("state_dir", defaultStateDir(), "Directory for storing state")
var watchlistFilename = flag.String("watchlist", filepath.Join(defaultConfigDir(), "watchlist"), "File containing identifiers to watch (- for stdin)") var watchlistFilename = flag.String("watchlist", filepath.Join(defaultConfigDir(), "watchlist"), "File containing identifiers to watch (- for stdin)")
var bygoneSSL = flag.Bool("bygonessl", false, "Only print certificates which predate domain registration and live into it (requires 'issued_before' option in watchlist)")
type watchlistItem struct { type watchlistItem struct {
Domain []string Domain []string
AcceptSuffix bool AcceptSuffix bool
NotBefore *time.Time // optional
} }
var watchlist []watchlistItem var watchlist []watchlistItem
func parseWatchlistItem(str string) (watchlistItem, error) { func parseWatchlistItem(str string) (watchlistItem, error) {
if str == "." { // "." as in root zone (matches everything) fields := strings.Fields(str)
if len(fields) == 0 {
return watchlistItem{}, fmt.Errorf("Empty domain")
}
domain := fields[0]
var notBefore *time.Time = nil
// parse options
for i := 1; i < len(fields); i++ {
chunks := strings.SplitN(fields[i], ":", 2)
if len(chunks) != 2 {
return watchlistItem{}, fmt.Errorf("Missing Value `%s'", fields[i])
}
switch chunks[0] {
case "issued_before":
notBeforeTime, err := time.Parse("2006-01-02", chunks[1])
if err != nil {
return watchlistItem{}, fmt.Errorf("Invalid Date `%s': %s", chunks[1], err)
}
notBefore = &notBeforeTime
default:
return watchlistItem{}, fmt.Errorf("Unknown Option `%s'", fields[i])
}
}
if *bygoneSSL && notBefore == nil {
return watchlistItem{}, fmt.Errorf("`%s' must have issued_before argument when using -bygonessl", domain)
}
// parse domain
// "." as in root zone (matches everything)
if domain == "." {
return watchlistItem{ return watchlistItem{
Domain: []string{}, Domain: []string{},
AcceptSuffix: true, AcceptSuffix: true,
}, nil NotBefore: notBefore,
} else {
acceptSuffix := false
if strings.HasPrefix(str, ".") {
acceptSuffix = true
str = str[1:]
}
asciiDomain, err := idna.ToASCII(strings.ToLower(trimTrailingDots(str)))
if err != nil {
return watchlistItem{}, fmt.Errorf("Invalid domain `%s': %s", str, err)
}
return watchlistItem{
Domain: strings.Split(asciiDomain, "."),
AcceptSuffix: acceptSuffix,
}, nil }, nil
} }
acceptSuffix := false
if strings.HasPrefix(domain, ".") {
acceptSuffix = true
domain = domain[1:]
}
asciiDomain, err := idna.ToASCII(strings.ToLower(trimTrailingDots(domain)))
if err != nil {
return watchlistItem{}, fmt.Errorf("Invalid domain `%s': %s", domain, err)
}
return watchlistItem{
Domain: strings.Split(asciiDomain, "."),
AcceptSuffix: acceptSuffix,
NotBefore: notBefore,
}, nil
} }
func readWatchlist(reader io.Reader) ([]watchlistItem, error) { func readWatchlist(reader io.Reader) ([]watchlistItem, error) {
@ -125,20 +162,23 @@ func dnsNameMatches(dnsName []string, watchDomain []string, acceptSuffix bool) b
return len(watchDomain) == 0 && (acceptSuffix || len(dnsName) == 0) return len(watchDomain) == 0 && (acceptSuffix || len(dnsName) == 0)
} }
func dnsNameIsWatched(dnsName string) bool { func anyDnsNameIsWatched(info *certspotter.EntryInfo) bool {
labels := strings.Split(dnsName, ".") dnsNames := info.Identifiers.DNSNames
for _, item := range watchlist {
if dnsNameMatches(labels, item.Domain, item.AcceptSuffix) {
return true
}
}
return false
}
func anyDnsNameIsWatched(dnsNames []string) bool {
for _, dnsName := range dnsNames { for _, dnsName := range dnsNames {
if dnsNameIsWatched(dnsName) { labels := strings.Split(dnsName, ".")
return true for _, item := range watchlist {
if dnsNameMatches(labels, item.Domain, item.AcceptSuffix) {
if item.NotBefore != nil {
// BygoneSSL Check
// was the SSL certificate issued before the domain was registered
// and valid after
if item.NotBefore.Before(*info.CertInfo.NotAfter()) &&
item.NotBefore.After(*info.CertInfo.NotBefore()) {
info.Bygone = true
}
}
return true
}
} }
} }
return false return false
@ -162,8 +202,10 @@ func processEntry(scanner *certspotter.Scanner, entry *ct.LogEntry) {
// parse error), report the certificate because we can't say for sure it // parse error), report the certificate because we can't say for sure it
// doesn't match a domain we care about. We try very hard to make sure // doesn't match a domain we care about. We try very hard to make sure
// parsing identifiers always succeeds, so false alarms should be rare. // parsing identifiers always succeeds, so false alarms should be rare.
if info.Identifiers == nil || anyDnsNameIsWatched(info.Identifiers.DNSNames) { if info.Identifiers == nil || anyDnsNameIsWatched(&info) {
cmd.LogEntry(&info) if !*bygoneSSL || info.Bygone {
cmd.LogEntry(&info)
}
} }
} }

View File

@ -106,6 +106,7 @@ type EntryInfo struct {
Identifiers *Identifiers Identifiers *Identifiers
IdentifiersParseError error IdentifiersParseError error
Filename string Filename string
Bygone bool
} }
type CertInfo struct { type CertInfo struct {
@ -335,6 +336,9 @@ func (info *EntryInfo) Write(out io.Writer) {
writeField(out, "Issuer", info.CertInfo.Issuer, info.CertInfo.IssuerParseError) writeField(out, "Issuer", info.CertInfo.Issuer, info.CertInfo.IssuerParseError)
writeField(out, "Not Before", info.CertInfo.NotBefore(), info.CertInfo.ValidityParseError) writeField(out, "Not Before", info.CertInfo.NotBefore(), info.CertInfo.ValidityParseError)
writeField(out, "Not After", info.CertInfo.NotAfter(), info.CertInfo.ValidityParseError) writeField(out, "Not After", info.CertInfo.NotAfter(), info.CertInfo.ValidityParseError)
if info.Bygone {
writeField(out, "BygoneSSL", "True", info.CertInfo.ValidityParseError)
}
} }
writeField(out, "Log Entry", fmt.Sprintf("%d @ %s (%s)", info.Entry.Index, info.LogUri, info.typeFriendlyString()), nil) writeField(out, "Log Entry", fmt.Sprintf("%d @ %s (%s)", info.Entry.Index, info.LogUri, info.typeFriendlyString()), nil)
writeField(out, "crt.sh", "https://crt.sh/?sha256="+fingerprint, nil) writeField(out, "crt.sh", "https://crt.sh/?sha256="+fingerprint, nil)