6	cryptorand "crypto/rand"
 
18	"github.com/mjl-/mox/mlog"
 
21var pkglog = mlog.New("tlsrpt", nil)
 
24     "organization-name": "Company-X",
 
26       "start-datetime": "2016-04-01T00:00:00Z",
 
27       "end-datetime": "2016-04-01T23:59:59Z"
 
29     "contact-info": "sts-reporting@company-x.example",
 
30     "report-id": "5065427c-23d3-47ca-b6e0-946ea0e8c4be",
 
34         "policy-string": ["version: STSv1","mode: testing",
 
35               "mx: *.mail.company-y.example","max_age: 86400"],
 
36         "policy-domain": "company-y.example",
 
37         "mx-host": ["*.mail.company-y.example"]
 
40         "total-successful-session-count": 5326,
 
41         "total-failure-session-count": 303
 
44         "result-type": "certificate-expired",
 
45         "sending-mta-ip": "2001:db8:abcd:0012::1",
 
46         "receiving-mx-hostname": "mx1.mail.company-y.example",
 
47         "failed-session-count": 100
 
49         "result-type": "starttls-not-supported",
 
50         "sending-mta-ip": "2001:db8:abcd:0013::1",
 
51         "receiving-mx-hostname": "mx2.mail.company-y.example",
 
52         "receiving-ip": "203.0.113.56",
 
53         "failed-session-count": 200,
 
54         "additional-information": "https://reports.company-x.example/report_info ? id = 5065427 c - 23 d3# StarttlsNotSupported "
 
56         "result-type": "validation-failure",
 
57         "sending-mta-ip": "198.51.100.62",
 
58         "receiving-ip": "203.0.113.58",
 
59         "receiving-mx-hostname": "mx-backup.mail.company-y.example",
 
60         "failed-session-count": 3,
 
61         "failure-reason-code": "X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED"
 
