Validating a SSL certificate in Python
A way to perform SSL certificate validation in Python using PyOpenSSL and is multiplatform.
I'm working in porting the rabbit-vs to Python 3 while documenting it in an appropriate manner and doing quite a lot of code refactoring. Right now I'm in the stage of porting the plugins and I decided to take a look again at the techniques used in them.
In the previous version of the SSL certificate validation plugin I used to use M2Crypto library but there's no port to Py3k of that. So I had to look for another technique, after reading a while I finally decided to use PyOpenSSL.
What are the advantages of using PyOpenSSL?
- Works in Python 3
- Works in Linux and Windows
- Based on OpenSSL which is present almost in every system.
What is it going to be checked?
Basically what is going to be checked is whether the certificate's signature is valid, the correctness of its format, if it's valid in time, if it is for the server we are accessing and, optionally, if the certificate is trusted. Other things to be checked are the size of its public key and if the signature algorithm used is strong enough.
Procedure
Creating the SSL Context.
First of all, it is necessary to create an SSL Context, the context is the object that will let us create the SSL Layer on top of a socket in order to get an SSL Connection. The purpose of this context is to indicate the type of SSL we want the connection to be, the verification mode that is going to be used and where to look for the root certificates in case we want to check the trustworthiness of the certificate.
The code to create a SSL.Context
object is:
from OpenSSL import SSL
context = SSL.Context(SSL.TLSv1_METHOD) # Use TLS Method
context.set_options(SSL.OP_NO_SSLv2) # Don't accept SSLv2
context.set_verify(SSL.VERIFY_NONE, callback)
context.load_verify_locations(ca_file, ca_path)
In the first line we create the object, in that moment we have to indicate which version of SSL the Context
will handle. In this case I want to use TLSv1.
After that we set the option OP_NO_SSLv2
, this is in order to not establish SSLv2 connections, which are really insecure.
The third line of code sets the verification mode and the callback function to call when verifying, I'll go deeper into this afterwards.
The last line of code sets two things that are fundamental if we want to validate if a certificate is trustworthy or not. The first parameter is the location of a file whose content must be a list of trusted/root certificates encoded in PEM and the second parameter is the path to a folder that contains trusted/root certificates. The ones that are loaded from there are the ones that are going to be used when checking the certificate's trustworthiness.
Creating an SSL Connection
This basically consists of creating a socket and wrapping it with an SSL Context. In that way we create an SSL Connection which can connect to SSL services and do the corresponding handshake.
The following is the Python code to do that:
from socket import socket
sock = socket()
ssl_sock = SSL.Connection(context, sock)
ssl_sock.connect((ip_addr, port))
ssl_sock.do_handshake()
Verification routine
When the do_handshake()
method is called, the SSL initialization is executed and if the verification method is set (using the set_verify()
method) it is performed. The callback function will get called for each of the certificates in the certificate chain that is being validated, it receives five arguments:
SSL.Connection
object that triggered the verification.OpenSSL.crypto.X509
the certificate being validated.- An integer containing the error number (0 in case no error) of the error detected. You can find their meaning in the OpenSSL documentation.
- An integer indicating the depth of the certificate being validated. If it is 0 then it means it is the given certificate is the one being validated, in other case is one of the chain of certificates.
- An integer that indicates whether the validation of the certificate currently being validated (the one in the second argument) passed or not the validation. A value of 1 is a successful validation and 0 an unsuccessful one.
The callback function must return a boolean value indicating the result of the verification, it must return True for a successful verification and False otherwise.
In this callback function you can do as you want. In the rabbit's plugin case I decided to take into account some of the errors, I could ignore trust errors when they are not needed and I decided to raise an Exception when a certificate was not valid.
For example, if one is only interested in checking whether the certificate at depth 0 is time valid and no other error is contemplated a possible callback function would be:
def callback_function(conn, cert, errno, depth, result):
if depth == 0 and (errno == 9 or errno == 10):
return False # or raise Exception("Certificate not yet valid or expired")
return True
The behavior of what happens if a callback functions returns False depends on the verification method set: if SSL.VERIFY_NONE
was used then the verification chain is not followed but if SSL.VERIFY_PEER
was used then a callback function returning False will raise an OpenSSL.SSL.Error
exception.
Hashing algorithm used to sign the certificate and public key size
To access the information of the certificate first we need to get it. In PyOpenSSL certificates are modeled as OpenSSL.crypto.X509
objects. To grab the certificate from a connection all it has to be done is call the get_peer_certificate()
method of the SSL.Connection
object.
Once we have the certificate object we can retrieve its public key (OpenSSL.crypto.PKey
object) using the get_pubkey()
method and its size by calling the bits()
method on the returned object.
To retrieve the hashing algorithm used, the method to call is get_signature_algorithm()
on the certificate object.
Verifying the host matches the common name on the certificate
The first thing to do is to get the common name from the certificate. This information is located inside a X509Name
object corresponding to the subject of the certificate. This object is obtained using the get_subject()
method on the certificate we are analyzing. Once the X509Name
object has been obtained the commonName
attribute can be accessed to obtain the common name from the certificate.
The next step is to convert that common name to a regex, why is this necessary? Because a certificate can be issued for a whole domain or subdomain. For example a certificate issued for *.xxx.com
is valid for www.xxx.com or mail.xxx.com.
To do that we need to replace the dots for escaped dots and after that the wildcard for a wildcard in regex, which is the combination of the dot and the asterisk.
Once the regex is prepared then what has to be checked is whether the host name being tested matches the regex.
In code:
import re
cert = ssl_sock.get_peer_certificate()
common_name = cert.get_subject().commonName.decode()
regex = common_name.replace('.', r'\.').replace('*',r'.*') + '$'
if re.matches(regex, host_name):
#matches
pass
else:
#invalid
pass
Comments powered by Talkyard.