Add middleware/dnssec (#133)

This adds an online dnssec middleware. The middleware will sign
responses on the fly. Negative responses are signed with NSEC black
lies.
This commit is contained in:
Miek Gieben
2016-04-26 17:57:11 +01:00
parent 8e6c690484
commit 1aa1a92198
39 changed files with 1206 additions and 144 deletions

View File

@@ -0,0 +1,35 @@
# dnssec
`dnssec` enables on-the-fly DNSSEC signing of served data.
## Syntax
~~~
dnssec [zones...]
~~~
* `zones` zones that should be signed. If empty the zones from the configuration block
are used.
If keys are not specified (see below) a key is generated and used for all signing operations. The
DNSSEC signing will treat this key a CSK (common signing key) forgoing the ZSK/KSK split. All
signing operations are done online. Authenticated denial of existence is implemented with NSEC black
lies. Using ECDSA as an algorithm is preferred as this leads to smaller signatures (compared to
RSA).
A signing key can be specified by using the `key` directive.
TODO(miek): think about key rollovers.
~~~
dnssec [zones... ] {
key file [key...]
}
~~~
* `key file` indicates key file(s) should be read from disk. When multiple keys are specified, RRset
will be signed with all keys. Generating a key can be done with `dnssec-keygen`: `dnssec-keygen -a
ECDSAP256SHA256 <zonename>`. A key created for zone *A* can be safely used for zone *B*.
## Examples

View File

@@ -0,0 +1,24 @@
package dnssec
import "github.com/miekg/dns"
// nsec returns an NSEC useful for NXDOMAIN respsones.
// 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(name, zone string, ttl, incep, expir uint32) ([]dns.RR, error) {
nsec := &dns.NSEC{}
nsec.Hdr = dns.RR_Header{Name: name, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC}
nsec.NextDomain = "\\000." + name
nsec.TypeBitMap = []uint16{dns.TypeRRSIG, dns.TypeNSEC}
sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir)
if err != nil {
return nil, err
}
return append(sigs, nsec), nil
}

View File

@@ -0,0 +1,50 @@
package dnssec
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
)
func TestZoneSigningBlackLies(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testNxdomainMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Ns, 2) {
t.Errorf("authority section should have 2 sig")
}
var nsec *dns.NSEC
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeNSEC {
nsec = r.(*dns.NSEC)
}
}
if m.Rcode != dns.RcodeSuccess {
t.Errorf("expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode)
}
if nsec == nil {
t.Fatalf("expected NSEC, got none")
}
if nsec.Hdr.Name != "ww.miek.nl." {
t.Errorf("expected %s, got %s", "ww.miek.nl.", nsec.Hdr.Name)
}
if nsec.NextDomain != "\\000.ww.miek.nl." {
t.Errorf("expected %s, got %s", "\\000.ww.miek.nl.", nsec.NextDomain)
}
t.Logf("%+v\n", m)
}
func testNxdomainMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
Question: []dns.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")},
}
}

View File

@@ -0,0 +1,23 @@
package dnssec
import (
"hash/fnv"
"strconv"
"github.com/miekg/dns"
)
// Key serializes the RRset and return a signature cache key.
func key(rrs []dns.RR) string {
h := fnv.New64()
buf := make([]byte, 256)
for _, r := range rrs {
off, err := dns.PackRR(r, buf, 0, nil, false)
if err == nil {
h.Write(buf[:off])
}
}
i := h.Sum64()
return strconv.FormatUint(i, 10)
}

View File

@@ -0,0 +1,32 @@
package dnssec
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
)
func TestCacheSet(t *testing.T) {
fPriv, rmPriv, _ := test.TempFile(t, ".", privKey)
fPub, rmPub, _ := test.TempFile(t, ".", pubKey)
defer rmPriv()
defer rmPub()
dnskey, err := ParseKeyFile(fPub, fPriv)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
m := testMsg()
state := middleware.State{Req: m}
k := key(m.Answer) // calculate *before* we add the sig
d := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil)
m = d.Sign(state, "miek.nl.", time.Now().UTC())
_, ok := d.get(k)
if !ok {
t.Errorf("signature was not added to the cache")
}
}

View File

@@ -0,0 +1,71 @@
package dnssec
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"errors"
"os"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
)
type DNSKEY struct {
K *dns.DNSKEY
s crypto.Signer
keytag uint16
}
// ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other
// utilities. It adds ".key" for the public key and ".private" for the private key.
func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) {
f, e := os.Open(pubFile)
if e != nil {
return nil, e
}
k, e := dns.ReadRR(f, pubFile)
if e != nil {
return nil, e
}
f, e = os.Open(privFile)
if e != nil {
return nil, e
}
p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, privFile)
if e != nil {
return nil, e
}
if v, ok := p.(*rsa.PrivateKey); ok {
return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
}
if v, ok := p.(*ecdsa.PrivateKey); ok {
return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
}
return &DNSKEY{k.(*dns.DNSKEY), nil, 0}, errors.New("no known? private key found")
}
// getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true.
func (d Dnssec) getDNSKEY(state middleware.State, zone string, do bool) *dns.Msg {
keys := make([]dns.RR, len(d.keys))
for i, k := range d.keys {
keys[i] = dns.Copy(k.K)
keys[i].Header().Name = zone
}
m := new(dns.Msg)
m.SetReply(state.Req)
m.Answer = keys
if !do {
return m
}
incep, expir := incepExpir(time.Now().UTC())
if sigs, err := d.sign(keys, zone, 3600, incep, expir); err == nil {
m.Answer = append(m.Answer, sigs...)
}
return m
}

127
middleware/dnssec/dnssec.go Normal file
View File

@@ -0,0 +1,127 @@
package dnssec
import (
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/singleflight"
"github.com/miekg/dns"
gcache "github.com/patrickmn/go-cache"
)
type Dnssec struct {
Next middleware.Handler
zones []string
keys []*DNSKEY
inflight *singleflight.Group
cache *gcache.Cache
}
func NewDnssec(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec {
return Dnssec{Next: next,
zones: zones,
keys: keys,
cache: gcache.New(defaultDuration, purgeDuration),
inflight: new(singleflight.Group),
}
}
// Sign signs the message m. it takes care of negative or nodata responses. It
// uses NSEC black lies for authenticated denial of existence. Signatures
// creates will be cached for a short while. By default we sign for 8 days,
// starting 3 hours ago.
func (d Dnssec) Sign(state middleware.State, zone string, now time.Time) *dns.Msg {
req := state.Req
mt, _ := middleware.Classify(req) // TODO(miek): need opt record here?
if mt == middleware.Delegation {
return req
}
incep, expir := incepExpir(now)
if mt == middleware.NameError {
if req.Ns[0].Header().Rrtype != dns.TypeSOA || len(req.Ns) > 1 {
return req
}
ttl := req.Ns[0].Header().Ttl
if sigs, err := d.sign(req.Ns, zone, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
if sigs, err := d.nsec(state.Name(), zone, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode
req.Rcode = dns.RcodeSuccess
}
return req
}
for _, r := range rrSets(req.Answer) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
req.Answer = append(req.Answer, sigs...)
}
}
for _, r := range rrSets(req.Ns) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
}
for _, r := range rrSets(req.Extra) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
req.Extra = append(req.Extra, sigs...)
}
}
return req
}
func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) {
k := key(rrs)
sgs, ok := d.get(k)
if ok {
return sgs, nil
}
sigs, err := d.inflight.Do(k, func() (interface{}, error) {
sigs := make([]dns.RR, len(d.keys))
var e error
for i, k := range d.keys {
sig := k.NewRRSIG(signerName, ttl, incep, expir)
e = sig.Sign(k.s, rrs)
sigs[i] = sig
}
d.set(k, sigs)
return sigs, e
})
return sigs.([]dns.RR), err
}
func (d Dnssec) set(key string, sigs []dns.RR) {
// we insert the sigs with a duration that is 24 hours less then the expiration, as these
// sigs have *just* been made the duration is 7 days.
d.cache.Set(key, sigs, eightDays-24*time.Hour)
}
func (d Dnssec) get(key string) ([]dns.RR, bool) {
if s, ok := d.cache.Get(key); ok {
return s.([]dns.RR), true
}
return nil, false
}
func incepExpir(now time.Time) (uint32, uint32) {
incep := uint32(now.Add(-3 * time.Hour).Unix()) // -(2+1) hours, be sure to catch daylight saving time and such
expir := uint32(now.Add(eightDays).Unix()) // sign for 8 days
return incep, expir
}
const (
purgeDuration = 3 * time.Hour
defaultDuration = 24 * time.Hour
eightDays = 8 * 24 * time.Hour
)

View File

@@ -0,0 +1,193 @@
package dnssec
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
)
func TestZoneSigning(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
if !section(m.Ns, 1) {
t.Errorf("authority section should have 1 sig")
}
}
func TestZoneSigningDouble(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
fPriv1, rmPriv1, _ := test.TempFile(t, ".", privKey1)
fPub1, rmPub1, _ := test.TempFile(t, ".", pubKey1)
defer rmPriv1()
defer rmPub1()
key1, err := ParseKeyFile(fPub1, fPriv1)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
d.keys = append(d.keys, key1)
m := testMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 2) {
t.Errorf("answer section should have 1 sig")
}
if !section(m.Ns, 2) {
t.Errorf("authority section should have 1 sig")
}
t.Logf("%+v\n", m)
}
// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org.
func TestSigningDifferentZone(t *testing.T) {
fPriv, rmPriv, _ := test.TempFile(t, ".", privKey)
fPub, rmPub, _ := test.TempFile(t, ".", pubKey)
defer rmPriv()
defer rmPub()
key, err := ParseKeyFile(fPub, fPriv)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
m := testMsgEx()
state := middleware.State{Req: m}
d := NewDnssec([]string{"example.org."}, []*DNSKEY{key}, nil)
m = d.Sign(state, "example.org.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
if !section(m.Ns, 1) {
t.Errorf("authority section should have 1 sig")
}
t.Logf("%+v\n", m)
}
func TestSigningCname(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsgCname()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
t.Logf("%+v\n", m)
}
func TestZoneSigningDelegation(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testDelegationMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Ns, 0) {
t.Errorf("authority section should have 0 sig")
t.Logf("%v\n", m)
}
if !section(m.Extra, 0) {
t.Errorf("answer section should have 0 sig")
t.Logf("%v\n", m)
}
}
func section(rss []dns.RR, nrSigs int) bool {
i := 0
for _, r := range rss {
if r.Header().Rrtype == dns.TypeRRSIG {
i++
}
}
return nrSigs == i
}
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.")},
}
}
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.")},
}
}
func testMsgCname() *dns.Msg {
return &dns.Msg{
Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")},
}
}
func testDelegationMsg() *dns.Msg {
return &dns.Msg{
Ns: []dns.RR{
test.NS("miek.nl. 3600 IN NS linode.atoom.net."),
test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."),
test.NS("miek.nl. 3600 IN NS omval.tednet.nl."),
},
Extra: []dns.RR{
test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"),
test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"),
},
}
}
func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) {
k, rm1, rm2 := newKey(t)
d := NewDnssec(zones, []*DNSKEY{k}, nil)
return d, rm1, rm2
}
func newKey(t *testing.T) (*DNSKEY, func(), func()) {
fPriv, rmPriv, _ := test.TempFile(t, ".", privKey)
fPub, rmPub, _ := test.TempFile(t, ".", pubKey)
key, err := ParseKeyFile(fPub, fPriv)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
return key, rmPriv, rmPub
}
const (
pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==`
privKey = `Private-key-format: v1.3
Algorithm: 13 (ECDSAP256SHA256)
PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs=
Created: 20160423195532
Publish: 20160423195532
Activate: 20160423195532
`
pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==`
privKey1 = `Private-key-format: v1.3
Algorithm: 13 (ECDSAP256SHA256)
PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c=
Created: 20160423211746
Publish: 20160423211746
Activate: 20160423211746
`
)

