checker-dane/checker/rules_match.go

80 lines
2.3 KiB
Go

package checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// recordsMatchChainRule is the core DANE check: for every endpoint whose
// handshake succeeded, at least one declared TLSA record must match the
// certificate chain presented by the server (RFC 6698 §2.1 OR semantics).
//
// This is the most common DANE outage vector, a certificate rotation
// without a matching TLSA rollover, so it deserves its own rule and its
// own per-endpoint states.
type recordsMatchChainRule struct{}
func (r *recordsMatchChainRule) Name() string { return "dane.records_match_chain" }
func (r *recordsMatchChainRule) Description() string {
return "Verifies that at least one TLSA record matches the certificate chain presented by each endpoint."
}
func (r *recordsMatchChainRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
rc := loadRuleContext(ctx, obs)
if rc.err != nil {
return []sdk.CheckState{observationErrorState(rc.err)}
}
var out []sdk.CheckState
tested := 0
for _, t := range rc.data.Targets {
probe := rc.probes[t.Ref]
if !probeUsable(probe) {
continue // covered by probeAvailableRule / handshakeOKRule
}
if len(t.Records) == 0 {
continue // covered by hasRecordsRule
}
tested++
subj := targetSubject(t)
meta := targetMeta(t)
s := summarizeMatches(t, probe)
meta["matched"] = s.matched
meta["unmatched"] = s.unmatched
if s.matched > 0 {
out = append(out, sdk.CheckState{
Status: sdk.StatusOK,
Code: "dane_match_ok",
Subject: subj,
Message: fmt.Sprintf("%d/%d TLSA record(s) match the presented certificate chain.", s.matched, s.matched+s.unmatched),
Meta: meta,
})
continue
}
msg := "No TLSA record matches the presented certificate chain."
if s.firstUnmatchedReason != "" {
msg += " " + s.firstUnmatchedReason
}
meta["first_unmatched_index"] = s.firstUnmatchedIdx
out = append(out, sdk.CheckState{
Status: sdk.StatusCrit,
Code: "dane_no_match",
Subject: subj,
Message: msg,
Meta: meta,
})
}
if len(out) == 0 {
if tested == 0 {
return []sdk.CheckState{{
Status: sdk.StatusUnknown,
Code: "dane_records_match_chain_skipped",
Message: "No usable probe/records pair to evaluate.",
}}
}
}
return out
}