Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Indicate known certificates in sent mails feature #90

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions cmd/certspotter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
Expand All @@ -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 {
Expand Down Expand Up @@ -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()

Expand Down
5 changes: 5 additions & 0 deletions man/certspotter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions monitor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Config struct {
State StateProvider
StartAtEnd bool
WatchList WatchList
KeyList KeyList
Verbose bool
HealthCheckInterval time.Duration
}
7 changes: 6 additions & 1 deletion monitor/discoveredcert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 77 additions & 0 deletions monitor/keylist.go
Original file line number Diff line number Diff line change
@@ -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{}
}
2 changes: 2 additions & 0 deletions monitor/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down