80 lines
2.3 KiB
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
|
|
}
|