View File

@@ -0,0 +1,61 @@
package dnssec
import (
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
// ServeDNS implements the middleware.Handler interface.
func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := middleware.State{W: w, Req: r}
do := state.Do()
qname := state.Name()
qtype := state.QType()
zone := middleware.Zones(d.zones).Matches(qname)
if zone == "" {
return d.Next.ServeDNS(ctx, w, r)
}
// Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let
// the query through.
if qtype == dns.TypeDNSKEY {
for _, z := range d.zones {
if qname == z {
resp := d.getDNSKEY(state, z, do)
state.SizeAndDo(resp)
w.WriteMsg(resp)
return dns.RcodeSuccess, nil
}
}
}
drr := NewDnssecResponseWriter(w, d)
return d.Next.ServeDNS(ctx, drr, r)
}
var (
cacheHitCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: middleware.Namespace,
Subsystem: subsystem,
Name: "hit_count_total",
Help: "Counter of signatures that were found in the cache.",
}, []string{"zone"})
cacheMissCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: middleware.Namespace,
Subsystem: subsystem,
Name: "miss_count_total",
Help: "Counter of signatures that were not found in the cache.",
}, []string{"zone"})
)
const subsystem = "dnssec"
func init() {
prometheus.MustRegister(cacheHitCount)
prometheus.MustRegister(cacheMissCount)
}

View File

