checker-tls/checker/starttls_ldap.go

142 lines
3.5 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)
}
if length <= 0 || length > 4096 {
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
}