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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -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 = ¬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{
|
return watchlistItem{
|
||||||
Domain: []string{},
|
Domain: []string{},
|
||||||
AcceptSuffix: true,
|
AcceptSuffix: true,
|
||||||
|
NotBefore: notBefore,
|
||||||
}, nil
|
}, nil
|
||||||
} else {
|
|
||||||
acceptSuffix := false
|
|
||||||
if strings.HasPrefix(str, ".") {
|
|
||||||
acceptSuffix = true
|
|
||||||
str = str[1:]
|
|
||||||
}
|
}
|
||||||
asciiDomain, err := idna.ToASCII(strings.ToLower(trimTrailingDots(str)))
|
|
||||||
|
acceptSuffix := false
|
||||||
|
if strings.HasPrefix(domain, ".") {
|
||||||
|
acceptSuffix = true
|
||||||
|
domain = domain[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
asciiDomain, err := idna.ToASCII(strings.ToLower(trimTrailingDots(domain)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return watchlistItem{}, fmt.Errorf("Invalid domain `%s': %s", str, err)
|
return watchlistItem{}, fmt.Errorf("Invalid domain `%s': %s", domain, err)
|
||||||
}
|
}
|
||||||
return watchlistItem{
|
return watchlistItem{
|
||||||
Domain: strings.Split(asciiDomain, "."),
|
Domain: strings.Split(asciiDomain, "."),
|
||||||
AcceptSuffix: acceptSuffix,
|
AcceptSuffix: acceptSuffix,
|
||||||
|
NotBefore: notBefore,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readWatchlist(reader io.Reader) ([]watchlistItem, error) {
|
func readWatchlist(reader io.Reader) ([]watchlistItem, error) {
|
||||||
|
@ -125,22 +162,25 @@ 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 {
|
||||||
|
dnsNames := info.Identifiers.DNSNames
|
||||||
|
for _, dnsName := range dnsNames {
|
||||||
labels := strings.Split(dnsName, ".")
|
labels := strings.Split(dnsName, ".")
|
||||||
for _, item := range watchlist {
|
for _, item := range watchlist {
|
||||||
if dnsNameMatches(labels, item.Domain, item.AcceptSuffix) {
|
if dnsNameMatches(labels, item.Domain, item.AcceptSuffix) {
|
||||||
return true
|
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 false
|
|
||||||
}
|
|
||||||
|
|
||||||
func anyDnsNameIsWatched(dnsNames []string) bool {
|
|
||||||
for _, dnsName := range dnsNames {
|
|
||||||
if dnsNameIsWatched(dnsName) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,9 +202,11 @@ 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) {
|
||||||
|
if !*bygoneSSL || info.Bygone {
|
||||||
cmd.LogEntry(&info)
|
cmd.LogEntry(&info)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue