plugin/dnssec: on delegation, sign DS or NSEC of no DS. (#5899)

* When returning NS for delegation point, we sign any DS Record or if not
  found we generate a NSEC proving absence of DS. This follow behaviour
  describe in rfc4035 (Section 3.1.4)
* DS request at apex behave as before.
* Fix edge case of requesting NSEC which prove that NSEC does not exist.

Signed-off-by: Jeremiejig <me@jeremiejig.fr>
This commit is contained in:
jeremiejig
2023-04-22 22:32:01 +02:00
committed by GitHub
parent 0862dd1cb5
commit 13e66918e3
5 changed files with 365 additions and 9 deletions

View File

@@ -1,6 +1,8 @@
package dnssec
import (
"strings"
"github.com/coredns/coredns/plugin/pkg/response"
"github.com/coredns/coredns/request"
@@ -11,15 +13,27 @@ import (
// See https://tools.ietf.org/html/draft-valsorda-dnsop-black-lies-00
// For example, a request for the non-existing name a.example.com would
// cause the following NSEC record to be generated:
//
// a.example.com. 3600 IN NSEC \000.a.example.com. ( RRSIG NSEC ... )
//
// This inturn makes every NXDOMAIN answer a NODATA one, don't forget to flip
// the header rcode to NOERROR.
func (d Dnssec) nsec(state request.Request, mt response.Type, ttl, incep, expir uint32, server string) ([]dns.RR, error) {
nsec := &dns.NSEC{}
nsec.Hdr = dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC}
nsec.NextDomain = "\\000." + state.QName()
if state.QName() == "." {
nsec.NextDomain = "\\000." // If You want to play as root server
}
if state.Name() == state.Zone {
nsec.TypeBitMap = filter18(state.QType(), apexBitmap, mt)
} else if mt == response.Delegation || state.QType() == dns.TypeDS {
nsec.TypeBitMap = delegationBitmap[:]
if mt == response.Delegation {
labels := dns.SplitDomainName(state.QName())
labels[0] += "\\000"
nsec.NextDomain = strings.Join(labels, ".") + "."
}
} else {
nsec.TypeBitMap = filter14(state.QType(), zoneBitmap, mt)
}
@@ -34,13 +48,14 @@ func (d Dnssec) nsec(state request.Request, mt response.Type, ttl, incep, expir
// The NSEC bit maps we return.
var (
zoneBitmap = [...]uint16{dns.TypeA, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
apexBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeSOA, dns.TypeHINFO, dns.TypeMX, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
delegationBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
zoneBitmap = [...]uint16{dns.TypeA, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
apexBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeSOA, dns.TypeHINFO, dns.TypeMX, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
)
// filter14 filters out t from bitmap (if it exists). If mt is not an NODATA response, just return the entire bitmap.
func filter14(t uint16, bitmap [14]uint16, mt response.Type) []uint16 {
if mt != response.NoData && mt != response.NameError {
if mt != response.NoData && mt != response.NameError || t == dns.TypeNSEC {
return zoneBitmap[:]
}
for i := range bitmap {
@@ -52,7 +67,7 @@ func filter14(t uint16, bitmap [14]uint16, mt response.Type) []uint16 {
}
func filter18(t uint16, bitmap [18]uint16, mt response.Type) []uint16 {
if mt != response.NoData && mt != response.NameError {
if mt != response.NoData && mt != response.NameError || t == dns.TypeNSEC {
return apexBitmap[:]
}
for i := range bitmap {

View File

@@ -71,16 +71,188 @@ func TestBlackLiesNoError(t *testing.T) {
}
}
func TestBlackLiesApexNsec(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testNsecMsg()
m.SetQuestion("miek.nl.", dns.TypeNSEC)
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC(), server)
if len(m.Ns) > 0 {
t.Error("Authority section should be empty")
}
if len(m.Answer) != 2 {
t.Errorf("Answer section should have 2 RRs")
}
sig, nsec := false, false
for _, rr := range m.Answer {
if _, ok := rr.(*dns.RRSIG); ok {
sig = true
}
if rnsec, ok := rr.(*dns.NSEC); ok {
nsec = true
var bitpresent uint
for _, typeBit := range rnsec.TypeBitMap {
switch typeBit {
case dns.TypeSOA:
bitpresent |= 4
case dns.TypeNSEC:
bitpresent |= 1
case dns.TypeRRSIG:
bitpresent |= 2
}
}
if bitpresent != 7 {
t.Error("NSEC must have SOA, RRSIG and NSEC in its bitmap")
}
}
}
if !sig || !nsec {
t.Errorf("Expected RRSIG and NSEC in answer section")
}
}
func TestBlackLiesNsec(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testNsecMsg()
m.SetQuestion("www.miek.nl.", dns.TypeNSEC)
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC(), server)
if len(m.Ns) > 0 {
t.Error("Authority section should be empty")
}
if len(m.Answer) != 2 {
t.Errorf("Answer section should have 2 RRs")
}
sig, nsec := false, false
for _, rr := range m.Answer {
if _, ok := rr.(*dns.RRSIG); ok {
sig = true
}
if rnsec, ok := rr.(*dns.NSEC); ok {
nsec = true
var bitpresent uint
for _, typeBit := range rnsec.TypeBitMap {
switch typeBit {
case dns.TypeNSEC:
bitpresent |= 1
case dns.TypeRRSIG:
bitpresent |= 2
}
}
if bitpresent != 3 {
t.Error("NSEC must have RRSIG and NSEC in its bitmap")
}
}
}
if !sig || !nsec {
t.Errorf("Expected RRSIG and NSEC in answer section")
}
}
func TestBlackLiesApexDS(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testApexDSMsg()
m.SetQuestion("miek.nl.", dns.TypeDS)
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC(), server)
if !section(m.Ns, 2) {
t.Errorf("Authority section should have 2 sigs")
}
var nsec *dns.NSEC
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeNSEC {
nsec = r.(*dns.NSEC)
}
}
if nsec == nil {
t.Error("Expected NSEC, got none")
} else if correctNsecForDS(nsec) {
t.Error("NSEC DS at the apex zone should cover all apex type.")
}
}
func TestBlackLiesDS(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testApexDSMsg()
m.SetQuestion("sub.miek.nl.", dns.TypeDS)
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC(), server)
if !section(m.Ns, 2) {
t.Errorf("Authority section should have 2 sigs")
}
var nsec *dns.NSEC
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeNSEC {
nsec = r.(*dns.NSEC)
}
}
if nsec == nil {
t.Error("Expected NSEC, got none")
} else if !correctNsecForDS(nsec) {
t.Error("NSEC DS should cover delegation type only.")
}
}
func correctNsecForDS(nsec *dns.NSEC) bool {
var bitmask uint
/* Coherent TypeBitMap for NSEC of DS should contain at least:
* {TypeNS, TypeNSEC, TypeRRSIG} and no SOA.
* Any missing type will confuse resolver because
* it will prove that the dns query cannot be a delegation point,
* which will break trust resolution for unsigned delegated domain.
* No SOA is obvious for none apex query.
*/
for _, typeBitmask := range nsec.TypeBitMap {
switch typeBitmask {
case dns.TypeNS:
bitmask |= 1
case dns.TypeNSEC:
bitmask |= 2
case dns.TypeRRSIG:
bitmask |= 4
case dns.TypeSOA:
return false
}
}
return bitmask == 7
}
func testNxdomainMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
Question: []dns.Question{{Name: "ww.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}},
Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")},
Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")},
}
}
func testSuccessMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess},
Question: []dns.Question{{Name: "www.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}},
Answer: []dns.RR{test.TXT(`www.miek.nl. 1800 IN TXT "response"`)},
Answer: []dns.RR{test.TXT(`www.miek.nl. 1800 IN TXT "response"`)},
}
}
func testNsecMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
Question: []dns.Question{{Name: "www.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeNSEC}},
Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")},
}
}
func testApexDSMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
Question: []dns.Question{{Name: "miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeDS}},
Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")},
}
}

View File

@@ -48,6 +48,22 @@ func (d Dnssec) Sign(state request.Request, now time.Time, server string) *dns.M
mt, _ := response.Typify(req, time.Now().UTC()) // TODO(miek): need opt record here?
if mt == response.Delegation {
// We either sign DS or NSEC of DS.
ttl := req.Ns[0].Header().Ttl
ds := []dns.RR{}
for i := range req.Ns {
if req.Ns[i].Header().Rrtype == dns.TypeDS {
ds = append(ds, req.Ns[i])
}
}
if len(ds) == 0 {
if sigs, err := d.nsec(state, mt, ttl, incep, expir, server); err == nil {
req.Ns = append(req.Ns, sigs...)
}
} else if sigs, err := d.sign(ds, state.Zone, ttl, incep, expir, server); err == nil {
req.Ns = append(req.Ns, sigs...)
}
return req
}
@@ -66,6 +82,10 @@ func (d Dnssec) Sign(state request.Request, now time.Time, server string) *dns.M
}
if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode
req.Rcode = dns.RcodeSuccess
if state.QType() == dns.TypeNSEC { // If original query was NSEC move Ns to Answer without SOA
req.Answer = req.Ns[len(req.Ns)-2 : len(req.Ns)]
req.Ns = nil
}
}
return req
}

View File

@@ -123,6 +123,60 @@ func TestSigningEmpty(t *testing.T) {
}
}
func TestDelegationSigned(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsgDelegationSigned()
m.SetQuestion("sub.miek.nl.", dns.TypeNS)
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC(), server)
if !section(m.Ns, 1) {
t.Errorf("Authority section should have 1 RRSIGs")
}
if !section(m.Extra, 0) {
t.Error("Extra section should not have RRSIGs")
}
}
func TestDelegationUnSigned(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsgDelegationUnSigned()
m.SetQuestion("sub.miek.nl.", dns.TypeNS)
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC(), server)
if !section(m.Ns, 1) {
t.Errorf("Authority section should have 1 RRSIG")
}
if !section(m.Extra, 0) {
t.Error("Extra section should not have RRSIG")
}
var nsec *dns.NSEC
var rrsig *dns.RRSIG
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeNSEC {
nsec = r.(*dns.NSEC)
}
if r.Header().Rrtype == dns.TypeRRSIG {
rrsig = r.(*dns.RRSIG)
}
}
if nsec == nil {
t.Error("Authority section should hold a NSEC record")
}
if rrsig.TypeCovered != dns.TypeNSEC {
t.Errorf("RRSIG should cover type %s, got %s",
dns.TypeToString[dns.TypeNSEC], dns.TypeToString[rrsig.TypeCovered])
}
if !correctNsecForDS(nsec) {
t.Error("NSEC as invalid TypeBitMap for a DS")
}
}
func section(rss []dns.RR, nrSigs int) bool {
i := 0
for _, r := range rss {
@@ -137,13 +191,13 @@ func testMsg() *dns.Msg {
// don't care about the message header
return &dns.Msg{
Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")},
Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")},
Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")},
}
}
func testMsgEx() *dns.Msg {
return &dns.Msg{
Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")},
Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")},
Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")},
}
}
@@ -163,6 +217,29 @@ func testMsgDname() *dns.Msg {
}
}
func testMsgDelegationSigned() *dns.Msg {
return &dns.Msg{
Ns: []dns.RR{
test.NS("sub.miek.nl. 1800 IN NS ns1.sub.miek.nl."),
test.DS("sub." + dsKey),
},
Extra: []dns.RR{
test.A("ns1.sub.miek.nl. 1800 IN A 192.0.2.1"),
},
}
}
func testMsgDelegationUnSigned() *dns.Msg {
return &dns.Msg{
Ns: []dns.RR{
test.NS("sub.miek.nl. 1800 IN NS ns1.sub.miek.nl."),
},
Extra: []dns.RR{
test.A("ns1.sub.miek.nl. 1800 IN A 192.0.2.1"),
},
}
}
func testEmptyMsg() *dns.Msg {
// don't care about the message header
return &dns.Msg{
@@ -197,6 +274,7 @@ Created: 20160423195532
Publish: 20160423195532
Activate: 20160423195532
`
dsKey = `miek.nl. IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9`
pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==`
privKey1 = `Private-key-format: v1.3
Algorithm: 13 (ECDSAP256SHA256)

View File

@@ -37,6 +37,73 @@ var dnsTestCases = []test.Case{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeNS, Do: true,
Answer: []dns.RR{
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"),
},
},
{
Qname: "deleg.miek.nl.", Qtype: dns.TypeNS, Do: true,
Ns: []dns.RR{
test.DS("deleg.miek.nl. 1800 IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9"),
test.NS("deleg.miek.nl. 1800 IN NS ns01.deleg.miek.nl."),
test.RRSIG("deleg.miek.nl. 1800 IN RRSIG DS 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"),
},
},
{
Qname: "unsigned.miek.nl.", Qtype: dns.TypeNS, Do: true,
Ns: []dns.RR{
test.NS("unsigned.miek.nl. 1800 IN NS ns01.deleg.miek.nl."),
test.NSEC("unsigned.miek.nl. 1800 IN NSEC unsigned\\000.miek.nl. NS RRSIG NSEC"),
test.RRSIG("unsigned.miek.nl. 1800 IN RRSIG NSEC 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"),
},
},
{ // DS should not come from dnssec plugin
Qname: "deleg.miek.nl.", Qtype: dns.TypeDS,
Answer: []dns.RR{
test.DS("deleg.miek.nl. 1800 IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9"),
},
Ns: []dns.RR{
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
},
},
{
Qname: "unsigned.miek.nl.", Qtype: dns.TypeDS,
Ns: []dns.RR{
test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeDS, Do: true,
Ns: []dns.RR{
test.NSEC("miek.nl. 1800 IN NSEC \\000.miek.nl. A HINFO NS SOA MX TXT AAAA LOC SRV CERT SSHFP RRSIG NSEC DNSKEY TLSA HIP OPENPGPKEY SPF"),
test.RRSIG("miek.nl. 1800 IN RRSIG NSEC 13 2 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"),
test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220141741 20171212111741 18512 miek.nl. 8bLTReqmuQtw=="),
test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
},
},
{
Qname: "deleg.miek.nl.", Qtype: dns.TypeDS, Do: true,
Answer: []dns.RR{
test.DS("deleg.miek.nl. 1800 IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9"),
test.RRSIG("deleg.miek.nl. 1800 IN RRSIG DS 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"),
},
Ns: []dns.RR{
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 3600 20161217114912 20161209084912 18512 miek.nl. ad9gA8VWgF1H8ze9/0Rk2Q=="),
},
},
{
Qname: "unsigned.miek.nl.", Qtype: dns.TypeDS, Do: true,
Ns: []dns.RR{
test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220141741 20171212111741 18512 miek.nl. 8bLTReqmuQtw=="),
test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
test.NSEC("unsigned.miek.nl. 1800 IN NSEC \\000.unsigned.miek.nl. NS RRSIG NSEC"),
test.RRSIG("unsigned.miek.nl. 1800 IN RRSIG NSEC 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{
@@ -179,4 +246,8 @@ $ORIGIN miek.nl.
a IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
www IN CNAME a`
www IN CNAME a
deleg IN NS ns01.deleg
IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9
unsigned IN NS ns01.deleg
`