Add tlsenum package and add version/cipher enumeration into the checker
tlsenum package probes a remote endpoint with one ClientHello per (version, cipher) pair via utls, so the checker can report the exact set the server accepts rather than only the suite Go's stdlib happens to negotiate. Probe accepts an Upgrader callback so STARTTLS dialects plug in without tlsenum learning about them; the checker bridges its existing dialect registry through upgraderFor.
This commit is contained in:
parent
8a7f9feaf7
commit
a9f37c79cf
18 changed files with 1569 additions and 5 deletions
|
|
@ -132,6 +132,24 @@ func TestStartTLS_IMAP_OK(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_IMAP_Refused(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)
|
||||
_, _ = io.WriteString(c, "* CAPABILITY IMAP4rev1 STARTTLS\r\nA001 OK CAPABILITY completed\r\n")
|
||||
_, _ = readLineCRLF(br)
|
||||
_, err := io.WriteString(c, "A002 NO STARTTLS unavailable\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_NotAdvertised(t *testing.T) {
|
||||
err := runStartTLS(t, starttlsIMAP, "imap.example.com", func(c net.Conn) error {
|
||||
br := bufio.NewReader(c)
|
||||
|
|
@ -185,6 +203,24 @@ func TestStartTLS_POP3_NotAdvertised(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_POP3_Refused(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)
|
||||
_, _ = io.WriteString(c, "+OK capa list\r\nUSER\r\nSTLS\r\n.\r\n")
|
||||
_, _ = readLineCRLF(br)
|
||||
_, err := io.WriteString(c, "-ERR STLS unavailable\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_XMPP_OK(t *testing.T) {
|
||||
err := runStartTLS(t, starttlsXMPPClient, "xmpp.example.com", func(c net.Conn) error {
|
||||
br := bufio.NewReader(c)
|
||||
|
|
@ -225,6 +261,47 @@ func TestStartTLS_XMPP_NotAdvertised(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_XMPP_Refused(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
|
||||
}
|
||||
_, _ = 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'/></stream:features>`)
|
||||
if _, err := br.Read(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.WriteString(c, `<failure xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>`)
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected failure error")
|
||||
}
|
||||
if errors.Is(err, errStartTLSNotOffered) {
|
||||
t.Fatalf("<failure/> should not be classified as not-offered: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_XMPP_StreamError(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:error><host-unknown xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>`)
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected stream:error to surface as error")
|
||||
}
|
||||
}
|
||||
|
||||
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).
|
||||
|
|
@ -250,6 +327,86 @@ func TestStartTLS_LDAP_OK(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_LDAP_WrongTag(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
|
||||
}
|
||||
_, err := c.Write([]byte{0x42, 0x00})
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong tag")
|
||||
}
|
||||
if errors.Is(err, errStartTLSNotOffered) {
|
||||
t.Fatalf("malformed response should not be classified as not-offered: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_LDAP_OversizedLength(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
|
||||
}
|
||||
// SEQUENCE with long-form length = 0x10000 (64 KiB) — beyond our 16 KiB cap.
|
||||
_, err := c.Write([]byte{0x30, 0x83, 0x01, 0x00, 0x00})
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected oversized-length error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_LDAP_TruncatedBody(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
|
||||
}
|
||||
// Announce 12 bytes of body, only send 5 then close.
|
||||
_, err := c.Write([]byte{0x30, 0x0c, 0x02, 0x01, 0x01, 0x78, 0x07})
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error on truncated body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_LDAP_DiagnosticMessageOver4KiB(t *testing.T) {
|
||||
// A real-world response with a verbose diagnosticMessage can exceed the
|
||||
// previous 4 KiB cap. Confirm the bumped 16 KiB cap accepts it.
|
||||
const diagLen = 8000
|
||||
diag := make([]byte, diagLen)
|
||||
for i := range diag {
|
||||
diag[i] = 'x'
|
||||
}
|
||||
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
|
||||
}
|
||||
// Body: messageID(3) + extResp tag(1) + extResp len(3) + resultCode(3) + matchedDN(2) + diag tag+long-len(4) + diag bytes
|
||||
// extResp inner length = resultCode(3) + matchedDN(2) + diagTLV(4+diagLen) = 9 + diagLen
|
||||
extInner := 9 + diagLen
|
||||
// Outer SEQUENCE inner length = messageID(3) + extResp TLV(1+3+extInner)
|
||||
outerInner := 3 + 4 + extInner
|
||||
buf := []byte{0x30, 0x82, byte(outerInner >> 8), byte(outerInner & 0xff)}
|
||||
buf = append(buf, 0x02, 0x01, 0x01) // messageID
|
||||
buf = append(buf, 0x78, 0x82, byte(extInner>>8), byte(extInner&0xff))
|
||||
buf = append(buf, 0x0a, 0x01, 0x00) // resultCode = success
|
||||
buf = append(buf, 0x04, 0x00) // matchedDN ""
|
||||
buf = append(buf, 0x04, 0x82, byte(diagLen>>8), byte(diagLen&0xff))
|
||||
buf = append(buf, diag...)
|
||||
_, err := c.Write(buf)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected success with verbose diagnosticMessage, 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue