support filtering

This commit is contained in:
Dirk Stöcker 2024-05-23 15:44:12 +02:00
parent cd4d796a7c
commit 6f019acd7b
6 changed files with 121 additions and 1 deletions

View File

@ -99,6 +99,16 @@ func defaultWatchListPathIfExists() string {
return "" return ""
} }
} }
func defaultKeyListPath() string {
return filepath.Join(defaultConfigDir(), "keylist")
}
func defaultKeyListPathIfExists() string {
if fileExists(defaultKeyListPath()) {
return defaultKeyListPath()
} else {
return ""
}
}
func defaultScriptDir() string { func defaultScriptDir() string {
return filepath.Join(defaultConfigDir(), "hooks.d") return filepath.Join(defaultConfigDir(), "hooks.d")
} }
@ -124,6 +134,15 @@ func readWatchListFile(filename string) (monitor.WatchList, error) {
return monitor.ReadWatchList(file) return monitor.ReadWatchList(file)
} }
func readKeyListFile(filename string) (monitor.KeyList, error) {
file, err := os.Open(filename)
if err != nil {
return nil, simplifyError(err)
}
defer file.Close()
return monitor.ReadKeyList(file)
}
func readEmailFile(filename string) ([]string, error) { func readEmailFile(filename string) ([]string, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
@ -166,6 +185,7 @@ func main() {
verbose bool verbose bool
version bool version bool
watchlist string watchlist string
keylist string
} }
flag.IntVar(&flags.batchSize, "batch_size", 1000, "Max number of entries to request per call to get-entries (advanced)") flag.IntVar(&flags.batchSize, "batch_size", 1000, "Max number of entries to request per call to get-entries (advanced)")
flag.Func("email", "Email address to contact when matching certificate is discovered (repeatable)", appendFunc(&flags.email)) flag.Func("email", "Email address to contact when matching certificate is discovered (repeatable)", appendFunc(&flags.email))
@ -179,6 +199,7 @@ func main() {
flag.BoolVar(&flags.verbose, "verbose", false, "Be verbose") flag.BoolVar(&flags.verbose, "verbose", false, "Be verbose")
flag.BoolVar(&flags.version, "version", false, "Print version and exit") flag.BoolVar(&flags.version, "version", false, "Print version and exit")
flag.StringVar(&flags.watchlist, "watchlist", defaultWatchListPathIfExists(), "File containing domain names to watch") flag.StringVar(&flags.watchlist, "watchlist", defaultWatchListPathIfExists(), "File containing domain names to watch")
flag.StringVar(&flags.keylist, "keylist", defaultKeyListPathIfExists(), "File containing known key information")
flag.Parse() flag.Parse()
if flags.version { if flags.version {
@ -242,6 +263,15 @@ func main() {
config.WatchList = watchlist config.WatchList = watchlist
} }
if flags.keylist != "" {
keylist, err := readKeyListFile(flags.keylist)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: error reading keylist from %q: %s\n", programName, flags.keylist, err)
os.Exit(1)
}
config.KeyList = keylist
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() defer stop()

View File

@ -115,6 +115,11 @@ You can use Cert Spotter to detect:
certspotter reads the watch list only when starting up, so you must restart certspotter reads the watch list only when starting up, so you must restart
certspotter if you change it. certspotter if you change it.
-keylist *PATH*
: File containing known key information, one per line. A line consist of a
identifier and the sha256 of the public key separated by semi colon
# NOTIFICATIONS # NOTIFICATIONS
When certspotter detects a certificate matching your watchlist, or encounters When certspotter detects a certificate matching your watchlist, or encounters

View File

@ -18,6 +18,7 @@ type Config struct {
State StateProvider State StateProvider
StartAtEnd bool StartAtEnd bool
WatchList WatchList WatchList WatchList
KeyList KeyList
Verbose bool Verbose bool
HealthCheckInterval time.Duration HealthCheckInterval time.Duration
} }

View File

@ -23,6 +23,7 @@ import (
type DiscoveredCert struct { type DiscoveredCert struct {
WatchItem WatchItem WatchItem WatchItem
KeyItem KeyItem
LogEntry *LogEntry LogEntry *LogEntry
Info *certspotter.CertInfo Info *certspotter.CertInfo
Chain []ct.ASN1Cert // first entry is the leaf certificate or precertificate Chain []ct.ASN1Cert // first entry is the leaf certificate or precertificate
@ -172,5 +173,9 @@ func certNotificationText(cert *DiscoveredCert, paths *certPaths) string {
} }
func certNotificationSummary(cert *DiscoveredCert) string { func certNotificationSummary(cert *DiscoveredCert) string {
return fmt.Sprintf("Certificate Discovered for %s", cert.WatchItem) keytext := ""
if cert.KeyItem.String() != "" {
keytext = fmt.Sprintf(" (known as %s)", cert.KeyItem.String())
}
return fmt.Sprintf("Certificate Discovered for %s%s", cert.WatchItem, keytext)
} }

77
monitor/keylist.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright (C) 2016, 2023 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package monitor
import (
"bufio"
"encoding/hex"
"crypto/sha256"
"fmt"
"io"
"software.sslmate.com/src/certspotter"
"strings"
)
type KeyItem struct {
domain string
keyinfo string
}
type KeyList []KeyItem
func ParseKeyItem(str string) (KeyItem, error) {
fields := strings.Split(str, ";")
if len(fields) == 0 {
return KeyItem{}, fmt.Errorf("empty domain")
}
if len(fields) == 1 {
return KeyItem{}, fmt.Errorf("empty key info")
}
return KeyItem{
domain: fields[0],
keyinfo: fields[1],
}, nil
}
func ReadKeyList(reader io.Reader) (KeyList, error) {
items := make(KeyList, 0, 50)
scanner := bufio.NewScanner(reader)
lineNo := 0
for scanner.Scan() {
line := scanner.Text()
lineNo++
if line == "" || strings.HasPrefix(line, "#") {
continue
}
item, err := ParseKeyItem(line)
if err != nil {
return nil, fmt.Errorf("%w on line %d", err, lineNo)
}
items = append(items, item)
}
return items, scanner.Err()
}
func (item KeyItem) String() string {
return item.domain
}
func (list KeyList) Matches(certInfo *certspotter.CertInfo) (bool, KeyItem) {
for _, item := range list {
// better would be to convert the input and compare the bytes (no uppercase/lower-case issue)
pub := sha256.Sum256(certInfo.TBS.PublicKey.FullBytes)
sum := hex.EncodeToString(pub[:])
if sum == item.keyinfo {
return true, item
}
}
return false, KeyItem{}
}

View File

@ -91,11 +91,13 @@ func processCertificate(ctx context.Context, config *Config, entry *LogEntry, ce
if !matched { if !matched {
return nil return nil
} }
matched, keyItem := config.KeyList.Matches(certInfo)
cert := &DiscoveredCert{ cert := &DiscoveredCert{
WatchItem: watchItem, WatchItem: watchItem,
LogEntry: entry, LogEntry: entry,
Info: certInfo, Info: certInfo,
KeyItem: keyItem,
Chain: chain, Chain: chain,
TBSSHA256: sha256.Sum256(certInfo.TBS.Raw), TBSSHA256: sha256.Sum256(certInfo.TBS.Raw),
SHA256: sha256.Sum256(chain[0]), SHA256: sha256.Sum256(chain[0]),