-
Test the local resolver configuration by verifying the validity of the root zone DNSKEY and SOA RRSets.
-
Test whether DNSSEC is enabled for a given TLD.
-
Check whether an email domain is fully protected (across all of its MX hosts) by DANE TLSA records, and whether these match the actual certificate chains seen at each IP address of each MX host.
-
Perform certificate chain verification at a time offset from the current time to ensure that that certificates are not about to expire too soon.
A non-zero exit status is returned if any DNS lookups fail or if the MX records or MX hosts are in an unsigned zone, or if for one of the MX hosts no associated secure TLSA records are found. A non-zero exit status is also returned if any of the SMTP connections fail to establish a TLS connection or yield a certificate chain that does not match the TLSA records.
Note that danecheck
prefers ECDSA to RSA, and only makes one
connection to each IP address, so for hosts that have both ECDSA and RSA
certificates, only the ECDSA certificate will be checked. Such hosts
are rare, and when their TLSA records are only correct for one of RSA
and ECDSA, it is almost always RSA that is properly configured and ECDSA
that is neglected. So, for now, testing ECDSA in preference to RSA is
typically a feature, not a bug.
The danecheck
command options are as below.
$ danecheck --help
danecheck - check for and validate SMTP TLSA records
Usage: danecheck [-N | (-n|--nameserver ADDRESS)] [-t|--timeout TIMEOUT]
[-r|--tries NUMTRIES] [-u|--udpsize SIZE] [-H|--helo HELO]
[-s|--smtptimeout TIMEOUT] [-l|--linelimit LENGTH]
[-R|--reserved] [-D|--down HOSTNAME] [-U|--down-only]
[-4|--noipv4] [-6|--noipv6] [-A|--all] [-d|--days DAYS]
[-e|--eechecks] [DOMAIN]
Available options:
-h,--help Show this help text
-N Use /etc/resolv.conf nameserver list
-n,--nameserver ADDRESS Use nameserver at ADDRESS (default: "127.0.0.1")
-t,--timeout TIMEOUT DNS request TIMEOUT (default: 3000 ms)
-r,--tries NUMTRIES at most NUMTRIES requests per lookup (default: 6)
-u,--udpsize SIZE set EDNS UDP buffer SIZE (default: 1216)
-H,--helo HELO send specified client HELO name
-s,--smtptimeout TIMEOUT SMTP TIMEOUT (default: 30000 ms)
-l,--linelimit LENGTH Maximum server SMTP response LENGTH (default: 4096)
-R,--reserved connect to reserved IP addresses
-D,--down HOSTNAME Specify one or more HOSTNAMEs that are down
-U,--down-only Limit connections to just the '-D' option hosts
-4,--noipv4 disable SMTP via IPv4
-6,--noipv6 disable SMTP via IPv6
-A,--all scan all MX hosts, not just those with TLSA RRs
-d,--days DAYS check validity at DAYS in the future
-e,--eechecks check end-entity (leaf) certificate dates and names
DOMAIN check the specified DOMAIN (default: ".")
When scanning the root domain, what's checked is secure retrieval of the root DNSKEY and SOA RRSets. Similarly, when scanning a top-level domain, what's checked is secure retrieval of its DS, DNSKEY and SOA records. For all other domains, MX records, address records and TLSA records are retrieved and must be DNSSEC signed.
Each MX host is expected to have TLSA records, an SMTP connection is made to each address of each such MX host (with the '-A' option connections are made to all MX hosts). A TLS handshake is performed to retrieve the hosts's certificate chain which is verified against the DNS TLSA RRs If anything is unavailable, insecure or wrong, a non-zero exit code is returned.
The '-D' option can be used multiple times to skip SMTP connections to MX hosts that are expected to be down. The '-U' option inverts the action of the '-D' option, and connects to only those MX hosts that are specified via the '-D' option (none if no such hosts are specified).
Reserved addresses include the address blocks from the IANA IPv4 and IPv6 special purpose address registries:
these include, for example, the RFC1918 private IPv4 ranges, and
should not appear among the addresses of MX hosts of internet-facing
email domains. If you're testing a non-public domain on an internal
network, you can use the -R
option to enable connections to
reserved addresses.
Haskell and stack can be downloaded from the Haskell platform website, and are also available as packages for various operating systems.
-
Older versions of
stack
can be used to install a more current version, which typically installs into~/.local/bin
.$ stack upgrade $ stack update
Some of the Haskell packages required for danecheck
depend on
optional C-libraries that may require the installation of additional
OS packages. Below is a partial list of known optional dependencies
absent on some systems. There are likely more on some systems.
- libicu for Unicode to ASCII conversion of domain names
The danecheck
repository uses submodules for some of its dependencies,
the --recursive
option to git clone
will automatically clone the
submodules.
$ git clone --recursive https://github.com/vdukhovni/danecheck
$ cd danecheck
Using a sufficiently recent version of stack
, in the top-level directory
of the cloned project run
$ stack install
which will compile and install a copy of the danecheck
executable in
Stack's default installation directory (typically ~/.local/bin).
It is assumed by default that your system has a working DNSSEC-validating resolver (BIND 9, unbound or similar) running locally and listening on the loopback interface at UDP and TCP at 127.0.0.1:53.
By default the system's /etc/resolv.conf
file is ignored and the
default nameserver list consists of just "127.0.0.1". If you want
to specify a different validating resolver, use the -n
option to
select an alternate IP address. The /etc/resol.conf nameserver list
can be selected via the "-N" option.
Assuming the installation directory is ~/.local/bin:
$ PATH=$HOME/.local/bin:$PATH
$ danecheck || printf "ERROR: root zone record validation failed" >&2
This should output a validated copy of the root zone DNSKEY and SOA RRSets and not print the ERROR message. For example (key base64 data abbreviated):
$ danecheck
. IN DNSKEY 256 3 8 AwEAAbPwrxwt...I5QymeSkJJzc= ; AD=1 NoError
. IN DNSKEY 257 3 8 AwEAAaz/tAm8...NR1AkUTV74bU= ; AD=1 NoError
. IN SOA a.root-servers.net. [email protected]. 2019101700 1800 900 604800 86400 ; AD=1 NoError
The ; AD=1 NoError
DNS comments appended to each output line indicates
that the resolver obtained a DNSSEC validated result. The .
between the
first and second DNS labels of the SOA contact mailbox field is displayed
as an @
sign, since some domains have literal .
characters in the
localpart (first label) of the address. However, at present, the trailing
.
is not presently stripped from the domain part of the address.
If your domain's ancestor TLD is not DNSSEC signed (still the case for
some ccTLD domains), then DNSSEC will not be used for your domain either,
except from resolvers that have configured a custom trust-anchor for
your domain or one if its ancestor domains. When checking the DNSSEC
status of a TLD danecheck
outputs its DS, DNSKEY and SOA RRsets.
For example:
$ danecheck org
org. IN DS 9795 7 1 364dfab3daf254cab477b5675b10766ddaa24982 ; AD=1 NoError
org. IN DS 9795 7 2 3922b31b6f3a4ea92b19eb7b52120f031fd8e05ff0b03bafcf9f891bfe7ff8e5 ; AD=1 NoError
org. IN DNSKEY 256 3 7 AwEAAb7ojfnp...nL5k7Y/VeZRpR ; AD=1 NoError
org. IN DNSKEY 256 3 7 AwEAAdEExfqc...9U9q91zgJ9bkn ; AD=1 NoError
org. IN DNSKEY 257 3 7 AwEAAZTjbIO5...r8ti6MNoJEHU= ; AD=1 NoError
org. IN DNSKEY 257 3 7 AwEAAcMnWBKL...nwXCNDXk0kk0= ; AD=1 NoError
org. IN SOA a0.org.afilias-nst.info. [email protected]. 2013668330 1800 900 604800 86400 ; AD=1 NoError
With your resolver tested for working root zone security and DNSSEC working for your TLD, you can proceed to regularly test your own domain. Example:
$ domain=openssl.org
$ danecheck "$domain" || printf "ERROR: DANE security check failed for: %s\n" "$domain"
openssl.org. IN DS 44671 8 2 30abf6c1b7de947ddf1c1b01206c126685e5eda880bf9dc8815eae7c474f83f9 ; AD=1 NoError
openssl.org. IN DNSKEY 256 3 8 AwEAAaJsnu//...v0lJQkbhta8V7 ; AD=1 NoError
openssl.org. IN DNSKEY 257 3 8 AwEAAbxptd2o...XBUsIsxlbmYs= ; AD=1 NoError
openssl.org. IN MX 50 mta.openssl.org. ; AD=1 NoError
mta.openssl.org. IN A 194.97.150.230 ; AD=1 NoError
mta.openssl.org. IN AAAA 2001:608:c00:180::1:e6 ; AD=1 NoError
_25._tcp.mta.openssl.org. IN TLSA 3 1 1 6cf12d78fbf242909d01b96ab5590812954058dc32f8415f048fff064291921e ; AD=1 NoError
mta.openssl.org[194.97.150.230]: pass: TLSA match: depth = 0, name = mta.openssl.org
TLS = TLS12 with ECDHE-RSA-AES256GCM-SHA384,P256
name = mta.openssl.org
depth = 0
Issuer CommonName = Let's Encrypt Authority X3
Issuer Organization = Let's Encrypt
notBefore = 2019-08-26T11:00:16Z
notAfter = 2019-11-24T11:00:16Z
Subject CommonName = mta.openssl.org
pkey sha256 [matched] <- 3 1 1 6cf12d78fbf242909d01b96ab5590812954058dc32f8415f048fff064291921e
depth = 1
Issuer CommonName = DST Root CA X3
Issuer Organization = Digital Signature Trust Co.
notBefore = 2016-03-17T16:40:46Z
notAfter = 2021-03-17T16:40:46Z
Subject CommonName = Let's Encrypt Authority X3
Subject Organization = Let's Encrypt
pkey sha256 [nomatch] <- 2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
mta.openssl.org[2001:608:c00:180::1:e6]: pass: TLSA match: depth = 0, name = mta.openssl.org
TLS = TLS12 with ECDHE-RSA-AES256GCM-SHA384,P256
name = mta.openssl.org
depth = 0
Issuer CommonName = Let's Encrypt Authority X3
Issuer Organization = Let's Encrypt
notBefore = 2019-08-26T11:00:16Z
notAfter = 2019-11-24T11:00:16Z
Subject CommonName = mta.openssl.org
pkey sha256 [matched] <- 3 1 1 6cf12d78fbf242909d01b96ab5590812954058dc32f8415f048fff064291921e
depth = 1
Issuer CommonName = DST Root CA X3
Issuer Organization = Digital Signature Trust Co.
notBefore = 2016-03-17T16:40:46Z
notAfter = 2021-03-17T16:40:46Z
Subject CommonName = Let's Encrypt Authority X3
Subject Organization = Let's Encrypt
pkey sha256 [nomatch] <- 2 1 1 60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
If the exit code indicates failure you should check the output for:
- DNS Failures
- Any failed DNS queries (not
NoError
orNODATA
) or insecure answers (AD=0
) - Non-existent MX hosts or TLSA records
- Any failed DNS queries (not
- SMTP failures
- Failures to connect to an MX host at one or more of its IP addresses
- Rejected or timed-out SMTP commands
- Lack of STARTTLS support
- Failure to complete the TLS handshake
- Chain verification failures
- Failure to find matching TLSA records
- Name check failure with DANE-TA(2) TLSA records
- Certificate expiration with DANE-TA(2) TLSA records
If some of your MX hosts are down, and you want to verify the certificate
chains of only the remaining hosts, you can specify the --down
option
one or more times to skip SMTP tests for those hosts, their DNS security
(including presence of TLSA records) will still be tested and will be
required for the overall check to succeed. In the example below, the host
bh.nic.cz
is down and is skipped, allowing the overall check to succeed.
$ danecheck --down bh.nic.cz cznic.cz; echo $?
cznic.cz. IN DS 61281 13 2 fac1a7f06c7c...c6d07e7d8ef7 ; AD=1 NoError
cznic.cz. IN DNSKEY 256 3 13 rs6oetkFuqOg...swO3BfKoLw== ; AD=1 NoError
cznic.cz. IN DNSKEY 257 3 13 LM4zvjUgZi2X...TrDzWmmHwQ== ; AD=1 NoError
cznic.cz. IN MX 10 mail.nic.cz. ; AD=1 NoError
cznic.cz. IN MX 15 mx.nic.cz. ; AD=1 NoError
cznic.cz. IN MX 20 bh.nic.cz. ; AD=1 NoError
mail.nic.cz. IN A 217.31.204.67 ; AD=1 NoError
mail.nic.cz. IN AAAA 2001:1488:800:400::400 ; AD=1 NoError
_25._tcp.mail.nic.cz. IN TLSA 3 1 1 4f9736249ab5...6194f5bb2e09 ; AD=1 NoError
mail.nic.cz[217.31.204.67]: pass: TLSA match: depth = 0, name = mail.nic.cz
TLS = TLS12 with ECDHE-RSA-AES256GCM-SHA384
name = jabber.nic.cz
name = lists.nic.cz
name = mail.nic.cz
name = nic.cz
depth = 0
Issuer CommonName = Let's Encrypt Authority X3
Issuer Organization = Let's Encrypt
notBefore = 2017-08-03T13:02:00Z
notAfter = 2017-11-01T13:02:00Z
Subject CommonName = mail.nic.cz
pkey sha256 [matched] <- 3 1 1 4f9736249ab5...6194f5bb2e09
depth = 1
Issuer CommonName = DST Root CA X3
Issuer Organization = Digital Signature Trust Co.
notBefore = 2016-03-17T16:40:46Z
notAfter = 2021-03-17T16:40:46Z
Subject CommonName = Let's Encrypt Authority X3
Subject Organization = Let's Encrypt
pkey sha256 [nomatch] <- 2 1 1 60b87575447d...0517616e8a18
mx.nic.cz. IN A 217.31.58.56 ; AD=1 NoError
mx.nic.cz. IN AAAA 2001:1ab0:7e1e:c574:7a2b:cbff:fe33:7019 ; AD=1 NoError
_25._tcp.mx.nic.cz. IN TLSA 3 1 1 a9205f093637...b519bf47a523 ; AD=1 NoError
mx.nic.cz[217.31.58.56]: pass: TLSA match: depth = 0, name = mx.nic.cz
TLS = TLS12 with ECDHE-RSA-AES256GCM-SHA384
name = mx.nic.cz
depth = 0
Issuer CommonName = CZ.NIC SHA2 Root Certification Authority
Issuer Organization = CZ.NIC, z.s.p.o.
notBefore = 2017-02-13T09:29:27Z
notAfter = 2019-02-13T09:29:27Z
Subject CommonName = mx.nic.cz
Subject Organization = CZ.NIC
pkey sha256 [matched] <- 3 1 1 a9205f093637...b519bf47a523
depth = 1
Issuer CommonName = CZ.NIC SHA2 Root Certification Authority
Issuer Organization = CZ.NIC, z.s.p.o.
notBefore = 2016-02-19T13:58:59Z
notAfter = 2026-02-16T13:58:59Z
Subject CommonName = CZ.NIC SHA2 Root Certification Authority
Subject Organization = CZ.NIC, z.s.p.o.
pkey sha256 [nomatch] <- 2 1 1 eac0fdbe097f...81ab000c2955
bh.nic.cz. IN A 217.31.204.252 ; AD=1 NoError
bh.nic.cz. IN AAAA ? ; AD=1 NODATA
_25._tcp.bh.nic.cz. IN TLSA 3 1 1 4f9736249ab5...6194f5bb2e09 ; AD=1 NoError
0
Here STARTTLS is not offered (to at least some SMTP clients), even though TLSA records are published:
$ danecheck rnrfunco.net
rnrfunco.net. IN MX 10 tusk.sgt.com. ; AD=1 NoError
tusk.sgt.com. IN A 204.107.130.104 ; AD=1 NoError
tusk.sgt.com. IN AAAA ? ; AD=1 NODATA
_25._tcp.tusk.sgt.com. IN TLSA 3 0 1 bd60df4cc8c2...50ac0045659f ; AD=1 NoError
tusk.sgt.com[204.107.130.104]: STARTTLS not offered
Here none of the TLSA record match the certificate chain:
$ danecheck dipietro.id.au
dipietro.id.au. IN MX 10 mail.dipietro.id.au. ; AD=1 NoError
mail.dipietro.id.au. IN A 14.203.171.177 ; AD=1 NoError
mail.dipietro.id.au. IN AAAA ? ; AD=1 NODATA
_25._tcp.mail.dipietro.id.au. IN TLSA 3 1 1 7bf7ea3b070b...34e1e0044e6d ; AD=1 NoError
mail.dipietro.id.au[14.203.171.177]: fail: TLSA mismatch
TLS = TLS12 with ECDHE-RSA-AES256GCM-SHA384
name = cloud.dipietro.id.au
name = dipietro.id.au
name = mail.dipietro.id.au
name = www.dipietro.id.au
name = xmpp.dipietro.id.au
depth = 0
Issuer CommonName = Let's Encrypt Authority X3
Issuer Organization = Let's Encrypt
notBefore = 2017-07-27T01:31:00Z
notAfter = 2017-10-25T01:31:00Z
Subject CommonName = dipietro.id.au
pkey sha256 [nomatch] <- 3 1 1 51955a5a7b2e...7b158b18db73
depth = 1
Issuer CommonName = DST Root CA X3
Issuer Organization = Digital Signature Trust Co.
notBefore = 2016-03-17T16:40:46Z
notAfter = 2021-03-17T16:40:46Z
Subject CommonName = Let's Encrypt Authority X3
Subject Organization = Let's Encrypt
pkey sha256 [nomatch] <- 2 1 1 60b87575447d...0517616e8a18
Here TLSA record lookups ServFail due to a buggy nameserver:
$ danecheck truman.edu
truman.edu. IN DS 52166 5 1 fc1b03d050bf...a69d7ed8676d ; AD=1 NoError
truman.edu. IN DNSKEY 256 3 5 AwEAAdKNi1TB...RSK2WheyT8zF ; AD=1 NoError
truman.edu. IN DNSKEY 257 3 5 AwEAAZianXgr...ZXk7AnTMbHM= ; AD=1 NoError
truman.edu. IN MX 5 barracuda.truman.edu. ; AD=1 NoError
barracuda.truman.edu. IN A 150.243.160.93 ; AD=1 NoError
barracuda.truman.edu. IN AAAA ? ; AD=0 ServFail
_25._tcp.barracuda.truman.edu. IN TLSA ? ; AD=0 ServFail