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 ""
}
}
func defaultKeyListPath() string {
return filepath.Join(defaultConfigDir(), "keylist")
}
func defaultKeyListPathIfExists() string {
if fileExists(defaultKeyListPath()) {
return defaultKeyListPath()
} else {
return ""
}
}
func defaultScriptDir() string {
return filepath.Join(defaultConfigDir(), "hooks.d")
}
@ -124,6 +134,15 @@ func readWatchListFile(filename string) (monitor.WatchList, error) {
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) {
file, err := os.Open(filename)
if err != nil {
@ -166,6 +185,7 @@ func main() {
verbose bool
version bool
watchlist string
keylist string
}
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))
@ -179,6 +199,7 @@ func main() {
flag.BoolVar(&flags.verbose, "verbose", false, "Be verbose")
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.keylist, "keylist", defaultKeyListPathIfExists(), "File containing known key information")
flag.Parse()
if flags.version {
@ -242,6 +263,15 @@ func main() {
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)
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 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
When certspotter detects a certificate matching your watchlist, or encounters

View File

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

View File

@ -23,6 +23,7 @@ import (
type DiscoveredCert struct {
WatchItem WatchItem
KeyItem KeyItem
LogEntry *LogEntry
Info *certspotter.CertInfo
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 {
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 {
return nil
}
matched, keyItem := config.KeyList.Matches(certInfo)
cert := &DiscoveredCert{
WatchItem: watchItem,
LogEntry: entry,
Info: certInfo,
KeyItem: keyItem,
Chain: chain,
TBSSHA256: sha256.Sum256(certInfo.TBS.Raw),
SHA256: sha256.Sum256(chain[0]),