67var tlsrptMessage = strings.ReplaceAll(`From: tlsrpt@mail.sender.example.com
 
68Date: Fri, May 09 2017 16:54:30 -0800
 
69To: mts-sts-tlsrpt@example.net
 
70Subject: Report Domain: example.net
 
71Submitter: mail.sender.example.com
 
72Report-ID: <735ff.e317+bf22029@example.net>
 
73TLS-Report-Domain: example.net
 
74TLS-Report-Submitter: mail.sender.example.com
 
76Content-Type: multipart/report; report-type="tlsrpt";
 
77	boundary="----=_NextPart_000_024E_01CC9B0A.AFE54C00"
 
78Content-Language: en-us
 
80This is a multipart message in MIME format.
 
82------=_NextPart_000_024E_01CC9B0A.AFE54C00
 
83Content-Type: text/plain; charset="us-ascii"
 
84Content-Transfer-Encoding: 7bit
 
86This is an aggregate TLS report from mail.sender.example.com
 
88------=_NextPart_000_024E_01CC9B0A.AFE54C00
 
89Content-Type: application/tlsrpt+json
 
90Content-Transfer-Encoding: 8bit
 
91Content-Disposition: attachment;
 
92	filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
 
96------=_NextPart_000_024E_01CC9B0A.AFE54C00--
 
99// Message without multipart.
 
100var tlsrptMessage2 = strings.ReplaceAll(`From: tlsrpt@mail.sender.example.com
 
101To: mts-sts-tlsrpt@example.net
 
102Subject: Report Domain: example.net
 
103Report-ID: <735ff.e317+bf22029@example.net>
 
104TLS-Report-Domain: example.net
 
105TLS-Report-Submitter: mail.sender.example.com
 
107Content-Type: application/tlsrpt+json
 
108Content-Transfer-Encoding: 8bit
 
109Content-Disposition: attachment;
 
110	filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
 
115func TestReport(t *testing.T) {
 
118	var report ReportJSON
 
119	dec := json.NewDecoder(strings.NewReader(reportJSON))
 
120	dec.DisallowUnknownFields()
 
121	if err := dec.Decode(&report); err != nil {
 
122		t.Fatalf("parsing report: %s", err)
 
125	if _, err := ParseMessage(pkglog.Logger, strings.NewReader(tlsrptMessage)); err != nil {
 
126		t.Fatalf("parsing TLSRPT from message: %s", err)
 
129	if _, err := ParseMessage(pkglog.Logger, strings.NewReader(tlsrptMessage2)); err != nil {
 
130		t.Fatalf("parsing TLSRPT from message: %s", err)
 
133	if _, err := ParseMessage(pkglog.Logger, strings.NewReader(strings.ReplaceAll(tlsrptMessage, "multipart/report", "multipart/related"))); err != ErrNoReport {
 
134		t.Fatalf("got err %v, expected ErrNoReport", err)
 
137	if _, err := ParseMessage(pkglog.Logger, strings.NewReader(strings.ReplaceAll(tlsrptMessage, "application/tlsrpt+json", "application/json"))); err != ErrNoReport {
 
138		t.Fatalf("got err %v, expected ErrNoReport", err)
 
141	files, err := os.ReadDir("../testdata/tlsreports")
 
143		t.Fatalf("listing reports: %s", err)
 
145	for _, file := range files {
 
146		f, err := os.Open("../testdata/tlsreports/" + file.Name())
 
148			t.Fatalf("open %q: %s", file, err)
 
150		if _, err := ParseMessage(pkglog.Logger, f); err != nil {
 
151			t.Fatalf("parsing TLSRPT from message %q: %s", file.Name(), err)
 
157func TestTLSFailureDetails(t *testing.T) {
 
158	const alert70 = "tls-remote-alert-70-protocol-version-not-supported"
 
160	test := func(expResultType ResultType, expReasonCode string, client func(net.Conn) error, server func(net.Conn)) {
 
163		cconn, sconn := net.Pipe()
 
169			t.Fatalf("expected tls error")
 
172		resultType, reasonCode := TLSFailureDetails(err)
 
173		if resultType != expResultType || !(reasonCode == expReasonCode || expReasonCode == alert70 && reasonCode == "tls-remote-alert-70") {
 
174			t.Fatalf("got %v %v, expected %v %v", resultType, reasonCode, expResultType, expReasonCode)
 
178	newPool := func(certs ...tls.Certificate) *x509.CertPool {
 
179		pool := x509.NewCertPool()
 
180		for _, cert := range certs {
 
181			pool.AddCert(cert.Leaf)
 
186	// Expired certificate.
 
187	expiredCert := fakeCert(t, "localhost", true)
 
188	test(ResultCertificateExpired, "",
 
189		func(conn net.Conn) error {
 
190			config := tls.Config{ServerName: "localhost", RootCAs: newPool(expiredCert)}
 
191			return tls.Client(conn, &config).Handshake()
 
193		func(conn net.Conn) {
 
194			config := tls.Config{Certificates: []tls.Certificate{expiredCert}}
 
195			tls.Server(conn, &config).Handshake()
 
199	// Hostname mismatch.
 
200	okCert := fakeCert(t, "localhost", false)
 
201	test(ResultCertificateHostMismatch, "", func(conn net.Conn) error {
 
202		config := tls.Config{ServerName: "otherhost", RootCAs: newPool(okCert)}
 
203		return tls.Client(conn, &config).Handshake()
 
205		func(conn net.Conn) {
 
206			config := tls.Config{Certificates: []tls.Certificate{okCert}}
 
207			tls.Server(conn, &config).Handshake()
 
211	// Not signed by trusted CA.
 
212	test(ResultCertificateNotTrusted, "", func(conn net.Conn) error {
 
213		config := tls.Config{ServerName: "localhost", RootCAs: newPool()}
 
214		return tls.Client(conn, &config).Handshake()
 
216		func(conn net.Conn) {
 
217			config := tls.Config{Certificates: []tls.Certificate{okCert}}
 
218			tls.Server(conn, &config).Handshake()
 
222	// We don't support the right protocol version.
 
223	test(ResultValidationFailure, alert70, func(conn net.Conn) error {
 
224		config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert), MinVersion: tls.VersionTLS10, MaxVersion: tls.VersionTLS10}
 
225		return tls.Client(conn, &config).Handshake()
 
227		func(conn net.Conn) {
 
228			config := tls.Config{Certificates: []tls.Certificate{okCert}, MinVersion: tls.VersionTLS12}
 
229			tls.Server(conn, &config).Handshake()
 
233	// todo: ideally a test for tls-local-alert-*
 
235	// Remote is not speaking TLS.
 
236	test(ResultValidationFailure, "tls-record-header-error", func(conn net.Conn) error {
 
237		config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
 
238		return tls.Client(conn, &config).Handshake()
 
240		func(conn net.Conn) {
 
241			go io.Copy(io.Discard, conn)
 
242			buf := make([]byte, 128)
 
244				_, err := conn.Write(buf)
 
252	// Context deadline exceeded during handshake.
 
253	test(ResultValidationFailure, "io-timeout-during-handshake",
 
254		func(conn net.Conn) error {
 
255			config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
 
256			ctx, cancel := context.WithTimeout(context.Background(), 1)
 
258			return tls.Client(conn, &config).HandshakeContext(ctx)
 
260		func(conn net.Conn) {},
 
263	// Timeout during handshake.
 
264	test(ResultValidationFailure, "io-timeout-during-handshake",
 
265		func(conn net.Conn) error {
 
266			config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
 
267			conn.SetDeadline(time.Now())
 
268			return tls.Client(conn, &config).Handshake()
 
270		func(conn net.Conn) {},
 
273	// Closing connection during handshake.
 
274	test(ResultValidationFailure, "connection-closed-during-handshake", func(conn net.Conn) error {
 
275		config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
 
276		return tls.Client(conn, &config).Handshake()
 
278		func(conn net.Conn) {
 
284// Just a cert that appears valid.
 
285func fakeCert(t *testing.T, name string, expired bool) tls.Certificate {
 
286	notAfter := time.Now()
 
288		notAfter = notAfter.Add(-time.Hour)
 
290		notAfter = notAfter.Add(time.Hour)
 
293	privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
 
294	template := &x509.Certificate{
 
295		SerialNumber: big.NewInt(1), // Required field...
 
296		DNSNames:     []string{name},
 
297		NotBefore:    time.Now().Add(-time.Hour),
 
300	localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
 
302		t.Fatalf("making certificate: %s", err)
 
304	cert, err := x509.ParseCertificate(localCertBuf)
 
306		t.Fatalf("parsing generated certificate: %s", err)
 
308	c := tls.Certificate{
 
309		Certificate: [][]byte{localCertBuf},
 
316func FuzzParseMessage(f *testing.F) {
 
318	f.Fuzz(func(t *testing.T, s string) {
 
319		ParseMessage(pkglog.Logger, strings.NewReader(s))