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.
145 lines
3.7 KiB
Go
145 lines
3.7 KiB
Go
package checker
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
)
|
|
|
|
func init() {
|
|
registerStartTLS("ldap", starttlsLDAP)
|
|
}
|
|
|
|
// starttlsLDAP implements RFC 2830 StartTLS.
|
|
//
|
|
// It sends a single ExtendedRequest with OID 1.3.6.1.4.1.1466.20037 on
|
|
// messageID=1 and reads back the ExtendedResponse. On resultCode 0 the
|
|
// connection is ready for the TLS handshake. On resultCode 2
|
|
// (protocolError) or 53 (unwillingToPerform) we wrap errStartTLSNotOffered
|
|
// -- the server is reachable but cannot upgrade -- so the caller can surface
|
|
// that as a missing-STARTTLS issue rather than a handshake failure.
|
|
func starttlsLDAP(conn net.Conn, sni string) error {
|
|
// Fixed LDAPMessage:
|
|
// SEQUENCE {
|
|
// INTEGER messageID = 1,
|
|
// [APPLICATION 23] SEQUENCE {
|
|
// [0] OCTET STRING "1.3.6.1.4.1.1466.20037"
|
|
// }
|
|
// }
|
|
request := []byte{
|
|
0x30, 0x1d,
|
|
0x02, 0x01, 0x01,
|
|
0x77, 0x18,
|
|
0x80, 0x16,
|
|
'1', '.', '3', '.', '6', '.', '1', '.', '4', '.', '1', '.',
|
|
'1', '4', '6', '6', '.', '2', '0', '0', '3', '7',
|
|
}
|
|
if _, err := conn.Write(request); err != nil {
|
|
return fmt.Errorf("write StartTLS request: %w", err)
|
|
}
|
|
|
|
r := bufio.NewReader(conn)
|
|
|
|
tag, err := r.ReadByte()
|
|
if err != nil {
|
|
return fmt.Errorf("read response: %w", err)
|
|
}
|
|
if tag != 0x30 {
|
|
return fmt.Errorf("unexpected LDAP response tag 0x%02x", tag)
|
|
}
|
|
length, err := readBERLength(r)
|
|
if err != nil {
|
|
return fmt.Errorf("read response length: %w", err)
|
|
}
|
|
// 16 KiB comfortably accommodates an ExtendedResponse with a verbose
|
|
// diagnosticMessage while still bounding memory against a hostile peer.
|
|
const maxLDAPResponseBytes = 16 * 1024
|
|
if length <= 0 || length > maxLDAPResponseBytes {
|
|
return fmt.Errorf("unreasonable LDAP response length %d", length)
|
|
}
|
|
body := make([]byte, length)
|
|
if _, err := io.ReadFull(r, body); err != nil {
|
|
return fmt.Errorf("read response body: %w", err)
|
|
}
|
|
|
|
// messageID INTEGER -- skip.
|
|
pos := 0
|
|
if pos >= len(body) || body[pos] != 0x02 {
|
|
return fmt.Errorf("expected INTEGER messageID")
|
|
}
|
|
pos++
|
|
if pos >= len(body) {
|
|
return fmt.Errorf("truncated messageID length")
|
|
}
|
|
msgIDLen := int(body[pos])
|
|
pos++
|
|
pos += msgIDLen
|
|
if pos >= len(body) {
|
|
return fmt.Errorf("truncated LDAP response")
|
|
}
|
|
|
|
// [APPLICATION 24] constructed = 0x78.
|
|
if body[pos] != 0x78 {
|
|
return fmt.Errorf("expected ExtendedResponse tag, got 0x%02x", body[pos])
|
|
}
|
|
pos++
|
|
// Skip extendedResp length (possibly multi-byte).
|
|
if pos >= len(body) {
|
|
return fmt.Errorf("truncated ExtendedResponse length")
|
|
}
|
|
if body[pos] < 0x80 {
|
|
pos++
|
|
} else {
|
|
n := int(body[pos] & 0x7f)
|
|
pos += 1 + n
|
|
}
|
|
|
|
// resultCode ENUMERATED (tag 0x0a).
|
|
if pos+2 > len(body) || body[pos] != 0x0a {
|
|
return fmt.Errorf("expected resultCode ENUMERATED")
|
|
}
|
|
rcLen := int(body[pos+1])
|
|
if rcLen < 1 || pos+2+rcLen > len(body) {
|
|
return fmt.Errorf("invalid resultCode length %d", rcLen)
|
|
}
|
|
rc := 0
|
|
for i := 0; i < rcLen; i++ {
|
|
rc = (rc << 8) | int(body[pos+2+i])
|
|
}
|
|
switch rc {
|
|
case 0:
|
|
return nil
|
|
case 2, 53:
|
|
return fmt.Errorf("%w: LDAP StartTLS refused (resultCode=%d)", errStartTLSNotOffered, rc)
|
|
default:
|
|
return fmt.Errorf("server refused StartTLS (LDAP resultCode=%d)", rc)
|
|
}
|
|
}
|
|
|
|
// readBERLength reads a definite-form BER length (short or long form).
|
|
func readBERLength(r *bufio.Reader) (int, error) {
|
|
b, err := r.ReadByte()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if b < 0x80 {
|
|
return int(b), nil
|
|
}
|
|
n := int(b & 0x7f)
|
|
if n == 0 {
|
|
return 0, fmt.Errorf("indefinite-form length not supported")
|
|
}
|
|
if n > 4 {
|
|
return 0, fmt.Errorf("length octet count %d too large", n)
|
|
}
|
|
length := 0
|
|
for i := 0; i < n; i++ {
|
|
bb, err := r.ReadByte()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
length = (length << 8) | int(bb)
|
|
}
|
|
return length, nil
|
|
}
|