2025, Dec 26 03:00
Diagnosing Python TLS handshake failures: why SSLSocket.getpeercert() and version() return nothing
Learn why Python's ssl won't expose TLS version or peer certificates after a failed handshake, and what errors like WRONG_VERSION_NUMBER or UNKNOWN_CA mean.
When a TLS handshake dies before it starts, you often want to see what the client actually sent: the TLS version it tried, and anything you can glean from the peer certificate. In Python’s ssl stack that sounds straightforward with SSLSocket.getpeercert() and SSLSocket.version(). The catch is that these APIs only work after a successful handshake, so a failure like WRONG_VERSION_NUMBER or UNKNOWN_CA leaves you blind.
Reproducing the dead end in Python
The following snippet shows the typical sequence developers attempt: wrap a socket, try to read TLS metadata, then handle a failed handshake. The point here is the ordering and the resulting errors when you query the socket before the handshake finishes.
import socket
import ssl
def probe_tls_metadata(host, port):
base_sock = socket.create_connection((host, port))
ctx_client = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# Do not auto-handshake so we can try to query state first
tls_sock = ctx_client.wrap_socket(base_sock, server_hostname=host, do_handshake_on_connect=False)
try:
# This will fail if the handshake hasn't completed
print("peer cert:", tls_sock.getpeercert())
except Exception as exc:
print("getpeercert before handshake:", repr(exc))
try:
# This also requires a finished handshake
print("tls version:", tls_sock.version())
except Exception as exc:
print("version before handshake:", repr(exc))
try:
tls_sock.do_handshake()
except ssl.SSLError as err:
print("handshake failed:", err)
# Even after a failed handshake, the peer details remain unavailable
try:
print("peer cert after failure:", tls_sock.getpeercert())
except Exception as exc:
print("getpeercert after failure:", repr(exc))
tls_sock.close()
# Example usage (replace with your endpoint)
# probe_tls_metadata("localhost", 443)
The important observation is that querying getpeercert() or version() when the handshake hasn’t completed throws, and that does not change after a failed handshake.
Why the APIs refuse to help during a failed handshake
This behavior is baked into CPython’s ssl module. The Python-level SSLSocket.getpeercert() delegates to a C implementation that explicitly rejects calls when the TLS state machine has not reached the “handshake finished” point. In other words, no completed handshake, no peer data.
static PyObject* _ssl__Sock_peer_info(PySSLSocket *holder, int raw_flag)
{
int verify_state;
X509 *peer_obj;
PyObject *result_obj;
if (!SSL_is_init_finished(holder->ssl)) {
PyErr_SetString(PyExc_ValueError, "handshake not done yet");
return NULL;
}
/* ... rest of implementation omitted ... */
}
The guard is SSL_is_init_finished(...). If that check fails, Python raises a ValueError with the message “handshake not done yet” and returns no data. That’s why even with do_handshake_on_connect=False you cannot peek at certificate fields or the negotiated protocol version when the handshake fails.
So, can you get TLS handshake details on failure?
Not via Python’s ssl public API. Extracting certificate fields or the negotiated TLS version from a failed handshake is not supported without modifying CPython. The same limitation applies when using pyOpenSSL after consulting its documentation.
Why this matters
When diagnosing errors such as WRONG_VERSION_NUMBER or UNKNOWN_CA, it’s tempting to interrogate the SSLSocket for peer certificate attributes or the attempted protocol version right at the failure point. Python’s design prevents that, so the usual inspection hooks won’t surface the failing parameters. Knowing this saves time and steers the investigation away from API calls that cannot work in this state.
Practical takeaways
If you hit handshake errors while using ssl.SSLContext.wrap_socket and want visibility into the TLS transaction, you won’t be able to extract certificate fields or TLS version from the SSLSocket on failure. Plan your diagnostics accordingly and log the full exception details. If you absolutely must access handshake internals at that stage, the path forward involves changes at the CPython layer; pyOpenSSL does not provide a workaround.
For better debugging outcomes, ensure your repro is minimal and the full error information is captured end to end. That way, you can focus on the actual failure signal instead of chasing unavailable handshake metadata.