aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugin/dnssec/README.md9
-rw-r--r--plugin/dnssec/cache_test.go6
-rw-r--r--plugin/dnssec/dnskey.go12
-rw-r--r--plugin/dnssec/dnssec.go46
-rw-r--r--plugin/dnssec/dnssec_test.go4
-rw-r--r--plugin/dnssec/handler_test.go4
-rw-r--r--plugin/dnssec/setup.go31
-rw-r--r--plugin/dnssec/setup_test.go61
8 files changed, 128 insertions, 45 deletions
diff --git a/plugin/dnssec/README.md b/plugin/dnssec/README.md
index 7b315b35d..c8e17bf4f 100644
--- a/plugin/dnssec/README.md
+++ b/plugin/dnssec/README.md
@@ -21,8 +21,13 @@ dnssec [ZONES... ] {
}
~~~
-The specified key is used for all signing operations. The DNSSEC signing will treat this key as a
-CSK (common signing key), forgoing the ZSK/KSK split. All signing operations are done online.
+The signing behavior depends on the keys specified. If multiple keys are specified of which there is
+at least one key with the SEP bit set and at least one key with the SEP bit unset, signing will happen
+in split ZSK/KSK mode. DNSKEY records will be signed with all keys that have the SEP bit set. All other
+records will be signed with all keys that do not have the SEP bit set.
+
+In any other case, each specified key will be treated as 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). NSEC3 is *not* supported.
diff --git a/plugin/dnssec/cache_test.go b/plugin/dnssec/cache_test.go
index 3df862a36..8d5ea8876 100644
--- a/plugin/dnssec/cache_test.go
+++ b/plugin/dnssec/cache_test.go
@@ -24,7 +24,7 @@ func TestCacheSet(t *testing.T) {
m := testMsg()
state := request.Request{Req: m, Zone: "miek.nl."}
k := hash(m.Answer) // calculate *before* we add the sig
- d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil, c)
+ d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, nil, c)
d.Sign(state, time.Now().UTC(), server)
_, ok := d.get(k, server)
@@ -48,7 +48,7 @@ func TestCacheNotValidExpired(t *testing.T) {
m := testMsg()
state := request.Request{Req: m, Zone: "miek.nl."}
k := hash(m.Answer) // calculate *before* we add the sig
- d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil, c)
+ d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, nil, c)
d.Sign(state, time.Now().UTC().AddDate(0, 0, -9), server)
_, ok := d.get(k, server)
@@ -72,7 +72,7 @@ func TestCacheNotValidYet(t *testing.T) {
m := testMsg()
state := request.Request{Req: m, Zone: "miek.nl."}
k := hash(m.Answer) // calculate *before* we add the sig
- d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil, c)
+ d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, nil, c)
d.Sign(state, time.Now().UTC().AddDate(0, 0, +9), server)
_, ok := d.get(k, server)
diff --git a/plugin/dnssec/dnskey.go b/plugin/dnssec/dnskey.go
index ed99fe6c2..4576306ef 100644
--- a/plugin/dnssec/dnskey.go
+++ b/plugin/dnssec/dnskey.go
@@ -28,6 +28,7 @@ func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) {
if e != nil {
return nil, e
}
+ defer f.Close()
k, e := dns.ReadRR(f, pubFile)
if e != nil {
return nil, e
@@ -37,6 +38,7 @@ func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) {
if e != nil {
return nil, e
}
+ defer f.Close()
dk, ok := k.(*dns.DNSKEY)
if !ok {
@@ -76,3 +78,13 @@ func (d Dnssec) getDNSKEY(state request.Request, zone string, do bool, server st
}
return m
}
+
+// Return true iff this is a zone key with the SEP bit unset. This implies a ZSK (rfc4034 2.1.1).
+func (k DNSKEY) isZSK() bool {
+ return k.K.Flags & (1<<8) == (1<<8) && k.K.Flags & 1 == 0
+}
+
+// Return true iff this is a zone key with the SEP bit set. This implies a KSK (rfc4034 2.1.1).
+func (k DNSKEY) isKSK() bool {
+ return k.K.Flags & (1<<8) == (1<<8) && k.K.Flags & 1 == 1
+}
diff --git a/plugin/dnssec/dnssec.go b/plugin/dnssec/dnssec.go
index 68b9eb52d..6f943ec88 100644
--- a/plugin/dnssec/dnssec.go
+++ b/plugin/dnssec/dnssec.go
@@ -18,19 +18,21 @@ import (
type Dnssec struct {
Next plugin.Handler
- zones []string
- keys []*DNSKEY
- inflight *singleflight.Group
- cache *cache.Cache
+ zones []string
+ keys []*DNSKEY
+ splitkeys bool
+ inflight *singleflight.Group
+ cache *cache.Cache
}
// New returns a new Dnssec.
-func New(zones []string, keys []*DNSKEY, next plugin.Handler, c *cache.Cache) Dnssec {
+func New(zones []string, keys []*DNSKEY, splitkeys bool, next plugin.Handler, c *cache.Cache) Dnssec {
return Dnssec{Next: next,
- zones: zones,
- keys: keys,
- cache: c,
- inflight: new(singleflight.Group),
+ zones: zones,
+ keys: keys,
+ splitkeys: splitkeys,
+ cache: c,
+ inflight: new(singleflight.Group),
}
}
@@ -97,15 +99,29 @@ func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32,
}
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 {
+ var sigs []dns.RR
+ for _, k := range d.keys {
+ if d.splitkeys {
+ if len(rrs) > 0 && rrs[0].Header().Rrtype == dns.TypeDNSKEY {
+ // We are signing a DNSKEY RRSet. With split keys, we need to use a KSK here.
+ if !k.isKSK() {
+ continue
+ }
+ } else {
+ // For non-DNSKEY RRSets, we want to use a ZSK.
+ if !k.isZSK() {
+ continue
+ }
+ }
+ }
sig := k.newRRSIG(signerName, ttl, incep, expir)
- e = sig.Sign(k.s, rrs)
- sigs[i] = sig
+ if e := sig.Sign(k.s, rrs); e != nil {
+ return sigs, e
+ }
+ sigs = append(sigs, sig)
}
d.set(k, sigs)
- return sigs, e
+ return sigs, nil
})
return sigs.([]dns.RR), err
}
diff --git a/plugin/dnssec/dnssec_test.go b/plugin/dnssec/dnssec_test.go
index ebd84c1df..8f3a3ba65 100644
--- a/plugin/dnssec/dnssec_test.go
+++ b/plugin/dnssec/dnssec_test.go
@@ -70,7 +70,7 @@ func TestSigningDifferentZone(t *testing.T) {
m := testMsgEx()
state := request.Request{Req: m, Zone: "example.org."}
c := cache.New(defaultCap)
- d := New([]string{"example.org."}, []*DNSKEY{key}, nil, c)
+ d := New([]string{"example.org."}, []*DNSKEY{key}, false, nil, c)
m = d.Sign(state, time.Now().UTC(), server)
if !section(m.Answer, 1) {
t.Errorf("Answer section should have 1 RRSIG")
@@ -218,7 +218,7 @@ func testEmptyMsg() *dns.Msg {
func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) {
k, rm1, rm2 := newKey(t)
c := cache.New(defaultCap)
- d := New(zones, []*DNSKEY{k}, nil, c)
+ d := New(zones, []*DNSKEY{k}, false, nil, c)
return d, rm1, rm2
}
diff --git a/plugin/dnssec/handler_test.go b/plugin/dnssec/handler_test.go
index ed9ddc5a5..35444eecb 100644
--- a/plugin/dnssec/handler_test.go
+++ b/plugin/dnssec/handler_test.go
@@ -104,7 +104,7 @@ func TestLookupZone(t *testing.T) {
defer rm1()
defer rm2()
c := cache.New(defaultCap)
- dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm, c)
+ dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, fm, c)
for _, tc := range dnsTestCases {
m := tc.Msg()
@@ -125,7 +125,7 @@ func TestLookupDNSKEY(t *testing.T) {
defer rm1()
defer rm2()
c := cache.New(defaultCap)
- dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler(), c)
+ dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, test.ErrorHandler(), c)
for _, tc := range dnssecTestCases {
m := tc.Msg()
diff --git a/plugin/dnssec/setup.go b/plugin/dnssec/setup.go
index 39b40d575..a699ee33c 100644
--- a/plugin/dnssec/setup.go
+++ b/plugin/dnssec/setup.go
@@ -25,14 +25,14 @@ func init() {
}
func setup(c *caddy.Controller) error {
- zones, keys, capacity, err := dnssecParse(c)
+ zones, keys, capacity, splitkeys, err := dnssecParse(c)
if err != nil {
return plugin.Error("dnssec", err)
}
ca := cache.New(capacity)
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
- return New(zones, keys, next, ca)
+ return New(zones, keys, splitkeys, next, ca)
})
c.OnStartup(func() error {
@@ -43,7 +43,7 @@ func setup(c *caddy.Controller) error {
return nil
}
-func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
+func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, bool, error) {
zones := []string{}
keys := []*DNSKEY{}
@@ -53,7 +53,7 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
i := 0
for c.Next() {
if i > 0 {
- return nil, nil, 0, plugin.ErrOnce
+ return nil, nil, 0, false, plugin.ErrOnce
}
i++
@@ -71,21 +71,21 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
case "key":
k, e := keyParse(c)
if e != nil {
- return nil, nil, 0, e
+ return nil, nil, 0, false, e
}
keys = append(keys, k...)
case "cache_capacity":
if !c.NextArg() {
- return nil, nil, 0, c.ArgErr()
+ return nil, nil, 0, false, c.ArgErr()
}
value := c.Val()
cacheCap, err := strconv.Atoi(value)
if err != nil {
- return nil, nil, 0, err
+ return nil, nil, 0, false, err
}
capacity = cacheCap
default:
- return nil, nil, 0, c.Errf("unknown property '%s'", x)
+ return nil, nil, 0, false, c.Errf("unknown property '%s'", x)
}
}
@@ -94,6 +94,17 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
zones[i] = plugin.Host(zones[i]).Normalize()
}
+ // Check if we have both KSKs and ZSKs.
+ zsk, ksk := 0, 0
+ for _, k := range keys {
+ if k.isKSK() {
+ ksk++
+ } else if k.isZSK() {
+ zsk++
+ }
+ }
+ splitkeys := zsk > 0 && ksk > 0
+
// Check if each keys owner name can actually sign the zones we want them to sign.
for _, k := range keys {
kname := plugin.Name(k.K.Header().Name)
@@ -105,11 +116,11 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
}
}
if !ok {
- return zones, keys, capacity, fmt.Errorf("key %s (keyid: %d) can not sign any of the zones", string(kname), k.tag)
+ return zones, keys, capacity, splitkeys, fmt.Errorf("key %s (keyid: %d) can not sign any of the zones", string(kname), k.tag)
}
}
- return zones, keys, capacity, nil
+ return zones, keys, capacity, splitkeys, nil
}
func keyParse(c *caddy.Controller) ([]*DNSKEY, error) {
diff --git a/plugin/dnssec/setup_test.go b/plugin/dnssec/setup_test.go
index b4ca7484b..1ca515f02 100644
--- a/plugin/dnssec/setup_test.go
+++ b/plugin/dnssec/setup_test.go
@@ -18,56 +18,71 @@ func TestSetupDnssec(t *testing.T) {
t.Fatalf("Failed to write private key file: %s", err)
}
defer func() { os.Remove("Kcluster.local.private") }()
+ if err := ioutil.WriteFile("ksk_Kcluster.local.key", []byte(kskpub), 0644); err != nil {
+ t.Fatalf("Failed to write pub key file: %s", err)
+ }
+ defer func() { os.Remove("ksk_Kcluster.local.key") }()
+ if err := ioutil.WriteFile("ksk_Kcluster.local.private", []byte(kskpriv), 0644); err != nil {
+ t.Fatalf("Failed to write private key file: %s", err)
+ }
+ defer func() { os.Remove("ksk_Kcluster.local.private") }()
tests := []struct {
input string
shouldErr bool
expectedZones []string
expectedKeys []string
+ expectedSplitkeys bool
expectedCapacity int
expectedErrContent string
}{
- {`dnssec`, false, nil, nil, defaultCap, ""},
- {`dnssec example.org`, false, []string{"example.org."}, nil, defaultCap, ""},
- {`dnssec 10.0.0.0/8`, false, []string{"10.in-addr.arpa."}, nil, defaultCap, ""},
+ {`dnssec`, false, nil, nil, false, defaultCap, ""},
+ {`dnssec example.org`, false, []string{"example.org."}, nil, false, defaultCap, ""},
+ {`dnssec 10.0.0.0/8`, false, []string{"10.in-addr.arpa."}, nil, false, defaultCap, ""},
{
`dnssec example.org {
cache_capacity 100
- }`, false, []string{"example.org."}, nil, 100, "",
+ }`, false, []string{"example.org."}, nil, false, 100, "",
},
{
`dnssec cluster.local {
key file Kcluster.local
- }`, false, []string{"cluster.local."}, nil, defaultCap, "",
+ }`, false, []string{"cluster.local."}, nil, false, defaultCap, "",
},
{
`dnssec example.org cluster.local {
key file Kcluster.local
- }`, false, []string{"example.org.", "cluster.local."}, nil, defaultCap, "",
+ }`, false, []string{"example.org.", "cluster.local."}, nil, false, defaultCap, "",
},
// fails
{
`dnssec example.org {
key file Kcluster.local
- }`, true, []string{"example.org."}, nil, defaultCap, "can not sign any",
+ }`, true, []string{"example.org."}, nil, false, defaultCap, "can not sign any",
},
{
`dnssec example.org {
key
- }`, true, []string{"example.org."}, nil, defaultCap, "argument count",
+ }`, true, []string{"example.org."}, nil, false, defaultCap, "argument count",
},
{
`dnssec example.org {
key file
- }`, true, []string{"example.org."}, nil, defaultCap, "argument count",
+ }`, true, []string{"example.org."}, nil, false, defaultCap, "argument count",
},
{`dnssec
- dnssec`, true, nil, nil, defaultCap, ""},
+ dnssec`, true, nil, nil, false, defaultCap, ""},
+ {
+ `dnssec cluster.local {
+ key file Kcluster.local
+ key file ksk_Kcluster.local
+ }`, false, []string{"cluster.local."}, nil, true, defaultCap, "",
+ },
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
- zones, keys, capacity, err := dnssecParse(c)
+ zones, keys, capacity, splitkeys, err := dnssecParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
@@ -93,6 +108,9 @@ func TestSetupDnssec(t *testing.T) {
t.Errorf("Dnssec not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, k, keys[i].K.Header().Name)
}
}
+ if splitkeys != test.expectedSplitkeys {
+ t.Errorf("Detected split keys does not match. Expected: %t, actual %t", test.expectedSplitkeys, splitkeys)
+ }
if capacity != test.expectedCapacity {
t.Errorf("Dnssec not correctly set capacity for input '%s' Expected: '%d', actual: '%d'", test.input, capacity, test.expectedCapacity)
}
@@ -120,3 +138,24 @@ Created: 20170901060531
Publish: 20170901060531
Activate: 20170901060531
`
+
+const kskpub = `; This is a zone-signing key, keyid 45330, for cluster.local.
+; Created: 20170901060531 (Fri Sep 1 08:05:31 2017)
+; Publish: 20170901060531 (Fri Sep 1 08:05:31 2017)
+; Activate: 20170901060531 (Fri Sep 1 08:05:31 2017)
+cluster.local. IN DNSKEY 257 3 5 AwEAAcFpDv+Cb23kFJowu+VU++b2N1uEHi6Ll9H0BzLasFOdJjEEclCO q/KlD4682vOMXxJNN8ZwOyiCa7Y0TEYqSwWvhHyn3bHCwuy4I6fss4Wd 7Y9dU+6QTgJ8LimGG40Iizjc9zqoU8Q+q81vIukpYWOHioHoY7hsWBvS RSlzDJk3`
+
+const kskpriv = `Private-key-format: v1.3
+Algorithm: 5 (RSASHA1)
+Modulus: wWkO/4JvbeQUmjC75VT75vY3W4QeLouX0fQHMtqwU50mMQRyUI6r8qUPjrza84xfEk03xnA7KIJrtjRMRipLBa+EfKfdscLC7Lgjp+yzhZ3tj11T7pBOAnwuKYYbjQiLONz3OqhTxD6rzW8i6SlhY4eKgehjuGxYG9JFKXMMmTc=
+PublicExponent: AQAB
+PrivateExponent: K5XyZFBPrjMVFX5gCZlyPyVDamNGrfSVXSIiMSqpS96BSdCXtmHAjCj4bZFPwkzi6+vs4tJN8p4ZifEVM0a6qwPZyENBrc2qbsweOXE6l8BaPVWFX30xvVRzGXuNtXxlBXE17zoHty5r5mRyRou1bc2HUS5otdkEjE30RiocQVk=
+Prime1: 7RRFUxaZkVNVH1DaT/SV5Sb8kABB389qLwU++argeDCVf+Wm9BBlTrsz2U6bKlfpaUmYZKtCCd+CVxqzMyuu0w==
+Prime2: 0NiY3d7Fa08IGY9L4TaFc02A721YcDNBBf95BP31qGvwnYsLFM/1xZwaEsIjohg8g+m/GpyIlvNMbK6pywIVjQ==
+Exponent1: XjXO8pype9mMmvwrNNix9DTQ6nxfsQugW30PMHGZ78kGr6NX++bEC0xS50jYWjRDGcbYGzD+9iNujSScD3qNZw==
+Exponent2: wkoOhLIfhUIj7etikyUup2Ld5WAbW15DSrotstg0NrgcQ+Q7reP96BXeJ79WeREFE09cyvv/EjdLzPv81/CbbQ==
+Coefficient: ah4LL0KLTO8kSKHK+X9Ud8grYi94QSNdbX11ge/eFcS/41QhDuZRTAFv4y0+IG+VWd+XzojLsQs+jzLe5GzINg==
+Created: 20170901060531
+Publish: 20170901060531
+Activate: 20170901060531
+`