mirror of
https://github.com/SSLMate/certspotter.git
synced 2025-06-27 10:15:33 +02:00

Specifically, certspotter no longer terminates unless it receives SIGTERM or SIGINT or there is a serious error. Although using cron made sense in the early days of Certificate Transparency, certspotter now needs to run continuously to reliably keep up with the high growth rate of contemporary CT logs, and to gracefully handle the many transient errors that can arise when monitoring CT. Closes: #63 Closes: #37 Closes: #32 (presumably by eliminating $DNS_NAMES and $IP_ADDRESSES) Closes: #21 (with $WATCH_ITEM) Closes: #25
136 lines
3.3 KiB
Go
136 lines
3.3 KiB
Go
// 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"
|
|
"fmt"
|
|
"golang.org/x/net/idna"
|
|
"io"
|
|
"software.sslmate.com/src/certspotter"
|
|
"strings"
|
|
)
|
|
|
|
type WatchItem struct {
|
|
domain []string
|
|
acceptSuffix bool
|
|
}
|
|
|
|
type WatchList []WatchItem
|
|
|
|
func ParseWatchItem(str string) (WatchItem, error) {
|
|
fields := strings.Fields(str)
|
|
if len(fields) == 0 {
|
|
return WatchItem{}, fmt.Errorf("empty domain")
|
|
}
|
|
domain := fields[0]
|
|
|
|
for _, field := range fields[1:] {
|
|
switch {
|
|
case strings.HasPrefix(field, "valid_at:"):
|
|
// Ignore for backwards compatibility
|
|
default:
|
|
return WatchItem{}, fmt.Errorf("unknown parameter %q", field)
|
|
}
|
|
}
|
|
|
|
if domain == "." {
|
|
// "." as in root zone -> matches everything
|
|
return WatchItem{
|
|
domain: []string{},
|
|
acceptSuffix: true,
|
|
}, nil
|
|
}
|
|
|
|
acceptSuffix := false
|
|
if strings.HasPrefix(domain, ".") {
|
|
acceptSuffix = true
|
|
domain = domain[1:]
|
|
}
|
|
|
|
asciiDomain, err := idna.ToASCII(strings.ToLower(strings.TrimRight(domain, ".")))
|
|
if err != nil {
|
|
return WatchItem{}, fmt.Errorf("invalid domain %q (%w)", domain, err)
|
|
}
|
|
return WatchItem{
|
|
domain: strings.Split(asciiDomain, "."),
|
|
acceptSuffix: acceptSuffix,
|
|
}, nil
|
|
}
|
|
|
|
func ReadWatchList(reader io.Reader) (WatchList, error) {
|
|
items := make(WatchList, 0, 50)
|
|
scanner := bufio.NewScanner(reader)
|
|
lineNo := 0
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
lineNo++
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
item, err := ParseWatchItem(line)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w on line %d", err, lineNo)
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, scanner.Err()
|
|
}
|
|
|
|
func (item WatchItem) String() string {
|
|
if item.acceptSuffix {
|
|
return "." + strings.Join(item.domain, ".")
|
|
} else {
|
|
return strings.Join(item.domain, ".")
|
|
}
|
|
}
|
|
|
|
func (item WatchItem) matchesDNSName(dnsName []string) bool {
|
|
watchDomain := item.domain
|
|
for len(dnsName) > 0 && len(watchDomain) > 0 {
|
|
certLabel := dnsName[len(dnsName)-1]
|
|
watchLabel := watchDomain[len(watchDomain)-1]
|
|
|
|
if !dnsLabelMatches(certLabel, watchLabel) {
|
|
return false
|
|
}
|
|
|
|
dnsName = dnsName[:len(dnsName)-1]
|
|
watchDomain = watchDomain[:len(watchDomain)-1]
|
|
}
|
|
return len(watchDomain) == 0 && (item.acceptSuffix || len(dnsName) == 0)
|
|
}
|
|
|
|
func dnsLabelMatches(certLabel string, watchLabel string) bool {
|
|
// For fail-safe behavior, if a label was unparsable, it matches everything.
|
|
// Similarly, redacted labels match everything, since the label _might_ be
|
|
// for a name we're interested in.
|
|
|
|
return certLabel == "*" ||
|
|
certLabel == "?" ||
|
|
certLabel == certspotter.UnparsableDNSLabelPlaceholder ||
|
|
certspotter.MatchesWildcard(watchLabel, certLabel)
|
|
}
|
|
|
|
func (list WatchList) Matches(identifiers *certspotter.Identifiers) (bool, WatchItem) {
|
|
dnsNames := make([][]string, len(identifiers.DNSNames))
|
|
for i, dnsName := range identifiers.DNSNames {
|
|
dnsNames[i] = strings.Split(dnsName, ".")
|
|
}
|
|
for _, item := range list {
|
|
for _, dnsName := range dnsNames {
|
|
if item.matchesDNSName(dnsName) {
|
|
return true, item
|
|
}
|
|
}
|
|
}
|
|
return false, WatchItem{}
|
|
}
|