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 }