mirror of
https://github.com/mjl-/mox.git
synced 2025-07-10 07:14:40 +03:00
webmail: when composing a message, show security status in a bar below addressee input field
the bar is currently showing 3 properties: 1. mta-sts enforced; 2. mx lookup returned dnssec-signed response; 3. first delivery destination host has dane records the colors are: red for not-implemented, green for implemented, gray for error, nothing for unknown/irrelevant. the plan is to implement "requiretls" soon and start caching per domain whether delivery can be done with starttls and whether the domain supports requiretls. and show that in two new parts of the bar. thanks to damian poddebniak for pointing out that security indicators should always be visible, not only for positive/negative result. otherwise users won't notice their absence.
This commit is contained in:
124
webmail/api.go
124
webmail/api.go
@ -11,12 +11,14 @@ import (
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
@ -35,8 +37,11 @@ import (
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/moxio"
|
||||
"github.com/mjl-/mox/moxvar"
|
||||
"github.com/mjl-/mox/mtasts"
|
||||
"github.com/mjl-/mox/mtastsdb"
|
||||
"github.com/mjl-/mox/queue"
|
||||
"github.com/mjl-/mox/smtp"
|
||||
"github.com/mjl-/mox/smtpclient"
|
||||
"github.com/mjl-/mox/store"
|
||||
)
|
||||
|
||||
@ -1617,6 +1622,125 @@ func (Webmail) ThreadMute(ctx context.Context, messageIDs []int64, mute bool) {
|
||||
})
|
||||
}
|
||||
|
||||
// SecurityResult indicates whether a security feature is supported.
|
||||
type SecurityResult string
|
||||
|
||||
const (
|
||||
SecurityResultError SecurityResult = "error"
|
||||
SecurityResultNo SecurityResult = "no"
|
||||
SecurityResultYes SecurityResult = "yes"
|
||||
// Unknown whether supported. Finding out may only be (reasonably) possible when
|
||||
// trying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future
|
||||
// lookups.
|
||||
SecurityResultUnknown SecurityResult = "unknown"
|
||||
)
|
||||
|
||||
// RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain).
|
||||
// Fields are nil when an error occurred during analysis.
|
||||
type RecipientSecurity struct {
|
||||
MTASTS SecurityResult // Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.
|
||||
DNSSEC SecurityResult // Whether MX lookup response was DNSSEC-signed.
|
||||
DANE SecurityResult // Whether first delivery destination has DANE records.
|
||||
}
|
||||
|
||||
// RecipientSecurity looks up security properties of the address in the
|
||||
// single-address message addressee (as it appears in a To/Cc/Bcc/etc header).
|
||||
func (Webmail) RecipientSecurity(ctx context.Context, messageAddressee string) (RecipientSecurity, error) {
|
||||
resolver := dns.StrictResolver{Pkg: "webmail"}
|
||||
return recipientSecurity(ctx, resolver, messageAddressee)
|
||||
}
|
||||
|
||||
// separate function for testing with mocked resolver.
|
||||
func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddressee string) (RecipientSecurity, error) {
|
||||
log := xlog.WithContext(ctx)
|
||||
|
||||
rs := RecipientSecurity{
|
||||
SecurityResultUnknown,
|
||||
SecurityResultUnknown,
|
||||
SecurityResultUnknown,
|
||||
}
|
||||
|
||||
msgAddr, err := mail.ParseAddress(messageAddressee)
|
||||
if err != nil {
|
||||
return rs, fmt.Errorf("parsing message addressee: %v", err)
|
||||
}
|
||||
|
||||
addr, err := smtp.ParseAddress(msgAddr.Address)
|
||||
if err != nil {
|
||||
return rs, fmt.Errorf("parsing address: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// MTA-STS.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
policy, _, err := mtastsdb.Get(ctx, resolver, addr.Domain)
|
||||
if policy != nil && policy.Mode == mtasts.ModeEnforce {
|
||||
rs.MTASTS = SecurityResultYes
|
||||
} else if err == nil {
|
||||
rs.MTASTS = SecurityResultNo
|
||||
} else {
|
||||
rs.MTASTS = SecurityResultError
|
||||
}
|
||||
}()
|
||||
|
||||
// DNSSEC and DANE.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
_, origNextHopAuthentic, expandedNextHopAuthentic, _, hosts, _, err := smtpclient.GatherDestinations(ctx, log, resolver, dns.IPDomain{Domain: addr.Domain})
|
||||
if err != nil {
|
||||
rs.DNSSEC = SecurityResultError
|
||||
return
|
||||
}
|
||||
if origNextHopAuthentic && expandedNextHopAuthentic {
|
||||
rs.DNSSEC = SecurityResultYes
|
||||
} else {
|
||||
rs.DNSSEC = SecurityResultNo
|
||||
}
|
||||
|
||||
if !origNextHopAuthentic {
|
||||
rs.DANE = SecurityResultNo
|
||||
return
|
||||
}
|
||||
|
||||
// We're only looking at the first host to deliver to (typically first mx destination).
|
||||
if len(hosts) == 0 || hosts[0].Domain.IsZero() {
|
||||
return // Should not happen.
|
||||
}
|
||||
host := hosts[0]
|
||||
|
||||
// Resolve the IPs. Required for DANE to prevent bad DNS servers from causing an
|
||||
// error result instead of no-DANE result.
|
||||
authentic, expandedAuthentic, expandedHost, _, _, err := smtpclient.GatherIPs(ctx, log, resolver, host, map[string][]net.IP{})
|
||||
if err != nil {
|
||||
rs.DANE = SecurityResultError
|
||||
return
|
||||
}
|
||||
if !authentic {
|
||||
rs.DANE = SecurityResultNo
|
||||
return
|
||||
}
|
||||
|
||||
daneRequired, _, _, err := smtpclient.GatherTLSA(ctx, log, resolver, host.Domain, expandedAuthentic, expandedHost)
|
||||
if err != nil {
|
||||
rs.DANE = SecurityResultError
|
||||
return
|
||||
} else if daneRequired {
|
||||
rs.DANE = SecurityResultYes
|
||||
} else {
|
||||
rs.DANE = SecurityResultNo
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func slicesAny[T any](l []T) []any {
|
||||
r := make([]any, len(l))
|
||||
for i, v := range l {
|
||||
|
Reference in New Issue
Block a user