Initial BygoneSSL support
This commit is contained in:
parent
ca1acc7d77
commit
e5fd2e9efc
16
README
16
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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue