Skip to content

Commit dc80277

Browse files
committed
wip
1 parent a6e7456 commit dc80277

15 files changed

Lines changed: 678 additions & 0 deletions

pkg/dnssec/chain.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package dnssec
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/miekg/dns"
10+
"github.com/qdm12/dns/v2/internal/server"
11+
)
12+
13+
// delegationChain is the DNSSEC chain of trust from the
14+
// queried zone to the root (.) zone.
15+
// See https://www.ietf.org/rfc/rfc4033.txt
16+
type delegationChain []*signedZone
17+
18+
// newDelegationChain queries the RRs required for the zone validation.
19+
// It begins the queries at the desired zone and then go
20+
// up the delegation tree until it reaches the root zone.
21+
// It returns a new delegation chain of signed zones where the
22+
// first signed zone (index 0) is the child zone and the last signed
23+
// zone is the root zone.
24+
func newDelegationChain(ctx context.Context, exchange server.Exchange,
25+
zone string, qClass uint16) (chain delegationChain, err error) {
26+
zoneParts := strings.Split(zone, ".")
27+
chain = make(delegationChain, len(zoneParts))
28+
29+
type result struct {
30+
i int
31+
signedZone *signedZone
32+
err error
33+
}
34+
results := make(chan result)
35+
36+
for i := range zoneParts {
37+
// 'example.com.', 'com.', '.'
38+
go func(i int, results chan<- result) {
39+
result := result{i: i}
40+
zoneName := dns.Fqdn(strings.Join(zoneParts[i:], "."))
41+
result.signedZone, result.err = queryDelegation(ctx, exchange, zoneName, qClass)
42+
if result.err != nil {
43+
result.err = fmt.Errorf("querying delegation for %s: %w", zoneName, result.err)
44+
}
45+
results <- result
46+
}(i, results)
47+
}
48+
49+
for range chain {
50+
result := <-results
51+
if result.err != nil && err == nil {
52+
err = result.err
53+
continue
54+
}
55+
chain[result.i] = result.signedZone
56+
}
57+
close(results)
58+
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
return chain, nil
64+
}
65+
66+
// queryDelegation obtains the DNSKEY records and the DS
67+
// records for a given zone. It does not query the
68+
// (non existent) DS record for the root zone.
69+
func queryDelegation(ctx context.Context, exchange server.Exchange,
70+
zone string, qClass uint16) (sz *signedZone, err error) {
71+
if zone == "." {
72+
// Only query DNSKEY since root zone has no DS record.
73+
rrsig, rrset, err := queryDNSKey(ctx, exchange, zone, qClass)
74+
if err != nil {
75+
return nil, fmt.Errorf("querying DNSKEY records: %w", err)
76+
}
77+
return &signedZone{
78+
zone: zone,
79+
dnsKeyRRSig: rrsig,
80+
dnsKeyRRSet: rrset,
81+
keyTagToDNSKey: dnsKeyRRSetToMap(rrset),
82+
}, nil
83+
}
84+
85+
ctx, cancel := context.WithCancel(ctx)
86+
defer cancel()
87+
88+
type result struct {
89+
t uint16
90+
rrsig *dns.RRSIG
91+
rrset []dns.RR
92+
err error
93+
}
94+
results := make(chan result)
95+
96+
go func(ctx context.Context, exchange server.Exchange, zone string, results chan<- result) {
97+
result := result{t: dns.TypeDNSKEY}
98+
result.rrsig, result.rrset, result.err = queryDNSKey(ctx, exchange, zone, qClass)
99+
if result.err != nil {
100+
result.err = fmt.Errorf("querying DNSKEY records: %w", result.err)
101+
}
102+
results <- result
103+
}(ctx, exchange, zone, results)
104+
105+
go func(ctx context.Context, exchange server.Exchange, zone string, results chan<- result) {
106+
result := result{t: dns.TypeDS}
107+
result.rrsig, result.rrset, result.err = queryDS(ctx, exchange, zone, qClass)
108+
if result.err != nil {
109+
result.err = fmt.Errorf("querying DS records: %w", result.err)
110+
}
111+
results <- result
112+
}(ctx, exchange, zone, results)
113+
114+
sz = &signedZone{
115+
zone: zone,
116+
}
117+
for i := 0; i < 2; i++ {
118+
result := <-results
119+
if result.err != nil {
120+
if err == nil { // first error encountered
121+
err = result.err
122+
cancel()
123+
}
124+
continue
125+
}
126+
if result.t == dns.TypeDS {
127+
sz.dsRRSig, sz.dsRRSet = result.rrsig, result.rrset
128+
} else {
129+
sz.dnsKeyRRSig, sz.dnsKeyRRSet = result.rrsig, result.rrset
130+
sz.keyTagToDNSKey = dnsKeyRRSetToMap(result.rrset)
131+
}
132+
}
133+
close(results)
134+
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
return sz, nil
140+
}
141+
142+
var (
143+
ErrRecordNotFound = errors.New("record not found")
144+
ErrRRSigNotFound = errors.New("RRSIG not found")
145+
)
146+
147+
func queryDNSKey(ctx context.Context, exchange server.Exchange,
148+
zone string, qClass uint16) (rrsig *dns.RRSIG,
149+
rrset []dns.RR, err error) {
150+
rrsig, rrset, err = fetchRRSetWithRRSig(ctx, exchange, zone, qClass, dns.TypeDNSKEY)
151+
switch {
152+
case err != nil:
153+
return nil, nil, err
154+
case len(rrset) == 0:
155+
return nil, nil, fmt.Errorf("%w", ErrRecordNotFound)
156+
case rrsig == nil:
157+
return nil, nil, fmt.Errorf("%w", ErrRRSigNotFound)
158+
}
159+
return rrsig, rrset, nil
160+
}
161+
162+
func queryDS(ctx context.Context, exchange server.Exchange,
163+
zone string, qClass uint16) (rrsig *dns.RRSIG,
164+
rrset []dns.RR, err error) {
165+
rrsig, rrset, err = fetchRRSetWithRRSig(ctx, exchange, zone, qClass, dns.TypeDS)
166+
switch {
167+
case err != nil:
168+
return nil, nil, err
169+
case len(rrset) == 0:
170+
return nil, nil, fmt.Errorf("%w", ErrRecordNotFound)
171+
case rrsig == nil:
172+
return nil, nil, fmt.Errorf("%w", ErrRRSigNotFound)
173+
}
174+
return rrsig, rrset, nil
175+
}
176+
177+
var (
178+
ErrRRSetValidation = errors.New("RRSet validation failed")
179+
)
180+
181+
// verify uses the zone data in the signed zone and its parent signed zones
182+
// to validate the DNSSEC chain of trust.
183+
// It starts the verification in the RRSet supplied as parameter (verifies
184+
// the RRSIG on the answer RRs), and, assuming a signature is correct and
185+
// valid, it walks through the linked list of signed zones checking the RRSIGs on
186+
// the DNSKEY and DS resource record sets, as well as correctness of each
187+
// delegation using the lower level methods in signedZone.
188+
func (dc delegationChain) verify(rrsig *dns.RRSIG, rrset []dns.RR) error {
189+
if rrsig == nil {
190+
return ErrRRSigNotFound
191+
}
192+
193+
signedZone := dc[0] // child desired zone
194+
195+
// Verify desired RRSet
196+
err := signedZone.verifyRRSIG(rrsig, rrset)
197+
if err != nil {
198+
return fmt.Errorf("for zone %s and RRSIG key tag %d: %w",
199+
signedZone.zone, rrsig.KeyTag, err)
200+
}
201+
202+
for i, signedZone := range dc {
203+
// Verify DNSKEY signature
204+
err := signedZone.verifyRRSIG(signedZone.dnsKeyRRSig, signedZone.dnsKeyRRSet)
205+
if err != nil {
206+
return fmt.Errorf("for zone %s and RRSIG key tag %d: %w",
207+
signedZone.zone, signedZone.dsRRSig.KeyTag, err)
208+
}
209+
210+
if signedZone.zone == "." { // last element in chain
211+
err = verifyRootSignedZone(signedZone)
212+
if err != nil {
213+
return fmt.Errorf("failed validating root zone: %w", err)
214+
}
215+
216+
break
217+
}
218+
219+
// Verify DS signature with parent zone DNSKEY
220+
parentSignedZone := dc[i+1]
221+
err = parentSignedZone.verifyRRSIG(signedZone.dsRRSig, signedZone.dsRRSet)
222+
if err != nil {
223+
return fmt.Errorf("for zone %s and RRSIG key tag %d: %w",
224+
signedZone.zone, signedZone.dsRRSig.KeyTag, ErrRRSetValidation)
225+
}
226+
227+
// Verify DS hash
228+
err = signedZone.verifyDSRRSet()
229+
if err != nil {
230+
return err
231+
}
232+
}
233+
234+
return nil
235+
}

pkg/dnssec/ds.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dnssec
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/miekg/dns"
9+
)
10+
11+
var (
12+
ErrInvalidDS = errors.New("DS RR record does not match DNS key")
13+
ErrUnknownDsDigestType = errors.New("unknown DS digest type")
14+
)
15+
16+
func verifyDS(receivedDS *dns.DS, dnsKey *dns.DNSKEY) error {
17+
calculatedDS := dnsKey.ToDS(receivedDS.DigestType)
18+
if calculatedDS == nil {
19+
return fmt.Errorf("%w: %s", ErrUnknownDsDigestType,
20+
dns.HashToString[receivedDS.DigestType])
21+
}
22+
23+
if !strings.EqualFold(receivedDS.Digest, calculatedDS.Digest) {
24+
return fmt.Errorf("%w: DS record has digest %s "+
25+
"but calculated digest is %s", ErrInvalidDS,
26+
receivedDS.Digest, calculatedDS.Digest)
27+
}
28+
29+
return nil
30+
}

pkg/dnssec/integration_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//go:build integration
2+
// +build integration
3+
4+
package dnssec
5+
6+
import (
7+
"context"
8+
"net"
9+
"testing"
10+
"time"
11+
12+
"github.com/miekg/dns"
13+
"github.com/qdm12/dns/v2/internal/server"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func getRRSetWithoutValidation(t *testing.T, zone string,
19+
qType, qClass uint16) (rrset []dns.RR) {
20+
t.Helper()
21+
22+
request := new(dns.Msg)
23+
request.SetQuestion(zone, qType)
24+
request.Question[0].Qclass = qClass
25+
26+
response, _, err := new(dns.Client).Exchange(request, "1.1.1.1:53")
27+
require.NoError(t, err)
28+
29+
// Clear TTL since they are not predicatable
30+
for _, rr := range response.Answer {
31+
rr.Header().Ttl = 0
32+
}
33+
34+
return response.Answer
35+
}
36+
37+
func testExchange() server.Exchange {
38+
client := &dns.Client{}
39+
dialer := &net.Dialer{}
40+
return func(ctx context.Context, request *dns.Msg) (response *dns.Msg, err error) {
41+
netConn, err := dialer.DialContext(ctx, "udp", "1.1.1.1:53")
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
dnsConn := &dns.Conn{Conn: netConn}
47+
response, _, err = client.ExchangeWithConn(request, dnsConn)
48+
49+
_ = dnsConn.Close()
50+
51+
return response, err
52+
}
53+
}
54+
55+
func Test_fetchAndValidateZone(t *testing.T) {
56+
t.Parallel()
57+
58+
testCases := map[string]struct {
59+
zone string
60+
dnsType uint16
61+
exchange server.Exchange
62+
rrset []dns.RR
63+
errWrapped error
64+
errMessage string
65+
}{
66+
"valid DNSSEC": {
67+
zone: "qqq.ninja.",
68+
dnsType: dns.TypeA,
69+
rrset: getRRSetWithoutValidation(t, "qqq.ninja.", dns.TypeA, dns.ClassINET),
70+
exchange: testExchange(),
71+
},
72+
"www.iana.org.": {
73+
zone: "vip.icann.org.",
74+
dnsType: dns.TypeA,
75+
exchange: testExchange(),
76+
},
77+
"no DNSSEC": {
78+
zone: "github.com.",
79+
dnsType: dns.TypeA,
80+
rrset: getRRSetWithoutValidation(t, "github.com.", dns.TypeA, dns.ClassINET),
81+
exchange: testExchange(),
82+
},
83+
"bad DNSSEC already failed by upstream": {
84+
zone: "dnssec-failed.org.",
85+
dnsType: dns.TypeA,
86+
exchange: testExchange(),
87+
errWrapped: ErrValidationFailedUpstream,
88+
errMessage: "cannot fetch desired RRSet and RRSig: " +
89+
"for dnssec-failed.org. IN A: " +
90+
"DNSSEC validation might had failed upstream",
91+
},
92+
}
93+
for name, testCase := range testCases {
94+
testCase := testCase
95+
t.Run(name, func(t *testing.T) {
96+
t.Parallel()
97+
98+
deadline, ok := t.Deadline()
99+
if !ok {
100+
deadline = time.Now().Add(5 * time.Second)
101+
}
102+
103+
ctx, cancel := context.WithDeadline(context.Background(), deadline)
104+
defer cancel()
105+
106+
rrset, err := fetchAndValidateZone(ctx, testCase.exchange,
107+
testCase.zone, dns.ClassINET, testCase.dnsType)
108+
109+
// Remove TTL fields from rrset
110+
for i := range rrset {
111+
rrset[i].Header().Ttl = 0
112+
}
113+
114+
assert.Equal(t, testCase.rrset, rrset)
115+
require.ErrorIs(t, err, testCase.errWrapped)
116+
if testCase.errWrapped != nil {
117+
assert.EqualError(t, err, testCase.errMessage)
118+
}
119+
})
120+
}
121+
}
122+
123+
func Benchmark_fetchAndValidateZone(b *testing.B) {
124+
ctx := context.Background()
125+
const zone = "qqq.ninja."
126+
const dnsType = dns.TypeA
127+
exchange := testExchange()
128+
129+
for i := 0; i < b.N; i++ {
130+
_, _ = fetchAndValidateZone(ctx, exchange, zone, dns.ClassINET, dnsType)
131+
}
132+
}

0 commit comments

Comments
 (0)