Back in 2013 Bart Dopheide submitted a patch to the mailing list to add LDAP StartTLS (elevate connection to TLS after initial connection is initiated) support to the list of supported protocols in protocol.c (https://www.stunnel.org/pipermail/stunnel-users/2013-November/004437.html). It doesn't look like this patch was ever accepted into stunnel.
I've run into a similar requirement and have updated the patch to work against stunnel 5.56. In addition, there are a few other minor changes, the most significant being as follows. It would appear that Windows Active Directory servers do not implement the ldap extended response PDU in the same way as OpenLDAP (see this thread: https://www.openldap.org/lists/openldap-software/200401/msg00800.html). With this patch you can specify either "protocol = winldap" or "protocol = openldap" and have it work either way. I haven't modified the logic of Bart's original patch as far as OpenLDAP goes, but I have split the code path where applicable to handle the Windows case.
I am testing this against a virtual Windows Server 2016 Active Directory instance with self-signed certificates with the following config:
[stunnel.winldap]
client = yes
accept = localhost:3268
connect =mymachine.example.com:3268
protocol = winldap
And the following ldapsearch command:
LDAPTLS_REQCERT=never ldapsearch -Z -h localhost -p 3268 -D "nginx_bind_dn@localdomain.lan" -W -b "dc=localdomain,dc=lan" -s sub "objectClass=person"
The ldapsearch output returns as expected and I can see in wireshark that the LDAP_START_TLS_OID request/response happens as it should.
The patch is at the end of this message.
-SG
diff -Nurp a/src/protocol.c b/src/protocol.c
--- a/src/protocol.c 2019-05-15 13:35:16.000000000 -0600
+++ b/src/protocol.c 2019-12-03 13:54:47.536940900 -0700
@@ -64,6 +64,8 @@ NOEXPORT char *pop3_server(CLI *, SERVIC
NOEXPORT char *imap_client(CLI *, SERVICE_OPTIONS *, const PHASE);
NOEXPORT char *imap_server(CLI *, SERVICE_OPTIONS *, const PHASE);
NOEXPORT char *nntp_client(CLI *, SERVICE_OPTIONS *, const PHASE);
+NOEXPORT char *openldap_client(CLI *, SERVICE_OPTIONS *, const PHASE);
+NOEXPORT char *winldap_client(CLI *, SERVICE_OPTIONS *, const PHASE);
NOEXPORT char *connect_server(CLI *, SERVICE_OPTIONS *, const PHASE);
NOEXPORT char *connect_client(CLI *, SERVICE_OPTIONS *, const PHASE);
#ifndef OPENSSL_NO_MD4
@@ -113,6 +115,14 @@ char *protocol(CLI *c, SERVICE_OPTIONS *
return opt->option.client ?
nntp_client(c, opt, phase) :
"The 'nntp' protocol is not supported in the server mode";
+ if(!strcasecmp(opt->protocol, "openldap"))
+ return opt->option.client ?
+ openldap_client(c, opt, phase) :
+ "The 'openldap' protocol is not supported in the server mode";
+ if(!strcasecmp(opt->protocol, "winldap"))
+ return opt->option.client ?
+ winldap_client(c, opt, phase) :
+ "The 'winldap' protocol is not supported in the server mode";
if(!strcasecmp(opt->protocol, "connect"))
return opt->option.client ?
connect_client(c, opt, phase) :
@@ -1119,6 +1129,182 @@ NOEXPORT char *nntp_client(CLI *c, SERVI
return NULL;
}
+/**************************************** LDAP, RFC 2830 */
+uint8_t ldap_startssl_message[0x1d + 2] =
+{
+ 0x30, /* tag = UNIVERSAL SEQUENCE */
+ 0x1d, /* len = 29 (the remaining number of bytes in this message) */
+ 0x02, /* messageID */
+ 0x01, /* len = 1 */
+ 0x01, /* value = 1 (this is messageID 1) */
+ /* --- */
+ 0x77, /* protocolOp = APPLICATION (23) (=ExtendedRequest)
+ * 0b01xxxxxx => APPLICATION
+ * 0bxx1xxxxx => ?
+ * 0xxxx10111 => 23
+ */
+ 0x18, /* len = 24 */
+ 0x80, /* type = requstName? */
+ 0x16, /* len = 22 */
+ /* OID: 1.3.6.1.4.1.1466.20037 (=LDAP_START_TLS_OID)*/
+ '1', '.',
+ '3', '.',
+ '6', '.',
+ '1', '.',
+ '4', '.',
+ '1', '.',
+ '1', '4', '6', '6', '.',
+ '2', '0', '0', '3', '7'
+ /* No requestValue, as per RFC2830 (in 2.1: "The requestValue field is absent") */
+};
+
+typedef enum {
+ LDAP_OPENLDAP,
+ LDAP_WINLDAP
+} LDAP_MODE;
+
+#define LDAP_UNIVERSAL_SEQUENCE 0x30
+#define LDAP_WINLDAP_FOUR_BYTE_LEN_FLAG 0x84
+#define LDAP_RESPONSE_MSG_ID_TYPE_INT 0x02
+#define LDAP_RESPONSE_EXPECTED_MSG_ID_LEN 0x01
+#define LDAP_RESPONSE_EXPECTED_MSG_ID 0x01
+#define LDAP_RESPONSE_EXT_RESP 0x0a
+#define LDAP_RESPONSE_EXT_RESP_APPLICATION 0x78
+#define LDAP_RESPONSE_EXPECTED_ERR_LEN 0x01
+#define LDAP_RESPONSE_SUCCESS 0x00
+
+NOEXPORT char *ldap_client(CLI *c, SERVICE_OPTIONS *opt, const PHASE phase, const LDAP_MODE ldap_mode) {
+
+ /* thanks to these threads for help with these PDUs
+ https://www.stunnel.org/pipermail/stunnel-users/2013-November/004437.html
+ https://www.openldap.org/lists/openldap-software/200401/msg00800.html */
+
+ uint8_t buffer_8[1];
+ uint32_t buffer_32[1];
+ uint32_t resp_len;
+ uint8_t ldap_response[256];
+ uint8_t *resp_ptr;
+
+ (void)opt; /* squash the unused parameter warning */
+
+ if(phase!=PROTOCOL_MIDDLE)
+ return NULL;
+
+ /* send "Start TLS" request to AD server */
+ s_log(LOG_DEBUG, "Requesting LDAP Start TLS");
+ s_write(c, c->remote_fd.fd, ldap_startssl_message, (size_t)ldap_startssl_message[1] + 2);
+
+ /* LDAP_UNIVERSAL_SEQUENCE (1 byte) */
+ s_read(c, c->remote_fd.fd, buffer_8, 1);
+ if(buffer_8[0] != LDAP_UNIVERSAL_SEQUENCE) {
+ s_log(LOG_ERR, "start tag is not UNIVERSAL SEQUENCE");
+ throw_exception(c, 1);
+ }
+
+ if(ldap_mode == LDAP_OPENLDAP) {
+ /* OpenLDAP - response length (1 byte) */
+ s_log(LOG_DEBUG, "Reading OpenLDAP message size (1 byte)");
+ s_read(c, c->remote_fd.fd, buffer_8, 1);
+ resp_len = buffer_8;
+
+ } else if(ldap_mode == LDAP_WINLDAP) {
+
+ /* WinLDAP - "response length is 4 bytes" flag - LDAP_WINLDAP_FOUR_BYTE_LEN_FLAG (1-byte) */
+ s_read(c, c->remote_fd.fd, buffer_8, 1);
+ if(buffer_8[0] != LDAP_WINLDAP_FOUR_BYTE_LEN_FLAG) {
+ s_log(LOG_ERR, "LDAP message length flag is an unexpected value");
+ throw_exception(c, 1);
+ }
+
+ /* WinLDAP - response length (4 bytes, network byte order) */
+ s_log(LOG_DEBUG, "Reading WinLDAP message size (4 bytes)");
+ s_read(c, c->remote_fd.fd, buffer_32, 4);
+ resp_len = ntohl(buffer_32[0]);
+
+ } else {
+ s_log(LOG_ERR, "Unsupported LDAP mode");
+ throw_exception(c, 1);
+ }
+
+ /* LDAP response message */
+ s_log(LOG_DEBUG, "Reading LDAP message (%u byte(s))", resp_len);
+ s_read(c, c->remote_fd.fd, ldap_response, resp_len);
+
+ resp_ptr = &ldap_response[0];
+
+ /* LDAP_RESPONSE_MSG_ID_TYPE_INT - 1 byte */
+ if(*resp_ptr != LDAP_RESPONSE_MSG_ID_TYPE_INT) {
+ s_log(LOG_ERR, "LDAP response has an incorrect message ID type");
+ throw_exception(c, 1);
+ }
+ resp_ptr++;
+
+ /* LDAP_RESPONSE_EXPECTED_MSG_ID_LEN - 1 byte */
+ if(*resp_ptr != LDAP_RESPONSE_EXPECTED_MSG_ID_LEN) {
+ s_log(LOG_ERR, "LDAP response has an unexpected message ID length");
+ throw_exception(c, 1);
+ }
+ resp_ptr++;
+
+ /* LDAP_RESPONSE_EXPECTED_MSG_ID - 1 byte */
+ if(*resp_ptr != LDAP_RESPONSE_EXPECTED_MSG_ID) {
+ s_log(LOG_ERR, "LDAP response has an unexpected message ID");
+ throw_exception(c, 1);
+ }
+ resp_ptr++;
+
+ /* LDAP_RESPONSE_EXT_RESP_APPLICATION - 1 byte */
+ if(*resp_ptr != LDAP_RESPONSE_EXT_RESP_APPLICATION) {
+ s_log(LOG_ERR, "LDAP response protocolOp is not APPLICATION");
+ throw_exception(c, 1);
+ }
+ resp_ptr++;
+
+ if(ldap_mode == LDAP_WINLDAP) {
+ /* WinLDAP - "response length is 4 bytes" flag - LDAP_WINLDAP_FOUR_BYTE_LEN_FLAG (1-byte) */
+ if(*resp_ptr != LDAP_WINLDAP_FOUR_BYTE_LEN_FLAG) {
+ s_log(LOG_ERR, "LDAP extendedResp length flag is an unexpected value");
+ throw_exception(c, 1);
+ }
+ /* WinLDAP - extended response message length (4-bytes) */
+ resp_ptr += 5;
+
+ } else {
+ /* OpenLDAP - extended response message length (1-byte) */
+ resp_ptr++;
+ }
+
+ /* LDAP_RESPONSE_EXT_RESP - 1 byte */
+ if(*resp_ptr != LDAP_RESPONSE_EXT_RESP) {
+ s_log(LOG_ERR, "LDAP response type is not EXT_RESP");
+ throw_exception(c, 1);
+ }
+ resp_ptr++;
+
+ /* LDAP_RESPONSE_EXT_RESP - 1 byte */
+ if(*resp_ptr != LDAP_RESPONSE_EXPECTED_ERR_LEN) {
+ s_log(LOG_ERR, "LDAP response has an unexpected error code length");
+ throw_exception(c, 1);
+ }
+ resp_ptr++;
+
+ if(*resp_ptr != LDAP_RESPONSE_SUCCESS) {
+ s_log(LOG_ERR, "LDAP response has indicated an error (%u)", *resp_ptr);
+ throw_exception(c, 1);
+ }
+
+ return NULL;
+}
+
+
+NOEXPORT char *openldap_client(CLI *c, SERVICE_OPTIONS *opt, const PHASE phase) {
+ return ldap_client(c, opt, phase, LDAP_OPENLDAP);
+}
+
+NOEXPORT char *winldap_client(CLI *c, SERVICE_OPTIONS *opt, const PHASE phase) {
+ return ldap_client(c, opt, phase, LDAP_WINLDAP);
+}
+
/**************************************** connect */
NOEXPORT char *connect_server(CLI *c, SERVICE_OPTIONS *opt, const PHASE phase) {