author | Alberto Bertogli
<albertito@blitiri.com.ar> 2017-02-26 02:32:59 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2017-02-28 22:27:15 UTC |
parent | d66b06de5108ba6cb5b2e8774a2e256e8e3735f4 |
chasquid.go | +9 | -1 |
internal/courier/smtp.go | +88 | -12 |
internal/courier/smtp_test.go | +1 | -1 |
diff --git a/chasquid.go b/chasquid.go index 80fb3af..640850d 100644 --- a/chasquid.go +++ b/chasquid.go @@ -1,6 +1,7 @@ package main import ( + "context" "expvar" "flag" "fmt" @@ -19,6 +20,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/systemd" "blitiri.com.ar/go/chasquid/internal/userdb" @@ -135,12 +137,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. diff --git a/internal/courier/smtp.go b/internal/courier/smtp.go index 159eee4..3fa98ad 100644 --- a/internal/courier/smtp.go +++ b/internal/courier/smtp.go @@ -1,6 +1,7 @@ package courier import ( + "context" "crypto/tls" "expvar" "flag" @@ -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" ) @@ -26,6 +28,11 @@ var ( smtpPort = flag.String("testing__outgoing_smtp_port", "25", "port to use for outgoing SMTP connections, ONLY FOR TESTING") + // 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") + // Fake MX records, used for testing only. fakeMX = map[string][]string{} ) @@ -34,20 +41,25 @@ var ( 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 } func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) { a := &attempt{ - courier: s, - from: from, - to: to, - data: data, - tr: trace.New("Courier.SMTP", to), + courier: s, + from: from, + to: to, + data: data, + toDomain: envelope.DomainOf(to), + tr: trace.New("Courier.SMTP", to), } defer a.tr.Finish() a.tr.Debugf("%s -> %s", from, to) @@ -57,8 +69,9 @@ func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) { a.from = "" } - toDomain := envelope.DomainOf(to) - mxs, err := lookupMXs(a.tr, toDomain) + a.stsPolicy = s.fetchSTSPolicy(a.tr, a.toDomain) + + mxs, err := lookupMXs(a.tr, a.toDomain, a.stsPolicy) if err != nil { // Note this is considered a permanent error. // This is in line with what other servers (Exim) do. However, the @@ -104,6 +117,8 @@ type attempt struct { toDomain string helloDomain string + stsPolicy *sts.Policy + tr *trace.Trace } @@ -171,6 +186,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) } @@ -195,7 +222,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) { if v, ok := fakeMX[domain]; ok { return v, nil } @@ -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 +} diff --git a/internal/courier/smtp_test.go b/internal/courier/smtp_test.go index 703acc3..e1e624c 100644 --- a/internal/courier/smtp_test.go +++ b/internal/courier/smtp_test.go @@ -23,7 +23,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.