250 lines
8.5 KiB
Go
250 lines
8.5 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/miekg/dns"
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// allDNSKEYs flattens every server's DNSKEY into a single deduplicated slice,
|
|
// keyed by (KeyTag, Algorithm). The first occurrence wins because all servers
|
|
// should agree (dnssec_dnskey_consistent enforces that separately).
|
|
func allDNSKEYs(d *DNSSECData) []DNSKEYRecord {
|
|
seen := map[string]DNSKEYRecord{}
|
|
for _, name := range sortedServers(d) {
|
|
for _, k := range d.Servers[name].DNSKEYs {
|
|
id := fmt.Sprintf("%d/%d", k.KeyTag, k.Algorithm)
|
|
if _, ok := seen[id]; !ok {
|
|
seen[id] = k
|
|
}
|
|
}
|
|
}
|
|
out := make([]DNSKEYRecord, 0, len(seen))
|
|
for _, v := range seen {
|
|
out = append(out, v)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].KeyTag < out[j].KeyTag })
|
|
return out
|
|
}
|
|
|
|
type algorithmAllowedRule struct{}
|
|
|
|
func (algorithmAllowedRule) Name() string { return "dnssec_algorithm_allowed" }
|
|
func (algorithmAllowedRule) Description() string {
|
|
return "Rejects DNSKEYs that use a forbidden algorithm or are not in the allowed list."
|
|
}
|
|
|
|
func (algorithmAllowedRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
keys := allDNSKEYs(data)
|
|
if len(keys) == 0 {
|
|
return skipped("no DNSKEY observed")
|
|
}
|
|
allowed := defaultAllowedAlgorithms()
|
|
if v, ok := sdk.GetOption[[]uint8](opts, "allowedAlgorithms"); ok && len(v) > 0 {
|
|
allowed = v
|
|
}
|
|
forbidden := defaultForbiddenAlgorithms()
|
|
if v, ok := sdk.GetOption[[]uint8](opts, "forbiddenAlgorithms"); ok && len(v) > 0 {
|
|
forbidden = v
|
|
}
|
|
allowedSet := map[uint8]bool{}
|
|
for _, a := range allowed {
|
|
allowedSet[a] = true
|
|
}
|
|
forbiddenSet := map[uint8]bool{}
|
|
for _, a := range forbidden {
|
|
forbiddenSet[a] = true
|
|
}
|
|
|
|
var states []sdk.CheckState
|
|
for _, k := range keys {
|
|
switch {
|
|
case forbiddenSet[k.Algorithm]:
|
|
states = append(states, withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: fmt.Sprintf("KeyTag %d", k.KeyTag),
|
|
Message: fmt.Sprintf("DNSKEY uses forbidden algorithm %d (%s)", k.Algorithm, dns.AlgorithmToString[k.Algorithm]),
|
|
}, "Roll the key to a modern algorithm: 13 (ECDSAP256SHA256) or 15 (Ed25519).", "dnssec.algorithm_disallowed"))
|
|
case !allowedSet[k.Algorithm]:
|
|
states = append(states, withMeta(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: fmt.Sprintf("KeyTag %d", k.KeyTag),
|
|
Message: fmt.Sprintf("DNSKEY uses algorithm %d (%s), not in the allowed list", k.Algorithm, dns.AlgorithmToString[k.Algorithm]),
|
|
}, "Add the algorithm to allowedAlgorithms or roll the key to one of: 8, 13, 14, 15, 16.", "dnssec.algorithm_disallowed"))
|
|
default:
|
|
states = append(states, sdk.CheckState{
|
|
Status: sdk.StatusOK,
|
|
Subject: fmt.Sprintf("KeyTag %d", k.KeyTag),
|
|
Message: fmt.Sprintf("DNSKEY algorithm %d (%s) accepted", k.Algorithm, dns.AlgorithmToString[k.Algorithm]),
|
|
})
|
|
}
|
|
}
|
|
return states
|
|
}
|
|
|
|
type algorithmModernRule struct{}
|
|
|
|
func (algorithmModernRule) Name() string { return "dnssec_algorithm_modern" }
|
|
func (algorithmModernRule) Description() string {
|
|
return "Recommends ECDSAP256SHA256 (13) or Ed25519 (15) over RSA."
|
|
}
|
|
|
|
func (algorithmModernRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
keys := allDNSKEYs(data)
|
|
if len(keys) == 0 {
|
|
return skipped("no DNSKEY observed")
|
|
}
|
|
hasModern := false
|
|
hasLegacy := false
|
|
for _, k := range keys {
|
|
switch k.Algorithm {
|
|
case dns.ECDSAP256SHA256, dns.ECDSAP384SHA384, dns.ED25519, dns.ED448:
|
|
hasModern = true
|
|
case dns.RSASHA256, dns.RSASHA512, dns.RSASHA1, dns.RSASHA1NSEC3SHA1:
|
|
hasLegacy = true
|
|
}
|
|
}
|
|
switch {
|
|
case hasModern && !hasLegacy:
|
|
return okState(data.Domain, "zone uses modern elliptic-curve algorithms (13/14/15/16)")
|
|
case hasLegacy:
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: data.Domain,
|
|
Message: "zone still uses RSA-family DNSKEYs; modern operators prefer 13 (ECDSAP256SHA256) or 15 (Ed25519) for smaller responses and faster validation",
|
|
}, "Plan an algorithm rollover. `dnssec-keygen -a ECDSAP256SHA256 -K /var/lib/bind <zone>` (BIND), then add the new key, wait for the parent's DS to update, then drop the old key.",
|
|
"dnssec.algorithm_legacy")}
|
|
}
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusInfo,
|
|
Subject: data.Domain,
|
|
Message: "no modern or legacy algorithms detected; review DNSKEY policy manually",
|
|
}}
|
|
}
|
|
|
|
type rsaKeySizeRule struct{}
|
|
|
|
func (rsaKeySizeRule) Name() string { return "dnssec_rsa_keysize" }
|
|
func (rsaKeySizeRule) Description() string {
|
|
return "Verifies RSA DNSKEYs reach a minimum modulus size (default 2048 bits)."
|
|
}
|
|
|
|
func (rsaKeySizeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
keys := allDNSKEYs(data)
|
|
if len(keys) == 0 {
|
|
return skipped("no DNSKEY observed")
|
|
}
|
|
minSize := optionUint(opts, "minRSAKeySize", defaultMinRSAKeySize)
|
|
|
|
var states []sdk.CheckState
|
|
for _, k := range keys {
|
|
switch k.Algorithm {
|
|
case dns.RSASHA1, dns.RSASHA1NSEC3SHA1, dns.RSASHA256, dns.RSASHA512:
|
|
default:
|
|
continue
|
|
}
|
|
if k.KeySize == 0 {
|
|
continue // could not estimate; rule_keys parser limitation
|
|
}
|
|
switch {
|
|
case k.KeySize < 1024:
|
|
states = append(states, withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: fmt.Sprintf("KeyTag %d", k.KeyTag),
|
|
Message: fmt.Sprintf("RSA DNSKEY %d uses a %d-bit modulus: practically broken", k.KeyTag, k.KeySize),
|
|
}, "Roll the key to at least 2048-bit RSA, or better, ECDSAP256SHA256 (algo 13).", "dnssec.rsa_keysize_small"))
|
|
case uint(k.KeySize) < minSize:
|
|
states = append(states, withMeta(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: fmt.Sprintf("KeyTag %d", k.KeyTag),
|
|
Message: fmt.Sprintf("RSA DNSKEY %d uses a %d-bit modulus (recommended ≥ %d)", k.KeyTag, k.KeySize, minSize),
|
|
}, "Roll to a 2048-bit RSA key, or migrate to ECDSAP256SHA256 (algo 13).", "dnssec.rsa_keysize_small"))
|
|
default:
|
|
states = append(states, sdk.CheckState{
|
|
Status: sdk.StatusOK,
|
|
Subject: fmt.Sprintf("KeyTag %d", k.KeyTag),
|
|
Message: fmt.Sprintf("RSA DNSKEY %d uses a %d-bit modulus", k.KeyTag, k.KeySize),
|
|
})
|
|
}
|
|
}
|
|
if len(states) == 0 {
|
|
return okState(data.Domain, "no RSA DNSKEY in use")
|
|
}
|
|
return states
|
|
}
|
|
|
|
type kskPresentRule struct{}
|
|
|
|
func (kskPresentRule) Name() string { return "dnssec_ksk_present" }
|
|
func (kskPresentRule) Description() string {
|
|
return "Verifies at least one DNSKEY has the SEP bit (KSK)."
|
|
}
|
|
|
|
func (kskPresentRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
keys := allDNSKEYs(data)
|
|
if len(keys) == 0 {
|
|
return skipped("no DNSKEY observed")
|
|
}
|
|
required := sdk.GetBoolOption(opts, "requireSEP", defaultRequireSEP)
|
|
if !required {
|
|
return okState(data.Domain, "requireSEP=false")
|
|
}
|
|
for _, k := range keys {
|
|
if k.IsKSK {
|
|
return okState(data.Domain, fmt.Sprintf("KSK present (KeyTag %d, algorithm %d)", k.KeyTag, k.Algorithm))
|
|
}
|
|
}
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: data.Domain,
|
|
Message: "no DNSKEY carries the SEP (KSK) flag",
|
|
}, "Re-publish the apex DNSKEY RRset with at least one key flagged as SEP (flags 257). Most signers do this automatically; check that the KSK was not accidentally removed during a rollover.",
|
|
"dnssec.no_ksk")}
|
|
}
|
|
|
|
type dnskeyCountRule struct{}
|
|
|
|
func (dnskeyCountRule) Name() string { return "dnssec_dnskey_count" }
|
|
func (dnskeyCountRule) Description() string {
|
|
return "Warns when too many DNSKEYs are published, inflating responses and amplification potential."
|
|
}
|
|
|
|
func (dnskeyCountRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
keys := allDNSKEYs(data)
|
|
n := len(keys)
|
|
switch {
|
|
case n == 0:
|
|
return skipped("no DNSKEY observed")
|
|
case n >= 8:
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: data.Domain,
|
|
Message: fmt.Sprintf("%d DNSKEYs published; large RRsets bloat responses and increase amplification factor", n),
|
|
}, "Drop retired keys after their successor has fully rolled.", "dnssec.dnskey_count")}
|
|
default:
|
|
return okState(data.Domain, fmt.Sprintf("%d DNSKEY(s) published", n))
|
|
}
|
|
}
|