This commit is contained in:
Mechiel Lukkien
2023-01-30 14:27:06 +01:00
commit cb229cb6cf
1256 changed files with 491723 additions and 0 deletions

6
tlsrpt/doc.go Normal file
View File

@ -0,0 +1,6 @@
// Package tlsrpt implements SMTP TLS Reporting, RFC 8460.
//
// TLSRPT allows a domain to publish a policy requesting feedback of TLS
// connectivity to its SMTP servers. Reports can be sent to an address defined
// in the TLSRPT DNS record. These reports can be parsed by tlsrpt.
package tlsrpt

91
tlsrpt/lookup.go Normal file
View File

@ -0,0 +1,91 @@
package tlsrpt
import (
"context"
"errors"
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
)
var xlog = mlog.New("tlsrpt")
var (
metricLookup = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "mox_tlsrpt_lookup_duration_seconds",
Help: "TLSRPT lookups with result.",
Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
},
[]string{"result"},
)
)
var (
ErrNoRecord = errors.New("tlsrpt: no tlsrpt dns txt record")
ErrMultipleRecords = errors.New("tlsrpt: multiple tlsrpt records") // Must be treated as if domain does not implement TLSRPT.
ErrDNS = errors.New("tlsrpt: temporary error")
ErrRecordSyntax = errors.New("tlsrpt: record syntax error")
)
// Lookup looks up a TLSRPT DNS TXT record for domain at "_smtp._tls.<domain>" and
// parses it.
func Lookup(ctx context.Context, resolver dns.Resolver, domain dns.Domain) (rrecord *Record, rtxt string, rerr error) {
log := xlog.WithContext(ctx)
start := time.Now()
defer func() {
result := "ok"
if rerr != nil {
if errors.Is(rerr, ErrNoRecord) {
result = "notfound"
} else if errors.Is(rerr, ErrMultipleRecords) {
result = "multiple"
} else if errors.Is(rerr, ErrDNS) {
result = "temperror"
} else if errors.Is(rerr, ErrRecordSyntax) {
result = "malformed"
} else {
result = "error"
}
}
metricLookup.WithLabelValues(result).Observe(float64(time.Since(start)) / float64(time.Second))
log.Debugx("tlsrpt lookup result", rerr, mlog.Field("domain", domain), mlog.Field("record", rrecord), mlog.Field("duration", time.Since(start)))
}()
name := "_smtp._tls." + domain.ASCII + "."
txts, err := dns.WithPackage(resolver, "tlsrpt").LookupTXT(ctx, name)
if dns.IsNotFound(err) {
return nil, "", ErrNoRecord
} else if err != nil {
return nil, "", fmt.Errorf("%w: %s", ErrDNS, err)
}
var text string
var record *Record
for _, txt := range txts {
r, istlsrpt, err := ParseRecord(txt)
if !istlsrpt {
// This is a loose but probably reasonable interpretation of ../rfc/8460:375 which
// wants us to discard otherwise valid records that start with e.g. "v=TLSRPTv1 ;"
// (note the space before the ";") when multiple TXT records were returned.
continue
}
if err != nil {
return nil, "", fmt.Errorf("parsing record: %w", err)
}
if record != nil {
return nil, "", ErrMultipleRecords
}
record = r
text = txt
}
if record == nil {
return nil, "", ErrNoRecord
}
return record, text, nil
}

46
tlsrpt/lookup_test.go Normal file
View File

@ -0,0 +1,46 @@
package tlsrpt
import (
"context"
"errors"
"reflect"
"testing"
"github.com/mjl-/mox/dns"
)
func TestLookup(t *testing.T) {
resolver := dns.MockResolver{
TXT: map[string][]string{
"_smtp._tls.basic.example.": {"v=TLSRPTv1; rua=mailto:tlsrpt@basic.example"},
"_smtp._tls.one.example.": {"v=TLSRPTv1; rua=mailto:tlsrpt@basic.example", "other"},
"_smtp._tls.multiple.example.": {"v=TLSRPTv1; rua=mailto:tlsrpt@basic.example", "v=TLSRPTv1; rua=mailto:tlsrpt@basic.example"},
"_smtp._tls.malformed.example.": {"v=TLSRPTv1; bad"},
"_smtp._tls.other.example.": {"other"},
},
Fail: map[dns.Mockreq]struct{}{
{Type: "txt", Name: "_smtp._tls.temperror.example."}: {},
},
}
test := func(domain string, expRecord *Record, expErr error) {
t.Helper()
d := dns.Domain{ASCII: domain}
record, _, err := Lookup(context.Background(), resolver, d)
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
t.Fatalf("lookup, got err %#v, expected %#v", err, expErr)
}
if err == nil && !reflect.DeepEqual(record, expRecord) {
t.Fatalf("lookup, got %#v, expected %#v", record, expRecord)
}
}
test("basic.example", &Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:tlsrpt@basic.example"}}}, nil)
test("one.example", &Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:tlsrpt@basic.example"}}}, nil)
test("multiple.example", nil, ErrMultipleRecords)
test("absent.example", nil, ErrNoRecord)
test("other.example", nil, ErrNoRecord)
test("malformed.example", nil, ErrRecordSyntax)
test("temperror.example", nil, ErrDNS)
}

226
tlsrpt/parse.go Normal file
View File

@ -0,0 +1,226 @@
package tlsrpt
import (
"fmt"
"net/url"
"strings"
)
// Extension is an additional key/value pair for a TLSRPT record.
type Extension struct {
Key string
Value string
}
// Record is a parsed TLSRPT record, to be served under "_smtp._tls.<domain>".
//
// Example:
//
// v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;
type Record struct {
Version string // "TLSRPTv1", for "v=".
RUAs [][]string // Aggregate reporting URI, for "rua=". "rua=" can occur multiple times, each can be a list. Must be URL-encoded strings, with ",", "!" and ";" encoded.
Extensions []Extension
}
// String returns a string or use as a TLSRPT DNS TXT record.
func (r Record) String() string {
b := &strings.Builder{}
fmt.Fprint(b, "v="+r.Version)
for _, rua := range r.RUAs {
fmt.Fprint(b, "; rua="+strings.Join(rua, ","))
}
for _, p := range r.Extensions {
fmt.Fprint(b, "; "+p.Key+"="+p.Value)
}
return b.String()
}
type parseErr string
func (e parseErr) Error() string {
return string(e)
}
var _ error = parseErr("")
// ParseRecord parses a TLSRPT record.
func ParseRecord(txt string) (record *Record, istlsrpt bool, err error) {
defer func() {
x := recover()
if x == nil {
return
}
if xerr, ok := x.(parseErr); ok {
record = nil
err = fmt.Errorf("%w: %s", ErrRecordSyntax, xerr)
return
}
panic(x)
}()
p := newParser(txt)
record = &Record{
Version: "TLSRPTv1",
}
p.xtake("v=TLSRPTv1")
p.xdelim()
istlsrpt = true
for {
k := p.xkey()
p.xtake("=")
// note: duplicates are allowed.
switch k {
case "rua":
record.RUAs = append(record.RUAs, p.xruas())
default:
v := p.xvalue()
record.Extensions = append(record.Extensions, Extension{k, v})
}
if !p.delim() || p.empty() {
break
}
}
if !p.empty() {
p.xerrorf("leftover chars")
}
if record.RUAs == nil {
p.xerrorf("missing rua")
}
return
}
type parser struct {
s string
o int
}
func newParser(s string) *parser {
return &parser{s: s}
}
func (p *parser) xerrorf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
if p.o < len(p.s) {
msg += fmt.Sprintf(" (remain %q)", p.s[p.o:])
}
panic(parseErr(msg))
}
func (p *parser) xtake(s string) string {
if !p.prefix(s) {
p.xerrorf("expected %q", s)
}
p.o += len(s)
return s
}
func (p *parser) xdelim() {
if !p.delim() {
p.xerrorf("expected semicolon")
}
}
func (p *parser) xtaken(n int) string {
r := p.s[p.o : p.o+n]
p.o += n
return r
}
func (p *parser) prefix(s string) bool {
return strings.HasPrefix(p.s[p.o:], s)
}
func (p *parser) take(s string) bool {
if p.prefix(s) {
p.o += len(s)
return true
}
return false
}
func (p *parser) xtakefn1(fn func(rune, int) bool) string {
for i, b := range p.s[p.o:] {
if !fn(b, i) {
if i == 0 {
p.xerrorf("expected at least one char")
}
return p.xtaken(i)
}
}
if p.empty() {
p.xerrorf("expected at least 1 char")
}
return p.xtaken(len(p.s) - p.o)
}
// ../rfc/8460:368
func (p *parser) xkey() string {
return p.xtakefn1(func(b rune, i int) bool {
return i < 32 && (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || (i > 0 && b == '_' || b == '-' || b == '.'))
})
}
// ../rfc/8460:371
func (p *parser) xvalue() string {
return p.xtakefn1(func(b rune, i int) bool {
return b > ' ' && b < 0x7f && b != '=' && b != ';'
})
}
// ../rfc/8460:399
func (p *parser) delim() bool {
o := p.o
e := len(p.s)
for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
o++
}
if o >= e || p.s[o] != ';' {
return false
}
o++
for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
o++
}
p.o = o
return true
}
func (p *parser) empty() bool {
return p.o >= len(p.s)
}
func (p *parser) wsp() {
for p.o < len(p.s) && (p.s[p.o] == ' ' || p.s[p.o] == '\t') {
p.o++
}
}
// ../rfc/8460:358
func (p *parser) xruas() []string {
l := []string{p.xuri()}
p.wsp()
for p.take(",") {
p.wsp()
l = append(l, p.xuri())
p.wsp()
}
return l
}
// ../rfc/8460:360
func (p *parser) xuri() string {
v := p.xtakefn1(func(b rune, i int) bool {
return b != ',' && b != '!' && b != ' ' && b != '\t' && b != ';'
})
u, err := url.Parse(v)
if err != nil {
p.xerrorf("parsing uri %q: %s", v, err)
}
if u.Scheme == "" {
p.xerrorf("missing scheme in uri")
}
return v
}

83
tlsrpt/parse_test.go Normal file
View File

@ -0,0 +1,83 @@
package tlsrpt
import (
"reflect"
"testing"
)
func TestRecord(t *testing.T) {
good := func(txt string, want Record) {
t.Helper()
r, _, err := ParseRecord(txt)
if err != nil {
t.Fatalf("parse: %s", err)
}
if !reflect.DeepEqual(r, &want) {
t.Fatalf("want %v, got %v", want, *r)
}
}
bad := func(txt string) {
t.Helper()
r, _, err := ParseRecord(txt)
if err == nil {
t.Fatalf("parse, expected error, got record %v", r)
}
}
good("v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;", Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:tlsrpt@mox.example"}}})
good("v=TLSRPTv1; rua=mailto:tlsrpt@mox.example , \t\t https://mox.example/tlsrpt ", Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:tlsrpt@mox.example", "https://mox.example/tlsrpt"}}})
good("v=TLSRPTv1; rua=mailto:tlsrpt@mox.example; ext=yes", Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:tlsrpt@mox.example"}}, Extensions: []Extension{{"ext", "yes"}}})
good("v=TLSRPTv1 ; rua=mailto:x@x.example; rua=mailto:y@x.example", Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:x@x.example"}, {"mailto:y@x.example"}}})
bad("v=TLSRPTv0")
bad("v=TLSRPTv10")
bad("v=TLSRPTv2")
bad("v=TLSRPTv1") // missing rua
bad("v=TLSRPTv1;") // missing rua
bad("v=TLSRPTv1; ext=1") // missing rua
bad("v=TLSRPTv1; rua=") // empty rua
bad("v=TLSRPTv1; rua=noscheme")
bad("v=TLSRPTv1; rua=,, ,") // empty uris
bad("v=TLSRPTv1; rua=mailto:x@x.example; more=") // empty value in extension
bad("v=TLSRPTv1; rua=mailto:x@x.example; a12345678901234567890123456789012=1") // extension name too long
bad("v=TLSRPTv1; rua=mailto:x@x.example; 1%=a") // invalid extension name
bad("v=TLSRPTv1; rua=mailto:x@x.example; test==") // invalid extension name
bad("v=TLSRPTv1; rua=mailto:x@x.example;;") // additional semicolon
bad("v=TLSRPTv1; rua=mailto:x@x.example other") // trailing characters.
bad("v=TLSRPTv1; rua=http://bad/%") // bad URI
const want = `v=TLSRPTv1; rua=mailto:x@mox.example; more=a; ext=2`
record := Record{Version: "TLSRPTv1", RUAs: [][]string{{"mailto:x@mox.example"}}, Extensions: []Extension{{"more", "a"}, {"ext", "2"}}}
got := record.String()
if got != want {
t.Fatalf("record string, got %q, want %q", got, want)
}
}
func FuzzParseRecord(f *testing.F) {
f.Add("v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;")
f.Add("v=TLSRPTv1; rua=mailto:tlsrpt@mox.example , \t\t https://mox.example/tlsrpt ")
f.Add("v=TLSRPTv1; rua=mailto:tlsrpt@mox.example; ext=yes")
f.Add("v=TLSRPTv0")
f.Add("v=TLSRPTv10")
f.Add("v=TLSRPTv2")
f.Add("v=TLSRPTv1") // missing rua
f.Add("v=TLSRPTv1;") // missing rua
f.Add("v=TLSRPTv1; ext=1") // missing rua
f.Add("v=TLSRPTv1; rua=") // empty rua
f.Add("v=TLSRPTv1; rua=noscheme")
f.Add("v=TLSRPTv1; rua=,, ,") // empty uris
f.Add("v=TLSRPTv1; rua=mailto:x@x.example; more=") // empty value in extension
f.Add("v=TLSRPTv1; rua=mailto:x@x.example; a12345678901234567890123456789012=1") // extension name too long
f.Add("v=TLSRPTv1; rua=mailto:x@x.example; 1%=a") // invalid extension name
f.Add("v=TLSRPTv1; rua=mailto:x@x.example; test==") // invalid extension name
f.Add("v=TLSRPTv1; rua=mailto:x@x.example;;") // additional semicolon
f.Fuzz(func(t *testing.T, s string) {
r, _, err := ParseRecord(s)
if err == nil {
_ = r.String()
}
})
}

153
tlsrpt/report.go Normal file
View File

@ -0,0 +1,153 @@
package tlsrpt
import (
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/mjl-/mox/message"
"github.com/mjl-/mox/moxio"
)
var ErrNoReport = errors.New("no tlsrpt report found")
// ../rfc/8460:628
// Report is a TLSRPT report, transmitted in JSON format.
type Report struct {
OrganizationName string `json:"organization-name"`
DateRange TLSRPTDateRange `json:"date-range"`
ContactInfo string `json:"contact-info"` // Email address.
ReportID string `json:"report-id"`
Policies []Result `json:"policies"`
}
// note: with TLSRPT prefix to prevent clash in sherpadoc types.
type TLSRPTDateRange struct {
Start time.Time `json:"start-datetime"`
End time.Time `json:"end-datetime"`
}
type Result struct {
Policy ResultPolicy `json:"policy"`
Summary Summary `json:"summary"`
FailureDetails []FailureDetails `json:"failure-details"`
}
type ResultPolicy struct {
Type string `json:"policy-type"`
String []string `json:"policy-string"`
Domain string `json:"policy-domain"`
MXHost []string `json:"mx-host"` // Example in RFC has errata, it originally was a single string. ../rfc/8460-eid6241 ../rfc/8460:1779
}
type Summary struct {
TotalSuccessfulSessionCount int64 `json:"total-successful-session-count"`
TotalFailureSessionCount int64 `json:"total-failure-session-count"`
}
// ResultType represents a TLS error.
type ResultType string
// ../rfc/8460:1377
// https://www.iana.org/assignments/starttls-validation-result-types/starttls-validation-result-types.xhtml
const (
ResultSTARTTLSNotSupported ResultType = "starttls-not-supported"
ResultCertificateHostMismatch ResultType = "certificate-host-mismatch"
ResultCertificateExpired ResultType = "certificate-expired"
ResultTLSAInvalid ResultType = "tlsa-invalid"
ResultDNSSECInvalid ResultType = "dnssec-invalid"
ResultDANERequired ResultType = "dane-required"
ResultCertificateNotTrusted ResultType = "certificate-not-trusted"
ResultSTSPolicyInvalid ResultType = "sts-policy-invalid"
ResultSTSWebPKIInvalid ResultType = "sts-webpki-invalid"
ResultValidationFailure ResultType = "validation-failure" // Other error.
ResultSTSPolicyFetch ResultType = "sts-policy-fetch-error"
)
type FailureDetails struct {
ResultType ResultType `json:"result-type"`
SendingMTAIP string `json:"sending-mta-ip"`
ReceivingMXHostname string `json:"receiving-mx-hostname"`
ReceivingMXHelo string `json:"receiving-mx-helo"`
ReceivingIP string `json:"receiving-ip"`
FailedSessionCount int64 `json:"failed-session-count"`
AdditionalInformation string `json:"additional-information"`
FailureReasonCode string `json:"failure-reason-code"`
}
// Parse parses a Report.
// The maximum size is 20MB.
func Parse(r io.Reader) (*Report, error) {
r = &moxio.LimitReader{R: r, Limit: 20 * 1024 * 1024}
var report Report
if err := json.NewDecoder(r).Decode(&report); err != nil {
return nil, err
}
// note: there may be leftover data, we ignore it.
return &report, nil
}
// ParseMessage parses a Report from a mail message.
// The maximum size of the message is 15MB, the maximum size of the
// decompressed report is 20MB.
func ParseMessage(r io.ReaderAt) (*Report, error) {
// ../rfc/8460:905
p, err := message.Parse(&moxio.LimitAtReader{R: r, Limit: 15 * 1024 * 1024})
if err != nil {
return nil, fmt.Errorf("parsing mail message: %s", err)
}
// Using multipart appears optional, and similar to DMARC someone may decide to
// send it like that, so accept a report if it's the entire message.
const allow = true
return parseMessageReport(p, allow)
}
func parseMessageReport(p message.Part, allow bool) (*Report, error) {
if p.MediaType != "MULTIPART" {
if !allow {
return nil, ErrNoReport
}
return parseReport(p)
}
for {
sp, err := p.ParseNextPart()
if err == io.EOF {
return nil, ErrNoReport
}
if err != nil {
return nil, err
}
if p.MediaSubType == "REPORT" && p.ContentTypeParams["report-type"] != "tlsrpt" {
return nil, fmt.Errorf("unknown report-type parameter %q", p.ContentTypeParams["report-type"])
}
report, err := parseMessageReport(*sp, p.MediaSubType == "REPORT")
if err == ErrNoReport {
continue
} else if err != nil || report != nil {
return report, err
}
}
}
func parseReport(p message.Part) (*Report, error) {
mt := strings.ToLower(p.MediaType + "/" + p.MediaSubType)
switch mt {
case "application/tlsrpt+json":
return Parse(p.Reader())
case "application/tlsrpt+gzip":
gzr, err := gzip.NewReader(p.Reader())
if err != nil {
return nil, fmt.Errorf("decoding gzip TLSRPT report: %s", err)
}
return Parse(gzr)
}
return nil, ErrNoReport
}

149
tlsrpt/report_test.go Normal file
View File

@ -0,0 +1,149 @@
package tlsrpt
import (
"encoding/json"
"os"
"strings"
"testing"
)
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 Report
dec := json.NewDecoder(strings.NewReader(reportJSON))
dec.DisallowUnknownFields()
if err := dec.Decode(&report); err != nil {
t.Fatalf("parsing report: %s", err)
}
if _, err := ParseMessage(strings.NewReader(tlsrptMessage)); err != nil {
t.Fatalf("parsing TLSRPT from message: %s", err)
}
if _, err := ParseMessage(strings.NewReader(tlsrptMessage2)); err != nil {
t.Fatalf("parsing TLSRPT from message: %s", err)
}
if _, err := ParseMessage(strings.NewReader(strings.ReplaceAll(tlsrptMessage, "multipart/report", "multipart/related"))); err != ErrNoReport {
t.Fatalf("got err %v, expected ErrNoReport", err)
}
if _, err := ParseMessage(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(f); err != nil {
t.Fatalf("parsing TLSRPT from message %q: %s", file.Name(), err)
}
f.Close()
}
}
func FuzzParseMessage(f *testing.F) {
f.Add(tlsrptMessage)
f.Fuzz(func(t *testing.T, s string) {
ParseMessage(strings.NewReader(s))
})
}