Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ When querying DNSSEC enabled zones, it performs a full verification of the resou
* Performs the cryptographic verification of the `RRSIG` of the `DNSKEY` RRset with the public KSK
* Checks the validity period of the `RRSIG` records

Following these cryptographic verifications, the package then validates the authentication chain by walking up the delegation chain, checking the public `DNSKEY` RRs against the `DS` records in each parent zone, up to the TLD zone. (For a more in-depth description of how DNSSEC works, see [this guide](https://www.cloudflare.com/dns/dnssec/how-dnssec-works/).)
Following these cryptographic verifications, the package then validates the authentication chain by walking up the delegation chain, checking the public `DNSKEY` RRs against the `DS` records in each parent zone, up to the root zone.

### Root Zone Trust Anchor

The library includes the official IANA root zone trust anchor (KSK-2017, key tag 20326) to validate the root zone DNSKEY. This ensures that the entire chain of trust is verified all the way up to the root, preventing a misbehaving server from spoofing the DNS hierarchy.

The root zone validation follows [RFC 4033](https://tools.ietf.org/html/rfc4033) and [RFC 4034](https://tools.ietf.org/html/rfc4034) specifications for DNSSEC validation.

(For a more in-depth description of how DNSSEC works, see [this guide](https://www.cloudflare.com/dns/dnssec/how-dnssec-works/).)

In case of any validation errors, the method returns a non-nil `err` value, and an empty result set.

Expand Down
15 changes: 13 additions & 2 deletions authchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func (authChain *AuthenticationChain) Populate(domainName string) error {
// valid, it walks through the delegationChain checking the RRSIGs on
// the DNSKEY and DS resource record sets, as well as correctness of each
// delegation using the lower level methods in SignedZone.
func (authChain *AuthenticationChain) Verify(answerRRset *RRSet) error {
// The trustAnchor parameter is used to validate the root zone DNSKEY.
func (authChain *AuthenticationChain) Verify(answerRRset *RRSet, trustAnchor *TrustAnchor) error {

signedZone := authChain.delegationChain[0]
if !signedZone.checkHasDnskeys() {
Expand All @@ -64,7 +65,8 @@ func (authChain *AuthenticationChain) Verify(answerRRset *RRSet) error {
return ErrInvalidRRsig
}

for _, signedZone := range authChain.delegationChain {
for i := range authChain.delegationChain {
signedZone := authChain.delegationChain[i]

if signedZone.dnskey.IsEmpty() {
log.Printf("DNSKEY RR does not exist on %s\n", signedZone.zone)
Expand Down Expand Up @@ -95,6 +97,15 @@ func (authChain *AuthenticationChain) Verify(answerRRset *RRSet) error {
log.Printf("DS does not validate: %s", err)
return ErrDsInvalid
}
} else {
// This is the root zone (no parent), validate against trust anchor
if signedZone.zone == "." {
err := trustAnchor.VerifyRootZone(signedZone)
if err != nil {
log.Printf("Root zone does not match trust anchor: %s\n", err)
return ErrRootZoneNotTrusted
}
}
}
}
return nil
Expand Down
8 changes: 8 additions & 0 deletions goresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Resolver struct {
timeNow func() time.Time
dnsClient *dns.Client
dnsClientConfig *dns.ClientConfig
trustAnchor *TrustAnchor
}

// Errors returned by the verification/validation methods at all levels.
Expand Down Expand Up @@ -109,5 +110,12 @@ func NewResolver(resolvConf string) (res *Resolver, err error) {
}
resolver.queryFn = localQuery
resolver.timeNow = time.Now

// Initialize the root zone trust anchor
resolver.trustAnchor, err = NewTrustAnchor()
if err != nil {
return nil, err
}

return resolver, nil
}
6 changes: 3 additions & 3 deletions lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (resolver *Resolver) LookupIP(qname string) (ips []net.IP, err error) {
}
resultIPs := make([]net.IP, MaxReturnedIPAddressesCount)
for _, answer := range answers {
err = authChain.Verify(answer)
err = authChain.Verify(answer, resolver.trustAnchor)
if err != nil {
log.Printf("DNSSEC validation failed: %s\n", err)
continue
Expand Down Expand Up @@ -100,7 +100,7 @@ func (resolver *Resolver) LookupIPType(qname string, qtype uint16) (ips []net.IP
return nil, err
}

err = authChain.Verify(answer)
err = authChain.Verify(answer, resolver.trustAnchor)
if err != nil {
log.Printf("DNSSEC validation failed: %s\n", err)
return nil, err
Expand Down Expand Up @@ -142,7 +142,7 @@ func (resolver *Resolver) StrictNSQuery(qname string, qtype uint16) (rrSet []dns
return nil, err
}

err = authChain.Verify(answer)
err = authChain.Verify(answer, resolver.trustAnchor)
if err != nil {
log.Printf("DNSSEC validation failed: %s\n", err)
return nil, err
Expand Down
83 changes: 83 additions & 0 deletions trustanchor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package goresolver

import (
"errors"

"github.com/miekg/dns"
)

// ErrRootZoneNotTrusted is returned when the root zone DNSKEY does not
// match any configured trust anchor
var ErrRootZoneNotTrusted = errors.New("root zone DNSKEY does not match trust anchor")

// TrustAnchor represents a DNSSEC trust anchor for the root zone
type TrustAnchor struct {
dnskeys []*dns.DNSKEY
}

// defaultRootTrustAnchors contains the official IANA root zone trust anchors
// These are the KSK (Key Signing Key) records for the root zone
//
// KSK-2017 (Key Tag 20326) - Currently active
// Reference: https://www.iana.org/dnssec/files
var defaultRootTrustAnchors = []string{
// KSK-2017 (Key Tag 20326)
". IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=",
}

// NewTrustAnchor creates a new TrustAnchor with the default root zone
// trust anchors
func NewTrustAnchor() (*TrustAnchor, error) {
ta := &TrustAnchor{
dnskeys: make([]*dns.DNSKEY, 0, len(defaultRootTrustAnchors)),
}

for _, anchor := range defaultRootTrustAnchors {
rr, err := dns.NewRR(anchor)
if err != nil {
return nil, err
}
dnskey, ok := rr.(*dns.DNSKEY)
if !ok {
return nil, errors.New("trust anchor is not a DNSKEY record")
}
ta.dnskeys = append(ta.dnskeys, dnskey)
}

return ta, nil
}

// VerifyRootZone validates that the root zone DNSKEY matches one of the
// configured trust anchors. It returns nil if validation succeeds.
func (ta *TrustAnchor) VerifyRootZone(rootZone SignedZone) error {
if rootZone.zone != "." {
return errors.New("not a root zone")
}

// Check that at least one of the KSKs in the root zone matches
// a trust anchor
for _, trustAnchor := range ta.dnskeys {
trustAnchorKeyTag := trustAnchor.KeyTag()

// Look up the key in the root zone by key tag
rootKey := rootZone.lookupPubKey(trustAnchorKeyTag)
if rootKey == nil {
continue
}

// Compare the DNSKEY records
if keysMatch(trustAnchor, rootKey) {
return nil
}
}

return ErrRootZoneNotTrusted
}

// keysMatch compares two DNSKEY records for equality
func keysMatch(a, b *dns.DNSKEY) bool {
return a.Flags == b.Flags &&
a.Protocol == b.Protocol &&
a.Algorithm == b.Algorithm &&
a.PublicKey == b.PublicKey
}
149 changes: 149 additions & 0 deletions trustanchor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package goresolver

import (
"testing"

"github.com/miekg/dns"
)

func TestNewTrustAnchor(t *testing.T) {
ta, err := NewTrustAnchor()
if err != nil {
t.Fatalf("NewTrustAnchor failed: %v", err)
}
if ta == nil {
t.Fatal("NewTrustAnchor returned nil")
}
if len(ta.dnskeys) == 0 {
t.Fatal("NewTrustAnchor should have at least one trust anchor")
}

// Verify that KSK-2017 (key tag 20326) is present
foundKSK := false
for _, key := range ta.dnskeys {
if key.KeyTag() == 20326 {
foundKSK = true
// Verify it's a KSK (Key Signing Key)
if key.Flags != 257 {
t.Errorf("Expected KSK flag 257, got %d", key.Flags)
}
// Verify algorithm is RSA/SHA-256 (8)
if key.Algorithm != 8 {
t.Errorf("Expected algorithm 8, got %d", key.Algorithm)
}
}
}
if !foundKSK {
t.Error("KSK-2017 (key tag 20326) not found in trust anchors")
}
}

func TestVerifyRootZone_Valid(t *testing.T) {
ta, err := NewTrustAnchor()
if err != nil {
t.Fatalf("NewTrustAnchor failed: %v", err)
}

// Create a mock root zone with the correct KSK
rootZone := &SignedZone{
zone: ".",
dnskey: &RRSet{rrSet: []dns.RR{ta.dnskeys[0]}},
ds: &RRSet{},
pubKeyLookup: make(map[uint16]*dns.DNSKEY),
}

// Add the trust anchor key to the root zone
for _, key := range ta.dnskeys {
rootZone.addPubKey(key)
}

err = ta.VerifyRootZone(*rootZone)
if err != nil {
t.Errorf("VerifyRootZone should succeed with matching trust anchor, got: %v", err)
}
}

func TestVerifyRootZone_Invalid(t *testing.T) {
ta, err := NewTrustAnchor()
if err != nil {
t.Fatalf("NewTrustAnchor failed: %v", err)
}

// Create a fake DNSKEY
fakeKey := &dns.DNSKEY{
Hdr: dns.RR_Header{
Name: ".",
Rrtype: dns.TypeDNSKEY,
Class: dns.ClassINET,
},
Flags: 257,
Protocol: 3,
Algorithm: 8,
PublicKey: "FakeKeyDataThatDoesNotMatchTrustAnchor==",
}

// Create a mock root zone with a different (wrong) key
rootZone := &SignedZone{
zone: ".",
dnskey: &RRSet{rrSet: []dns.RR{fakeKey}},
ds: &RRSet{},
pubKeyLookup: make(map[uint16]*dns.DNSKEY),
}
rootZone.addPubKey(fakeKey)

err = ta.VerifyRootZone(*rootZone)
if err != ErrRootZoneNotTrusted {
t.Errorf("VerifyRootZone should fail with wrong key, expected ErrRootZoneNotTrusted, got: %v", err)
}
}

func TestVerifyRootZone_NotRootZone(t *testing.T) {
ta, err := NewTrustAnchor()
if err != nil {
t.Fatalf("NewTrustAnchor failed: %v", err)
}

// Try to verify a non-root zone
zone := &SignedZone{
zone: "example.com.",
dnskey: &RRSet{},
ds: &RRSet{},
pubKeyLookup: make(map[uint16]*dns.DNSKEY),
}

err = ta.VerifyRootZone(*zone)
if err == nil || err.Error() != "not a root zone" {
t.Errorf("VerifyRootZone should fail for non-root zone, got: %v", err)
}
}

func TestKeysMatch(t *testing.T) {
key1 := &dns.DNSKEY{
Flags: 257,
Protocol: 3,
Algorithm: 8,
PublicKey: "TestKeyData",
}

key2 := &dns.DNSKEY{
Flags: 257,
Protocol: 3,
Algorithm: 8,
PublicKey: "TestKeyData",
}

key3 := &dns.DNSKEY{
Flags: 257,
Protocol: 3,
Algorithm: 8,
PublicKey: "DifferentKeyData",
}

if !keysMatch(key1, key2) {
t.Error("keysMatch should return true for identical keys")
}

if keysMatch(key1, key3) {
t.Error("keysMatch should return false for different keys")
}
}