142 lines
3.5 KiB
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
|
|
}
|