Initial commit
This commit is contained in:
commit
a2a7921cb8
20 changed files with 1868 additions and 0 deletions
203
checker/ccadb.go
Normal file
203
checker/ccadb.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:generate wget -O AllCAAIdentifiersReport.csv https://ccadb.my.salesforce-sites.com/ccadb/AllCAAIdentifiersReportCSVV2
|
||||
//go:embed AllCAAIdentifiersReport.csv
|
||||
var ccadbCSV []byte
|
||||
|
||||
// ccadbIndex is the in-memory representation of AllCAAIdentifiersReport.csv.
|
||||
// Two indexes are maintained because CCADB rows sometimes have an empty
|
||||
// Subject Key Identifier column (very rare; a handful of legacy entries)
|
||||
// and we want to still resolve those via Subject DN.
|
||||
type ccadbIndex struct {
|
||||
bySKI map[string][]string
|
||||
byDN map[string][]string
|
||||
}
|
||||
|
||||
var (
|
||||
ccadbOnce sync.Once
|
||||
ccadb *ccadbIndex
|
||||
ccadbErr error
|
||||
)
|
||||
|
||||
// loadCCADB parses the embedded CSV once. Failure means the binary
|
||||
// itself is broken.
|
||||
func loadCCADB() (*ccadbIndex, error) {
|
||||
ccadbOnce.Do(func() {
|
||||
ccadb, ccadbErr = parseCCADB(bytes.NewReader(ccadbCSV))
|
||||
})
|
||||
return ccadb, ccadbErr
|
||||
}
|
||||
|
||||
// parseCCADB is exposed for testing with alternate CSV inputs.
|
||||
func parseCCADB(r io.Reader) (*ccadbIndex, error) {
|
||||
reader := csv.NewReader(r)
|
||||
reader.FieldsPerRecord = -1 // some rows carry a trailing empty field
|
||||
|
||||
header, err := reader.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read header: %w", err)
|
||||
}
|
||||
|
||||
idxSubject := -1
|
||||
idxSKI := -1
|
||||
idxDomains := -1
|
||||
for i, h := range header {
|
||||
switch strings.TrimSpace(h) {
|
||||
case "Subject":
|
||||
idxSubject = i
|
||||
case "Subject Key Identifier (Hex)":
|
||||
idxSKI = i
|
||||
case "Recognized CAA Domains":
|
||||
idxDomains = i
|
||||
}
|
||||
}
|
||||
if idxSubject < 0 || idxSKI < 0 || idxDomains < 0 {
|
||||
return nil, fmt.Errorf("unexpected CCADB header: %v", header)
|
||||
}
|
||||
minCols := max(idxSubject, idxSKI, idxDomains)
|
||||
|
||||
idx := &ccadbIndex{
|
||||
bySKI: map[string][]string{},
|
||||
byDN: map[string][]string{},
|
||||
}
|
||||
for {
|
||||
row, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read row: %w", err)
|
||||
}
|
||||
if len(row) <= minCols {
|
||||
continue
|
||||
}
|
||||
|
||||
domains := splitCAADomains(row[idxDomains])
|
||||
if len(domains) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if ski := strings.ToUpper(strings.TrimSpace(row[idxSKI])); ski != "" {
|
||||
idx.bySKI[ski] = mergeDomains(idx.bySKI[ski], domains)
|
||||
}
|
||||
if dn := normalizeDN(row[idxSubject]); dn != "" {
|
||||
idx.byDN[dn] = mergeDomains(idx.byDN[dn], domains)
|
||||
}
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
// Lookup resolves an observed issuer to its CAA identifier domains.
|
||||
// AKI takes precedence; DN is the fallback for rows without an SKI.
|
||||
// The returned slice is a fresh copy; callers may retain or mutate it.
|
||||
func Lookup(aki, dn string) ([]string, bool) {
|
||||
idx, err := loadCCADB()
|
||||
if err != nil || idx == nil {
|
||||
return nil, false
|
||||
}
|
||||
if aki != "" {
|
||||
if d, ok := idx.bySKI[strings.ToUpper(strings.TrimSpace(aki))]; ok && len(d) > 0 {
|
||||
return append([]string(nil), d...), true
|
||||
}
|
||||
}
|
||||
if dn != "" {
|
||||
if d, ok := idx.byDN[normalizeDN(dn)]; ok && len(d) > 0 {
|
||||
return append([]string(nil), d...), true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// splitCAADomains lowercases because CAA identifiers are case-insensitive.
|
||||
func splitCAADomains(raw string) []string {
|
||||
var out []string
|
||||
for d := range strings.SplitSeq(raw, ",") {
|
||||
d = strings.TrimSpace(strings.ToLower(d))
|
||||
if d != "" {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// mergeDomains appends new entries to an existing slice, de-duplicating.
|
||||
// CCADB occasionally lists the same CA twice (cross-signs, re-issues);
|
||||
// we don't want that to bloat the lookup result.
|
||||
func mergeDomains(existing, add []string) []string {
|
||||
if len(existing) == 0 {
|
||||
return append([]string(nil), add...)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, d := range existing {
|
||||
seen[d] = true
|
||||
}
|
||||
for _, d := range add {
|
||||
if !seen[d] {
|
||||
existing = append(existing, d)
|
||||
seen[d] = true
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
// normalizeDN canonicalizes a subject DN so Go's comma-joined form
|
||||
// compares equal to CCADB's semicolon-joined form for the same RDNs.
|
||||
// Intentionally permissive: escaping differences are ignored; AKI is
|
||||
// the common path anyway.
|
||||
func normalizeDN(dn string) string {
|
||||
if dn == "" {
|
||||
return ""
|
||||
}
|
||||
fields := splitRDNs(dn)
|
||||
for i, f := range fields {
|
||||
f = strings.TrimSpace(f)
|
||||
if eq := strings.IndexByte(f, '='); eq > 0 {
|
||||
f = strings.ToUpper(f[:eq]) + "=" + strings.TrimSpace(f[eq+1:])
|
||||
}
|
||||
fields[i] = f
|
||||
}
|
||||
sort.Strings(fields)
|
||||
return strings.Join(fields, ",")
|
||||
}
|
||||
|
||||
// splitRDNs splits a DN string on either ',' or ';', respecting
|
||||
// backslash escapes. Most RDN values in CCADB do not contain escaped
|
||||
// separators, but a handful (paths in OU values) do.
|
||||
func splitRDNs(dn string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
escape := false
|
||||
for i := 0; i < len(dn); i++ {
|
||||
c := dn[i]
|
||||
if escape {
|
||||
cur.WriteByte(c)
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
switch c {
|
||||
case '\\':
|
||||
cur.WriteByte(c)
|
||||
escape = true
|
||||
case ',', ';':
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
default:
|
||||
cur.WriteByte(c)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue