diff options
author | 2019-08-29 15:41:59 +0100 | |
---|---|---|
committer | 2019-08-29 15:41:59 +0100 | |
commit | b8a0b52a5edc05145588598e7a5e2f00b82bb84d (patch) | |
tree | 64c8cb1a06028a4ea69a3df6d74c6f233055e70a | |
parent | eec24cb0138e74eb63f59521681f3e3b3555d4f0 (diff) | |
download | coredns-b8a0b52a5edc05145588598e7a5e2f00b82bb84d.tar.gz coredns-b8a0b52a5edc05145588598e7a5e2f00b82bb84d.tar.zst coredns-b8a0b52a5edc05145588598e7a5e2f00b82bb84d.zip |
plugin/sign: a plugin that signs zone (#2993)
* plugin/sign: a plugin that signs zones
Sign is a plugin that signs zone data (on disk). The README.md details
what exactly happens to should be accurate related to the code.
Signs are signed with a CSK, resigning and first time signing is all
handled by *sign* plugin.
Logging with a test zone looks something like this:
~~~ txt
[INFO] plugin/sign: Signing "miek.nl." because open plugin/sign/testdata/db.miek.nl.signed: no such file or directory
[INFO] plugin/sign: Signed "miek.nl." with key tags "59725" in 11.670985ms, saved in "plugin/sign/testdata/db.miek.nl.signed". Next: 2019-07-20T15:49:06.560Z
[INFO] plugin/file: Successfully reloaded zone "miek.nl." in "plugin/sign/testdata/db.miek.nl.signed" with serial 1563636548
[INFO] plugin/sign: Signing "miek.nl." because resign was: 10m0s ago
[INFO] plugin/sign: Signed "miek.nl." with key tags "59725" in 2.055895ms, saved in "plugin/sign/testdata/db.miek.nl.signed". Next: 2019-07-20T16:09:06.560Z
[INFO] plugin/file: Successfully reloaded zone "miek.nl." in "plugin/sign/testdata/db.miek.nl.signed" with serial 1563637748
~~~
Signed-off-by: Miek Gieben <miek@miek.nl>
* Adjust readme and remove timestamps
Signed-off-by: Miek Gieben <miek@miek.nl>
* Comment on the newline
Signed-off-by: Miek Gieben <miek@miek.nl>
* Update plugin/sign/README.md
Co-Authored-By: Michael Grosser <development@stp-ip.net>
-rw-r--r-- | core/dnsserver/zdirectives.go | 1 | ||||
-rw-r--r-- | core/plugin/zplugin.go | 1 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 8 | ||||
-rw-r--r-- | plugin.cfg | 1 | ||||
-rw-r--r-- | plugin/sign/README.md | 161 | ||||
-rw-r--r-- | plugin/sign/dnssec.go | 20 | ||||
-rw-r--r-- | plugin/sign/file.go | 93 | ||||
-rw-r--r-- | plugin/sign/file_test.go | 43 | ||||
-rw-r--r-- | plugin/sign/keys.go | 119 | ||||
-rw-r--r-- | plugin/sign/log_test.go | 5 | ||||
-rw-r--r-- | plugin/sign/nsec.go | 41 | ||||
-rw-r--r-- | plugin/sign/resign_test.go | 42 | ||||
-rw-r--r-- | plugin/sign/setup.go | 114 | ||||
-rw-r--r-- | plugin/sign/setup_test.go | 75 | ||||
-rw-r--r-- | plugin/sign/sign.go | 37 | ||||
-rw-r--r-- | plugin/sign/signer.go | 222 | ||||
-rw-r--r-- | plugin/sign/signer_test.go | 102 | ||||
-rw-r--r-- | plugin/sign/testdata/Kmiek.nl.+013+59725.key | 5 | ||||
-rw-r--r-- | plugin/sign/testdata/Kmiek.nl.+013+59725.private | 6 | ||||
-rw-r--r-- | plugin/sign/testdata/db.miek.nl | 17 |
21 files changed, 1114 insertions, 1 deletions
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index b48154448..6dfb99226 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -51,4 +51,5 @@ var Directives = []string{ "erratic", "whoami", "on", + "sign", } diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index e761f8586..d522ed294 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -40,6 +40,7 @@ import ( _ "github.com/coredns/coredns/plugin/root" _ "github.com/coredns/coredns/plugin/route53" _ "github.com/coredns/coredns/plugin/secondary" + _ "github.com/coredns/coredns/plugin/sign" _ "github.com/coredns/coredns/plugin/template" _ "github.com/coredns/coredns/plugin/tls" _ "github.com/coredns/coredns/plugin/trace" @@ -42,7 +42,7 @@ require ( github.com/tinylib/msgp v1.1.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect go.etcd.io/etcd v0.0.0-20190823073701-67d0c21bb04c - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect google.golang.org/api v0.9.0 @@ -353,6 +353,8 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU= +golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -382,6 +384,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -416,6 +420,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -458,6 +464,8 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190626174449-989357319d63 h1:UsSJe9fhWNSz6emfIGPpH5DF23t7ALo2Pf3sC+/hsdg= google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df h1:k3DT34vxk64+4bD5x+fRy6U0SXxZehzUHRSYUJcKfII= google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= diff --git a/plugin.cfg b/plugin.cfg index 2bd07c557..ff856f724 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -60,3 +60,4 @@ grpc:grpc erratic:erratic whoami:whoami on:github.com/caddyserver/caddy/onevent +sign:sign diff --git a/plugin/sign/README.md b/plugin/sign/README.md new file mode 100644 index 000000000..4753598d4 --- /dev/null +++ b/plugin/sign/README.md @@ -0,0 +1,161 @@ +# sign + +## Name + +*sign* - add DNSSEC records to zone files. + +## Description + +The *sign* plugin is used to sign (see RFC 6781) zones. In this process DNSSEC resource records are +added. The signatures that sign the resource records sets have an expiration date, this means the +signing process must be repeated before this expiration data is reached. Otherwise the zone's data +will go BAD (RFC 4035, Section 5.5). The *sign* plugin takes care of this. *Sign* works, but has +a couple of limitations, see the "Bugs" section. + +Only NSEC is supported, *sign* does not support NSEC3. + +*Sign* works in conjunction with the *file* and *auto* plugins; this plugin **signs** the zones +files, *auto* and *file* **serve** the zones *data*. + +For this plugin to work at least one Common Signing Key, (see coredns-keygen(1)) is needed. This key +(or keys) will be used to sign the entire zone. *Sign* does not support the ZSK/KSK split, nor will +it do key or algorithm rollovers - it just signs. + +*Sign* will: + + * (Re)-sign the zone with the CSK(s) when: + + - the last time it was signed is more than a 6 days ago. Each zone will have some jitter + applied to the inception date. + + - the signature only has 14 days left before expiring. + + Both these dates are only checked on the SOA's signature(s). + + * Create signatures that have an inception of -3 hours (minus a jitter between 0 and 18 hours) + and a expiration of +32 days for every given DNSKEY. + + * Add or replace *all* apex CDS/CDNSKEY records with the ones derived from the given keys. For + each key two CDS are created one with SHA1 and another with SHA256. + + * Update the SOA's serial number to the *Unix epoch* of when the signing happens. This will + overwrite *any* previous serial number. + +Thus there are two ways that dictate when a zone is signed. Normally every 6 days (plus jitter) it +will be resigned. If for some reason we fail this check, the 14 days before expiring kicks in. + +Keys are named (following BIND9): `K<name>+<alg>+<id>.key` and `K<name>+<alg>+<id>.private`. +The keys **must not** be included in your zone; they will be added by *sign*. These keys can be +generated with `coredns-keygen` or BIND9's `dnssec-keygen`. You don't have to adhere to this naming +scheme, but then you need to name your keys explicitly, see the `keys file` directive. + +A generated zone is written out in a file named `db.<name>.signed` in the directory named by the +`directory` directive (which defaults to `/var/lib/coredns`). + +## Syntax + +~~~ +sign DBFILE [ZONES...] { + key file|directory KEY...|DIR... + directory DIR +} +~~~ + +* **DBFILE** the zone database file to read and parse. If the path is relative, the path from the + *root* directive will be prepended to it. +* **ZONES** zones it should be sign for. If empty, the zones from the configuration block are + used. +* `key` specifies the key(s) (there can be multiple) to sign the zone. If `file` is + used the **KEY**'s filenames are used as is. If `directory` is used, *sign* will look in **DIR** + for `K<name>+<alg>+<id>` files. Any metadata in these files (Activate, Publish, etc.) is + *ignored*. These keys must also be Key Signing Keys (KSK). +* `directory` specifies the **DIR** where CoreDNS should save zones that have been signed. + If not given this defaults to `/var/lib/coredns`. The zones are saved under the name + `db.<name>.signed`. If the path is relative the path from the *root* directive will be prepended + to it. + +Keys can be generated with `coredns-keygen`, to create one for use in the *sign* plugin, use: +`coredns-keygen example.org` or `dnssec-keygen -a ECDSAP256SHA256 -f KSK example.org`. + +## Examples + +Sign the `example.org` zone contained in the file `db.example.org` and write the result to +`./db.example.org.signed` to let the *file* plugin pick it up and serve it. The keys used +are read from `/etc/coredns/keys/Kexample.org.key` and `/etc/coredns/keys/Kexample.org.private`. + +~~~ txt +example.org { + file db.example.org.signed + + sign db.example.org { + key file /etc/coredns/keys/Kexample.org + directory . + } +} +~~~ + +Running this leads to the following log output (note the timers in this example have been set to +shorter intervals). + +~~~ txt +[WARNING] plugin/file: Failed to open "open /tmp/db.example.org.signed: no such file or directory": trying again in 1m0s +[INFO] plugin/sign: Signing "example.org." because open /tmp/db.example.org.signed: no such file or directory +[INFO] plugin/sign: Successfully signed zone "example.org." in "/tmp/db.example.org.signed" with key tags "59725" and 1564766865 SOA serial, elapsed 9.357933ms, next: 2019-08-02T22:27:45.270Z +[INFO] plugin/file: Successfully reloaded zone "example.org." in "/tmp/db.example.org.signed" with serial 1564766865 +~~~ + +Or use a single zone file for *multiple* zones, note that the **ZONES** are repeated for both plugins. +Also note this outputs *multiple* signed output files. Here we use the default output directory +`/var/lib/coredns`. + +~~~ txt +. { + file /var/lib/coredns/db.example.org.signed example.org + file /var/lib/coredns/db.example.net.signed example.net + sign db.example.org example.org example.net { + key directory /etc/coredns/keys + } +} +~~~ + +This is the same configuration, but the zones are put in the server block, but note that you still +need to specify what file is served for what zone in the *file* plugin: + +~~~ txt +example.org example.net { + file var/lib/coredns/db.example.org.signed example.org + file var/lib/coredns/db.example.net.signed example.net + sign db.example.org { + key directory /etc/coredns/keys + } +} +~~~ + +Be careful to fully list the origins you want to sign, if you don't: + +~~~ txt +example.org example.net { + sign plugin/sign/testdata/db.example.org miek.org { + key file /etc/coredns/keys/Kexample.org + } +} +~~~ + +This will lead to `db.example.org` be signed *twice*, as this entire section is parsed twice because +you have specified the origins `example.org` and `example.net` in the server block. + +Forcibly resigning a zone can be accomplished by removing the signed zone file (CoreDNS will keep on +serving it from memory), and sending SIGUSR1 to the process to make it reload and resign the zone +file. + +## Also See + +The DNSSEC RFCs: RFC 4033, RFC 4034 and RFC 4035. And the BCP on DNSSEC, RFC 6781. Further more the +manual pages coredns-keygen(1) and dnssec-keygen(8). And the *file* plugin's documentation. + +Coredns-keygen can be found at <https://github.com/coredns/coredns-utils> in the coredns-keygen directory. + +## Bugs + +`keys directory` is not implemented. Glue records are currently signed, and no DS records are added +for child zones. diff --git a/plugin/sign/dnssec.go b/plugin/sign/dnssec.go new file mode 100644 index 000000000..a95e08644 --- /dev/null +++ b/plugin/sign/dnssec.go @@ -0,0 +1,20 @@ +package sign + +import ( + "github.com/miekg/dns" +) + +func (p Pair) signRRs(rrs []dns.RR, signerName string, ttl, incep, expir uint32) (*dns.RRSIG, error) { + rrsig := &dns.RRSIG{ + Hdr: dns.RR_Header{Rrtype: dns.TypeRRSIG, Ttl: ttl}, + Algorithm: p.Public.Algorithm, + SignerName: signerName, + KeyTag: p.KeyTag, + OrigTtl: ttl, + Inception: incep, + Expiration: expir, + } + + e := rrsig.Sign(p.Private, rrs) + return rrsig, e +} diff --git a/plugin/sign/file.go b/plugin/sign/file.go new file mode 100644 index 000000000..b1190126d --- /dev/null +++ b/plugin/sign/file.go @@ -0,0 +1,93 @@ +package sign + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/file/tree" + + "github.com/miekg/dns" +) + +// write writes out the zone file to a temporary file which is then moved into the correct place. +func (s *Signer) write(z *file.Zone) error { + f, err := ioutil.TempFile(s.directory, "signed-") + if err != nil { + return err + } + + if err := write(f, z); err != nil { + f.Close() + return err + } + + f.Close() + return os.Rename(f.Name(), filepath.Join(s.directory, s.signedfile)) +} + +func write(w io.Writer, z *file.Zone) error { + if _, err := io.WriteString(w, z.Apex.SOA.String()); err != nil { + return err + } + w.Write([]byte("\n")) // RR Stringer() method doesn't include newline, which ends the RR in a zone file, write that here. + for _, rr := range z.Apex.SIGSOA { + io.WriteString(w, rr.String()) + w.Write([]byte("\n")) + } + for _, rr := range z.Apex.NS { + io.WriteString(w, rr.String()) + w.Write([]byte("\n")) + } + for _, rr := range z.Apex.SIGNS { + io.WriteString(w, rr.String()) + w.Write([]byte("\n")) + } + err := z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error { + for _, r := range e.All() { + io.WriteString(w, r.String()) + w.Write([]byte("\n")) + } + return nil + }) + return err +} + +// Parse parses the zone in filename and returns a new Zone or an error. This +// is similar to the Parse function in the *file* plugin. However when parsing +// the record types DNSKEY, RRSIG, CDNSKEY and CDS are *not* included in the returned +// zone (if encountered). +func Parse(f io.Reader, origin, fileName string) (*file.Zone, error) { + zp := dns.NewZoneParser(f, dns.Fqdn(origin), fileName) + zp.SetIncludeAllowed(true) + z := file.NewZone(origin, fileName) + seenSOA := false + + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + if err := zp.Err(); err != nil { + return nil, err + } + + switch rr.(type) { + case *dns.DNSKEY, *dns.RRSIG, *dns.CDNSKEY, *dns.CDS: + continue + case *dns.SOA: + seenSOA = true + if err := z.Insert(rr); err != nil { + return nil, err + } + default: + if err := z.Insert(rr); err != nil { + return nil, err + } + } + } + if !seenSOA { + return nil, fmt.Errorf("file %q has no SOA record", fileName) + } + + return z, nil +} diff --git a/plugin/sign/file_test.go b/plugin/sign/file_test.go new file mode 100644 index 000000000..72d2b02ac --- /dev/null +++ b/plugin/sign/file_test.go @@ -0,0 +1,43 @@ +package sign + +import ( + "os" + "testing" + + "github.com/miekg/dns" +) + +func TestFileParse(t *testing.T) { + f, err := os.Open("testdata/db.miek.nl") + if err != nil { + t.Fatal(err) + } + z, err := Parse(f, "miek.nl.", "testdata/db.miek.nl") + if err != nil { + t.Fatal(err) + } + s := &Signer{ + directory: ".", + signedfile: "db.miek.nl.test", + } + + s.write(z) + defer os.Remove("db.miek.nl.test") + + f, err = os.Open("db.miek.nl.test") + if err != nil { + t.Fatal(err) + } + z, err = Parse(f, "miek.nl.", "db.miek.nl.test") + if err != nil { + t.Fatal(err) + } + if x := z.Apex.SOA.Header().Name; x != "miek.nl." { + t.Errorf("Expected SOA name to be %s, got %s", x, "miek.nl.") + } + apex, _ := z.Search("miek.nl.") + key := apex.Type(dns.TypeDNSKEY) + if key != nil { + t.Errorf("Expected no DNSKEYs, but got %d", len(key)) + } +} diff --git a/plugin/sign/keys.go b/plugin/sign/keys.go new file mode 100644 index 000000000..346175be0 --- /dev/null +++ b/plugin/sign/keys.go @@ -0,0 +1,119 @@ +package sign + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/coredns/coredns/core/dnsserver" + + "github.com/caddyserver/caddy" + "github.com/miekg/dns" + "golang.org/x/crypto/ed25519" +) + +// Pair holds DNSSEC key information, both the public and private components are stored here. +type Pair struct { + Public *dns.DNSKEY + KeyTag uint16 + Private crypto.Signer +} + +// keyParse reads the public and private key from disk. +func keyParse(c *caddy.Controller) ([]Pair, error) { + if !c.NextArg() { + return nil, c.ArgErr() + } + pairs := []Pair{} + config := dnsserver.GetConfig(c) + + switch c.Val() { + case "file": + ks := c.RemainingArgs() + if len(ks) == 0 { + return nil, c.ArgErr() + } + for _, k := range ks { + base := k + // Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205 + if strings.HasSuffix(k, ".key") { + base = k[:len(k)-4] + } + if strings.HasSuffix(k, ".private") { + base = k[:len(k)-8] + } + if !filepath.IsAbs(base) && config.Root != "" { + base = filepath.Join(config.Root, base) + } + + pair, err := readKeyPair(base+".key", base+".private") + if err != nil { + return nil, err + } + pairs = append(pairs, pair) + } + case "directory": + return nil, fmt.Errorf("directory: not implemented") + } + + return pairs, nil +} + +func readKeyPair(public, private string) (Pair, error) { + rk, err := os.Open(public) + if err != nil { + return Pair{}, err + } + b, err := ioutil.ReadAll(rk) + if err != nil { + return Pair{}, err + } + dnskey, err := dns.NewRR(string(b)) + if err != nil { + return Pair{}, err + } + if _, ok := dnskey.(*dns.DNSKEY); !ok { + return Pair{}, fmt.Errorf("RR in %q is not a DNSKEY: %d", public, dnskey.Header().Rrtype) + } + ksk := dnskey.(*dns.DNSKEY).Flags&(1<<8) == (1<<8) && dnskey.(*dns.DNSKEY).Flags&1 == 1 + if !ksk { + return Pair{}, fmt.Errorf("DNSKEY in %q is not a CSK/KSK", public) + } + + rp, err := os.Open(private) + if err != nil { + return Pair{}, err + } + privkey, err := dnskey.(*dns.DNSKEY).ReadPrivateKey(rp, private) + if err != nil { + return Pair{}, err + } + switch signer := privkey.(type) { + case *ecdsa.PrivateKey: + return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil + case *ed25519.PrivateKey: + return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil + case *rsa.PrivateKey: + return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil + default: + return Pair{}, fmt.Errorf("unsupported algorithm %s", signer) + } +} + +// keyTag returns the key tags of the keys in ps as a formatted string. +func keyTag(ps []Pair) string { + if len(ps) == 0 { + return "" + } + s := "" + for _, p := range ps { + s += strconv.Itoa(int(p.KeyTag)) + "," + } + return s[:len(s)-1] +} diff --git a/plugin/sign/log_test.go b/plugin/sign/log_test.go new file mode 100644 index 000000000..2726cd179 --- /dev/null +++ b/plugin/sign/log_test.go @@ -0,0 +1,5 @@ +package sign + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/plugin/sign/nsec.go b/plugin/sign/nsec.go new file mode 100644 index 000000000..d726ade72 --- /dev/null +++ b/plugin/sign/nsec.go @@ -0,0 +1,41 @@ +package sign + +import ( + "sort" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/file/tree" + + "github.com/miekg/dns" +) + +// names returns the elements of the zone in nsec order. If the returned boolean is true there were +// no other apex records than SOA and NS, which are stored separately. +func names(origin string, z *file.Zone) ([]string, bool) { + // if there are no apex records other than NS and SOA we'll miss the origin + // in this list. Check the first element and if not origin prepend it. + n := []string{} + z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error { + n = append(n, e.Name()) + return nil + }) + if len(n) == 0 { + return nil, false + } + if n[0] != origin { + n = append([]string{origin}, n...) + return n, true + } + return n, false +} + +// NSEC returns an NSEC record according to name, next, ttl and bitmap. Note that the bitmap is sorted before use. +func NSEC(name, next string, ttl uint32, bitmap []uint16) *dns.NSEC { + sort.Slice(bitmap, func(i, j int) bool { return bitmap[i] < bitmap[j] }) + + return &dns.NSEC{ + Hdr: dns.RR_Header{Name: name, Ttl: ttl, Rrtype: dns.TypeNSEC, Class: dns.ClassINET}, + NextDomain: next, + TypeBitMap: bitmap, + } +} diff --git a/plugin/sign/resign_test.go b/plugin/sign/resign_test.go new file mode 100644 index 000000000..e2571cb5b --- /dev/null +++ b/plugin/sign/resign_test.go @@ -0,0 +1,42 @@ +package sign + +import ( + "strings" + "testing" + "time" +) + +func TestResignInception(t *testing.T) { + then := time.Date(2019, 7, 18, 22, 50, 0, 0, time.UTC) + // signed yesterday + zr := strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190808191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x != nil { + t.Errorf("Expected RRSIG to be valid for %s, got invalid: %s", then.Format(timeFmt), x) + } + + // inception starts after this date. + zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190808191936 20190731161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x == nil { + t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt)) + } +} + +func TestResignExpire(t *testing.T) { + then := time.Date(2019, 7, 18, 22, 50, 0, 0, time.UTC) + // expires tomorrow + zr := strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190717191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x == nil { + t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt)) + } + // expire too far away + zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190731191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x != nil { + t.Errorf("Expected RRSIG to be valid for %s, got invalid: %s", then.Format(timeFmt), x) + } + + // expired yesterday + zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190721191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x == nil { + t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt)) + } +} diff --git a/plugin/sign/setup.go b/plugin/sign/setup.go new file mode 100644 index 000000000..6cc3b711d --- /dev/null +++ b/plugin/sign/setup.go @@ -0,0 +1,114 @@ +package sign + +import ( + "fmt" + "math/rand" + "path/filepath" + "time" + + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/caddyserver/caddy" +) + +func init() { + caddy.RegisterPlugin("sign", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + sign, err := parse(c) + if err != nil { + return plugin.Error("sign", err) + } + + c.OnStartup(sign.OnStartup) + c.OnStartup(func() error { + for _, signer := range sign.signers { + go signer.refresh(DurationRefreshHours) + } + return nil + }) + c.OnShutdown(func() error { + for _, signer := range sign.signers { + close(signer.stop) + } + return nil + }) + + // Don't call AddPlugin, *sign* is not a plugin. + return nil +} + +func parse(c *caddy.Controller) (*Sign, error) { + sign := &Sign{} + config := dnsserver.GetConfig(c) + + for c.Next() { + if !c.NextArg() { + return nil, c.ArgErr() + } + dbfile := c.Val() + if !filepath.IsAbs(dbfile) && config.Root != "" { + dbfile = filepath.Join(config.Root, dbfile) + } + + origins := make([]string, len(c.ServerBlockKeys)) + copy(origins, c.ServerBlockKeys) + args := c.RemainingArgs() + if len(args) > 0 { + origins = args + } + for i := range origins { + origins[i] = plugin.Host(origins[i]).Normalize() + } + + signers := make([]*Signer, len(origins)) + for i := range origins { + signers[i] = &Signer{ + dbfile: dbfile, + origin: plugin.Host(origins[i]).Normalize(), + jitter: time.Duration(float32(DurationJitter) * rand.Float32()), + directory: "/var/lib/coredns", + stop: make(chan struct{}), + signedfile: fmt.Sprintf("db.%ssigned", origins[i]), // origins[i] is a fqdn, so it ends with a dot, hence %ssigned. + } + } + + for c.NextBlock() { + switch c.Val() { + case "key": + pairs, err := keyParse(c) + if err != nil { + return sign, err + } + for i := range signers { + for _, p := range pairs { + p.Public.Header().Name = signers[i].origin + } + signers[i].keys = append(signers[i].keys, pairs...) + } + case "directory": + dir := c.RemainingArgs() + if len(dir) == 0 || len(dir) > 1 { + return sign, fmt.Errorf("can only be one argument after %q", "directory") + } + if !filepath.IsAbs(dir[0]) && config.Root != "" { + dir[0] = filepath.Join(config.Root, dir[0]) + } + for i := range signers { + signers[i].directory = dir[0] + signers[i].signedfile = fmt.Sprintf("db.%ssigned", signers[i].origin) + } + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + sign.signers = append(sign.signers, signers...) + } + + return sign, nil +} diff --git a/plugin/sign/setup_test.go b/plugin/sign/setup_test.go new file mode 100644 index 000000000..ce25720a0 --- /dev/null +++ b/plugin/sign/setup_test.go @@ -0,0 +1,75 @@ +package sign + +import ( + "testing" + + "github.com/caddyserver/caddy" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + exp *Signer + }{ + {`sign testdata/db.miek.nl miek.nl { + key file testdata/Kmiek.nl.+013+59725 + }`, + false, + &Signer{ + keys: []Pair{}, + origin: "miek.nl.", + dbfile: "testdata/db.miek.nl", + directory: "/var/lib/coredns", + signedfile: "db.miek.nl.signed", + }, + }, + {`sign testdata/db.miek.nl example.org { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }`, + false, + &Signer{ + keys: []Pair{}, + origin: "example.org.", + dbfile: "testdata/db.miek.nl", + directory: "testdata", + signedfile: "db.example.org.signed", + }, + }, + // errors + {`sign db.example.org { + key file /etc/coredns/keys/Kexample.org + }`, + true, + nil, + }, + } + for i, tc := range tests { + c := caddy.NewTestController("dns", tc.input) + sign, err := parse(c) + + if err == nil && tc.shouldErr { + t.Fatalf("Test %d expected errors, but got no error", i) + } + if err != nil && !tc.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } + if tc.shouldErr { + continue + } + signer := sign.signers[0] + if x := signer.origin; x != tc.exp.origin { + t.Errorf("Test %d expected %s as origin, got %s", i, tc.exp.origin, x) + } + if x := signer.dbfile; x != tc.exp.dbfile { + t.Errorf("Test %d expected %s as dbfile, got %s", i, tc.exp.dbfile, x) + } + if x := signer.directory; x != tc.exp.directory { + t.Errorf("Test %d expected %s as directory, got %s", i, tc.exp.directory, x) + } + if x := signer.signedfile; x != tc.exp.signedfile { + t.Errorf("Test %d expected %s as signedfile, got %s", i, tc.exp.signedfile, x) + } + } +} diff --git a/plugin/sign/sign.go b/plugin/sign/sign.go new file mode 100644 index 000000000..ebe4522e2 --- /dev/null +++ b/plugin/sign/sign.go @@ -0,0 +1,37 @@ +// Package sign implements a zone signer as a plugin. +package sign + +import ( + "path/filepath" + "time" +) + +// Sign contains signers that sign the zones files. +type Sign struct { + signers []*Signer +} + +// OnStartup scans all signers and signs or resigns zones if needed. +func (s *Sign) OnStartup() error { + for _, signer := range s.signers { + why := signer.resign() + if why == nil { + log.Infof("Skipping signing zone %q in %q: signatures are valid", signer.origin, filepath.Join(signer.directory, signer.signedfile)) + continue + } + go signAndLog(signer, why) + } + return nil +} + +// Various duration constants for signing of the zones. +const ( + DurationExpireDays = 7 * 24 * time.Hour // max time allowed before expiration + DurationResignDays = 6 * 24 * time.Hour // if the last sign happenend this long ago, sign again + DurationSignatureExpireDays = 32 * 24 * time.Hour // sign for 32 days + DurationRefreshHours = 5 * time.Hour // check zones every 5 hours + DurationJitter = -18 * time.Hour // default max jitter + DurationSignatureInceptionHours = -3 * time.Hour // -(2+1) hours, be sure to catch daylight saving time and such, jitter is substracted +) + +const timeFmt = "2006-01-02T15:04:05.000Z07:00" diff --git a/plugin/sign/signer.go b/plugin/sign/signer.go new file mode 100644 index 000000000..a9a5f2e5a --- /dev/null +++ b/plugin/sign/signer.go @@ -0,0 +1,222 @@ +package sign + +import ( + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/file/tree" + clog "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("sign") + +// Signer holds the data needed to sign a zone file. +type Signer struct { + keys []Pair + origin string + dbfile string + directory string + jitter time.Duration + + signedfile string + stop chan struct{} + + expiration uint32 + inception uint32 + ttl uint32 +} + +// Sign signs a zone file according to the parameters in s. +func (s *Signer) Sign(now time.Time) (*file.Zone, error) { + rd, err := os.Open(s.dbfile) + if err != nil { + return nil, err + } + + z, err := Parse(rd, s.origin, s.dbfile) + if err != nil { + return nil, err + } + + s.inception, s.expiration = lifetime(now, s.jitter) + + s.ttl = z.Apex.SOA.Header().Ttl + z.Apex.SOA.Serial = uint32(now.Unix()) + + for _, pair := range s.keys { + pair.Public.Header().Ttl = s.ttl // set TTL on key so it matches the RRSIG. + z.Insert(pair.Public) + z.Insert(pair.Public.ToDS(dns.SHA1)) + z.Insert(pair.Public.ToDS(dns.SHA256)) + z.Insert(pair.Public.ToDS(dns.SHA1).ToCDS()) + z.Insert(pair.Public.ToDS(dns.SHA256).ToCDS()) + z.Insert(pair.Public.ToCDNSKEY()) + } + + names, apex := names(s.origin, z) + ln := len(names) + + var nsec *dns.NSEC + if apex { + nsec = NSEC(s.origin, names[(ln+1)%ln], s.ttl, []uint16{dns.TypeSOA, dns.TypeNS, dns.TypeRRSIG, dns.TypeNSEC}) + z.Insert(nsec) + } + + for _, pair := range s.keys { + rrsig, err := pair.signRRs([]dns.RR{z.Apex.SOA}, s.origin, s.ttl, s.inception, s.expiration) + if err != nil { + return nil, err + } + z.Insert(rrsig) + // NS apex may not be set if RR's have been discarded because the origin doesn't match. + if len(z.Apex.NS) > 0 { + rrsig, err = pair.signRRs(z.Apex.NS, s.origin, s.ttl, s.inception, s.expiration) + if err != nil { + return nil, err + } + z.Insert(rrsig) + } + if apex { + rrsig, err = pair.signRRs([]dns.RR{nsec}, s.origin, s.ttl, s.inception, s.expiration) + if err != nil { + return nil, err + } + z.Insert(rrsig) + } + } + + // We are walking the tree in the same direction, so names[] can be used here to indicated the next element. + i := 1 + err = z.Walk(func(e *tree.Elem, zrrs map[uint16][]dns.RR) error { + if !apex && e.Name() == s.origin { + nsec := NSEC(e.Name(), names[(ln+i)%ln], s.ttl, append(e.Types(), dns.TypeNS, dns.TypeSOA, dns.TypeNSEC, dns.TypeRRSIG)) + z.Insert(nsec) + } else { + nsec := NSEC(e.Name(), names[(ln+i)%ln], s.ttl, append(e.Types(), dns.TypeNSEC, dns.TypeRRSIG)) + z.Insert(nsec) + } + + for t, rrs := range zrrs { + if t == dns.TypeRRSIG { + continue + } + for _, pair := range s.keys { + rrsig, err := pair.signRRs(rrs, s.origin, s.ttl, s.inception, s.expiration) + if err != nil { + return err + } + e.Insert(rrsig) + } + } + i++ + return nil + }) + return z, err +} + +// resign checks if the signed zone exists, or needs resigning. +func (s *Signer) resign() error { + signedfile := filepath.Join(s.directory, s.signedfile) + rd, err := os.Open(signedfile) + if err != nil && os.IsNotExist(err) { + return err + } + + now := time.Now().UTC() + return resign(rd, now) +} + +// resign will scan rd and check the signature on the SOA record. We will resign on the basis +// of 2 conditions: +// * either the inception is more than 6 days ago, or +// * we only have 1 week left on the signature +// +// All SOA signatures will be checked. If the SOA isn't found in the first 100 +// records, we will resign the zone. +func resign(rd io.Reader, now time.Time) (why error) { + zp := dns.NewZoneParser(rd, ".", "resign") + zp.SetIncludeAllowed(true) + i := 0 + + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + if err := zp.Err(); err != nil { + return err + } + + switch x := rr.(type) { + case *dns.RRSIG: + if x.TypeCovered != dns.TypeSOA { + continue + } + incep, _ := time.Parse("20060102150405", dns.TimeToString(x.Inception)) + // If too long ago, resign. + if now.Sub(incep) >= 0 && now.Sub(incep) > DurationResignDays { + return fmt.Errorf("inception %q was more than: %s ago from %s: %s", incep.Format(timeFmt), DurationResignDays, now.Format(timeFmt), now.Sub(incep)) + } + // Inception hasn't even start yet. + if now.Sub(incep) < 0 { + return fmt.Errorf("inception %q date is in the future: %s", incep.Format(timeFmt), now.Sub(incep)) + } + + expire, _ := time.Parse("20060102150405", dns.TimeToString(x.Expiration)) + if expire.Sub(now) < DurationExpireDays { + return fmt.Errorf("expiration %q is less than: %s away from %s: %s", expire.Format(timeFmt), DurationExpireDays, now.Format(timeFmt), expire.Sub(now)) + } + } + i++ + if i > 100 { + // 100 is a random number. A SOA record should be the first in the zonefile, but RFC 1035 doesn't actually mandate this. So it could + // be 3rd or even later. The number 100 looks crazy high enough that it will catch all weird zones, but not high enough to keep the CPU + // busy with parsing all the time. + return fmt.Errorf("no SOA RRSIG found in first 100 records") + } + } + + return nil +} + +func signAndLog(s *Signer, why error) { + now := time.Now().UTC() + z, err := s.Sign(now) + log.Infof("Signing %q because %s", s.origin, why) + if err != nil { + log.Warningf("Error signing %q with key tags %q in %s: %s, next: %s", s.origin, keyTag(s.keys), time.Since(now), err, now.Add(DurationRefreshHours).Format(timeFmt)) + return + } + + if err := s.write(z); err != nil { + log.Warningf("Error signing %q: failed to move zone file into place: %s", s.origin, err) + return + } + log.Infof("Successfully signed zone %q in %q with key tags %q and %d SOA serial, elapsed %f, next: %s", s.origin, filepath.Join(s.directory, s.signedfile), keyTag(s.keys), z.Apex.SOA.Serial, time.Since(now).Seconds(), now.Add(DurationRefreshHours).Format(timeFmt)) +} + +// refresh checks every val if some zones need to be resigned. +func (s *Signer) refresh(val time.Duration) { + tick := time.NewTicker(val) + defer tick.Stop() + for { + select { + case <-s.stop: + return + case <-tick.C: + why := s.resign() + if why == nil { + continue + } + signAndLog(s, why) + } + } +} + +func lifetime(now time.Time, jitter time.Duration) (uint32, uint32) { + incep := uint32(now.Add(DurationSignatureInceptionHours).Add(jitter).Unix()) + expir := uint32(now.Add(DurationSignatureExpireDays).Unix()) + return incep, expir +} diff --git a/plugin/sign/signer_test.go b/plugin/sign/signer_test.go new file mode 100644 index 000000000..6ba94247d --- /dev/null +++ b/plugin/sign/signer_test.go @@ -0,0 +1,102 @@ +package sign + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/caddyserver/caddy" + "github.com/miekg/dns" +) + +func TestSign(t *testing.T) { + input := `sign testdata/db.miek.nl miek.nl { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }` + c := caddy.NewTestController("dns", input) + sign, err := parse(c) + if err != nil { + t.Fatal(err) + } + if len(sign.signers) != 1 { + t.Fatalf("Expected 1 signer, got %d", len(sign.signers)) + } + z, err := sign.signers[0].Sign(time.Now().UTC()) + if err != nil { + t.Error(err) + } + + apex, _ := z.Search("miek.nl.") + if x := apex.Type(dns.TypeDS); len(x) != 2 { + t.Errorf("Expected %d DS records, got %d", 2, len(x)) + } + if x := apex.Type(dns.TypeCDS); len(x) != 2 { + t.Errorf("Expected %d CDS records, got %d", 2, len(x)) + } + if x := apex.Type(dns.TypeCDNSKEY); len(x) != 1 { + t.Errorf("Expected %d CDNSKEY record, got %d", 1, len(x)) + } + if x := apex.Type(dns.TypeDNSKEY); len(x) != 1 { + t.Errorf("Expected %d DNSKEY record, got %d", 1, len(x)) + } +} + +func TestSignApexZone(t *testing.T) { + apex := `$TTL 30M +$ORIGIN example.org. +@ IN SOA linode miek.miek.nl. ( 1282630060 4H 1H 7D 4H ) + IN NS linode +` + if err := ioutil.WriteFile("db.apex-test.example.org", []byte(apex), 0644); err != nil { + t.Fatal(err) + } + defer os.Remove("db.apex-test.example.org") + input := `sign db.apex-test.example.org example.org { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }` + c := caddy.NewTestController("dns", input) + sign, err := parse(c) + if err != nil { + t.Fatal(err) + } + z, err := sign.signers[0].Sign(time.Now().UTC()) + if err != nil { + t.Error(err) + } + + el, _ := z.Search("example.org.") + nsec := el.Type(dns.TypeNSEC) + if len(nsec) != 1 { + t.Errorf("Expected 1 NSEC for %s, got %d", "example.org.", len(nsec)) + } + if x := nsec[0].(*dns.NSEC).NextDomain; x != "example.org." { + t.Errorf("Expected NSEC NextDomain %s, got %s", "example.org.", x) + } + if x := nsec[0].(*dns.NSEC).TypeBitMap; len(x) != 8 { + t.Errorf("Expected NSEC bitmap to be %d elements, got %d", 8, x) + } + if x := nsec[0].(*dns.NSEC).TypeBitMap; x[7] != dns.TypeCDNSKEY { + t.Errorf("Expected NSEC bitmap element 6 to be %d, got %d", dns.TypeCDNSKEY, x[7]) + } + if x := nsec[0].(*dns.NSEC).TypeBitMap; x[5] != dns.TypeDNSKEY { + t.Errorf("Expected NSEC bitmap element 5 to be %d, got %d", dns.TypeDNSKEY, x[5]) + } + dnskey := el.Type(dns.TypeDNSKEY) + if x := dnskey[0].Header().Ttl; x != 1800 { + t.Errorf("Expected DNSKEY TTL to be %d, got %d", 1800, x) + } + sigs := el.Type(dns.TypeRRSIG) + for _, s := range sigs { + if s.(*dns.RRSIG).TypeCovered == dns.TypeDNSKEY { + if s.(*dns.RRSIG).OrigTtl != dnskey[0].Header().Ttl { + t.Errorf("Expected RRSIG original TTL to match DNSKEY TTL, but %d != %d", s.(*dns.RRSIG).OrigTtl, dnskey[0].Header().Ttl) + } + if s.(*dns.RRSIG).SignerName != dnskey[0].Header().Name { + t.Errorf("Expected RRSIG signer name to match DNSKEY ownername, but %s != %s", s.(*dns.RRSIG).SignerName, dnskey[0].Header().Name) + } + } + } +} diff --git a/plugin/sign/testdata/Kmiek.nl.+013+59725.key b/plugin/sign/testdata/Kmiek.nl.+013+59725.key new file mode 100644 index 000000000..b3e3654e3 --- /dev/null +++ b/plugin/sign/testdata/Kmiek.nl.+013+59725.key @@ -0,0 +1,5 @@ +; This is a key-signing key, keyid 59725, for miek.nl. +; Created: 20190709192036 (Tue Jul 9 20:20:36 2019) +; Publish: 20190709192036 (Tue Jul 9 20:20:36 2019) +; Activate: 20190709192036 (Tue Jul 9 20:20:36 2019) +miek.nl. IN DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ 52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw== diff --git a/plugin/sign/testdata/Kmiek.nl.+013+59725.private b/plugin/sign/testdata/Kmiek.nl.+013+59725.private new file mode 100644 index 000000000..2545ed9a9 --- /dev/null +++ b/plugin/sign/testdata/Kmiek.nl.+013+59725.private @@ -0,0 +1,6 @@ +Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: rm7EdHRca//6xKpJzeoLt/mrfgQnltJ0WpQGtOG59yo= +Created: 20190709192036 +Publish: 20190709192036 +Activate: 20190709192036 diff --git a/plugin/sign/testdata/db.miek.nl b/plugin/sign/testdata/db.miek.nl new file mode 100644 index 000000000..4041b1b5e --- /dev/null +++ b/plugin/sign/testdata/db.miek.nl @@ -0,0 +1,17 @@ +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( 1282630060 4H 1H 7D 4H ) + IN NS linode.atoom.net. + IN MX 1 aspmx.l.google.com. + IN AAAA 2a01:7e00::f03c:91ff:fe79:234c + IN DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw== + +a IN AAAA 2a01:7e00::f03c:91ff:fe79:234c +www IN CNAME a + + +bla IN NS ns1.bla.com. +ns3.blaaat.miek.nl. IN AAAA ::1 ; non-glue, should be signed. +; in baliwick nameserver that requires glue, should not be signed +bla IN NS ns2.bla.miek.nl. +ns2.bla.miek.nl. IN A 127.0.0.1 |