128 lines
4.9 KiB
Go
128 lines
4.9 KiB
Go
// Package checker implements the DANE/TLSA checker for happyDomain.
|
|
//
|
|
// This checker is bound to the svcs.TLSAs service. Collect takes the TLSA
|
|
// records the user published (or plans to publish) for the service, derives
|
|
// one TLS endpoint per distinct (port, proto, base name), and declares those
|
|
// endpoints as tls.endpoint.v1 discovery entries. checker-tls then probes
|
|
// them; on the next evaluation, this checker reads the related TLS probes
|
|
// via obs.GetRelated and verifies each TLSA record matches the certificate
|
|
// chain the probe observed.
|
|
//
|
|
// The user-visible contract matches what DANE deployers expect:
|
|
//
|
|
// - Usage 0 (PKIX-TA) / 1 (PKIX-EE): also require the PKIX chain to be
|
|
// publicly trusted.
|
|
// - Usage 2 (DANE-TA) / 3 (DANE-EE): trust the TLSA as the anchor; PKIX
|
|
// validity is informational.
|
|
// - Selector 0 (Cert) / 1 (SPKI) and matching types 0/1/2 (Full/SHA-256/
|
|
// SHA-512) are matched against the chain slot implied by the usage.
|
|
package checker
|
|
|
|
import "time"
|
|
|
|
// ObservationKeyDANE is the observation key this checker writes.
|
|
const ObservationKeyDANE = "dane_checks"
|
|
|
|
// Option ids on CheckerOptions.
|
|
const (
|
|
// OptionService is auto-filled by the happyDomain host with the
|
|
// svcs.TLSAs service payload this checker is bound to.
|
|
OptionService = "service"
|
|
|
|
// OptionDomain is auto-filled with the domain apex. TLSA owner names
|
|
// in the service are relative to this apex.
|
|
OptionDomain = "domain_name"
|
|
|
|
// OptionSubdomain is the optional sub-zone under which the TLSAs
|
|
// service lives (matches the svcs.TLSAs analyzer's subdomain bucket).
|
|
OptionSubdomain = "subdomain"
|
|
|
|
// OptionProbeTimeoutMs is how long each underlying TLS probe is allowed.
|
|
// Passed through to checker-tls verbatim via the discovery entry options.
|
|
OptionProbeTimeoutMs = "probeTimeoutMs"
|
|
|
|
// OptionSTARTTLS is an optional per-endpoint STARTTLS hint keyed by
|
|
// "<port>/<proto>" → RFC 6335 service name (e.g. "25/tcp" → "smtp",
|
|
// "587/tcp" → "submission"). Common ports auto-map via a built-in table.
|
|
OptionSTARTTLS = "starttls"
|
|
|
|
// OptionDNSSECValidated reports whether the TLSA records the host
|
|
// submitted to this checker came from a DNSSEC-validated lookup.
|
|
// Only set by the standalone interactive flow; absent in managed mode
|
|
// where TLSA records come from the user's authoritative zone config.
|
|
OptionDNSSECValidated = "dnssec_validated"
|
|
)
|
|
|
|
// Severity constants mirror checker-tls.
|
|
const (
|
|
SeverityCrit = "crit"
|
|
SeverityWarn = "warn"
|
|
SeverityInfo = "info"
|
|
)
|
|
|
|
// TLSA field enum constants (RFC 6698 §2.1).
|
|
const (
|
|
UsagePKIXTA uint8 = 0
|
|
UsagePKIXEE uint8 = 1
|
|
UsageDANETA uint8 = 2
|
|
UsageDANEEE uint8 = 3
|
|
|
|
SelectorCert uint8 = 0
|
|
SelectorSPKI uint8 = 1
|
|
|
|
MatchingFull uint8 = 0
|
|
MatchingSHA256 uint8 = 1
|
|
MatchingSHA512 uint8 = 2
|
|
)
|
|
|
|
// DANEData is the full payload the checker writes under ObservationKeyDANE.
|
|
type DANEData struct {
|
|
// Targets is one entry per (port, proto, basename) triplet extracted
|
|
// from the TLSAs service.
|
|
Targets []TargetResult `json:"targets"`
|
|
// Invalid lists TLSA records that could not be parsed into a usable
|
|
// endpoint (malformed owner name, out-of-range port, etc.). They are
|
|
// surfaced by hasRecordsRule so a misconfigured zone fails loudly
|
|
// instead of silently passing as "no records".
|
|
Invalid []InvalidRecord `json:"invalid,omitempty"`
|
|
// DNSSECValidated reflects whether the resolver that fetched the TLSA
|
|
// records set the AD bit. Only populated by the standalone interactive
|
|
// flow (lookupTLSA); nil in managed mode where records come from the
|
|
// user's zone config and DNSSEC posture is checked elsewhere.
|
|
DNSSECValidated *bool `json:"dnssec_validated,omitempty"`
|
|
CollectedAt time.Time `json:"collected_at"`
|
|
}
|
|
|
|
// InvalidRecord describes a TLSA record dropped during Collect.
|
|
type InvalidRecord struct {
|
|
Owner string `json:"owner"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// TargetResult groups all TLSA records declared on a single endpoint and
|
|
// carries enough context to render an actionable HTML row per endpoint.
|
|
type TargetResult struct {
|
|
// Owner is the fully qualified DANE owner name (_<port>._<proto>.<host>).
|
|
Owner string `json:"owner"`
|
|
// Host is the connection target (typically the base name the TLSA
|
|
// records live under, or its MX/SRV target when relevant).
|
|
Host string `json:"host"`
|
|
Port uint16 `json:"port"`
|
|
Proto string `json:"proto"`
|
|
STARTTLS string `json:"starttls,omitempty"`
|
|
|
|
// Ref ties this target to the tls.endpoint.v1 discovery entry the
|
|
// checker emitted, so the rule can pick the matching RelatedObservation.
|
|
Ref string `json:"ref"`
|
|
|
|
// Records are the TLSA records declared for this endpoint.
|
|
Records []TLSARecord `json:"records"`
|
|
}
|
|
|
|
// TLSARecord is a user-facing view of a single dns.TLSA record.
|
|
type TLSARecord struct {
|
|
Usage uint8 `json:"usage"`
|
|
Selector uint8 `json:"selector"`
|
|
MatchingType uint8 `json:"matching_type"`
|
|
Certificate string `json:"certificate"` // lowercase hex
|
|
}
|