This article will demonstrate how to use Python to get an SSL Certificate from a server. The server may be a remote web server, sip server, or any other type of server supporting SSL or TLS protected by a certificate. The server may even be a local server on the same machine as you are working from, but serving its own SSL certificate.
Python convert pem to X509
Before we begin getting the SSL certificate from the server let us write a helper method to convert a pem encoded certificate to a python X509 object. Here is the code to do so.
@staticmethod
def pem_to_x509(cert_data: str):
"""Converts a given pem encoded certificate to X509 object
@param cert_data: str pem encoded certificate data that includes the header and footer
@return: X509 object
"""
return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, str.encode(cert_data))
Note that this method is part of a larger Python class included at the bottom of this article. This will help make sense of the static method and comments.
The pyOpenSSL module is used here to convert the String pem encoded certificate data into a python X509 object. (https://cryptography.io/en/latest/x509/index.html)
Python get server certificate
The following python method will fetch a server certificate for a given DNS name and port.
def fetch_server_certificate(self, dns_name: str, port: int):
"""Fetch the server certificate from the given dns_name and port
@param dns_name: The dns name to fetch the certificate for
@param port: The port that is serving the certificate
@return: X509 certificate object
"""
pem_server_certificate = ssl.get_server_certificate((dns_name, port))
x509_server_certificate = self.pem_to_x509(pem_server_certificate)
return x509_server_certificate
This method uses the python ssl library to fetch the certificate being served by the given DNS name and port. The ssl.get_server_certificate
method returns a String pem encoded certificate. The method passes this pem encoded certificate to the previous function we wrote to convert it into a python X509 object that we can then extract the certificate data from, which will be covered in more detail below.
Python get server certificate SNI
The example above will fetch a server certificate but if SNI is enabled on the server, it very well may fetch the wrong one. For example, the microk8s NGINX Ingress controller will return the Kubernetes Ingress Controller Fake Certificate if using the ssl.get_server_certificate
method because it is using SNI. Instead of returning the fake certificate you will want to return the publicly trusted SSL certificate the same as your browser would.
See our article on openssl s_client to learn more about getting certificates from servers with SNI enabled.
To avoid the potential issues caused by SNI we recommend the following method instead which will utilize the python SSLContext and socket.
def fetch_server_certificate_sni(self, dns_name: str, port: int):
"""Fetch the server certificate from the given dns_name and port
This implementation supports SNI. Compare to ssl.get_server_certificate() which will return an incorrect
cert if SNI is enabled on the server
@param dns_name: The dns name to fetch the certificate for
@param port: The port that is serving the certificate
@return: X509 certificate object
"""
connection = ssl.create_connection((dns_name, port))
context = ssl.SSLContext()
sock = context.wrap_socket(connection, server_hostname=dns_name)
server_certificate = self.pem_to_x509(ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)))
sock.close()
return server_certificate
In English, the code performs the following steps:
- Create an SSL connection to the given DNS name and port.
- Create an SSLContext
- Create an SSLSocket from the connection and context created in steps 1 and 2
- Get the certificate from the socket and convert it to an X509 object
- Close the socket
- Return the certificate
Python SSL and X509 Utils class
Here is the class code for the above methods.
import OpenSSL
import ssl
class SSLUtils:
@staticmethod
def pem_to_x509(cert_data: str):
"""Converts a given pem encoded certificate to X509 object
@param cert_data: str pem encoded certificate data that includes the header and footer
@return: X509 object
"""
return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, str.encode(cert_data))
def fetch_server_certificate(self, dns_name: str, port: int):
"""Fetch the server certificate from the given dns_name and port
@param dns_name: The dns name to fetch the certificate for
@param port: The port that is serving the certificate
@return: X509 certificate object
"""
pem_server_certificate = ssl.get_server_certificate((dns_name, port))
x509_server_certificate = self.pem_to_x509(pem_server_certificate)
return x509_server_certificate
def fetch_server_certificate_sni(self, dns_name: str, port: int):
"""Fetch the server certificate from the given dns_name and port
This implementation supports SNI. Compare to ssl.get_server_certificate() which will return an incorrect
cert if SNI is enabled on the server
@param dns_name: The dns name to fetch the certificate for
@param port: The port that is serving the certificate
@return: X509 certificate object
"""
connection = ssl.create_connection((dns_name, port))
context = ssl.SSLContext()
sock = context.wrap_socket(connection, server_hostname=dns_name)
server_certificate = self.pem_to_x509(ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)))
sock.close()
return server_certificate
sslUtils = SSLUtils()
print(sslUtils.fetch_server_certificate("example.com", 443).get_subject())
print(sslUtils.fetch_server_certificate_sni("example.com", 443).get_subject())
Note that the output from running the file prints the same server certificate subject using the two different methods.
<X509Name object '/C=US/ST=California/L=Los Angeles/O=Internet\xC2\xA0Corporation\xC2\xA0for\xC2\xA0Assigned\xC2\xA0Names\xC2\xA0and\xC2\xA0Numbers/CN=www.example.org'>
<X509Name object '/C=US/ST=California/L=Los Angeles/O=Internet\xC2\xA0Corporation\xC2\xA0for\xC2\xA0Assigned\xC2\xA0Names\xC2\xA0and\xC2\xA0Numbers/CN=www.example.org'>
Python X509 Certificate object
The reason we converted the pem encoded certificate in the previous examples to the python X509 object was for convenience of use. A pem encoded certificate is just text in a file. If that is all you need then there’s no reason to convert it but in many cases it is best to go ahead and convert it so that it can be parsed for the data it contains. The examples below will demonstrate how to parse the X509 certificate object.
Each of the following examples will assume you already have the X509 Certificate Object created in the variable named certificate
.
Python get certificate fingerprint
To get the certificate fingerprint you must use the digest method, passing in the algorithm for the fingerprint. The example below prints the sha256 and sha1 fingerprints.
print(f"SHA256: {certificate.digest('sha256').decode()}")
print(f"SHA1: {certificate.digest('sha1').decode()}")
Python get certificate serial number
To get the serial number from the x509 certificate:
print(f"Serial Number: {certificate.get_serial_number()}")
Python get certificate public key
To get the public key from the certificate:
print(f"Public Key: {certificate.get_pubkey()}")
Python get validity period
The validity period consists of the not before and not after dates. Not after is also known as the expiration date.
To get the not before date of the certificate:
print(f"Not Before: {certificate.get_notBefore().decode()}")
To get the not after or expiration date of the certificate:
print(f"Not After: {certificate.get_notAfter().decode()}")
Python get issuer
To get the issuer of the certificate:
print(f"Issuer: {certificate.get_issuer()}")
Python get subject
To get the subject or distinguished name of the certificate:
print(f"Subject: {certificate.get_subject()}")
Python get signature hash algorithm
To get the signature algorithm of the certificate:
print(f"Signature Algorithm: {certificate.get_signature_algorithm().decode()}")
Python get certificate extensions
X.509 certificates contain many extensions. To get all of the extensions:
for i in range(0, certificate.get_extension_count()):
ext = certificate.get_extension(i)
print(f"{ext.get_short_name().decode()}: {ext.get_data()}")
For example, subject alternative names are an extension. To get the sans of a certificate with python:
for i in range(0, certificate.get_extension_count()):
ext = certificate.get_extension(i)
if 'subjectAltName' in str(ext.get_short_name()):
print(ext.get_data())
Conclusion
This article has demonstrated how to get an SSL Certificate from a server. Two ways, one without SNI and one with SNI. After getting the certificate we have demonstrated how convert the pem encoded certificate to an X.509 Certificate Object and then how to parse the X.509 Certificate object to get the certificate data. Let us know in the comments if you have any questions or would like to see additional examples about using SSL Certificate in the python programming language.
Leave a Reply