99 lines
3.5 KiB
Go
99 lines
3.5 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// hasRecordsRule reports whether the TLSAs service declares any TLSA record
|
|
// at all. Without records there is nothing for DANE to validate.
|
|
type hasRecordsRule struct{}
|
|
|
|
func (r *hasRecordsRule) Name() string { return "dane.has_records" }
|
|
func (r *hasRecordsRule) Description() string {
|
|
return "Verifies that at least one TLSA record is declared on the service."
|
|
}
|
|
|
|
func (r *hasRecordsRule) 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 states []sdk.CheckState
|
|
for _, inv := range rc.data.Invalid {
|
|
states = append(states, sdk.CheckState{
|
|
Status: sdk.StatusError,
|
|
Code: "dane_invalid_owner",
|
|
Subject: inv.Owner,
|
|
Message: fmt.Sprintf("TLSA record %q is unusable: %s", inv.Owner, inv.Reason),
|
|
Meta: map[string]any{"owner": inv.Owner, "reason": inv.Reason},
|
|
})
|
|
}
|
|
if len(rc.data.Targets) == 0 {
|
|
if len(states) > 0 {
|
|
// Records exist but none are usable; flag the aggregate too so
|
|
// the UI doesn't only show per-record errors.
|
|
owners := make([]string, 0, len(rc.data.Invalid))
|
|
for _, inv := range rc.data.Invalid {
|
|
owners = append(owners, inv.Owner)
|
|
}
|
|
states = append(states, sdk.CheckState{
|
|
Status: sdk.StatusError,
|
|
Code: "dane_no_usable_records",
|
|
Message: fmt.Sprintf("No usable TLSA records (all %d declared records are malformed: %s).", len(rc.data.Invalid), strings.Join(owners, ", ")),
|
|
})
|
|
return states
|
|
}
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusUnknown,
|
|
Code: "dane_no_records",
|
|
Message: "No TLSA records declared on this service.",
|
|
}}
|
|
}
|
|
states = append(states, sdk.CheckState{
|
|
Status: sdk.StatusOK,
|
|
Code: "dane_has_records_ok",
|
|
Message: "TLSA records are declared for all bound endpoints.",
|
|
Meta: map[string]any{"endpoints": len(rc.data.Targets)},
|
|
})
|
|
return states
|
|
}
|
|
|
|
// dnssecValidatedRule reports whether the TLSA records this checker is
|
|
// evaluating were fetched over a DNSSEC-validated path. Without DNSSEC,
|
|
// DANE is a downgrade primitive: an on-path attacker can forge TLSA
|
|
// answers and any "match" the rest of the rules report is meaningless.
|
|
// The rule only emits when the collector recorded a validation status:
|
|
// in managed mode the records come from the user's authoritative zone
|
|
// config and DNSSEC posture is checked by a different checker.
|
|
type dnssecValidatedRule struct{}
|
|
|
|
func (r *dnssecValidatedRule) Name() string { return "dane.dnssec_validated" }
|
|
func (r *dnssecValidatedRule) Description() string {
|
|
return "Verifies the TLSA records were fetched via a DNSSEC-validating resolver (AD bit set)."
|
|
}
|
|
|
|
func (r *dnssecValidatedRule) 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)}
|
|
}
|
|
if rc.data.DNSSECValidated == nil {
|
|
return nil
|
|
}
|
|
if *rc.data.DNSSECValidated {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusOK,
|
|
Code: "dane_dnssec_validated",
|
|
Message: "TLSA records were fetched over a DNSSEC-validated path (AD bit set).",
|
|
}}
|
|
}
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusError,
|
|
Code: "dane_dnssec_unvalidated",
|
|
Message: "TLSA records were fetched without DNSSEC validation (resolver did not set the AD bit). DANE matches are not trustworthy without DNSSEC.",
|
|
}}
|
|
}
|