implement dnssec-awareness throughout code, and dane for incoming/outgoing mail delivery

the vendored dns resolver code is a copy of the go stdlib dns resolver, with
awareness of the "authentic data" (i.e. dnssec secure) added, as well as support
for enhanced dns errors, and looking up tlsa records (for dane). ideally it
would be upstreamed, but the chances seem slim.

dnssec-awareness is added to all packages, e.g. spf, dkim, dmarc, iprev. their
dnssec status is added to the Received message headers for incoming email.

but the main reason to add dnssec was for implementing dane. with dane, the
verification of tls certificates can be done through certificates/public keys
published in dns (in the tlsa records). this only makes sense (is trustworthy)
if those dns records can be verified to be authentic.

mox now applies dane to delivering messages over smtp. mox already implemented
mta-sts for webpki/pkix-verification of certificates against the (large) pool
of CA's, and still enforces those policies when present. but it now also checks
for dane records, and will verify those if present. if dane and mta-sts are
both absent, the regular opportunistic tls with starttls is still done. and the
fallback to plaintext is also still done.

mox also makes it easy to setup dane for incoming deliveries, so other servers
can deliver with dane tls certificate verification. the quickstart now
generates private keys that are used when requesting certificates with acme.
the private keys are pre-generated because they must be static and known during
setup, because their public keys must be published in tlsa records in dns.
autocert would generate private keys on its own, so had to be forked to add the
option to provide the private key when requesting a new certificate. hopefully
upstream will accept the change and we can drop the fork.

with this change, using the quickstart to setup a new mox instance, the checks
at internet.nl result in a 100% score, provided the domain is dnssec-signed and
the network doesn't have any issues.
This commit is contained in:
Mechiel Lukkien
2023-10-10 12:09:35 +02:00
parent c4324fdaa1
commit daa908e9f4
177 changed files with 12907 additions and 3131 deletions

View File

@ -3,6 +3,13 @@ package main
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
@ -146,6 +153,36 @@ logging in with IMAP.
resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer resolveCancel()
fmt.Printf("Checking if DNS resolvers are DNSSEC-verifying...")
_, resolverDNSSECResult, err := resolver.LookupNS(resolveCtx, ".")
if err != nil {
fmt.Println("")
fatalf("checking dnssec support in resolver: %v", err)
} else if !resolverDNSSECResult.Authentic {
fmt.Printf(`
WARNING: It looks like the DNS resolvers configured on your system do not
verify DNSSEC, or aren't trusted (by having loopback IPs or through "options
trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP
used unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS
certificate with DANE (based on a public key in DNS), and will fallback to
either MTA-STS for verification, or use "opportunistic TLS" with no certificate
verification.
Recommended action: Install unbound, a DNSSEC-verifying recursive DNS resolver,
and enable support for "extended dns errors" (EDE):
cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
server:
ede: yes
val-log-level: 2
EOF
`)
} else {
fmt.Println(" OK")
}
// We are going to find the (public) IPs to listen on and possibly the host name.
// Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
@ -244,7 +281,7 @@ logging in with IMAP.
for _, ip := range publicIPs {
revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer revcancel()
l, err := resolver.LookupAddr(revctx, ip)
l, _, err := resolver.LookupAddr(revctx, ip)
if err != nil {
warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
}
@ -306,7 +343,7 @@ again with the -hostname flag.
fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer ipcancel()
ips, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
ips, domainDNSSECResult, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
ipcancel()
var xips []net.IPAddr
var hostIPs []string
@ -403,6 +440,23 @@ This likely means one of two things:
`, dnshostname, err)
} else if !domainDNSSECResult.Authentic {
if !dnswarned {
fmt.Printf("\n")
}
dnswarned = true
fmt.Printf(`
NOTE: It looks like the DNS records of your domain (zone) are not DNSSEC-signed.
Mail servers that send email to your domain, or receive email from your domain,
cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they receive are
authentic. DANE, for authenticated delivery without relying on a pool of
certificate authorities, requires DNSSEC, so will not be configured at this
time.
Recommended action: Continue now, but consider enabling DNSSEC for your domain
later at your DNS operator, and adding DANE records for protecting incoming
messages over SMTP.
`)
}
if !dnswarned {
@ -421,7 +475,7 @@ This likely means one of two things:
go func() {
revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
defer revcancel()
addrs, err := resolver.LookupAddr(revctx, s)
addrs, _, err := resolver.LookupAddr(revctx, s)
results <- result{s, addrs, err}
}()
}
@ -581,9 +635,57 @@ many authentication failures).
{CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
},
}
fmt.Println(
`Placeholder paths to TLS certificates to be provided by the existing webserver
have been placed in config/mox.conf and need to be edited.
No private keys for the public listener have been generated for use with DANE.
To configure DANE (which requires DNSSEC), set config field HostPrivateKeyFiles
in the "public" Listener to both RSA 2048-bit and ECDSA P-256 private key files
and check the admin page for the needed DNS records.`)
} else {
// todo: we may want to generate a second set of keys, make the user already add it to the DNS, but keep the private key offline. would require config option to specify a public key only, so the dane records can be generated.
hostRSAPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
if err != nil {
fatalf("generating rsa private key for host: %s", err)
}
hostECDSAPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
if err != nil {
fatalf("generating ecsa private key for host: %s", err)
}
now := time.Now()
timestamp := now.Format("20060102T150405")
hostRSAPrivateKeyFile := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048")
hostECDSAPrivateKeyFile := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256")
xwritehostkeyfile := func(path string, key crypto.Signer) {
buf, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
fatalf("marshaling host private key to pkcs8 for %s: %s", path, err)
}
var b bytes.Buffer
block := pem.Block{
Type: "PRIVATE KEY",
Bytes: buf,
}
err = pem.Encode(&b, &block)
if err != nil {
fatalf("pem-encoding host private key file for %s: %s", path, err)
}
xwritefile(path, b.Bytes(), 0600)
}
xwritehostkeyfile(filepath.Join("config", hostRSAPrivateKeyFile), hostRSAPrivateKey)
xwritehostkeyfile(filepath.Join("config", hostECDSAPrivateKeyFile), hostECDSAPrivateKey)
public.TLS = &config.TLS{
ACME: "letsencrypt",
HostPrivateKeyFiles: []string{
hostRSAPrivateKeyFile,
hostECDSAPrivateKeyFile,
},
HostPrivateRSA2048Keys: []crypto.Signer{hostRSAPrivateKey},
HostPrivateECDSAP256Keys: []crypto.Signer{hostECDSAPrivateKey},
}
public.AutoconfigHTTPS.Enabled = true
public.MTASTSHTTPS.Enabled = true
@ -780,7 +882,7 @@ configured correctly.
// priming dns caches with negative/absent records, causing our "quick setup" to
// appear to fail or take longer than "quick".
records, err := mox.DomainRecords(confDomain, domain)
records, err := mox.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic)
if err != nil {
fatalf("making required DNS records")
}
@ -837,9 +939,6 @@ To access these from your browser, run
"ssh -L 8080:localhost:80 you@yourmachine" locally and open
http://localhost:8080/[...].
For secure email exchange you should have a strictly validating DNSSEC
resolver. An easy and the recommended way is to install unbound.
If you run into problem, have questions/feedback or found a bug, please let us
know. Mox needs your help!