implement outgoing tls reports

we were already accepting, processing and displaying incoming tls reports. now
we start tracking TLS connection and security-policy-related errors for
outgoing message deliveries as well. we send reports once a day, to the
reporting addresses specified in TLSRPT records (rua) of a policy domain. these
reports are about MTA-STS policies and/or DANE policies, and about
STARTTLS-related failures.

sending reports is enabled by default, but can be disabled through setting
NoOutgoingTLSReports in mox.conf.

only at the end of the implementation process came the realization that the
TLSRPT policy domain for DANE (MX) hosts are separate from the TLSRPT policy
for the recipient domain, and that MTA-STS and DANE TLS/policy results are
typically delivered in separate reports. so MX hosts need their own TLSRPT
policies.

config for the per-host TLSRPT policy should be added to mox.conf for existing
installs, in field HostTLSRPT. it is automatically configured by quickstart for
new installs. with a HostTLSRPT config, the "dns records" and "dns check" admin
pages now suggest the per-host TLSRPT record. by creating that record, you're
requesting TLS reports about your MX host.

gathering all the TLS/policy results is somewhat tricky. the tentacles go
throughout the code. the positive result is that the TLS/policy-related code
had to be cleaned up a bit. for example, the smtpclient TLS modes now reflect
reality better, with independent settings about whether PKIX and/or DANE
verification has to be done, and/or whether verification errors have to be
ignored (e.g. for tls-required: no header). also, cached mtasts policies of
mode "none" are now cleaned up once the MTA-STS DNS record goes away.
This commit is contained in:
Mechiel Lukkien
2023-11-09 17:40:46 +01:00
parent df18ca3c02
commit 893a6f8911
58 changed files with 3246 additions and 504 deletions

View File

