How can I retrieve OpenLDAP server's STARTTLS certificate with Python's ssl library?

116 Views Asked by At

My goal is to retrieve the TLS certificates from online services and calculate the amount of seconds until they expire using Python. I have solved this for HTTPS:

import socket
import ssl
from datetime import datetime

from pytz as pytz
from asn1crypto.x509 import Certificate

def https_ttl(host, port):
    context = ssl.create_deafult_context()
    # we just want to check the certificate expiry date, we do not need to validate the chain of trust
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    with socket.create_connection((host, port)) as tcp_socket:
        with context.wrap_socket(tcp_socket, server_hostname=host) as ssl_socket:
            # getpeercert(binary_form=False) returns empty dict if verification is disabled
            cert = Certificate.load(ssl_socket.getpeercert(binary_form=True))

            return (cert.not_valid_after - datetime.now(pytz.utc)).total_seconds()

When I use https_ttl() to retrieve the certificate from an OpenLDAP server configured for STARTTLS, I receive this error message:

my_code.py:21: in https_ttl
    with self._context.wrap_socket(tcp_socket, server_hostname=host) as ssl_socket:
../../.pyenv/versions/3.8.12/lib/python3.8/ssl.py:500: in wrap_socket
    return self.sslsocket_class._create(
../../.pyenv/versions/3.8.12/lib/python3.8/ssl.py:1040: in _create
    self.do_handshake()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <ssl.SSLSocket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
block = False

    @_sslcopydoc
    def do_handshake(self, block=False):
        self._check_connected()
        timeout = self.gettimeout()
        try:
            if timeout == 0.0 and block:
                self.settimeout(None)
>           self._sslobj.do_handshake()
E           ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:1131)

../../.pyenv/versions/3.8.12/lib/python3.8/ssl.py:1309: SSLEOFError

However, I can retrieve the certificate using openssl directly:

$ echo | openssl s_client -connect localhost:8389 -starttls ldap 2> /dev/null | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p'
-----BEGIN CERTIFICATE-----
MIIXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXBgNV
BAMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXMDAx
WhcXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZW1h
bHlXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9hHH
HR2XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX3VYt
yyPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1ogL
8UzXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXunIo
SGVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXQO/u
PwCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXdt9F
qcXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8XKs
wHiXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXQIsu
AtEXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXz0U
ct8XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXJ3zc
ddcXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXYWM9
UB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXFJ+b
XCRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXK7yw
ZrUXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXqJi3
L6fXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX133L
WQLXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXkMas
YuWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXcH3a
7rdXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXDt0B
bdDXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXydm7
f6FXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXvgXA
PNzXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXTiN/
+J7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXsbC
BNGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXqEjB
2x5XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPwVF
ffBXXXXXXXXXXXXXXXXXXXXXX3pm
-----END CERTIFICATE-----

How can I replicate the openssl invocation using Python's ssl library?

1

There are 1 best solutions below

0
oschlueter On

Here is my solution based on @user207421's comment:

import socket
import ssl
from datetime import datetime

from pytz as pytz
from asn1crypto.x509 import Certificate

LDAP_START_TLS_SUCCESS = 0
LDAP_START_TLS_RESP_LENGTH = 14

def ldap_starttls_ttl(host, port):
    """Initiates STARTTLS handshake with OpenLDAP server and returns the certificate's time to live (TTL) in seconds."""
    context = ssl.create_deafult_context()
    # we just want to check the certificate expiry date, we do not need to validate the chain of trust
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    with socket.create_connection((host, port)) as tcp_socket:
        # send LDAP_START_TLS_OID (extracted from Wireshark)
        tcp_socket.send(
            b"\x30\x1d\x02\x01\x01\x77\x18\x80\x16\x31\x2e\x33\x2e\x36\x2e\x31"
            b"\x2e\x34\x2e\x31\x2e\x31\x34\x36\x36\x2e\x32\x30\x30\x33\x37"
        )

        extended_resp = tcp_socket.recv(LDAP_START_TLS_RESP_LENGTH)
        result_code = extended_resp[9]
        if result_code != LDAP_START_TLS_SUCCESS:
            raise ValueError(f'resultCode of LDAP_START_TLS_OID response is {result_code} instead of '
                             f'{LDAP_START_TLS_SUCCESS}')

        with context.wrap_socket(tcp_socket, server_hostname=host) as ssl_socket:
            # getpeercert(binary_form=False) returns empty dict if verification is disabled
            cert = Certificate.load(ssl_socket.getpeercert(binary_form=True))

            return (cert.not_valid_after - datetime.now(pytz.utc)).total_seconds()