From e5fd2e9efc08b9b1644efa29b0e2ebca381cf425 Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Wed, 4 Jul 2018 19:03:57 -0700 Subject: [PATCH] Initial BygoneSSL support --- README | 16 +++++++ cmd/certspotter/main.go | 102 ++++++++++++++++++++++++++++------------ helpers.go | 4 ++ 3 files changed, 92 insertions(+), 30 deletions(-) diff --git a/README b/README index aebe9c1..4cf81b1 100644 --- a/README +++ b/README @@ -81,6 +81,8 @@ COMMAND LINE FLAGS Default: use the logs trusted by Chromium. -state_dir PATH 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 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 release of Cert Spotter will support gossiping with other log monitors 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. + diff --git a/cmd/certspotter/main.go b/cmd/certspotter/main.go index 49492db..21a876d 100644 --- a/cmd/certspotter/main.go +++ b/cmd/certspotter/main.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "strings" + "time" "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 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 { Domain []string AcceptSuffix bool + NotBefore *time.Time // optional } var watchlist []watchlistItem 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 = ¬BeforeTime + 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{ Domain: []string{}, AcceptSuffix: true, - }, nil - } 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, + NotBefore: notBefore, }, 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) { @@ -125,20 +162,23 @@ func dnsNameMatches(dnsName []string, watchDomain []string, acceptSuffix bool) b return len(watchDomain) == 0 && (acceptSuffix || len(dnsName) == 0) } -func dnsNameIsWatched(dnsName string) bool { - labels := strings.Split(dnsName, ".") - for _, item := range watchlist { - if dnsNameMatches(labels, item.Domain, item.AcceptSuffix) { - return true - } - } - return false -} - -func anyDnsNameIsWatched(dnsNames []string) bool { +func anyDnsNameIsWatched(info *certspotter.EntryInfo) bool { + dnsNames := info.Identifiers.DNSNames for _, dnsName := range dnsNames { - if dnsNameIsWatched(dnsName) { - return true + labels := strings.Split(dnsName, ".") + 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 @@ -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 // 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. - if info.Identifiers == nil || anyDnsNameIsWatched(info.Identifiers.DNSNames) { - cmd.LogEntry(&info) + if info.Identifiers == nil || anyDnsNameIsWatched(&info) { + if !*bygoneSSL || info.Bygone { + cmd.LogEntry(&info) + } } } diff --git a/helpers.go b/helpers.go index 1e287d5..4b5f474 100644 --- a/helpers.go +++ b/helpers.go @@ -106,6 +106,7 @@ type EntryInfo struct { Identifiers *Identifiers IdentifiersParseError error Filename string + Bygone bool } 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, "Not Before", info.CertInfo.NotBefore(), 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, "crt.sh", "https://crt.sh/?sha256="+fingerprint, nil)