@ -5,6 +5,7 @@ import (
"bufio"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
@ -27,6 +28,7 @@ import (
"github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/sasl"
"github.com/mjl-/mox/smtp"
"github.com/mjl-/mox/tlsrpt"
)
// todo future: add function to deliver message to multiple recipients. requires more elaborate return value, indicating success per message: some recipients may succeed, others may fail, and we should still deliver. to prevent backscatter, we also sometimes don't allow multiple recipients. ../rfc/5321:1144
@ -71,22 +73,17 @@ var (
type TLSMode string
const (
// Required TLS with STARTTLS for SMTP servers, with either verified DANE TLSA
// record, or a WebPKI-verified certificate (with matching name, not expired, etc).
TLSStrictStartTLS TLSMode = "strictstarttls"
// TLS immediately ("implicit TLS"), directly starting TLS on the TCP connection,
// so not using STARTTLS. Whether PKIX and/or DANE is verified is specified
// separately.
TLSImmediate TLSMode = "immediate"
// Required TLS with STARTTLS for SMTP servers, without verifiying the certificate.
// This mode is needed to fallback after only unusable DANE records were found
// (e.g. with unknown parameters in the TLSA records). Also for allowing
// verification errors with DANE with message header TLS-Required no.
TLSUnverifiedStartTLS TLSMode = "unverifiedstarttls"
// Required TLS with STARTTLS for SMTP servers. The STARTTLS command is always
// executed, even if the server does not announce support.
// Whether PKIX and/or DANE is verified is specified separately.
TLSRequiredStartTLS TLSMode = "requiredstarttls"
// TLS immediately ("implicit TLS"), with either verified DANE TLSA records or a
// verified certificate: matching name, not expired, trusted by CA.
TLSStrictImmediate TLSMode = "strictimmediate"
// Use TLS if remote claims to support it, but do not verify the certificate
// (not trusted by CA, different host name or expired certificate is accepted).
// Use TLS with STARTTLS if remote claims to support it.
TLSOpportunistic TLSMode = "opportunistic"
// TLS must not be attempted, e.g. due to earlier TLS handshake error.
@ -101,12 +98,20 @@ type Client struct {
// can be wrapped in a tls.Client. We close origConn instead of conn because
// closing the TLS connection would send a TLS close notification, which may block
// for 5s if the server isn't reading it (because it is also sending it).
origConn net.Conn
conn net.Conn
remoteHostname dns.Domain // TLS with SNI and name verification.
daneRecords []adns.TLSA // For authenticating (START)TLS connection.
moreRemoteHostnames []dns.Domain // Additional allowed names in TLS certificate.
verifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
origConn net.Conn
conn net.Conn
tlsVerifyPKIX bool
ignoreTLSVerifyErrors bool
rootCAs *x509.CertPool
remoteHostname dns.Domain // TLS with SNI and name verification.
daneRecords []adns.TLSA // For authenticating (START)TLS connection.
daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
// TLS connection success/failure are added. These are always non-nil, regardless
// of what was passed in opts. It lets us unconditionally dereference them.
recipientDomainResult *tlsrpt.Result // Either "sts" or "no-policy-found".
hostResult *tlsrpt.Result // Either "dane" or "no-policy-found".
r *bufio.Reader
w *bufio.Writer
@ -121,8 +126,9 @@ type Client struct {
botched bool // If set, protocol is out of sync and no further commands can be sent.
needRset bool // If set, a new delivery requires an RSET command.
extEcodes bool // Remote server supports sending extended error codes.
extStartTLS bool // Remote server supports STARTTLS.
remoteHelo string // From 220 greeting line.
extEcodes bool // Remote server supports sending extended error codes.
extStartTLS bool // Remote server supports STARTTLS.
ext8bitmime bool
extSize bool // Remote server supports SIZE parameter.
maxSize int64 // Max size of email message.
@ -177,6 +183,33 @@ func (e Error) Error() string {
return s
}
// Opts influence behaviour of Client.
type Opts struct {
// If auth is non-empty, authentication will be done with the first algorithm
// supported by the server. If none of the algorithms are supported, an error is
// returned.
Auth []sasl.Client
DANERecords []adns.TLSA // If not nil, DANE records to verify.
DANEMoreHostnames []dns.Domain // For use with DANE, where additional certificate host names are allowed.
DANEVerifiedRecord *adns.TLSA // If non-empty, set to the DANE record that verified the TLS connection.
// If set, TLS verification errors (for DANE or PKIX) are ignored. Useful for
// delivering messages with message header "TLS-Required: No".
// Certificates are still verified, and results are still tracked for TLS
// reporting, but the connections will continue.
IgnoreTLSVerifyErrors bool
// If not nil, used instead of the system default roots for TLS PKIX verification.
RootCAs *x509.CertPool
// TLS verification successes/failures is added to these TLS reporting results.
// Once the STARTTLS handshake is attempted, a successful/failed connection is
// tracked.
RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
HostResult *tlsrpt.Result // DANE or no policy.
}
// New initializes an SMTP session on the given connection, returning a client that
// can be used to deliver messages.
//
@ -191,29 +224,38 @@ func (e Error) Error() string {
// records with preferences, other DNS records, MTA-STS, retries and special
// cases into account.
//
// tlsMode indicates if TLS is required, optional or should not be used. Only for
// strict TLS modes is the certificate verified: Either with DANE, or through
// the trusted CA pool with matching remoteHostname and not expired. For DANE,
// additional host names in moreRemoteHostnames are allowed during TLS certificate
// verification. By default, SMTP does not verify TLS for interopability reasons,
// but MTA-STS or DANE can require it. If opportunistic TLS is used, and a TLS
// error is encountered, the caller may want to try again (on a new connection)
// without TLS. For messages with header TLS-Required no, DANE records may be
// passed along with tlsMode TLSUnverifiedStartTLS. In that case, failing DANE
// verification causes an error to be logged, but the connection won't be aborted.
//
// If auth is non-empty, authentication will be done with the first algorithm
// supported by the server. If none of the algorithms are supported, an error is
// returned.
func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ehloHostname, remoteHostname dns.Domain, auth []sasl.Client, daneRecords []adns.TLSA, moreRemoteHostnames []dns.Domain, verifiedRecord *adns.TLSA) (*Client, error) {
// tlsMode indicates if and how TLS may/must (not) be used. tlsVerifyPKIX
// indicates if TLS certificates must be validated against the PKIX/WebPKI
// certificate authorities (if TLS is done). DANE-verification is done when
// opts.DANERecords is not nil. TLS verification errors will be ignored if
// opts.IgnoreTLSVerification is set. If TLS is done, PKIX verification is
// always performed for tracking the results for TLS reporting, but if
// tlsVerifyPKIX is false, the verification result does not affect the
// connection. At the time of writing, delivery of email on the internet is done
// with opportunistic TLS without PKIX verification by default. Recipient domains
// can opt-in to PKIX verification by publishing an MTA-STS policy, or opt-in to
// DANE verification by publishing DNSSEC-protected TLSA records in DNS.
func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, tlsVerifyPKIX bool, ehloHostname, remoteHostname dns.Domain, opts Opts) (*Client, error) {
ensureResult := func(r *tlsrpt.Result) *tlsrpt.Result {
if r == nil {
return &tlsrpt.Result{}
}
return r
}
c := &Client{
origConn: conn,
remoteHostname: remoteHostname,
daneRecords: daneRecords,
moreRemoteHostnames: moreRemoteHostnames,
verifiedRecord: verifiedRecord,
lastlog: time.Now(),
cmds: []string{"(none)"},
origConn: conn,
tlsVerifyPKIX: tlsVerifyPKIX,
ignoreTLSVerifyErrors: opts.IgnoreTLSVerifyErrors,
rootCAs: opts.RootCAs,
remoteHostname: remoteHostname,
daneRecords: opts.DANERecords,
daneMoreHostnames: opts.DANEMoreHostnames,
daneVerifiedRecord: opts.DANEVerifiedRecord,
lastlog: time.Now(),
cmds: []string{"(none)"},
recipientDomainResult: ensureResult(opts.RecipientDomainResult),
hostResult: ensureResult(opts.HostResult),
}
c.log = log.Fields(mlog.Field("smtpclient", "")).MoreFields(func() []mlog.Pair {
now := time.Now()
@ -224,13 +266,15 @@ func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ehl
return l
})
if tlsMode == TLSStrictImmediate {
// todo: we could also verify DANE here. not applicable to SMTP delivery.
config := c.tlsConfig(tlsMode)
if tlsMode == TLSImmediate {
config := c.tlsConfig()
tlsconn := tls.Client(conn, config)
// The tlsrpt tracking isn't used by caller, but won't hurt.
if err := tlsconn.HandshakeContext(ctx); err != nil {
c.tlsResultAdd(0, 1, err)
return nil, err
}
c.tlsResultAdd(1, 0, nil)
c.conn = tlsconn
tlsversion, ciphersuite := mox.TLSInfo(tlsconn)
c.log.Debug("tls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname))
@ -249,36 +293,103 @@ func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ehl
c.tw = moxio.NewTraceWriter(c.log, "LC: ", timeoutWriter{c.conn, 30 * time.Second, c.log})
c.w = bufio.NewWriter(c.tw)
if err := c.hello(ctx, tlsMode, ehloHostname, auth); err != nil {
if err := c.hello(ctx, tlsMode, ehloHostname, opts.Auth); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) tlsConfig(tlsMode TLSMode) *tls.Config {
if c.daneRecords != nil {
config := dane.TLSClientConfig(c.log, c.daneRecords, c.remoteHostname, c.moreRemoteHostnames, c.verifiedRecord)
if tlsMode == TLSUnverifiedStartTLS {
// In case of delivery with header "TLS-Required: No", the connection should not be
// aborted.
origVerify := config.VerifyConnection
config.VerifyConnection = func(cs tls.ConnectionState) error {
err := origVerify(cs)
if err != nil {
c.log.Infox("verifying dane failed, continuing due to tls mode unverified starttls, due to tls-required-no message header", err)
metricTLSRequiredNoIgnored.WithLabelValues("daneverification").Inc()
// reportedError wraps an error while indicating it was already tracked for TLS
// reporting.
type reportedError struct{ err error }
func (e reportedError) Error() string {
return e.err.Error()
}
func (e reportedError) Unwrap() error {
return e.err
}
func (c *Client) tlsConfig() *tls.Config {
// We always manage verification ourselves: We need to report in detail about
// failures. And we may have to verify both PKIX and DANE, record errors for
// each, and possibly ignore the errors.
verifyConnection := func(cs tls.ConnectionState) error {
// Collect verification errors. If there are none at the end, TLS validation
// succeeded. We may find validation problems below, record them for a TLS report
// but continue due to policies. We track the TLS reporting result in this
// function, wrapping errors in a reportedError.
var daneErr, pkixErr error
// DANE verification.
// daneRecords can be non-nil and empty, that's intended.
if c.daneRecords != nil {
verified, record, err := dane.Verify(c.log, c.daneRecords, cs, c.remoteHostname, c.daneMoreHostnames)
c.log.Debugx("dane verification", err, mlog.Field("verified", verified), mlog.Field("record", record))
if verified {
if c.daneVerifiedRecord != nil {
*c.daneVerifiedRecord = record
}
} else {
// Track error for reports.
// todo spec: may want to propose adding a result for no-dane-match. dane allows multiple records, some mismatching/failing isn't fatal and reporting on each record is probably not productive. ../rfc/8460:541
fd := c.tlsrptFailureDetails(tlsrpt.ResultValidationFailure, "dane-no-match")
if err != nil {
// todo future: potentially add more details. e.g. dane-ta verification errors. tlsrpt does not have "result types" to indicate those kinds of errors. we would probably have to pass c.daneResult to dane.Verify.
// We may have encountered errors while evaluation some of the TLSA records.
fd.FailureReasonCode += "+errors"
}
c.hostResult.Add(0, 0, fd)
if c.ignoreTLSVerifyErrors {
// We ignore the failure and continue the connection.
c.log.Infox("verifying dane failed, continuing with connection", err)
metricTLSRequiredNoIgnored.WithLabelValues("daneverification").Inc()
} else {
// This connection will fail.
daneErr = dane.ErrNoMatch
}
return nil
}
}
return &config
// PKIX verification.
opts := x509.VerifyOptions{
DNSName: cs.ServerName,
Intermediates: x509.NewCertPool(),
Roots: c.rootCAs,
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
if _, err := cs.PeerCertificates[0].Verify(opts); err != nil {
resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
fd := c.tlsrptFailureDetails(resultType, reasonCode)
c.recipientDomainResult.Add(0, 0, fd)
if c.tlsVerifyPKIX && !c.ignoreTLSVerifyErrors {
pkixErr = err
}
}
if daneErr != nil && pkixErr != nil {
return reportedError{errors.Join(daneErr, pkixErr)}
} else if daneErr != nil {
return reportedError{daneErr}
} else if pkixErr != nil {
return reportedError{pkixErr}
}
return nil
}
// todo: possibly accept older TLS versions for TLSOpportunistic?
return &tls.Config{
ServerName: c.remoteHostname.ASCII,
RootCAs: mox.Conf.Static.TLS.CertPool,
InsecureSkipVerify: tlsMode == TLSOpportunistic || tlsMode == TLSUnverifiedStartTLS,
ServerName: c.remoteHostname.ASCII, // For SNI.
// todo: possibly accept older TLS versions for TLSOpportunistic? or would our private key be at risk?
MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
InsecureSkipVerify: true, // VerifyConnection below is called and will do all verification.
VerifyConnection: verifyConnection,
}
}
@ -589,16 +700,18 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
// Read greeting.
c.cmds = []string{"(greeting)"}
c.cmdStart = time.Now()
code, _, lastLine, _ := c.xreadecode(false)
code, _, lastLine, lines := c.xreadecode(false)
if code != smtp.C220ServiceReady {
c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 220, got %d", ErrStatus, code)
}
// ../rfc/5321:2588
c.remoteHelo, _, _ = strings.Cut(lines[0], " ")
// Write EHLO, falling back to HELO if server doesn't appear to support it.
hello(true)
// Attempt TLS if remote understands STARTTLS and we aren't doing immediate TLS or if caller requires it.
if c.extStartTLS && (tlsMode != TLSSkip && tlsMode != TLSStrictImmediate) || tlsMode == TLSStrictStartTLS || tlsMode == TLSUnverifiedStartTLS {
if c.extStartTLS && tlsMode == TLSOpportunistic || tlsMode == TLSRequiredStartTLS {
c.log.Debug("starting tls client", mlog.Field("tlsmode", tlsMode), mlog.Field("servername", c.remoteHostname))
c.cmds[0] = "starttls"
c.cmdStart = time.Now()
@ -606,6 +719,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
code, secode, lastLine, _ := c.xread()
// ../rfc/3207:107
if code != smtp.C220ServiceReady {
c.tlsResultAddFailureDetails(0, 1, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, fmt.Sprintf("smtp-starttls-reply-code-%d", code)))
c.xerrorf(code/100 == 5, code, secode, lastLine, "%w: STARTTLS: got %d, expected 220", ErrTLS, code)
}
@ -621,9 +735,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
}
}
// For TLSStrictStartTLS, the Go TLS library performs the checks needed for MTA-STS.
// ../rfc/8461:646
tlsConfig := c.tlsConfig(tlsMode)
tlsConfig := c.tlsConfig()
nconn := tls.Client(conn, tlsConfig)
c.conn = nconn
@ -631,6 +743,10 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
defer cancel()
err := nconn.HandshakeContext(nctx)
if err != nil {
// For each STARTTLS failure, we track a failed TLS session. For deliveries with
// multiple MX targets, we may add multiple failures, and delivery may succeed with
// a later MX target with which we can do STARTTLS. ../rfc/8460:524
c.tlsResultAdd(0, 1, err)
c.xerrorf(false, 0, "", "", "%w: STARTTLS TLS handshake: %s", ErrTLS, err)
}
cancel()
@ -640,10 +756,23 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
c.w = bufio.NewWriter(c.tw)
tlsversion, ciphersuite := mox.TLSInfo(nconn)
c.log.Debug("starttls client handshake done", mlog.Field("tlsmode", tlsMode), mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", c.remoteHostname), mlog.Field("danerecord", c.verifiedRecord))
c.log.Debug("starttls client handshake done",
mlog.Field("tlsmode", tlsMode),
mlog.Field("verifypkix", c.tlsVerifyPKIX),
mlog.Field("verifydane", c.daneRecords != nil),
mlog.Field("ignoretlsverifyerrors", c.ignoreTLSVerifyErrors),
mlog.Field("tls", tlsversion),
mlog.Field("ciphersuite", ciphersuite),
mlog.Field("servername", c.remoteHostname),
mlog.Field("danerecord", c.daneVerifiedRecord))
c.tls = true
// Track successful TLS connection. ../rfc/8460:515
c.tlsResultAdd(1, 0, nil)
hello(false)
} else if tlsMode == TLSOpportunistic {
// Result: ../rfc/8460:538
c.tlsResultAddFailureDetails(0, 0, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, ""))
}
if len(auth) > 0 {
@ -652,6 +781,50 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
return
}
func addrIP(addr net.Addr) string {
if t, ok := addr.(*net.TCPAddr); ok {
return t.IP.String()
}
host, _, _ := net.SplitHostPort(addr.String())
ip := net.ParseIP(host)
if ip == nil {
return "" // For pipe during tests.
}
return ip.String()
}
// tlsrptFailureDetails returns FailureDetails with connection details (such as
// IP addresses) for inclusion in a TLS report.
func (c *Client) tlsrptFailureDetails(resultType tlsrpt.ResultType, reasonCode string) tlsrpt.FailureDetails {
return tlsrpt.FailureDetails{
ResultType: resultType,
SendingMTAIP: addrIP(c.origConn.LocalAddr()),
ReceivingMXHostname: c.remoteHostname.ASCII,
ReceivingMXHelo: c.remoteHelo,
ReceivingIP: addrIP(c.origConn.RemoteAddr()),
FailedSessionCount: 1,
FailureReasonCode: reasonCode,
}
}
// tlsResultAdd adds TLS success/failure to all results.
func (c *Client) tlsResultAdd(success, failure int64, err error) {
// Only track failure if not already done so in tls.Config.VerifyConnection.
var fds []tlsrpt.FailureDetails
var repErr reportedError
if err != nil && !errors.As(err, &repErr) {
resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
fd := c.tlsrptFailureDetails(resultType, reasonCode)
fds = []tlsrpt.FailureDetails{fd}
}
c.tlsResultAddFailureDetails(success, failure, fds...)
}
func (c *Client) tlsResultAddFailureDetails(success, failure int64, fds ...tlsrpt.FailureDetails) {
c.recipientDomainResult.Add(success, failure, fds...)
c.hostResult.Add(success, failure, fds...)
}
// ../rfc/4954:139
func (c *Client) auth(auth []sasl.Client) (rerr error) {
defer c.recover(&rerr)

View File

@ -23,7 +23,6 @@ import (
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/sasl"
"github.com/mjl-/mox/scram"
"github.com/mjl-/mox/smtp"
@ -49,6 +48,8 @@ func TestClient(t *testing.T) {
ehlo bool
tlsMode TLSMode
tlsPKIX bool
roots *x509.CertPool
tlsHostname dns.Domain
need8bitmime bool
needsmtputf8 bool
@ -60,8 +61,8 @@ func TestClient(t *testing.T) {
// Make fake cert, and make it trusted.
cert := fakeCert(t, false)
mox.Conf.Static.TLS.CertPool = x509.NewCertPool()
mox.Conf.Static.TLS.CertPool.AddCert(cert.Leaf)
roots := x509.NewCertPool()
roots.AddCert(cert.Leaf)
tlsConfig := tls.Config{
Certificates: []tls.Certificate{cert},
}
@ -280,7 +281,7 @@ func TestClient(t *testing.T) {
result <- err
panic("stop")
}
c, err := New(ctx, log, clientConn, opts.tlsMode, localhost, opts.tlsHostname, auths, nil, nil, nil)
c, err := New(ctx, log, clientConn, opts.tlsMode, opts.tlsPKIX, localhost, opts.tlsHostname, Opts{Auth: auths, RootCAs: opts.roots})
if (err == nil) != (expClientErr == nil) || err != nil && !errors.As(err, reflect.New(reflect.ValueOf(expClientErr).Type()).Interface()) && !errors.Is(err, expClientErr) {
fail("new client: got err %v, expected %#v", err, expClientErr)
}
@ -338,7 +339,9 @@ test
ehlo: true,
requiretls: true,
tlsMode: TLSStrictStartTLS,
tlsMode: TLSRequiredStartTLS,
tlsPKIX: true,
roots: roots,
tlsHostname: dns.Domain{ASCII: "mox.example"},
need8bitmime: true,
needsmtputf8: true,
@ -350,7 +353,7 @@ test
test(msg, options{ehlo: true, eightbitmime: true}, nil, nil, nil, nil)
test(msg, options{ehlo: true, eightbitmime: false, need8bitmime: true, nodeliver: true}, nil, nil, Err8bitmimeUnsupported, nil)
test(msg, options{ehlo: true, smtputf8: false, needsmtputf8: true, nodeliver: true}, nil, nil, ErrSMTPUTF8Unsupported, nil)
test(msg, options{ehlo: true, starttls: true, tlsMode: TLSStrictStartTLS, tlsHostname: dns.Domain{ASCII: "mismatch.example"}, nodeliver: true}, nil, ErrTLS, nil, &net.OpError{}) // Server TLS handshake is a net.OpError with "remote error" as text.
test(msg, options{ehlo: true, starttls: true, tlsMode: TLSRequiredStartTLS, tlsPKIX: true, tlsHostname: dns.Domain{ASCII: "mismatch.example"}, nodeliver: true}, nil, ErrTLS, nil, &net.OpError{}) // Server TLS handshake is a net.OpError with "remote error" as text.
test(msg, options{ehlo: true, maxSize: len(msg) - 1, nodeliver: true}, nil, nil, ErrSize, nil)
test(msg, options{ehlo: true, auths: []string{"PLAIN"}}, []sasl.Client{sasl.NewClientPlain("test", "test")}, nil, nil, nil)
test(msg, options{ehlo: true, auths: []string{"CRAM-MD5"}}, []sasl.Client{sasl.NewClientCRAMMD5("test", "test")}, nil, nil, nil)
@ -362,19 +365,19 @@ test
// Set an expired certificate. For non-strict TLS, we should still accept it.
// ../rfc/7435:424
cert = fakeCert(t, true)
mox.Conf.Static.TLS.CertPool = x509.NewCertPool()
mox.Conf.Static.TLS.CertPool.AddCert(cert.Leaf)
roots = x509.NewCertPool()
roots.AddCert(cert.Leaf)
tlsConfig = tls.Config{
Certificates: []tls.Certificate{cert},
}
test(msg, options{ehlo: true, starttls: true}, nil, nil, nil, nil)
test(msg, options{ehlo: true, starttls: true, roots: roots}, nil, nil, nil, nil)
// Again with empty cert pool so it isn't trusted in any way.
mox.Conf.Static.TLS.CertPool = x509.NewCertPool()
roots = x509.NewCertPool()
tlsConfig = tls.Config{
Certificates: []tls.Certificate{cert},
}
test(msg, options{ehlo: true, starttls: true}, nil, nil, nil, nil)
test(msg, options{ehlo: true, starttls: true, roots: roots}, nil, nil, nil, nil)
}
func TestErrors(t *testing.T) {
@ -385,7 +388,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.writeline("bogus") // Invalid, should be "220 <hostname>".
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
var xerr Error
if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
@ -396,7 +399,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.conn.Close()
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
var xerr Error
if err == nil || !errors.Is(err, io.ErrUnexpectedEOF) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v (%v), expected ErrUnexpectedEOF without Permanent", err, err))
@ -407,7 +410,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.writeline("521 not accepting connections")
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
var xerr Error
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
@ -418,7 +421,7 @@ func TestErrors(t *testing.T) {
run(t, func(s xserver) {
s.writeline("2200 mox.example") // Invalid, too many digits.
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
var xerr Error
if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
@ -432,7 +435,7 @@ func TestErrors(t *testing.T) {
s.writeline("250-mox.example")
s.writeline("500 different code") // Invalid.
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
_, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
var xerr Error
if err == nil || !errors.Is(err, ErrProtocol) || !errors.As(err, &xerr) || xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrProtocol without Permanent", err))
@ -448,7 +451,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("550 5.7.0 not allowed")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
if err != nil {
panic(err)
}
@ -468,7 +471,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("451 bad sender")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
if err != nil {
panic(err)
}
@ -490,7 +493,7 @@ func TestErrors(t *testing.T) {
s.readline("RCPT TO:")
s.writeline("451")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
if err != nil {
panic(err)
}
@ -514,7 +517,7 @@ func TestErrors(t *testing.T) {
s.readline("DATA")
s.writeline("550 no!")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
if err != nil {
panic(err)
}
@ -534,7 +537,7 @@ func TestErrors(t *testing.T) {
s.readline("STARTTLS")
s.writeline("502 command not implemented")
}, func(conn net.Conn) {
_, err := New(ctx, log, conn, TLSStrictStartTLS, localhost, dns.Domain{ASCII: "mox.example"}, nil, nil, nil, nil)
_, err := New(ctx, log, conn, TLSRequiredStartTLS, true, localhost, dns.Domain{ASCII: "mox.example"}, Opts{})
var xerr Error
if err == nil || !errors.Is(err, ErrTLS) || !errors.As(err, &xerr) || !xerr.Permanent {
panic(fmt.Errorf("got %#v, expected ErrTLS with Permanent", err))
@ -550,7 +553,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("451 enough")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSSkip, localhost, dns.Domain{ASCII: "mox.example"}, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSSkip, false, localhost, dns.Domain{ASCII: "mox.example"}, Opts{})
if err != nil {
panic(err)
}
@ -580,7 +583,7 @@ func TestErrors(t *testing.T) {
s.readline("DATA")
s.writeline("550 not now")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
if err != nil {
panic(err)
}
@ -610,7 +613,7 @@ func TestErrors(t *testing.T) {
s.readline("MAIL FROM:")
s.writeline("550 ok")
}, func(conn net.Conn) {
c, err := New(ctx, log, conn, TLSOpportunistic, localhost, zerohost, nil, nil, nil, nil)
c, err := New(ctx, log, conn, TLSOpportunistic, false, localhost, zerohost, Opts{})
if err != nil {
panic(err)
}

View File

@ -266,14 +266,16 @@ func GatherIPs(ctx context.Context, log *mlog.Log, resolver dns.Resolver, host d
// Only usable records are returned. If any record was found, DANE is required and
// this is indicated with daneRequired. If no usable records remain, the caller
// must do TLS, but not verify the remote TLS certificate.
//
// Returned values are always meaningful, also when an error was returned.
func GatherTLSA(ctx context.Context, log *mlog.Log, resolver dns.Resolver, host dns.Domain, expandedAuthentic bool, expandedHost dns.Domain) (daneRequired bool, daneRecords []adns.TLSA, tlsaBaseDomain dns.Domain, err error) {
// ../rfc/7672:912
// This function is only called when the lookup of host was authentic.
var l []adns.TLSA
tlsaBaseDomain = host
if host == expandedHost || !expandedAuthentic {
tlsaBaseDomain = host
l, err = lookupTLSACNAME(ctx, log, resolver, 25, "tcp", host)
} else if expandedAuthentic {
// ../rfc/7672:934
@ -286,8 +288,8 @@ func GatherTLSA(ctx context.Context, log *mlog.Log, resolver dns.Resolver, host
}
if len(l) == 0 || err != nil {
daneRequired = err != nil
log.Debugx("gathering tlsa records failed", err, mlog.Field("danerequired", daneRequired))
return daneRequired, nil, dns.Domain{}, err
log.Debugx("gathering tlsa records failed", err, mlog.Field("danerequired", daneRequired), mlog.Field("basedomain", tlsaBaseDomain))
return daneRequired, nil, tlsaBaseDomain, err
}
daneRequired = len(l) > 0
l = filterUsableTLSARecords(log, l)
@ -329,7 +331,7 @@ func lookupTLSACNAME(ctx context.Context, log *mlog.Log, resolver dns.Resolver,
return nil, fmt.Errorf("looking up tlsa records for tlsa candidate base domain: %w", err)
} else if !result.Authentic {
log.Debugx("tlsa lookup not authentic, not doing dane for host", err, mlog.Field("host", host), mlog.Field("name", name))
return nil, err
return nil, nil
}
return l, nil
}

View File

@ -283,12 +283,12 @@ func TestGatherTLSA(t *testing.T) {
test(domain("cnameloop.example"), false, domain("cnameloop.example"), true, nil, zerohost, errCNAMELimit)
test(domain("host0.example"), false, domain("inauthentic.example"), true, list0, domain("host0.example"), nil)
test(domain("inauthentic.example"), false, domain("inauthentic.example"), false, nil, zerohost, nil)
test(domain("temperror-cname.example"), false, domain("temperror-cname.example"), true, nil, zerohost, &adns.DNSError{})
test(domain("inauthentic.example"), false, domain("inauthentic.example"), false, nil, domain("inauthentic.example"), nil)
test(domain("temperror-cname.example"), false, domain("temperror-cname.example"), true, nil, domain("temperror-cname.example"), &adns.DNSError{})
test(domain("host1.example"), true, domain("cname-to-inauthentic.example"), true, list1, domain("host1.example"), nil)
test(domain("host1.example"), true, domain("danglingcname.example"), true, list1, domain("host1.example"), nil)
test(domain("danglingcname.example"), true, domain("danglingcname.example"), false, nil, zerohost, nil)
test(domain("danglingcname.example"), true, domain("danglingcname.example"), false, nil, domain("danglingcname.example"), nil)
}
func TestGatherTLSANames(t *testing.T) {