Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Aug 10, 2023
1 parent a6e7456 commit dc80277
Show file tree
Hide file tree
Showing 15 changed files with 678 additions and 0 deletions.
235 changes: 235 additions & 0 deletions pkg/dnssec/chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package dnssec

import (
"context"
"errors"
"fmt"
"strings"

"github.com/miekg/dns"
"github.com/qdm12/dns/v2/internal/server"
)

// delegationChain is the DNSSEC chain of trust from the
// queried zone to the root (.) zone.
// See https://www.ietf.org/rfc/rfc4033.txt
type delegationChain []*signedZone

// newDelegationChain queries the RRs required for the zone validation.
// It begins the queries at the desired zone and then go
// up the delegation tree until it reaches the root zone.
// It returns a new delegation chain of signed zones where the
// first signed zone (index 0) is the child zone and the last signed
// zone is the root zone.
func newDelegationChain(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (chain delegationChain, err error) {
zoneParts := strings.Split(zone, ".")
chain = make(delegationChain, len(zoneParts))

type result struct {
i int
signedZone *signedZone
err error
}
results := make(chan result)

for i := range zoneParts {
// 'example.com.', 'com.', '.'
go func(i int, results chan<- result) {
result := result{i: i}
zoneName := dns.Fqdn(strings.Join(zoneParts[i:], "."))
result.signedZone, result.err = queryDelegation(ctx, exchange, zoneName, qClass)
if result.err != nil {
result.err = fmt.Errorf("querying delegation for %s: %w", zoneName, result.err)
}
results <- result
}(i, results)
}

for range chain {
result := <-results
if result.err != nil && err == nil {
err = result.err
continue
}
chain[result.i] = result.signedZone
}
close(results)

if err != nil {
return nil, err
}

return chain, nil
}

// queryDelegation obtains the DNSKEY records and the DS
// records for a given zone. It does not query the
// (non existent) DS record for the root zone.
func queryDelegation(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (sz *signedZone, err error) {
if zone == "." {
// Only query DNSKEY since root zone has no DS record.
rrsig, rrset, err := queryDNSKey(ctx, exchange, zone, qClass)
if err != nil {
return nil, fmt.Errorf("querying DNSKEY records: %w", err)
}
return &signedZone{
zone: zone,
dnsKeyRRSig: rrsig,
dnsKeyRRSet: rrset,
keyTagToDNSKey: dnsKeyRRSetToMap(rrset),
}, nil
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()

type result struct {
t uint16
rrsig *dns.RRSIG
rrset []dns.RR
err error
}
results := make(chan result)

go func(ctx context.Context, exchange server.Exchange, zone string, results chan<- result) {
result := result{t: dns.TypeDNSKEY}
result.rrsig, result.rrset, result.err = queryDNSKey(ctx, exchange, zone, qClass)
if result.err != nil {
result.err = fmt.Errorf("querying DNSKEY records: %w", result.err)
}
results <- result
}(ctx, exchange, zone, results)

go func(ctx context.Context, exchange server.Exchange, zone string, results chan<- result) {
result := result{t: dns.TypeDS}
result.rrsig, result.rrset, result.err = queryDS(ctx, exchange, zone, qClass)
if result.err != nil {
result.err = fmt.Errorf("querying DS records: %w", result.err)
}
results <- result
}(ctx, exchange, zone, results)

sz = &signedZone{
zone: zone,
}
for i := 0; i < 2; i++ {
result := <-results
if result.err != nil {
if err == nil { // first error encountered
err = result.err
cancel()
}
continue
}
if result.t == dns.TypeDS {
sz.dsRRSig, sz.dsRRSet = result.rrsig, result.rrset
} else {
sz.dnsKeyRRSig, sz.dnsKeyRRSet = result.rrsig, result.rrset
sz.keyTagToDNSKey = dnsKeyRRSetToMap(result.rrset)
}
}
close(results)

if err != nil {
return nil, err
}

return sz, nil
}

var (
ErrRecordNotFound = errors.New("record not found")
ErrRRSigNotFound = errors.New("RRSIG not found")
)

func queryDNSKey(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (rrsig *dns.RRSIG,
rrset []dns.RR, err error) {
rrsig, rrset, err = fetchRRSetWithRRSig(ctx, exchange, zone, qClass, dns.TypeDNSKEY)
switch {
case err != nil:
return nil, nil, err
case len(rrset) == 0:
return nil, nil, fmt.Errorf("%w", ErrRecordNotFound)
case rrsig == nil:
return nil, nil, fmt.Errorf("%w", ErrRRSigNotFound)
}
return rrsig, rrset, nil
}

func queryDS(ctx context.Context, exchange server.Exchange,
zone string, qClass uint16) (rrsig *dns.RRSIG,
rrset []dns.RR, err error) {
rrsig, rrset, err = fetchRRSetWithRRSig(ctx, exchange, zone, qClass, dns.TypeDS)
switch {
case err != nil:
return nil, nil, err
case len(rrset) == 0:
return nil, nil, fmt.Errorf("%w", ErrRecordNotFound)
case rrsig == nil:
return nil, nil, fmt.Errorf("%w", ErrRRSigNotFound)
}
return rrsig, rrset, nil
}

var (
ErrRRSetValidation = errors.New("RRSet validation failed")
)

// verify uses the zone data in the signed zone and its parent signed zones
// to validate the DNSSEC chain of trust.
// It starts the verification in the RRSet supplied as parameter (verifies
// the RRSIG on the answer RRs), and, assuming a signature is correct and
// valid, it walks through the linked list of signed zones 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 (dc delegationChain) verify(rrsig *dns.RRSIG, rrset []dns.RR) error {
if rrsig == nil {
return ErrRRSigNotFound
}

signedZone := dc[0] // child desired zone

// Verify desired RRSet
err := signedZone.verifyRRSIG(rrsig, rrset)
if err != nil {
return fmt.Errorf("for zone %s and RRSIG key tag %d: %w",
signedZone.zone, rrsig.KeyTag, err)
}

for i, signedZone := range dc {
// Verify DNSKEY signature
err := signedZone.verifyRRSIG(signedZone.dnsKeyRRSig, signedZone.dnsKeyRRSet)
if err != nil {
return fmt.Errorf("for zone %s and RRSIG key tag %d: %w",
signedZone.zone, signedZone.dsRRSig.KeyTag, err)
}

if signedZone.zone == "." { // last element in chain
err = verifyRootSignedZone(signedZone)
if err != nil {
return fmt.Errorf("failed validating root zone: %w", err)
}

break
}

// Verify DS signature with parent zone DNSKEY
parentSignedZone := dc[i+1]
err = parentSignedZone.verifyRRSIG(signedZone.dsRRSig, signedZone.dsRRSet)
if err != nil {
return fmt.Errorf("for zone %s and RRSIG key tag %d: %w",
signedZone.zone, signedZone.dsRRSig.KeyTag, ErrRRSetValidation)
}

// Verify DS hash
err = signedZone.verifyDSRRSet()
if err != nil {
return err
}
}

return nil
}
30 changes: 30 additions & 0 deletions pkg/dnssec/ds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dnssec

import (
"errors"
"fmt"
"strings"

"github.com/miekg/dns"
)

var (
ErrInvalidDS = errors.New("DS RR record does not match DNS key")
ErrUnknownDsDigestType = errors.New("unknown DS digest type")
)

func verifyDS(receivedDS *dns.DS, dnsKey *dns.DNSKEY) error {
calculatedDS := dnsKey.ToDS(receivedDS.DigestType)
if calculatedDS == nil {
return fmt.Errorf("%w: %s", ErrUnknownDsDigestType,
dns.HashToString[receivedDS.DigestType])
}

if !strings.EqualFold(receivedDS.Digest, calculatedDS.Digest) {
return fmt.Errorf("%w: DS record has digest %s "+
"but calculated digest is %s", ErrInvalidDS,
receivedDS.Digest, calculatedDS.Digest)
}

return nil
}
132 changes: 132 additions & 0 deletions pkg/dnssec/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//go:build integration
// +build integration

package dnssec

import (
"context"
"net"
"testing"
"time"

"github.com/miekg/dns"
"github.com/qdm12/dns/v2/internal/server"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func getRRSetWithoutValidation(t *testing.T, zone string,
qType, qClass uint16) (rrset []dns.RR) {
t.Helper()

request := new(dns.Msg)
request.SetQuestion(zone, qType)
request.Question[0].Qclass = qClass

response, _, err := new(dns.Client).Exchange(request, "1.1.1.1:53")
require.NoError(t, err)

// Clear TTL since they are not predicatable
for _, rr := range response.Answer {
rr.Header().Ttl = 0
}

return response.Answer
}

func testExchange() server.Exchange {
client := &dns.Client{}
dialer := &net.Dialer{}
return func(ctx context.Context, request *dns.Msg) (response *dns.Msg, err error) {
netConn, err := dialer.DialContext(ctx, "udp", "1.1.1.1:53")
if err != nil {
return nil, err
}

dnsConn := &dns.Conn{Conn: netConn}
response, _, err = client.ExchangeWithConn(request, dnsConn)

_ = dnsConn.Close()

return response, err
}
}

func Test_fetchAndValidateZone(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
zone string
dnsType uint16
exchange server.Exchange
rrset []dns.RR
errWrapped error
errMessage string
}{
"valid DNSSEC": {
zone: "qqq.ninja.",
dnsType: dns.TypeA,
rrset: getRRSetWithoutValidation(t, "qqq.ninja.", dns.TypeA, dns.ClassINET),
exchange: testExchange(),
},
"www.iana.org.": {
zone: "vip.icann.org.",
dnsType: dns.TypeA,
exchange: testExchange(),
},
"no DNSSEC": {
zone: "github.com.",
dnsType: dns.TypeA,
rrset: getRRSetWithoutValidation(t, "github.com.", dns.TypeA, dns.ClassINET),
exchange: testExchange(),
},
"bad DNSSEC already failed by upstream": {
zone: "dnssec-failed.org.",
dnsType: dns.TypeA,
exchange: testExchange(),
errWrapped: ErrValidationFailedUpstream,
errMessage: "cannot fetch desired RRSet and RRSig: " +
"for dnssec-failed.org. IN A: " +
"DNSSEC validation might had failed upstream",
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

deadline, ok := t.Deadline()
if !ok {
deadline = time.Now().Add(5 * time.Second)
}

ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

rrset, err := fetchAndValidateZone(ctx, testCase.exchange,
testCase.zone, dns.ClassINET, testCase.dnsType)

// Remove TTL fields from rrset
for i := range rrset {
rrset[i].Header().Ttl = 0
}

assert.Equal(t, testCase.rrset, rrset)
require.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

func Benchmark_fetchAndValidateZone(b *testing.B) {
ctx := context.Background()
const zone = "qqq.ninja."
const dnsType = dns.TypeA
exchange := testExchange()

for i := 0; i < b.N; i++ {
_, _ = fetchAndValidateZone(ctx, exchange, zone, dns.ClassINET, dnsType)
}
}
Loading

0 comments on commit dc80277

Please sign in to comment.