mirror of
				https://github.com/SSLMate/certspotter.git
				synced 2025-07-03 10:47:17 +02:00 
			
		
		
		
	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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user