9

I am trying to get the server certificate of badssl.com subdomains (ex. https://expired.badssl.com).

import ssl
ssl.get_server_certificate(('expired.badssl.com', 443))

But when examining the above generated certificate I see that the certificate has

Identity: badssl-fallback-unknown-subdomain-or-no-sni

which means SNI is failing. How can I get the server certificate of different subdomains of badssl.com? (I am using python 2.7.12)

vishal
  • 1,081
  • 2
  • 10
  • 27

2 Answers2

20

Found the answer.

import ssl
hostname = "expired.badssl.com"
port = 443
conn = ssl.create_connection((hostname, port))
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
sock = context.wrap_socket(conn, server_hostname=hostname)
certificate = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
vishal
  • 1,081
  • 2
  • 10
  • 27
20

Searching for "Python ssl.get_server_certificate SNI" brought me easily to this answer. Although the OP himself answer is correct, I would like to provide a little more insight for future reference.

With some [hostname]s the fallowing call using Python 3.7:

ssl.get_server_certificate(("example.com", 443)

will complain with a traceback that ends with:

ssl.SSLError: [SSL: TLSV1_ALERT_INTERNAL_ERROR] tlsv1 alert internal error (_ssl.c:1045)

Doing some further investigation, making use of the openssl s_client utility, allows to discover that those same [hostname]s which made get_server_certificate to fail, also makes the fallowing command:

openssl s_client -showcerts -connect example.com:443

to fail with this error:

SSL23_GET_SERVER_HELLO:tlsv1 alert internal error:s23_clnt.c:802

Note that the error message is similar to the one returned by the python code.

Using the -servername switch did the trick:

openssl s_client -showcerts -connect example.com:443 -servername example.com

leading to the conclusion that the investigated hostname refers to a secure server that makes use of SNI (a good explanation on what that means is given by the SNI Wikipedia article).

So, switching again to Python and looking at the get_server_certificate method, examining the ssl module source (here for convenience), you can discover that the function includes this call:

context.wrap_socket(sock)

without the server_hostname=hostname key argument, which of course should mean that get_server_certificate cannot be used querying a SNI server. A little more effort is required:

hostname = "example.com"
port = 443

context = ssl.create_default_context()

with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as sslsock:

        der_cert = sslsock.getpeercert(True)

        # from binary DER format to PEM
        pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)          
        print(pem_cert)
wiredolphin
  • 1,431
  • 1
  • 18
  • 26
  • 3
    Thanks, works for me in Python 3.6. Extra thanks for the explanation! – Marian Oct 03 '18 at 18:43
  • 1
    It appears that, as of 2023, `get_server_certificate` [DOES include](https://github.com/python/cpython/blob/90f1d777177e28b6c7b8d9ba751550e373d61b0a/Lib/ssl.py#L1436) the `server_hostname` argument – Ben Mar 10 '23 at 18:29