Skip to content

Commit

Permalink
Reinstate the MTA-STS (Strict Transport Security) implementation
Browse files Browse the repository at this point in the history
This commit brings back the experimental MTA-STS (Strict Transport
Security) implementation, removed in commit
7f5bedf.

We will continue development in the "sts" branch, subject to rebase,
until it is ready to be integrated into "next" again.
  • Loading branch information
albertito committed Jul 1, 2018
1 parent a94253b commit 23deaf1
Show file tree
Hide file tree
Showing 6 changed files with 936 additions and 8 deletions.
10 changes: 9 additions & 1 deletion chasquid.go
Expand Up @@ -7,6 +7,7 @@
package main

import (
"context"
"expvar"
"flag"
"fmt"
Expand All @@ -25,6 +26,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/maillog"
"blitiri.com.ar/go/chasquid/internal/normalize"
"blitiri.com.ar/go/chasquid/internal/smtpsrv"
"blitiri.com.ar/go/chasquid/internal/sts"
"blitiri.com.ar/go/chasquid/internal/userdb"
"blitiri.com.ar/go/log"
"blitiri.com.ar/go/systemd"
Expand Down Expand Up @@ -146,12 +148,18 @@ func main() {

dinfo := s.InitDomainInfo(conf.DataDir + "/domaininfo")

stsCache, err := sts.NewCache(conf.DataDir + "/sts-cache")
if err != nil {
log.Fatalf("Failed to initialize STS cache: %v", err)
}
go stsCache.PeriodicallyRefresh(context.Background())

localC := &courier.Procmail{
Binary: conf.MailDeliveryAgentBin,
Args: conf.MailDeliveryAgentArgs,
Timeout: 30 * time.Second,
}
remoteC := &courier.SMTP{Dinfo: dinfo}
remoteC := &courier.SMTP{Dinfo: dinfo, STSCache: stsCache}
s.InitQueue(conf.DataDir+"/queue", localC, remoteC)

// Load the addresses and listeners.
Expand Down
25 changes: 25 additions & 0 deletions cmd/smtp-check/smtp-check.go
Expand Up @@ -5,12 +5,15 @@
package main

import (
"context"
"crypto/tls"
"flag"
"log"
"net"
"net/smtp"
"time"

"blitiri.com.ar/go/chasquid/internal/sts"
"blitiri.com.ar/go/chasquid/internal/tlsconst"
"blitiri.com.ar/go/spf"

Expand All @@ -37,6 +40,21 @@ func main() {
log.Fatalf("IDNA conversion failed: %v", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

log.Printf("=== STS policy")
policy, err := sts.UncheckedFetch(ctx, domain)
if err != nil {
log.Printf("Not available (%s)", err)
} else {
log.Printf("Parsed contents: [%+v]\n", *policy)
if err := policy.Check(); err != nil {
log.Fatalf("Invalid: %v", err)
}
log.Printf("OK")
}

mxs, err := net.LookupMX(domain)
if err != nil {
log.Fatalf("MX lookup: %v", err)
Expand Down Expand Up @@ -86,6 +104,13 @@ func main() {
c.Close()
}

if policy != nil {
if !policy.MXIsAllowed(mx.Host) {
log.Fatalf("NOT allowed by STS policy")
}
log.Printf("Allowed by policy")
}

log.Printf("")
}

Expand Down
88 changes: 82 additions & 6 deletions internal/courier/smtp.go
@@ -1,6 +1,7 @@
package courier

import (
"context"
"crypto/tls"
"expvar"
"flag"
Expand All @@ -13,6 +14,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/domaininfo"
"blitiri.com.ar/go/chasquid/internal/envelope"
"blitiri.com.ar/go/chasquid/internal/smtp"
"blitiri.com.ar/go/chasquid/internal/sts"
"blitiri.com.ar/go/chasquid/internal/trace"
)

Expand All @@ -30,17 +32,26 @@ var (
// TODO: replace this with proper lookup interception once it is supported
// by Go.
netLookupMX = net.LookupMX

// Enable STS policy checking; this is an experimental flag and will be
// removed in the future, once this is made the default.
enableSTS = flag.Bool("experimental__enable_sts", false,
"enable STS policy checking; EXPERIMENTAL")
)

// Exported variables.
var (
tlsCount = expvar.NewMap("chasquid/smtpOut/tlsCount")
slcResults = expvar.NewMap("chasquid/smtpOut/securityLevelChecks")

stsSecurityModes = expvar.NewMap("chasquid/smtpOut/sts/mode")
stsSecurityResults = expvar.NewMap("chasquid/smtpOut/sts/security")
)

// SMTP delivers remote mail via outgoing SMTP.
type SMTP struct {
Dinfo *domaininfo.DB
Dinfo *domaininfo.DB
STSCache *sts.PolicyCache
}

// Deliver an email. On failures, returns an error, and whether or not it is
Expand All @@ -62,7 +73,9 @@ func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
a.from = ""
}

mxs, err := lookupMXs(a.tr, a.toDomain)
a.stsPolicy = s.fetchSTSPolicy(a.tr, a.toDomain)

mxs, err := lookupMXs(a.tr, a.toDomain, a.stsPolicy)
if err != nil || len(mxs) == 0 {
// Note this is considered a permanent error.
// This is in line with what other servers (Exim) do. However, the
Expand Down Expand Up @@ -108,6 +121,8 @@ type attempt struct {
toDomain string
helloDomain string

stsPolicy *sts.Policy

tr *trace.Trace
}

Expand Down Expand Up @@ -175,6 +190,18 @@ retry:
}
slcResults.Add("pass", 1)

if a.stsPolicy != nil && a.stsPolicy.Mode == sts.Enforce {
// The connection MUST be validated TLS.
// https://tools.ietf.org/html/draft-ietf-uta-mta-sts-03#section-4.2
if secLevel != domaininfo.SecLevel_TLS_SECURE {
stsSecurityResults.Add("fail", 1)
return a.tr.Errorf("invalid security level (%v) for STS policy",
secLevel), false
}
stsSecurityResults.Add("pass", 1)
a.tr.Debugf("STS policy: connection is using valid TLS")
}

if err = c.MailAndRcpt(a.from, a.to); err != nil {
return a.tr.Errorf("MAIL+RCPT %v", err), smtp.IsPermanent(err)
}
Expand All @@ -199,7 +226,29 @@ retry:
return nil, false
}

func lookupMXs(tr *trace.Trace, domain string) ([]string, error) {
func (s *SMTP) fetchSTSPolicy(tr *trace.Trace, domain string) *sts.Policy {
if !*enableSTS {
return nil
}
if s.STSCache == nil {
return nil
}

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

policy, err := s.STSCache.Fetch(ctx, domain)
if err != nil {
return nil
}

tr.Debugf("got STS policy")
stsSecurityModes.Add(string(policy.Mode), 1)

return policy
}

func lookupMXs(tr *trace.Trace, domain string, policy *sts.Policy) ([]string, error) {
domain, err := idna.ToASCII(domain)
if err != nil {
return nil, err
Expand Down Expand Up @@ -239,12 +288,39 @@ func lookupMXs(tr *trace.Trace, domain string) ([]string, error) {
// This case is explicitly covered by the SMTP RFC.
// https://tools.ietf.org/html/rfc5321#section-5.1

// Cap the list of MXs to 5 hosts, to keep delivery attempt times sane
// and prevent abuse.
if len(mxs) > 5 {
mxs = filterMXs(tr, policy, mxs)
if len(mxs) == 0 {
tr.Errorf("domain %q has no valid MX/A record", domain)
} else if len(mxs) > 5 {
// Cap the list of MXs to 5 hosts, to keep delivery attempt times
// sane and prevent abuse.
mxs = mxs[:5]
}

tr.Debugf("MXs: %v", mxs)
return mxs, nil
}

func filterMXs(tr *trace.Trace, p *sts.Policy, mxs []string) []string {
if p == nil {
return mxs
}

filtered := []string{}
for _, mx := range mxs {
if p.MXIsAllowed(mx) {
filtered = append(filtered, mx)
} else {
tr.Printf("MX %q not allowed by policy, skipping", mx)
}
}

// We don't want to return an empty set if the mode is not enforce.
// This prevents failures for policies in reporting mode.
// https://tools.ietf.org/html/draft-ietf-uta-mta-sts-03#section-5.2
if len(filtered) == 0 && p.Mode != sts.Enforce {
filtered = mxs
}

return filtered
}
2 changes: 1 addition & 1 deletion internal/courier/smtp_test.go
Expand Up @@ -35,7 +35,7 @@ func newSMTP(t *testing.T) (*SMTP, string) {
t.Fatal(err)
}

return &SMTP{dinfo}, dir
return &SMTP{dinfo, nil}, dir
}

// Fake server, to test SMTP out.
Expand Down

0 comments on commit 23deaf1

Please sign in to comment.