2023-01-30 16:27:06 +03:00
|
|
|
// Package dmarc implements DMARC (Domain-based Message Authentication,
|
|
|
|
// Reporting, and Conformance; RFC 7489) verification.
|
|
|
|
//
|
|
|
|
// DMARC is a mechanism for verifying ("authenticating") the address in the "From"
|
|
|
|
// message header, since users will look at that header to identify the sender of a
|
|
|
|
// message. DMARC compares the "From"-(sub)domain against the SPF and/or
|
|
|
|
// DKIM-validated domains, based on the DMARC policy that a domain has published in
|
|
|
|
// DNS as TXT record under "_dmarc.<domain>". A DMARC policy can also ask for
|
|
|
|
// feedback about evaluations by other email servers, for monitoring/debugging
|
|
|
|
// problems with email delivery.
|
|
|
|
package dmarc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
mathrand "math/rand"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/mjl-/mox/dkim"
|
|
|
|
"github.com/mjl-/mox/dns"
|
|
|
|
"github.com/mjl-/mox/mlog"
|
|
|
|
"github.com/mjl-/mox/publicsuffix"
|
|
|
|
"github.com/mjl-/mox/spf"
|
2023-12-05 23:13:57 +03:00
|
|
|
"github.com/mjl-/mox/stub"
|
2023-01-30 16:27:06 +03:00
|
|
|
|
2023-12-05 15:35:58 +03:00
|
|
|
"golang.org/x/exp/slog"
|
|
|
|
)
|
2023-01-30 16:27:06 +03:00
|
|
|
|
|
|
|
var (
|
2023-12-05 23:13:57 +03:00
|
|
|
MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{}
|
2023-01-30 16:27:06 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// link errata:
|
|
|
|
// ../rfc/7489-eid5440 ../rfc/7489:1585
|
|
|
|
|
|
|
|
// Lookup errors.
|
|
|
|
var (
|
|
|
|
ErrNoRecord = errors.New("dmarc: no dmarc dns record")
|
|
|
|
ErrMultipleRecords = errors.New("dmarc: multiple dmarc dns records") // Must also be treated as if domain does not implement DMARC.
|
|
|
|
ErrDNS = errors.New("dmarc: dns lookup")
|
|
|
|
ErrSyntax = errors.New("dmarc: malformed dmarc dns record")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Status is the result of DMARC policy evaluation, for use in an Authentication-Results header.
|
|
|
|
type Status string
|
|
|
|
|
|
|
|
// ../rfc/7489:2339
|
|
|
|
|
|
|
|
const (
|
|
|
|
StatusNone Status = "none" // No DMARC TXT DNS record found.
|
|
|
|
StatusPass Status = "pass" // SPF and/or DKIM pass with identifier alignment.
|
|
|
|
StatusFail Status = "fail" // Either both SPF and DKIM failed or identifier did not align with a pass.
|
|
|
|
StatusTemperror Status = "temperror" // Typically a DNS lookup. A later attempt may results in a conclusion.
|
|
|
|
StatusPermerror Status = "permerror" // Typically a malformed DMARC DNS record.
|
|
|
|
)
|
|
|
|
|
|
|
|
// Result is a DMARC policy evaluation.
|
|
|
|
type Result struct {
|
|
|
|
// Whether to reject the message based on policies. If false, the message should
|
2023-12-12 17:47:26 +03:00
|
|
|
// not necessarily be accepted: other checks such as reputation-based and
|
|
|
|
// content-based analysis may lead to reject the message.
|
2023-01-30 16:27:06 +03:00
|
|
|
Reject bool
|
|
|
|
// Result of DMARC validation. A message can fail validation, but still
|
|
|
|
// not be rejected, e.g. if the policy is "none".
|
2023-11-01 19:55:40 +03:00
|
|
|
Status Status
|
|
|
|
AlignedSPFPass bool
|
|
|
|
AlignedDKIMPass bool
|
2023-01-30 16:27:06 +03:00
|
|
|
// Domain with the DMARC DNS record. May be the organizational domain instead of
|
|
|
|
// the domain in the From-header.
|
|
|
|
Domain dns.Domain
|
|
|
|
// Parsed DMARC record.
|
|
|
|
Record *Record
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
// Whether DMARC DNS response was DNSSEC-signed, regardless of whether SPF/DKIM records were DNSSEC-signed.
|
|
|
|
RecordAuthentic bool
|
2023-01-30 16:27:06 +03:00
|
|
|
// Details about possible error condition, e.g. when parsing the DMARC record failed.
|
|
|
|
Err error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lookup looks up the DMARC TXT record at "_dmarc.<domain>" for the domain in the
|
|
|
|
// "From"-header of a message.
|
|
|
|
//
|
|
|
|
// If no DMARC record is found for the "From"-domain, another lookup is done at
|
|
|
|
// the organizational domain of the domain (if different). The organizational
|
|
|
|
// domain is determined using the public suffix list. E.g. for
|
|
|
|
// "sub.example.com", the organizational domain is "example.com". The returned
|
|
|
|
// domain is the domain with the DMARC record.
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
//
|
|
|
|
// rauthentic indicates if the DNS results were DNSSEC-verified.
|
2023-12-12 17:47:26 +03:00
|
|
|
func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, msgFrom dns.Domain) (status Status, domain dns.Domain, record *Record, txt string, rauthentic bool, rerr error) {
|
2023-12-05 15:35:58 +03:00
|
|
|
log := mlog.New("dmarc", elog)
|
2023-01-30 16:27:06 +03:00
|
|
|
start := time.Now()
|
|
|
|
defer func() {
|
2023-12-05 18:06:50 +03:00
|
|
|
log.Debugx("dmarc lookup result", rerr,
|
2023-12-12 17:47:26 +03:00
|
|
|
slog.Any("fromdomain", msgFrom),
|
2023-12-05 18:06:50 +03:00
|
|
|
slog.Any("status", status),
|
|
|
|
slog.Any("domain", domain),
|
|
|
|
slog.Any("record", record),
|
|
|
|
slog.Duration("duration", time.Since(start)))
|
2023-01-30 16:27:06 +03:00
|
|
|
}()
|
|
|
|
|
|
|
|
// ../rfc/7489:859 ../rfc/7489:1370
|
2023-12-12 17:47:26 +03:00
|
|
|
domain = msgFrom
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
status, record, txt, authentic, err := lookupRecord(ctx, resolver, domain)
|
2023-01-30 16:27:06 +03:00
|
|
|
if status != StatusNone {
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return status, domain, record, txt, authentic, err
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
if record == nil {
|
|
|
|
// ../rfc/7489:761 ../rfc/7489:1377
|
2023-12-12 17:47:26 +03:00
|
|
|
domain = publicsuffix.Lookup(ctx, log.Logger, msgFrom)
|
|
|
|
if domain == msgFrom {
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return StatusNone, domain, nil, txt, authentic, err
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
var xauth bool
|
|
|
|
status, record, txt, xauth, err = lookupRecord(ctx, resolver, domain)
|
|
|
|
authentic = authentic && xauth
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return status, domain, record, txt, authentic, err
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
func lookupRecord(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (Status, *Record, string, bool, error) {
|
2023-01-30 16:27:06 +03:00
|
|
|
name := "_dmarc." + domain.ASCII + "."
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
txts, result, err := dns.WithPackage(resolver, "dmarc").LookupTXT(ctx, name)
|
2023-01-30 16:27:06 +03:00
|
|
|
if err != nil && !dns.IsNotFound(err) {
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return StatusTemperror, nil, "", result.Authentic, fmt.Errorf("%w: %s", ErrDNS, err)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
var record *Record
|
|
|
|
var text string
|
|
|
|
var rerr error = ErrNoRecord
|
|
|
|
for _, txt := range txts {
|
|
|
|
r, isdmarc, err := ParseRecord(txt)
|
|
|
|
if !isdmarc {
|
|
|
|
// ../rfc/7489:1374
|
|
|
|
continue
|
|
|
|
} else if err != nil {
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return StatusPermerror, nil, text, result.Authentic, fmt.Errorf("%w: %s", ErrSyntax, err)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
if record != nil {
|
2023-11-01 19:55:40 +03:00
|
|
|
// ../rfc/7489:1388
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return StatusNone, nil, "", result.Authentic, ErrMultipleRecords
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
text = txt
|
|
|
|
record = r
|
|
|
|
rerr = nil
|
|
|
|
}
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
return StatusNone, record, text, result.Authentic, rerr
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
2023-11-01 19:55:40 +03:00
|
|
|
func lookupReportsRecord(ctx context.Context, resolver dns.Resolver, dmarcDomain, extDestDomain dns.Domain) (Status, []*Record, []string, bool, error) {
|
|
|
|
// ../rfc/7489:1566
|
2023-08-23 15:27:21 +03:00
|
|
|
name := dmarcDomain.ASCII + "._report._dmarc." + extDestDomain.ASCII + "."
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
txts, result, err := dns.WithPackage(resolver, "dmarc").LookupTXT(ctx, name)
|
2023-08-23 15:27:21 +03:00
|
|
|
if err != nil && !dns.IsNotFound(err) {
|
2023-11-01 19:55:40 +03:00
|
|
|
return StatusTemperror, nil, nil, result.Authentic, fmt.Errorf("%w: %s", ErrDNS, err)
|
2023-08-23 15:27:21 +03:00
|
|
|
}
|
2023-11-01 19:55:40 +03:00
|
|
|
var records []*Record
|
|
|
|
var texts []string
|
2023-08-23 15:27:21 +03:00
|
|
|
var rerr error = ErrNoRecord
|
|
|
|
for _, txt := range txts {
|
|
|
|
r, isdmarc, err := ParseRecordNoRequired(txt)
|
|
|
|
// Examples in the RFC use "v=DMARC1", even though it isn't a valid DMARC record.
|
|
|
|
// Accept the specific example.
|
|
|
|
// ../rfc/7489-eid5440
|
|
|
|
if !isdmarc && txt == "v=DMARC1" {
|
|
|
|
xr := DefaultRecord
|
|
|
|
r, isdmarc, err = &xr, true, nil
|
|
|
|
}
|
|
|
|
if !isdmarc {
|
2023-11-01 19:55:40 +03:00
|
|
|
// ../rfc/7489:1586
|
2023-08-23 15:27:21 +03:00
|
|
|
continue
|
|
|
|
}
|
2023-11-01 19:55:40 +03:00
|
|
|
texts = append(texts, txt)
|
|
|
|
records = append(records, r)
|
|
|
|
if err != nil {
|
|
|
|
return StatusPermerror, records, texts, result.Authentic, fmt.Errorf("%w: %s", ErrSyntax, err)
|
2023-08-23 15:27:21 +03:00
|
|
|
}
|
2023-11-01 19:55:40 +03:00
|
|
|
// Multiple records are allowed for the _report record, unlike for policies. ../rfc/7489:1593
|
2023-08-23 15:27:21 +03:00
|
|
|
rerr = nil
|
|
|
|
}
|
2023-11-01 19:55:40 +03:00
|
|
|
return StatusNone, records, texts, result.Authentic, rerr
|
2023-08-23 15:27:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// LookupExternalReportsAccepted returns whether the extDestDomain has opted in
|
|
|
|
// to receiving dmarc reports for dmarcDomain (where the dmarc record was found),
|
|
|
|
// through a "._report._dmarc." DNS TXT DMARC record.
|
|
|
|
//
|
2023-11-01 19:55:40 +03:00
|
|
|
// accepts is true if the external domain has opted in.
|
|
|
|
// If a temporary error occurred, the returned status is StatusTemperror, and a
|
|
|
|
// later retry may give an authoritative result.
|
|
|
|
// The returned error is ErrNoRecord if no opt-in DNS record exists, which is
|
|
|
|
// not a failure condition.
|
2023-08-23 15:27:21 +03:00
|
|
|
//
|
|
|
|
// The normally invalid "v=DMARC1" record is accepted since it is used as
|
|
|
|
// example in RFC 7489.
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
//
|
|
|
|
// authentic indicates if the DNS results were DNSSEC-verified.
|
2023-12-05 15:35:58 +03:00
|
|
|
func LookupExternalReportsAccepted(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, dmarcDomain dns.Domain, extDestDomain dns.Domain) (accepts bool, status Status, records []*Record, txts []string, authentic bool, rerr error) {
|
|
|
|
log := mlog.New("dmarc", elog)
|
2023-08-23 15:27:21 +03:00
|
|
|
start := time.Now()
|
|
|
|
defer func() {
|
2023-12-05 18:06:50 +03:00
|
|
|
log.Debugx("dmarc externalreports result", rerr,
|
|
|
|
slog.Bool("accepts", accepts),
|
|
|
|
slog.Any("dmarcdomain", dmarcDomain),
|
|
|
|
slog.Any("extdestdomain", extDestDomain),
|
|
|
|
slog.Any("records", records),
|
|
|
|
slog.Duration("duration", time.Since(start)))
|
2023-08-23 15:27:21 +03:00
|
|
|
}()
|
|
|
|
|
2023-11-01 19:55:40 +03:00
|
|
|
status, records, txts, authentic, rerr = lookupReportsRecord(ctx, resolver, dmarcDomain, extDestDomain)
|
2023-08-23 15:27:21 +03:00
|
|
|
accepts = rerr == nil
|
2023-11-01 19:55:40 +03:00
|
|
|
return accepts, status, records, txts, authentic, rerr
|
2023-08-23 15:27:21 +03:00
|
|
|
}
|
|
|
|
|
2023-01-30 16:27:06 +03:00
|
|
|
// Verify evaluates the DMARC policy for the domain in the From-header of a
|
|
|
|
// message given the DKIM and SPF evaluation results.
|
|
|
|
//
|
|
|
|
// applyRandomPercentage determines whether the records "pct" is honored. This
|
|
|
|
// field specifies the percentage of messages the DMARC policy is applied to. It
|
|
|
|
// is used for slow rollout of DMARC policies and should be honored during normal
|
|
|
|
// email processing
|
|
|
|
//
|
|
|
|
// Verify always returns the result of verifying the DMARC policy
|
|
|
|
// against the message (for inclusion in Authentication-Result headers).
|
|
|
|
//
|
2023-12-12 17:47:26 +03:00
|
|
|
// useResult indicates if the result should be applied in a policy decision,
|
|
|
|
// based on the "pct" field in the DMARC record.
|
|
|
|
func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, msgFrom dns.Domain, dkimResults []dkim.Result, spfResult spf.Status, spfIdentity *dns.Domain, applyRandomPercentage bool) (useResult bool, result Result) {
|
2023-12-05 15:35:58 +03:00
|
|
|
log := mlog.New("dmarc", elog)
|
2023-01-30 16:27:06 +03:00
|
|
|
start := time.Now()
|
|
|
|
defer func() {
|
|
|
|
use := "no"
|
|
|
|
if useResult {
|
|
|
|
use = "yes"
|
|
|
|
}
|
|
|
|
reject := "no"
|
|
|
|
if result.Reject {
|
|
|
|
reject = "yes"
|
|
|
|
}
|
2023-12-05 23:13:57 +03:00
|
|
|
MetricVerify.ObserveLabels(float64(time.Since(start))/float64(time.Second), string(result.Status), reject, use)
|
2023-12-05 18:06:50 +03:00
|
|
|
log.Debugx("dmarc verify result", result.Err,
|
2023-12-12 17:47:26 +03:00
|
|
|
slog.Any("fromdomain", msgFrom),
|
2023-12-05 18:06:50 +03:00
|
|
|
slog.Any("dkimresults", dkimResults),
|
|
|
|
slog.Any("spfresult", spfResult),
|
|
|
|
slog.Any("status", result.Status),
|
|
|
|
slog.Bool("reject", result.Reject),
|
|
|
|
slog.Bool("use", useResult),
|
|
|
|
slog.Duration("duration", time.Since(start)))
|
2023-01-30 16:27:06 +03:00
|
|
|
}()
|
|
|
|
|
2023-12-12 17:47:26 +03:00
|
|
|
status, recordDomain, record, _, authentic, err := Lookup(ctx, log.Logger, resolver, msgFrom)
|
2023-01-30 16:27:06 +03:00
|
|
|
if record == nil {
|
2023-11-01 19:55:40 +03:00
|
|
|
return false, Result{false, status, false, false, recordDomain, record, authentic, err}
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
result.Domain = recordDomain
|
|
|
|
result.Record = record
|
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.
2023-10-10 13:09:35 +03:00
|
|
|
result.RecordAuthentic = authentic
|
2023-01-30 16:27:06 +03:00
|
|
|
|
|
|
|
// Record can request sampling of messages to apply policy.
|
|
|
|
// See ../rfc/7489:1432
|
|
|
|
useResult = !applyRandomPercentage || record.Percentage == 100 || mathrand.Intn(100) < record.Percentage
|
|
|
|
|
2023-11-01 19:55:40 +03:00
|
|
|
// We treat "quarantine" and "reject" the same. Thus, we also don't "downgrade"
|
|
|
|
// from reject to quarantine if this message was sampled out.
|
2023-01-30 16:27:06 +03:00
|
|
|
// ../rfc/7489:1446 ../rfc/7489:1024
|
2023-12-12 17:47:26 +03:00
|
|
|
if recordDomain != msgFrom && record.SubdomainPolicy != PolicyEmpty {
|
2023-01-30 16:27:06 +03:00
|
|
|
result.Reject = record.SubdomainPolicy != PolicyNone
|
|
|
|
} else {
|
|
|
|
result.Reject = record.Policy != PolicyNone
|
|
|
|
}
|
|
|
|
|
|
|
|
// ../rfc/7489:1338
|
|
|
|
result.Status = StatusFail
|
|
|
|
if spfResult == spf.StatusTemperror {
|
|
|
|
result.Status = StatusTemperror
|
|
|
|
result.Reject = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Below we can do a bunch of publicsuffix lookups. Cache the results, mostly to
|
2023-02-06 13:00:11 +03:00
|
|
|
// reduce log pollution.
|
2023-01-30 16:27:06 +03:00
|
|
|
pubsuffixes := map[dns.Domain]dns.Domain{}
|
|
|
|
pubsuffix := func(name dns.Domain) dns.Domain {
|
|
|
|
if r, ok := pubsuffixes[name]; ok {
|
|
|
|
return r
|
|
|
|
}
|
2023-12-05 15:35:58 +03:00
|
|
|
r := publicsuffix.Lookup(ctx, log.Logger, name)
|
2023-01-30 16:27:06 +03:00
|
|
|
pubsuffixes[name] = r
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// ../rfc/7489:1319
|
|
|
|
// ../rfc/7489:544
|
2023-12-12 17:47:26 +03:00
|
|
|
if spfResult == spf.StatusPass && spfIdentity != nil && (*spfIdentity == msgFrom || result.Record.ASPF == "r" && pubsuffix(msgFrom) == pubsuffix(*spfIdentity)) {
|
2023-11-01 19:55:40 +03:00
|
|
|
result.AlignedSPFPass = true
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, dkimResult := range dkimResults {
|
|
|
|
if dkimResult.Status == dkim.StatusTemperror {
|
|
|
|
result.Reject = false
|
|
|
|
result.Status = StatusTemperror
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// ../rfc/7489:511
|
2023-12-12 17:47:26 +03:00
|
|
|
if dkimResult.Status == dkim.StatusPass && dkimResult.Sig != nil && (dkimResult.Sig.Domain == msgFrom || result.Record.ADKIM == "r" && pubsuffix(msgFrom) == pubsuffix(dkimResult.Sig.Domain)) {
|
2023-01-30 16:27:06 +03:00
|
|
|
// ../rfc/7489:535
|
2023-11-01 19:55:40 +03:00
|
|
|
result.AlignedDKIMPass = true
|
|
|
|
break
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
}
|
2023-11-01 19:55:40 +03:00
|
|
|
|
|
|
|
if result.AlignedSPFPass || result.AlignedDKIMPass {
|
|
|
|
result.Reject = false
|
|
|
|
result.Status = StatusPass
|
|
|
|
}
|
2023-01-30 16:27:06 +03:00
|
|
|
return
|
|
|
|
}
|