mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
a9940f9855
all ui frontend code is now in typescript. we no longer need jshint, and we build the frontend code during "make build". this also changes tlsrpt types for a Report, not encoding field names with dashes, but to keep them valid identifiers in javascript. this makes it more conveniently to work with in the frontend, and works around a sherpats limitation.
321 lines
9.8 KiB
Go
321 lines
9.8 KiB
Go
package tlsrpt
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
cryptorand "crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/mlog"
|
|
)
|
|
|
|
var pkglog = mlog.New("tlsrpt", nil)
|
|
|
|
const reportJSON = `{
|
|
"organization-name": "Company-X",
|
|
"date-range": {
|
|
"start-datetime": "2016-04-01T00:00:00Z",
|
|
"end-datetime": "2016-04-01T23:59:59Z"
|
|
},
|
|
"contact-info": "sts-reporting@company-x.example",
|
|
"report-id": "5065427c-23d3-47ca-b6e0-946ea0e8c4be",
|
|
"policies": [{
|
|
"policy": {
|
|
"policy-type": "sts",
|
|
"policy-string": ["version: STSv1","mode: testing",
|
|
"mx: *.mail.company-y.example","max_age: 86400"],
|
|
"policy-domain": "company-y.example",
|
|
"mx-host": ["*.mail.company-y.example"]
|
|
},
|
|
"summary": {
|
|
"total-successful-session-count": 5326,
|
|
"total-failure-session-count": 303
|
|
},
|
|
"failure-details": [{
|
|
"result-type": "certificate-expired",
|
|
"sending-mta-ip": "2001:db8:abcd:0012::1",
|
|
"receiving-mx-hostname": "mx1.mail.company-y.example",
|
|
"failed-session-count": 100
|
|
}, {
|
|
"result-type": "starttls-not-supported",
|
|
"sending-mta-ip": "2001:db8:abcd:0013::1",
|
|
"receiving-mx-hostname": "mx2.mail.company-y.example",
|
|
"receiving-ip": "203.0.113.56",
|
|
"failed-session-count": 200,
|
|
"additional-information": "https://reports.company-x.example/report_info ? id = 5065427 c - 23 d3# StarttlsNotSupported "
|
|
}, {
|
|
"result-type": "validation-failure",
|
|
"sending-mta-ip": "198.51.100.62",
|
|
"receiving-ip": "203.0.113.58",
|
|
"receiving-mx-hostname": "mx-backup.mail.company-y.example",
|
|
"failed-session-count": 3,
|
|
"failure-reason-code": "X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED"
|
|
}]
|
|
}]
|
|
}`
|
|
|
|
// ../rfc/8460:1015
|
|
var tlsrptMessage = strings.ReplaceAll(`From: tlsrpt@mail.sender.example.com
|
|
Date: Fri, May 09 2017 16:54:30 -0800
|
|
To: mts-sts-tlsrpt@example.net
|
|
Subject: Report Domain: example.net
|
|
Submitter: mail.sender.example.com
|
|
Report-ID: <735ff.e317+bf22029@example.net>
|
|
TLS-Report-Domain: example.net
|
|
TLS-Report-Submitter: mail.sender.example.com
|
|
MIME-Version: 1.0
|
|
Content-Type: multipart/report; report-type="tlsrpt";
|
|
boundary="----=_NextPart_000_024E_01CC9B0A.AFE54C00"
|
|
Content-Language: en-us
|
|
|
|
This is a multipart message in MIME format.
|
|
|
|
------=_NextPart_000_024E_01CC9B0A.AFE54C00
|
|
Content-Type: text/plain; charset="us-ascii"
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
This is an aggregate TLS report from mail.sender.example.com
|
|
|
|
------=_NextPart_000_024E_01CC9B0A.AFE54C00
|
|
Content-Type: application/tlsrpt+json
|
|
Content-Transfer-Encoding: 8bit
|
|
Content-Disposition: attachment;
|
|
filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
|
|
|
|
`+reportJSON+`
|
|
|
|
------=_NextPart_000_024E_01CC9B0A.AFE54C00--
|
|
`, "\n", "\r\n")
|
|
|
|
// Message without multipart.
|
|
var tlsrptMessage2 = strings.ReplaceAll(`From: tlsrpt@mail.sender.example.com
|
|
To: mts-sts-tlsrpt@example.net
|
|
Subject: Report Domain: example.net
|
|
Report-ID: <735ff.e317+bf22029@example.net>
|
|
TLS-Report-Domain: example.net
|
|
TLS-Report-Submitter: mail.sender.example.com
|
|
MIME-Version: 1.0
|
|
Content-Type: application/tlsrpt+json
|
|
Content-Transfer-Encoding: 8bit
|
|
Content-Disposition: attachment;
|
|
filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
|
|
|
|
`+reportJSON+`
|
|
`, "\n", "\r\n")
|
|
|
|
func TestReport(t *testing.T) {
|
|
// ../rfc/8460:1756
|
|
|
|
var report ReportJSON
|
|
dec := json.NewDecoder(strings.NewReader(reportJSON))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&report); err != nil {
|
|
t.Fatalf("parsing report: %s", err)
|
|
}
|
|
|
|
if _, err := ParseMessage(pkglog.Logger, strings.NewReader(tlsrptMessage)); err != nil {
|
|
t.Fatalf("parsing TLSRPT from message: %s", err)
|
|
}
|
|
|
|
if _, err := ParseMessage(pkglog.Logger, strings.NewReader(tlsrptMessage2)); err != nil {
|
|
t.Fatalf("parsing TLSRPT from message: %s", err)
|
|
}
|
|
|
|
if _, err := ParseMessage(pkglog.Logger, strings.NewReader(strings.ReplaceAll(tlsrptMessage, "multipart/report", "multipart/related"))); err != ErrNoReport {
|
|
t.Fatalf("got err %v, expected ErrNoReport", err)
|
|
}
|
|
|
|
if _, err := ParseMessage(pkglog.Logger, strings.NewReader(strings.ReplaceAll(tlsrptMessage, "application/tlsrpt+json", "application/json"))); err != ErrNoReport {
|
|
t.Fatalf("got err %v, expected ErrNoReport", err)
|
|
}
|
|
|
|
files, err := os.ReadDir("../testdata/tlsreports")
|
|
if err != nil {
|
|
t.Fatalf("listing reports: %s", err)
|
|
}
|
|
for _, file := range files {
|
|
f, err := os.Open("../testdata/tlsreports/" + file.Name())
|
|
if err != nil {
|
|
t.Fatalf("open %q: %s", file, err)
|
|
}
|
|
if _, err := ParseMessage(pkglog.Logger, f); err != nil {
|
|
t.Fatalf("parsing TLSRPT from message %q: %s", file.Name(), err)
|
|
}
|
|
f.Close()
|
|
}
|
|
}
|
|
|
|
func TestTLSFailureDetails(t *testing.T) {
|
|
const alert70 = "tls-remote-alert-70-protocol-version-not-supported"
|
|
|
|
test := func(expResultType ResultType, expReasonCode string, client func(net.Conn) error, server func(net.Conn)) {
|
|
t.Helper()
|
|
|
|
cconn, sconn := net.Pipe()
|
|
defer cconn.Close()
|
|
defer sconn.Close()
|
|
go server(sconn)
|
|
err := client(cconn)
|
|
if err == nil {
|
|
t.Fatalf("expected tls error")
|
|
}
|
|
|
|
resultType, reasonCode := TLSFailureDetails(err)
|
|
if resultType != expResultType || !(reasonCode == expReasonCode || expReasonCode == alert70 && reasonCode == "tls-remote-alert-70") {
|
|
t.Fatalf("got %v %v, expected %v %v", resultType, reasonCode, expResultType, expReasonCode)
|
|
}
|
|
}
|
|
|
|
newPool := func(certs ...tls.Certificate) *x509.CertPool {
|
|
pool := x509.NewCertPool()
|
|
for _, cert := range certs {
|
|
pool.AddCert(cert.Leaf)
|
|
}
|
|
return pool
|
|
}
|
|
|
|
// Expired certificate.
|
|
expiredCert := fakeCert(t, "localhost", true)
|
|
test(ResultCertificateExpired, "",
|
|
func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool(expiredCert)}
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {
|
|
config := tls.Config{Certificates: []tls.Certificate{expiredCert}}
|
|
tls.Server(conn, &config).Handshake()
|
|
},
|
|
)
|
|
|
|
// Hostname mismatch.
|
|
okCert := fakeCert(t, "localhost", false)
|
|
test(ResultCertificateHostMismatch, "", func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "otherhost", RootCAs: newPool(okCert)}
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {
|
|
config := tls.Config{Certificates: []tls.Certificate{okCert}}
|
|
tls.Server(conn, &config).Handshake()
|
|
},
|
|
)
|
|
|
|
// Not signed by trusted CA.
|
|
test(ResultCertificateNotTrusted, "", func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool()}
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {
|
|
config := tls.Config{Certificates: []tls.Certificate{okCert}}
|
|
tls.Server(conn, &config).Handshake()
|
|
},
|
|
)
|
|
|
|
// We don't support the right protocol version.
|
|
test(ResultValidationFailure, alert70, func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert), MinVersion: tls.VersionTLS10, MaxVersion: tls.VersionTLS10}
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {
|
|
config := tls.Config{Certificates: []tls.Certificate{okCert}, MinVersion: tls.VersionTLS12}
|
|
tls.Server(conn, &config).Handshake()
|
|
},
|
|
)
|
|
|
|
// todo: ideally a test for tls-local-alert-*
|
|
|
|
// Remote is not speaking TLS.
|
|
test(ResultValidationFailure, "tls-record-header-error", func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {
|
|
go io.Copy(io.Discard, conn)
|
|
buf := make([]byte, 128)
|
|
for {
|
|
_, err := conn.Write(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
// Context deadline exceeded during handshake.
|
|
test(ResultValidationFailure, "io-timeout-during-handshake",
|
|
func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
|
defer cancel()
|
|
return tls.Client(conn, &config).HandshakeContext(ctx)
|
|
},
|
|
func(conn net.Conn) {},
|
|
)
|
|
|
|
// Timeout during handshake.
|
|
test(ResultValidationFailure, "io-timeout-during-handshake",
|
|
func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
|
|
conn.SetDeadline(time.Now())
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {},
|
|
)
|
|
|
|
// Closing connection during handshake.
|
|
test(ResultValidationFailure, "connection-closed-during-handshake", func(conn net.Conn) error {
|
|
config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
|
|
return tls.Client(conn, &config).Handshake()
|
|
},
|
|
func(conn net.Conn) {
|
|
conn.Close()
|
|
},
|
|
)
|
|
}
|
|
|
|
// Just a cert that appears valid.
|
|
func fakeCert(t *testing.T, name string, expired bool) tls.Certificate {
|
|
notAfter := time.Now()
|
|
if expired {
|
|
notAfter = notAfter.Add(-time.Hour)
|
|
} else {
|
|
notAfter = notAfter.Add(time.Hour)
|
|
}
|
|
|
|
privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1), // Required field...
|
|
DNSNames: []string{name},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: notAfter,
|
|
}
|
|
localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
|
|
if err != nil {
|
|
t.Fatalf("making certificate: %s", err)
|
|
}
|
|
cert, err := x509.ParseCertificate(localCertBuf)
|
|
if err != nil {
|
|
t.Fatalf("parsing generated certificate: %s", err)
|
|
}
|
|
c := tls.Certificate{
|
|
Certificate: [][]byte{localCertBuf},
|
|
PrivateKey: privKey,
|
|
Leaf: cert,
|
|
}
|
|
return c
|
|
}
|
|
|
|
func FuzzParseMessage(f *testing.F) {
|
|
f.Add(tlsrptMessage)
|
|
f.Fuzz(func(t *testing.T, s string) {
|
|
ParseMessage(pkglog.Logger, strings.NewReader(s))
|
|
})
|
|
}
|