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 }