checker-tls/checker/starttls_test.go
Pierre-Olivier Mercier e32633ca40 Harden STARTTLS handlers and add per-dialect tests
Bound line reads with readLineLimited to prevent a peer from exhausting
memory by withholding line terminators, wrap previously bare error
returns for consistent context, surface XML decoder Skip errors, and
replace the goto in the XMPP feature scan with a labeled break. New
starttls_test.go exercises SMTP/IMAP/POP3/XMPP/LDAP success and
not-advertised paths through net.Pipe-mocked servers.
2026-04-25 23:15:17 +07:00

274 lines
8.2 KiB
Go

package checker
import (
"bufio"
"errors"
"io"
"net"
"strings"
"testing"
"time"
)
// runStartTLS drives upgrader against a fake server. The server callback runs
// on the peer end of an in-memory pipe and may read/write the plaintext
// dialect transcript. The test deadline guards both ends from hanging.
func runStartTLS(t *testing.T, upgrader func(net.Conn, string) error, sni string, server func(net.Conn) error) error {
t.Helper()
clientConn, serverConn := net.Pipe()
deadline := time.Now().Add(2 * time.Second)
_ = clientConn.SetDeadline(deadline)
_ = serverConn.SetDeadline(deadline)
srvErr := make(chan error, 1)
go func() {
defer serverConn.Close()
srvErr <- server(serverConn)
}()
clientErr := upgrader(clientConn, sni)
clientConn.Close()
if err := <-srvErr; err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
t.Logf("server side returned: %v", err)
}
return clientErr
}
// readLineCRLF reads one CRLF-terminated line.
func readLineCRLF(r *bufio.Reader) (string, error) {
line, err := r.ReadString('\n')
return strings.TrimRight(line, "\r\n"), err
}
func TestStartTLS_SMTP_OK(t *testing.T) {
err := runStartTLS(t, starttlsSMTP, "mail.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
if _, err := io.WriteString(c, "220 mail.example.com ESMTP\r\n"); err != nil {
return err
}
ehlo, err := readLineCRLF(br)
if err != nil {
return err
}
if !strings.HasPrefix(ehlo, "EHLO ") {
return errors.New("expected EHLO")
}
if _, err := io.WriteString(c, "250-mail.example.com\r\n250-SIZE 10485760\r\n250 STARTTLS\r\n"); err != nil {
return err
}
stls, err := readLineCRLF(br)
if err != nil {
return err
}
if stls != "STARTTLS" {
return errors.New("expected STARTTLS")
}
_, err = io.WriteString(c, "220 ready\r\n")
return err
})
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
}
func TestStartTLS_SMTP_NotAdvertised(t *testing.T) {
err := runStartTLS(t, starttlsSMTP, "mail.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
_, _ = io.WriteString(c, "220 mail.example.com ESMTP\r\n")
if _, err := readLineCRLF(br); err != nil {
return err
}
_, err := io.WriteString(c, "250-mail.example.com\r\n250 SIZE 10485760\r\n")
return err
})
if !errors.Is(err, errStartTLSNotOffered) {
t.Fatalf("expected errStartTLSNotOffered, got: %v", err)
}
}
func TestStartTLS_SMTP_Refused(t *testing.T) {
err := runStartTLS(t, starttlsSMTP, "mail.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
_, _ = io.WriteString(c, "220 mail.example.com ESMTP\r\n")
_, _ = readLineCRLF(br)
_, _ = io.WriteString(c, "250-mail.example.com\r\n250 STARTTLS\r\n")
_, _ = readLineCRLF(br)
_, err := io.WriteString(c, "454 TLS not available\r\n")
return err
})
if err == nil {
t.Fatal("expected refusal error")
}
if errors.Is(err, errStartTLSNotOffered) {
t.Fatalf("refusal should not be classified as not-offered: %v", err)
}
}
func TestStartTLS_IMAP_OK(t *testing.T) {
err := runStartTLS(t, starttlsIMAP, "imap.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
_, _ = io.WriteString(c, "* OK IMAP4rev1 ready\r\n")
cap1, err := readLineCRLF(br)
if err != nil {
return err
}
if !strings.HasSuffix(cap1, "CAPABILITY") {
return errors.New("expected CAPABILITY")
}
_, _ = io.WriteString(c, "* CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED\r\nA001 OK CAPABILITY completed\r\n")
stls, err := readLineCRLF(br)
if err != nil {
return err
}
if !strings.HasSuffix(stls, "STARTTLS") {
return errors.New("expected STARTTLS")
}
_, err = io.WriteString(c, "A002 OK Begin TLS\r\n")
return err
})
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
}
func TestStartTLS_IMAP_NotAdvertised(t *testing.T) {
err := runStartTLS(t, starttlsIMAP, "imap.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
_, _ = io.WriteString(c, "* OK IMAP4rev1 ready\r\n")
_, _ = readLineCRLF(br)
_, err := io.WriteString(c, "* CAPABILITY IMAP4rev1 LOGINDISABLED\r\nA001 OK CAPABILITY completed\r\n")
return err
})
if !errors.Is(err, errStartTLSNotOffered) {
t.Fatalf("expected errStartTLSNotOffered, got: %v", err)
}
}
func TestStartTLS_POP3_OK(t *testing.T) {
err := runStartTLS(t, starttlsPOP3, "pop.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
_, _ = io.WriteString(c, "+OK POP3 ready\r\n")
capa, err := readLineCRLF(br)
if err != nil {
return err
}
if capa != "CAPA" {
return errors.New("expected CAPA")
}
_, _ = io.WriteString(c, "+OK capa list\r\nUSER\r\nSTLS\r\n.\r\n")
stls, err := readLineCRLF(br)
if err != nil {
return err
}
if stls != "STLS" {
return errors.New("expected STLS")
}
_, err = io.WriteString(c, "+OK begin TLS\r\n")
return err
})
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
}
func TestStartTLS_POP3_NotAdvertised(t *testing.T) {
err := runStartTLS(t, starttlsPOP3, "pop.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
_, _ = io.WriteString(c, "+OK POP3 ready\r\n")
_, _ = readLineCRLF(br)
_, err := io.WriteString(c, "+OK capa list\r\nUSER\r\n.\r\n")
return err
})
if !errors.Is(err, errStartTLSNotOffered) {
t.Fatalf("expected errStartTLSNotOffered, got: %v", err)
}
}
func TestStartTLS_XMPP_OK(t *testing.T) {
err := runStartTLS(t, starttlsXMPPClient, "xmpp.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
// Read the client's stream header (one line is enough for our writer).
buf := make([]byte, 1024)
if _, err := br.Read(buf); err != nil {
return err
}
_, _ = io.WriteString(c,
`<?xml version='1.0'?><stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='1' from='xmpp.example.com' version='1.0'>`+
`<stream:features><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required/></starttls></stream:features>`)
// Read the <starttls/> request from the client.
if _, err := br.Read(buf); err != nil {
return err
}
_, err := io.WriteString(c, `<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>`)
return err
})
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
}
func TestStartTLS_XMPP_NotAdvertised(t *testing.T) {
err := runStartTLS(t, starttlsXMPPClient, "xmpp.example.com", func(c net.Conn) error {
br := bufio.NewReader(c)
buf := make([]byte, 1024)
if _, err := br.Read(buf); err != nil {
return err
}
_, err := io.WriteString(c,
`<?xml version='1.0'?><stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='1' from='xmpp.example.com' version='1.0'>`+
`<stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism></mechanisms></stream:features>`)
return err
})
if !errors.Is(err, errStartTLSNotOffered) {
t.Fatalf("expected errStartTLSNotOffered, got: %v", err)
}
}
func TestStartTLS_LDAP_OK(t *testing.T) {
err := runStartTLS(t, starttlsLDAP, "ldap.example.com", func(c net.Conn) error {
// Drain the StartTLS request (fixed 31 bytes: 0x30 0x1d + 29 bytes).
req := make([]byte, 31)
if _, err := io.ReadFull(c, req); err != nil {
return err
}
// Build a minimal ExtendedResponse with resultCode=0.
// LDAPMessage SEQUENCE { messageID INTEGER 1, [APPLICATION 24] SEQUENCE { resultCode ENUMERATED 0, matchedDN "", diagnosticMessage "" } }
resp := []byte{
0x30, 0x0c, // SEQUENCE, length 12
0x02, 0x01, 0x01, // messageID = 1
0x78, 0x07, // [APPLICATION 24], length 7
0x0a, 0x01, 0x00, // resultCode ENUMERATED 0
0x04, 0x00, // matchedDN ""
0x04, 0x00, // diagnosticMessage ""
}
_, err := c.Write(resp)
return err
})
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
}
func TestStartTLS_LDAP_Refused(t *testing.T) {
err := runStartTLS(t, starttlsLDAP, "ldap.example.com", func(c net.Conn) error {
req := make([]byte, 31)
if _, err := io.ReadFull(c, req); err != nil {
return err
}
// resultCode = 53 (unwillingToPerform) -> classified as not-offered.
resp := []byte{
0x30, 0x0c,
0x02, 0x01, 0x01,
0x78, 0x07,
0x0a, 0x01, 0x35,
0x04, 0x00,
0x04, 0x00,
}
_, err := c.Write(resp)
return err
})
if !errors.Is(err, errStartTLSNotOffered) {
t.Fatalf("expected errStartTLSNotOffered for resultCode 53, got: %v", err)
}
}