mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 08:14:18 -04:00
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>
This commit is contained in:
@@ -51,4 +51,5 @@ var Directives = []string{
|
||||
"erratic",
|
||||
"whoami",
|
||||
"on",
|
||||
"sign",
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -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
|
||||
|
||||
8
go.sum
8
go.sum
@@ -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=
|
||||
|
||||
@@ -60,3 +60,4 @@ grpc:grpc
|
||||
erratic:erratic
|
||||
whoami:whoami
|
||||
on:github.com/caddyserver/caddy/onevent
|
||||
sign:sign
|
||||
|
||||
161
plugin/sign/README.md
Normal file
161
plugin/sign/README.md
Normal file
@@ -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.
|
||||
20
plugin/sign/dnssec.go
Normal file
20
plugin/sign/dnssec.go
Normal file
@@ -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
|
||||
}
|
||||
93
plugin/sign/file.go
Normal file
93
plugin/sign/file.go
Normal file
@@ -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
|
||||
}
|
||||
43
plugin/sign/file_test.go
Normal file
43
plugin/sign/file_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
119
plugin/sign/keys.go
Normal file
119
plugin/sign/keys.go
Normal file
@@ -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]
|
||||
}
|
||||
5
plugin/sign/log_test.go
Normal file
5
plugin/sign/log_test.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package sign
|
||||
|
||||
import clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
|
||||
func init() { clog.Discard() }
|
||||
41
plugin/sign/nsec.go
Normal file
41
plugin/sign/nsec.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
42
plugin/sign/resign_test.go
Normal file
42
plugin/sign/resign_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
114
plugin/sign/setup.go
Normal file
114
plugin/sign/setup.go
Normal file
@@ -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
|
||||
}
|
||||
75
plugin/sign/setup_test.go
Normal file
75
plugin/sign/setup_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
plugin/sign/sign.go
Normal file
37
plugin/sign/sign.go
Normal file
@@ -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"
|
||||
222
plugin/sign/signer.go
Normal file
222
plugin/sign/signer.go
Normal file
@@ -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
|
||||
}
|
||||
102
plugin/sign/signer_test.go
Normal file
102
plugin/sign/signer_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
plugin/sign/testdata/Kmiek.nl.+013+59725.key
vendored
Normal file
5
plugin/sign/testdata/Kmiek.nl.+013+59725.key
vendored
Normal file
@@ -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==
|
||||
6
plugin/sign/testdata/Kmiek.nl.+013+59725.private
vendored
Normal file
6
plugin/sign/testdata/Kmiek.nl.+013+59725.private
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
Private-key-format: v1.3
|
||||
Algorithm: 13 (ECDSAP256SHA256)
|
||||
PrivateKey: rm7EdHRca//6xKpJzeoLt/mrfgQnltJ0WpQGtOG59yo=
|
||||
Created: 20190709192036
|
||||
Publish: 20190709192036
|
||||
Activate: 20190709192036
|
||||
17
plugin/sign/testdata/db.miek.nl
vendored
Normal file
17
plugin/sign/testdata/db.miek.nl
vendored
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user