feat(everything): did a massive dnssec refactor to improve robustness

This commit is contained in:
2026-07-10 18:59:49 +01:00
parent e821d6677d
commit 4ef9ea5a4a
23 changed files with 809 additions and 1398 deletions
+95
View File
@@ -0,0 +1,95 @@
package dnssec
import (
"strings"
"github.com/miekg/dns"
)
// rootAnchor is one IANA root zone trust anchor: a KSK key tag and the SHA-256
// digest of that key. A fetched root KSK is trusted if it matches any anchor.
type rootAnchor struct {
keyTag uint16
digest string // SHA-256, uppercase hex (matches DNSKEY.ToDS output)
}
// rootTrustAnchors holds the currently-valid root KSK trust anchors, as
// published by IANA (root-anchors.xml). Both the KSK-2017 and KSK-2024 keys
// are listed so validation keeps working across the KSK rollover, during which
// the root publishes and may sign with either key. The expired 2010 anchor
// (key tag 19036) is intentionally omitted.
var rootTrustAnchors = []rootAnchor{
{keyTag: 20326, digest: "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"}, // KSK-2017
{keyTag: 38696, digest: "683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16"}, // KSK-2024
}
const rootDigestType uint8 = dns.SHA256
// rootHints holds the IPv4 addresses of the DNS root servers, used to
// bootstrap iterative resolution in chain-of-trust (authoritative) mode.
var rootHints = []string{
// Root servers. {a..m}.root-servers.net
"198.41.0.4:53",
"170.247.170.2:53",
"192.33.4.12:53",
"199.7.91.13:53",
"192.203.230.10:53",
"192.5.5.241:53",
"192.112.36.4:53",
"198.97.190.53:53",
"192.36.148.17:53",
"192.58.128.30:53",
"193.0.14.129:53",
"199.7.83.42:53",
"202.12.27.33:53",
}
// matchesRootAnchor reports whether a root DNSKEY matches any trusted anchor,
// by key tag and SHA-256 digest.
func matchesRootAnchor(k *dns.DNSKEY) bool {
tag := k.KeyTag()
for _, a := range rootTrustAnchors {
if tag != a.keyTag {
continue
}
ds := k.ToDS(rootDigestType)
if ds != nil && strings.EqualFold(ds.Digest, a.digest) {
return true
}
}
return false
}
// verifyRootAnchor confirms that a fetched root DNSKEY RRset contains a KSK
// matching one of the hardcoded trust anchors, and that the RRset is signed by
// a key in the set. On success the returned zone can validate child DS records.
func verifyRootAnchor(dnskeys *RRSet) (*SignedZone, error) {
if dnskeys == nil || dnskeys.IsEmpty() || !dnskeys.IsSigned() {
return nil, ErrDnskeyNotAvailable
}
zone := NewSignedZone(".")
zone.DNSKey = dnskeys
matched := false
for _, rr := range dnskeys.RRs {
k, ok := rr.(*dns.DNSKEY)
if !ok {
continue
}
zone.AddPubKey(k)
if matchesRootAnchor(k) {
matched = true
}
}
if !matched {
return nil, ErrDsInvalid
}
// The DNSKEY RRset must be signed by one of its own keys (the KSK).
if err := zone.VerifyRRSIG(dnskeys); err != nil {
return nil, err
}
return zone, nil
}
-226
View File
@@ -1,226 +0,0 @@
package dnssec
// CODE ADAPTED FROM THIS
// ISC License
//
// Copyright (c) 2012-2016 Peter Banik <peter@froggle.org>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import (
"fmt"
"strings"
"github.com/afonsofrancof/sdns-proxy/common/logger"
"github.com/miekg/dns"
)
type AuthenticationChain struct {
DelegationChain []SignedZone
}
func NewAuthenticationChain() *AuthenticationChain {
return &AuthenticationChain{}
}
func (ac *AuthenticationChain) Populate(domainName string, queryFunc func(string, uint16) (*dns.Msg, error)) error {
// Clean domain name and split into components
domainName = strings.TrimSuffix(domainName, ".")
qnameComponents := strings.Split(domainName, ".")
// Remove empty components
var cleanComponents []string
for _, comp := range qnameComponents {
if comp != "" {
cleanComponents = append(cleanComponents, comp)
}
}
// Build zones from root down to target
// For example.com: [".","com.","example.com."]
zones := []string{"."} // Start with root
// Add each level from TLD down to target
for i := len(cleanComponents) - 1; i >= 0; i-- {
zone := dns.Fqdn(strings.Join(cleanComponents[i:], "."))
zones = append(zones, zone)
}
logger.Debug("Building DNSSEC chain for zones: %v", zones)
ac.DelegationChain = make([]SignedZone, 0, len(zones))
// Query each zone from root down
for i, zoneName := range zones {
logger.Debug("Querying zone: %s", zoneName)
delegation, err := ac.queryDelegation(zoneName, queryFunc)
if err != nil {
return fmt.Errorf("failed to query zone %s: %w", zoneName, err)
}
// Set parent relationship (previous zone in chain is parent)
if i > 0 {
delegation.ParentZone = &ac.DelegationChain[i-1]
}
ac.DelegationChain = append(ac.DelegationChain, *delegation)
}
return nil
}
func (ac *AuthenticationChain) queryDelegation(domainName string, queryFunc func(string, uint16) (*dns.Msg, error)) (*SignedZone, error) {
signedZone := NewSignedZone(domainName)
// Query DNSKEY records
dnskeyRRset, err := ac.queryRRset(domainName, dns.TypeDNSKEY, queryFunc)
if err != nil {
return nil, err
}
signedZone.DNSKey = dnskeyRRset
logger.Debug("Found %d DNSKEY records for %s", len(dnskeyRRset.RRs), domainName)
// Populate public key lookup
for _, rr := range signedZone.DNSKey.RRs {
if dnskey, ok := rr.(*dns.DNSKEY); ok {
signedZone.AddPubKey(dnskey)
logger.Debug("Added DNSKEY for %s: keytag=%d, flags=%d, algorithm=%d", domainName, dnskey.KeyTag(), dnskey.Flags, dnskey.Algorithm)
}
}
// Only query DS records for non-root zones
if domainName != "." {
dsRRset, _ := ac.queryRRset(domainName, dns.TypeDS, queryFunc)
signedZone.DS = dsRRset
if dsRRset != nil && len(dsRRset.RRs) > 0 {
logger.Debug("Found %d DS records for %s", len(dsRRset.RRs), domainName)
for _, rr := range dsRRset.RRs {
if ds, ok := rr.(*dns.DS); ok {
logger.Debug("DS record for %s: keytag=%d", domainName, ds.KeyTag)
}
}
}
} else {
// Root zone has no DS records - trusted by default
signedZone.DS = NewRRSet()
logger.Debug("Root zone - no DS records, trusted by default")
}
return signedZone, nil
}
func (ac *AuthenticationChain) queryRRset(qname string, qtype uint16, queryFunc func(string, uint16) (*dns.Msg, error)) (*RRSet, error) {
r, err := queryFunc(qname, qtype)
if err != nil {
logger.Debug("cannot lookup %v", err)
return NewRRSet(), nil // Return empty RRSet instead of nil
}
if r.Rcode == dns.RcodeNameError {
logger.Debug("no such domain %s", qname)
return NewRRSet(), nil // Return empty RRSet instead of nil
}
result := NewRRSet()
if r.Answer == nil {
return result, nil
}
result.RRs = make([]dns.RR, 0, len(r.Answer))
for _, rr := range r.Answer {
switch t := rr.(type) {
case *dns.RRSIG:
if result.RRSig == nil || t.TypeCovered == qtype {
result.RRSig = t
}
default:
if rr != nil && rr.Header().Rrtype == qtype {
result.RRs = append(result.RRs, rr)
}
}
}
return result, nil
}
func (ac *AuthenticationChain) Verify(answerRRset *RRSet) error {
if len(ac.DelegationChain) == 0 {
return ErrDnskeyNotAvailable
}
// Find the target zone (last in chain)
targetZone := &ac.DelegationChain[len(ac.DelegationChain)-1]
// Verify the answer RRset against target zone's keys
err := targetZone.VerifyRRSIG(answerRRset)
if err != nil {
logger.Debug("Answer RRSIG verification failed: %v", err)
return ErrInvalidRRsig
}
// Validate the chain from root down
for _, zone := range ac.DelegationChain {
logger.Debug("Validating zone: %s", zone.Zone)
// Verify DNSKEY RRset signature
if !zone.HasDNSKeys() {
logger.Debug("No DNSKEYs for zone %s", zone.Zone)
return ErrDnskeyNotAvailable
}
err := zone.VerifyRRSIG(zone.DNSKey)
if err != nil {
logger.Debug("DNSKEY validation failed for %s: %v", zone.Zone, err)
return ErrRrsigValidationError
}
// Skip ALL validation for root - just trust it
if zone.Zone == "." {
logger.Debug("Root zone - trusted by default, no validation performed")
continue
}
// For non-root zones, validate DS records against parent zone
if zone.ParentZone == nil {
logger.Debug("Non-root zone %s has no parent", zone.Zone)
return fmt.Errorf("non-root zone %s has no parent", zone.Zone)
}
if zone.DS == nil || zone.DS.IsEmpty() {
logger.Debug("No DS records for zone %s", zone.Zone)
return ErrDsNotAvailable
}
// Verify DS signature using parent's key
err = zone.ParentZone.VerifyRRSIG(zone.DS)
if err != nil {
logger.Debug("DS signature validation failed for %s: %v", zone.Zone, err)
return ErrRrsigValidationError
}
// Verify DS matches this zone's DNSKEY
err = zone.VerifyDS(zone.DS.RRs)
if err != nil {
logger.Debug("DS-DNSKEY validation failed for %s: %v", zone.Zone, err)
return ErrDsInvalid
}
logger.Debug("Zone %s validated successfully", zone.Zone)
}
logger.Debug("DNSSEC validation successful for entire chain!")
return nil
}
-212
View File
@@ -1,212 +0,0 @@
package dnssec
import (
"fmt"
"net"
"strings"
"time"
"github.com/afonsofrancof/sdns-proxy/common/logger"
"github.com/miekg/dns"
)
type AuthoritativeQuerier struct {
client *dns.Client
// Cache of NS records to avoid repeated lookups
nsCache map[string][]string
ipCache map[string]string
}
func NewAuthoritativeQuerier() *AuthoritativeQuerier {
return &AuthoritativeQuerier{
client: &dns.Client{
Timeout: 10 * time.Second,
},
nsCache: make(map[string][]string),
ipCache: make(map[string]string),
}
}
func (aq *AuthoritativeQuerier) QueryAuthoritative(qname string, qtype uint16) (*dns.Msg, error) {
logger.Debug("Querying authoritative servers for %s type %d", qname, qtype)
var zone string
if qtype == dns.TypeDS {
zone = aq.getParentZone(qname)
if zone == "" {
logger.Debug("No parent zone for %s - returning NXDOMAIN for DS query", qname)
msg := &dns.Msg{}
msg.SetRcode(&dns.Msg{}, dns.RcodeNameError)
return msg, nil
}
} else {
zone = aq.findZone(qname)
}
logger.Debug("Determined zone: %s for query %s type %d", zone, qname, qtype)
// Get NS names (not IPs yet)
nsNames, err := aq.findAuthoritativeNSNames(zone)
if err != nil {
return nil, fmt.Errorf("failed to find authoritative servers: %w", err)
}
// Try servers one by one, resolving IPs lazily
var lastErr error
for _, nsName := range nsNames {
server := aq.resolveNSToIP(nsName)
if server == "" {
continue
}
logger.Debug("Trying server: %s (%s)", server, nsName)
msg, err := aq.queryServer(server, qname, qtype)
if err != nil {
logger.Debug("Server %s failed: %v", server, err)
lastErr = err
continue
}
logger.Debug("Server %s responded, authoritative: %v, rcode: %d, answers: %d", server, msg.Authoritative, msg.Rcode, len(msg.Answer))
if (msg.Rcode == dns.RcodeSuccess && len(msg.Answer) > 0) || msg.Rcode == dns.RcodeNameError {
return msg, nil
}
}
if lastErr != nil {
return nil, fmt.Errorf("all servers failed, last error: %w", lastErr)
}
return nil, fmt.Errorf("no authoritative response received")
}
func (aq *AuthoritativeQuerier) findAuthoritativeNSNames(zone string) ([]string, error) {
if nsNames, exists := aq.nsCache[zone]; exists {
logger.Debug("Using cached NS names for %s: %v", zone, nsNames)
return nsNames, nil
}
logger.Debug("Looking for NS records for zone: %s", zone)
// Use a public resolver to find the NS records
resolver := &dns.Client{Timeout: 5 * time.Second}
msg, _, err := resolver.Exchange(&dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{{Name: dns.Fqdn(zone), Qtype: dns.TypeNS, Qclass: dns.ClassINET}},
}, "8.8.8.8:53")
if err != nil {
return nil, fmt.Errorf("failed to query NS records: %w", err)
}
var nsNames []string
// Collect NS records from answer section
for _, rr := range msg.Answer {
if ns, ok := rr.(*dns.NS); ok {
nsNames = append(nsNames, ns.Ns)
}
}
// Also check authority section if answer is empty
if len(nsNames) == 0 {
for _, rr := range msg.Ns {
if ns, ok := rr.(*dns.NS); ok {
nsNames = append(nsNames, ns.Ns)
}
}
}
if len(nsNames) == 0 {
return nil, fmt.Errorf("no NS servers found for %s", zone)
}
logger.Debug("Found NS names for %s: %v", zone, nsNames)
aq.nsCache[zone] = nsNames
logger.Debug("Cached NS names for %s: %v", zone, nsNames)
return nsNames, nil
}
func (aq *AuthoritativeQuerier) getParentZone(qname string) string {
logger.Debug("Getting parent zone for: %s", qname)
// Clean the qname
qname = strings.TrimSuffix(qname, ".")
// Root zone has no parent
if qname == "" || qname == "." {
logger.Debug("Root zone has no parent")
return ""
}
labels := dns.SplitDomainName(qname)
logger.Debug("Labels for %s: %v", qname, labels)
if len(labels) <= 1 {
logger.Debug("Parent of TLD %s is root", qname)
return "." // Parent of TLD is root
}
parentLabels := labels[1:]
parent := dns.Fqdn(strings.Join(parentLabels, "."))
logger.Debug("Parent zone of %s is %s", qname, parent)
return parent
}
func (aq *AuthoritativeQuerier) findZone(qname string) string {
// For now, assume the zone is the domain itself
// In a more sophisticated implementation, you'd walk up the hierarchy
labels := dns.SplitDomainName(qname)
if len(labels) >= 2 {
return labels[len(labels)-2] + "." + labels[len(labels)-1] + "."
}
return qname
}
func (aq *AuthoritativeQuerier) resolveNSToIP(nsName string) string {
if ip, exists := aq.ipCache[nsName]; exists {
logger.Debug("Using cached IP for %s: %s", nsName, ip)
return ip
}
nsName = strings.TrimSuffix(nsName, ".")
logger.Debug("Resolving NS %s to IP", nsName)
ips, err := net.LookupIP(nsName)
if err != nil {
logger.Debug("Failed to resolve %s: %v", nsName, err)
return ""
}
for _, ip := range ips {
if ip.To4() != nil { // Prefer IPv4
result := ip.String() + ":53"
logger.Debug("Resolved %s to %s", nsName, result)
// Cache the result before returning
aq.ipCache[nsName] = result
return result
}
}
return ""
}
func (aq *AuthoritativeQuerier) queryServer(server, qname string, qtype uint16) (*dns.Msg, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(qname), qtype)
m.SetEdns0(4096, true) // Enable DNSSEC
logger.Debug("Querying %s for %s type %d", server, qname, qtype)
msg, _, err := aq.client.Exchange(m, server)
if err != nil {
return nil, err
}
logger.Debug("Response from %s: rcode=%d, answers=%d", server, msg.Rcode, len(msg.Answer))
return msg, err
}
+60 -76
View File
@@ -1,100 +1,84 @@
package dnssec
// CODE ADAPTED FROM THIS
// ISC License
//
// Copyright (c) 2012-2016 Peter Banik <peter@froggle.org>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// ./common/dnssec/validator.go
import (
"github.com/afonsofrancof/sdns-proxy/common/logger"
"github.com/miekg/dns"
)
type Validator struct {
queryFunc func(string, uint16) (*dns.Msg, error)
type ValidationStats struct {
Queries int
BytesSent int
BytesReceived int
Validated bool
}
func NewValidator(queryFunc func(string, uint16) (*dns.Msg, error)) *Validator {
return &Validator{
queryFunc: queryFunc,
}
type SendFunc func(msg *dns.Msg) (*dns.Msg, error)
type walker interface {
validate(answer *RRSet, qname string, qtype uint16) error
stats() ValidationStats
resetStats()
}
type Validator struct {
walker walker
}
func NewValidator(send SendFunc) *Validator {
return &Validator{walker: newTrustWalker(send)}
}
func NewAuthoritativeValidator() *Validator {
return &Validator{walker: newIterativeWalker()}
}
func (v *Validator) ValidateResponse(msg *dns.Msg, qname string, qtype uint16) error {
logger.Debug("Starting DNSSEC validation for %s %s", qname, dns.TypeToString[qtype])
if msg == nil || len(msg.Answer) == 0 {
logger.Debug("No result for %s %s", qname, dns.TypeToString[qtype])
return ErrNoResult
}
// Extract RRSet from response
rrset := NewRRSet()
for _, rr := range msg.Answer {
answer := extractRRSet(msg, qname, qtype)
if answer.IsEmpty() {
return ErrNoResult
}
if !answer.IsSigned() {
return ErrResourceNotSigned
}
if err := answer.CheckHeaderIntegrity(dns.Fqdn(qname)); err != nil {
return err
}
logger.Debug("Validating %s %s (signer: %s)", qname, dns.TypeToString[qtype], answer.SignerName())
return v.walker.validate(answer, qname, qtype)
}
func (v *Validator) TakeStats() ValidationStats {
s := v.walker.stats()
v.walker.resetStats()
return s
}
func extractRRSet(msg *dns.Msg, name string, qtype uint16) *RRSet {
if msg == nil {
return NewRRSet()
}
return extractRRSetFrom(msg.Answer, name, qtype)
}
func extractRRSetFrom(rrs []dns.RR, name string, qtype uint16) *RRSet {
set := NewRRSet()
fq := dns.Fqdn(name)
for _, rr := range rrs {
switch t := rr.(type) {
case *dns.RRSIG:
if t.TypeCovered == qtype {
rrset.RRSig = t
logger.Debug("Found RRSIG for %s %s (keytag: %d)", qname, dns.TypeToString[qtype], t.KeyTag)
if t.TypeCovered == qtype && dns.Fqdn(t.Header().Name) == fq {
set.RRSig = t
}
default:
if rr.Header().Rrtype == qtype {
rrset.RRs = append(rrset.RRs, rr)
logger.Debug("Found RR for %s %s: %s", qname, dns.TypeToString[qtype], rr.String())
if rr.Header().Rrtype == qtype && dns.Fqdn(rr.Header().Name) == fq {
set.RRs = append(set.RRs, rr)
}
}
}
if rrset.IsEmpty() {
logger.Debug("Empty RRSet for %s %s", qname, dns.TypeToString[qtype])
return ErrNoResult
}
if !rrset.IsSigned() {
logger.Debug("RRSet for %s %s is not signed", qname, dns.TypeToString[qtype])
return ErrResourceNotSigned
}
// Check header integrity
if err := rrset.CheckHeaderIntegrity(qname); err != nil {
logger.Debug("Header integrity check failed for %s %s: %v", qname, dns.TypeToString[qtype], err)
return err
}
// Build and verify authentication chain
signerName := rrset.SignerName()
logger.Debug("Building authentication chain for signer: %s", signerName)
authChain := NewAuthenticationChain()
if err := authChain.Populate(signerName, v.queryFunc); err != nil {
logger.Debug("Cannot populate authentication chain for %s: %v", signerName, err)
return err
}
if err := authChain.Verify(rrset); err != nil {
logger.Debug("DNSSEC validation failed for %s %s: %v", qname, dns.TypeToString[qtype], err)
return err
}
logger.Debug("DNSSEC validation successful for %s %s", qname, dns.TypeToString[qtype])
return nil
}
func NewValidatorWithAuthoritativeQueries() *Validator {
querier := NewAuthoritativeQuerier()
return NewValidator(querier.QueryAuthoritative)
return set
}
+248
View File
@@ -0,0 +1,248 @@
package dnssec
import (
"fmt"
"net"
"strings"
"time"
"github.com/afonsofrancof/sdns-proxy/common/logger"
"github.com/miekg/dns"
)
// iterativeWalker validates top-down.
type iterativeWalker struct {
client *dns.Client
st ValidationStats
ipCache map[string]string
}
func newIterativeWalker() *iterativeWalker {
return &iterativeWalker{
client: &dns.Client{Timeout: 5 * time.Second},
ipCache: make(map[string]string),
}
}
func (w *iterativeWalker) stats() ValidationStats { return w.st }
func (w *iterativeWalker) resetStats() { w.st = ValidationStats{} }
func (w *iterativeWalker) exchange(server, name string, qtype uint16) (*dns.Msg, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(name), qtype)
m.SetEdns0(4096, true)
m.RecursionDesired = false
if b, err := m.Pack(); err == nil {
w.st.BytesSent += len(b)
}
w.st.Queries++
resp, _, err := w.client.Exchange(m, server)
if err == nil && resp != nil {
if b, perr := resp.Pack(); perr == nil {
w.st.BytesReceived += len(b)
}
}
return resp, err
}
func (w *iterativeWalker) queryAny(servers []string, name string, qtype uint16) (*dns.Msg, error) {
var lastErr error
for _, s := range servers {
resp, err := w.exchange(s, name, qtype)
if err == nil && resp != nil {
return resp, nil
}
lastErr = err
}
if lastErr == nil {
lastErr = fmt.Errorf("no response from servers")
}
return nil, lastErr
}
func (w *iterativeWalker) validate(answer *RRSet, qname string, qtype uint16) error {
current, servers, err := w.trustRoot()
if err != nil {
return err
}
currentServers := servers
// Follow delegations from the root toward qname.
for {
resp, err := w.queryAny(currentServers, qname, qtype)
if err != nil {
return err
}
// Authoritative answer for the record: verify it and finish.
if hasAnswer(resp, qname, qtype) {
ans := extractRRSet(resp, qname, qtype)
if ans.IsEmpty() || !ans.IsSigned() {
return ErrResourceNotSigned
}
if err := current.VerifyRRSIG(ans); err != nil {
logger.Debug("answer RRSIG verification failed for %s: %v", qname, err)
return ErrInvalidRRsig
}
w.st.Validated = true
return nil
}
// Otherwise expect a referral to a child zone.
childZone, childServers, dsSet, err := w.parseReferral(resp)
if err != nil {
return err
}
if dsSet.IsEmpty() || !dsSet.IsSigned() {
// No signed DS at the cut means the child is not securely
// delegated; the chain cannot continue.
return ErrDsNotAvailable
}
// The DS must be signed by the current (parent) zone.
if err := current.VerifyRRSIG(dsSet); err != nil {
logger.Debug("DS RRSIG verification failed for %s: %v", childZone, err)
return ErrInvalidRRsig
}
// Fetch the child DNSKEY and verify it matches the DS.
child, err := w.fetchZoneKeys(childServers, childZone)
if err != nil {
return err
}
if err := child.VerifyDS(dsSet.RRs); err != nil {
return err
}
current = child
currentServers = childServers
}
}
func (w *iterativeWalker) trustRoot() (*SignedZone, []string, error) {
var lastErr error
for _, server := range rootHints {
resp, err := w.exchange(server, ".", dns.TypeDNSKEY)
if err != nil || resp == nil {
lastErr = err
continue
}
keyset := extractRRSet(resp, ".", dns.TypeDNSKEY)
zone, verr := verifyRootAnchor(keyset)
if verr != nil {
lastErr = verr
continue
}
return zone, rootHints, nil
}
if lastErr == nil {
lastErr = ErrDnskeyNotAvailable
}
return nil, nil, fmt.Errorf("root trust anchor validation failed: %w", lastErr)
}
func (w *iterativeWalker) fetchZoneKeys(servers []string, zoneName string) (*SignedZone, error) {
resp, err := w.queryAny(servers, zoneName, dns.TypeDNSKEY)
if err != nil {
return nil, err
}
keyset := extractRRSet(resp, zoneName, dns.TypeDNSKEY)
if keyset.IsEmpty() || !keyset.IsSigned() {
return nil, ErrDnskeyNotAvailable
}
zone := NewSignedZone(zoneName)
zone.DNSKey = keyset
for _, rr := range keyset.RRs {
if k, ok := rr.(*dns.DNSKEY); ok {
zone.AddPubKey(k)
}
}
if err := zone.VerifyRRSIG(keyset); err != nil {
return nil, err
}
return zone, nil
}
func (w *iterativeWalker) parseReferral(msg *dns.Msg) (childZone string, childServers []string, ds *RRSet, err error) {
if msg == nil {
return "", nil, nil, fmt.Errorf("nil referral")
}
// The delegated zone is the owner name of the NS records in the authority
// section. Collect NS names per zone.
nsNamesByZone := map[string][]string{}
for _, rr := range msg.Ns {
if ns, ok := rr.(*dns.NS); ok {
z := dns.Fqdn(ns.Header().Name)
nsNamesByZone[z] = append(nsNamesByZone[z], ns.Ns)
}
}
if len(nsNamesByZone) == 0 {
return "", nil, nil, fmt.Errorf("no referral NS records present")
}
// There should be exactly one delegated zone in a referral.
var nsNames []string
for z, names := range nsNamesByZone {
childZone = z
nsNames = names
break
}
ds = extractRRSetFrom(msg.Ns, childZone, dns.TypeDS)
childServers = w.resolveNS(nsNames, msg.Extra)
if len(childServers) == 0 {
return "", nil, nil, fmt.Errorf("could not resolve nameservers for %s", childZone)
}
return childZone, childServers, ds, nil
}
func (w *iterativeWalker) resolveNS(nsNames []string, extra []dns.RR) []string {
glue := map[string]string{}
for _, rr := range extra {
if a, ok := rr.(*dns.A); ok {
glue[dns.Fqdn(a.Header().Name)] = a.A.String()
}
}
var servers []string
for _, ns := range nsNames {
fq := dns.Fqdn(ns)
if ip, ok := glue[fq]; ok {
servers = append(servers, net.JoinHostPort(ip, "53"))
continue
}
if ip, ok := w.ipCache[fq]; ok {
servers = append(servers, net.JoinHostPort(ip, "53"))
continue
}
ips, e := net.LookupIP(strings.TrimSuffix(fq, "."))
if e != nil {
continue
}
for _, ip := range ips {
if v4 := ip.To4(); v4 != nil {
addr := v4.String()
w.ipCache[fq] = addr
servers = append(servers, net.JoinHostPort(addr, "53"))
break
}
}
}
return servers
}
func hasAnswer(msg *dns.Msg, qname string, qtype uint16) bool {
if msg == nil {
return false
}
fq := dns.Fqdn(qname)
for _, rr := range msg.Answer {
if rr.Header().Rrtype == qtype && dns.Fqdn(rr.Header().Name) == fq {
return true
}
}
return false
}
+136
View File
@@ -0,0 +1,136 @@
package dnssec
import (
"github.com/afonsofrancof/sdns-proxy/common/logger"
"github.com/miekg/dns"
)
// trustWalker validates bottom-up.
// The parent of each zone is discovered from the DS record's RRSIG signer name.
type trustWalker struct {
send SendFunc
st ValidationStats
}
func newTrustWalker(send SendFunc) *trustWalker {
return &trustWalker{send: send}
}
func (w *trustWalker) stats() ValidationStats { return w.st }
func (w *trustWalker) resetStats() { w.st = ValidationStats{} }
func (w *trustWalker) ask(name string, qtype uint16) (*dns.Msg, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(name), qtype)
m.Id = dns.Id()
m.RecursionDesired = true
m.SetEdns0(4096, true)
if b, err := m.Pack(); err == nil {
w.st.BytesSent += len(b)
}
w.st.Queries++
resp, err := w.send(m)
logger.Debug("trust walker got %s %s: %d answers", name, dns.TypeToString[qtype], len(resp.Answer))
if err == nil && resp != nil {
if b, perr := resp.Pack(); perr == nil {
w.st.BytesReceived += len(b)
}
}
return resp, err
}
func (w *trustWalker) validate(answer *RRSet, qname string, qtype uint16) error {
signer := dns.Fqdn(answer.SignerName())
if signer == "" {
return ErrResourceNotSigned
}
// Fetch the signing zone's keys and verify the answer against them.
signingZone, err := w.fetchSelfSignedKeys(signer)
if err != nil {
return err
}
if err := signingZone.VerifyRRSIG(answer); err != nil {
logger.Debug("answer RRSIG verification failed for %s: %v", qname, err)
return ErrInvalidRRsig
}
// Walk up: signing zone -> ... -> root, verifying DS linkage each step.
current := signingZone
name := signer
for name != "." {
dsResp, err := w.ask(name, dns.TypeDS)
if err != nil {
return err
}
dsSet := extractRRSet(dsResp, name, dns.TypeDS)
if dsSet.IsEmpty() || !dsSet.IsSigned() {
return ErrDsNotAvailable
}
// The DS must match the current zone's key.
if err := current.VerifyDS(dsSet.RRs); err != nil {
return err
}
// The parent zone is whoever signed the DS RRset.
parentName := dns.Fqdn(dsSet.SignerName())
var parent *SignedZone
if parentName == "." {
parent, err = w.fetchRoot()
} else {
parent, err = w.fetchSelfSignedKeys(parentName)
}
if err != nil {
return err
}
// The parent must have signed the DS RRset.
if err := parent.VerifyRRSIG(dsSet); err != nil {
logger.Debug("DS RRSIG verification failed for %s: %v", name, err)
return ErrInvalidRRsig
}
current = parent
name = parentName
}
w.st.Validated = true
return nil
}
func (w *trustWalker) fetchSelfSignedKeys(zoneName string) (*SignedZone, error) {
resp, err := w.ask(zoneName, dns.TypeDNSKEY)
if err != nil {
return nil, err
}
keyset := extractRRSet(resp, zoneName, dns.TypeDNSKEY)
if keyset.IsEmpty() || !keyset.IsSigned() {
return nil, ErrDnskeyNotAvailable
}
zone := NewSignedZone(zoneName)
zone.DNSKey = keyset
for _, rr := range keyset.RRs {
if k, ok := rr.(*dns.DNSKEY); ok {
zone.AddPubKey(k)
}
}
if err := zone.VerifyRRSIG(keyset); err != nil {
return nil, err
}
return zone, nil
}
// fetchRoot fetches the root DNSKEY set and verifies it against the anchor.
func (w *trustWalker) fetchRoot() (*SignedZone, error) {
resp, err := w.ask(".", dns.TypeDNSKEY)
if err != nil {
return nil, err
}
keyset := extractRRSet(resp, ".", dns.TypeDNSKEY)
return verifyRootAnchor(keyset)
}