@@ -0,0 +1,170 @@
package dnssec
import (
"sort"
"strings"
"testing"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/file"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
var dnssecTestCases = []test.Case{
{
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY,
Answer: []dns.RR{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, Do: true,
Answer: []dns.RR{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
test.RRSIG("miek.nl. 3600 IN RRSIG DNSKEY 13 2 3600 20160503150844 20160425120844 18512 miek.nl. Iw/kNOyM"),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
}
var dnsTestCases = []test.Case{
{
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY,
Answer: []dns.RR{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true,
Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
test.RRSIG("miek.nl. 1800 IN RRSIG MX 13 2 3600 20160503192428 20160425162428 18512 miek.nl. 4nxuGKitXjPVA9zP1JIUvA09"),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
{
Qname: "www.miek.nl.", Qtype: dns.TypeAAAA, Do: true,
Answer: []dns.RR{
test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
test.RRSIG("a.miek.nl. 1800 IN RRSIG AAAA 13 3 3600 20160503193047 20160425163047 18512 miek.nl. UAyMG+gcnoXW3"),
test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
test.RRSIG("www.miek.nl. 1800 IN RRSIG CNAME 13 3 3600 20160503193047 20160425163047 18512 miek.nl. E3qGZn"),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
{
Qname: "www.example.org.", Qtype: dns.TypeAAAA, Do: true,
Rcode: dns.RcodeServerFailure,
// Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS.
},
}
func TestLookupZone(t *testing.T) {
zone, err := file.Parse(strings.NewReader(dbMiekNL), "miek.nl.", "stdin")
if err != nil {
return
}
fm := file.File{Next: test.ErrorHandler(), Zones: file.Zones{Z: map[string]*file.Zone{"miek.nl.": zone}, Names: []string{"miek.nl."}}}
dnskey, rm1, rm2 := newKey(t)
defer rm1()
defer rm2()
dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm)
ctx := context.TODO()
for _, tc := range dnsTestCases {
m := tc.Msg()
rec := middleware.NewResponseRecorder(&test.ResponseWriter{})
_, err := dh.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg()
sort.Sort(test.RRSet(resp.Answer))
sort.Sort(test.RRSet(resp.Ns))
sort.Sort(test.RRSet(resp.Extra))
if !test.Header(t, tc, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
func TestLookupDNSKEY(t *testing.T) {
dnskey, rm1, rm2 := newKey(t)
defer rm1()
defer rm2()
dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler())
ctx := context.TODO()
for _, tc := range dnssecTestCases {
m := tc.Msg()
rec := middleware.NewResponseRecorder(&test.ResponseWriter{})
_, err := dh.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg()
sort.Sort(test.RRSet(resp.Answer))
sort.Sort(test.RRSet(resp.Ns))
sort.Sort(test.RRSet(resp.Extra))
if !test.Header(t, tc, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
const dbMiekNL = `
$TTL 30M
$ORIGIN miek.nl.
@ IN SOA linode.atoom.net. miek.miek.nl. (
1282630057 ; Serial
4H ; Refresh
1H ; Retry
7D ; Expire
4H ) ; Negative Cache TTL
IN NS linode.atoom.net.
IN MX 1 aspmx.l.google.com.
IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
a IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
www IN CNAME a`

View File

@@ -0,0 +1,48 @@
package dnssec
import (
"log"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
)
type DnssecResponseWriter struct {
dns.ResponseWriter
d Dnssec
}
func NewDnssecResponseWriter(w dns.ResponseWriter, d Dnssec) *DnssecResponseWriter {
return &DnssecResponseWriter{w, d}
}
func (d *DnssecResponseWriter) WriteMsg(res *dns.Msg) error {
// By definition we should sign anything that comes back, we should still figure out for
// which zone it should be.
state := middleware.State{W: d.ResponseWriter, Req: res}
qname := state.Name()
zone := middleware.Zones(d.d.zones).Matches(qname)
if zone == "" {
return d.ResponseWriter.WriteMsg(res)
}
if state.Do() {
res = d.d.Sign(state, zone, time.Now().UTC())
}
state.SizeAndDo(res)
return d.ResponseWriter.WriteMsg(res)
}
func (d *DnssecResponseWriter) Write(buf []byte) (int, error) {
log.Printf("[WARNING] Dnssec called with Write: not signing reply")
n, err := d.ResponseWriter.Write(buf)
return n, err
}
func (d *DnssecResponseWriter) Hijack() {
d.ResponseWriter.Hijack()
return
}

View File

@@ -0,0 +1,53 @@
package dnssec
import "github.com/miekg/dns"
// newRRSIG return a new RRSIG, with all fields filled out, except the signed data.
func (k *DNSKEY) NewRRSIG(signerName string, ttl, incep, expir uint32) *dns.RRSIG {
sig := new(dns.RRSIG)
sig.Hdr.Rrtype = dns.TypeRRSIG
sig.Algorithm = k.K.Algorithm
sig.KeyTag = k.keytag
sig.SignerName = signerName
sig.Hdr.Ttl = ttl
sig.OrigTtl = origTtl
sig.Inception = incep
sig.Expiration = expir
return sig
}
type rrset struct {
qname string
qtype uint16
}
// rrSets returns rrs as a map of RRsets. It skips RRSIG and OPT records as those don't need to be signed.
func rrSets(rrs []dns.RR) map[rrset][]dns.RR {
m := make(map[rrset][]dns.RR)
for _, r := range rrs {
if r.Header().Rrtype == dns.TypeRRSIG || r.Header().Rrtype == dns.TypeOPT {
continue
}
if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok {
s = append(s, r)
m[rrset{r.Header().Name, r.Header().Rrtype}] = s
continue
}
s := make([]dns.RR, 1, 3)
s[0] = r
m[rrset{r.Header().Name, r.Header().Rrtype}] = s
}
if len(m) > 0 {
return m
}
return nil
}
const origTtl = 3600