"If you don't monitor it, you can't manage it."
In the last installment of our Tao of Ops series I pointed out the above maxim as being a variation on the business management saying, "You can't manage what you can't measure" (often attributed to Peter Drucker). This has become one of the core principles I try to keep in mind while walking the Operations path.
Keeping this in mind, today I want to tackle testing the TLS Certificates that can be found everywhere in any shop doing web related production - something that needs to be done and can be rather involved in order to do properly.
According to Wikipedia TLS Certificates are:
Transport Layer Security (TLS) and its predecessor, Secure Sockets Layer (SSL), are cryptographic protocols designed to provide communication security over the Internet. They use X.509 certificates and hence asymmetric cryptography to authenticate the counterparty with whom they are communicating, and to exchange a symmetric key.
When it comes to anything that involves security - verifying is never going to be simple, and if it looks simple, then it's time to take a step back and ask yourself what you're missing. Crypto is hard and the code tends to be rather long, so I will be showing snippets of code below taken from kenkou, which is a site checking tool I've written that uses the Python pyOpenSSL library.
With the above definition and warnings fresh in our minds, let's take a look at what's required to make sure that your web site's certificate is valid. For the purposes of today's post we are going to limit the scope to:
- Whether or not all of the certificates in the chain returned are themselves valid
- Is the peer certificate itself not expired
- Does the domain name match the hostname(s) within the certificate
From checkCertificate()
we see the code required to open a socket to the remote site and prepare the context required to establish a secure connection.
# domain = 'example.com'
# config['cafile'] = '/etc/ssl/certs/ca-certificates.crt'
socket.getaddrinfo(domain, 443)[0][4][0]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((domain, 443))
ctx = SSL.Context(SSL.TLSv1_METHOD)
# prevent fallback to insecure SSLv2
ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
pyopenssl_check_callback)
ctx.load_verify_locations(config['cafile'])
ssl_sock = SSL.Connection(ctx, sock)
ssl_sock.set_connect_state()
ssl_sock.set_tlsext_host_name(domain)
ssl_sock.do_handshake()
Note that we are explicitly preventing the use of SSLv2, and we are asking pyOpenSSL to ensure we have a peer certificate. The pyopenssl_check_callback
is used to ensure that any certificates present in the chain are not expired:
def pyopenssl_check_callback(connection, x509, errnum, errdepth, ok):
'''callback for pyopenssl ssl check'''
log.debug('callback: %d %s' % (errdepth, x509.get_issuer().commonName))
if x509.has_expired():
raise CertificateError('Certificate %s has expired!' % x509.get_issuer().commonName)
if not ok:
return False
return ok
Now that we have an SSL context we can take a deeper look at the X.509 peer certificate. The code below calls match_hostname()
to perform a rigorous check per RFC 6125 that the domain requested matches the different possible hostnames that can be found in a certificate and can be found within kenkou.py
x509 = ssl_sock.get_peer_certificate()
try:
match_hostname(x509, domain)
except CertificateError:
print('Hostname does not match')
expire_date = datetime.datetime.strptime(x509.get_notAfter(), "%Y%m%d%H%M%SZ")
expire_td = expire_date - datetime.datetime.now()
if expire_td.days < 15:
print('Expires in %s days' % expire_td.days)
We can now feel confident that our certificate is valid. We can also now be warned/reminded when the certificate will be expiring. The ability to verify allows you be proactive instead of reactive, which is a much better way to walk the Operations path than constantly reacting to issues.
Want more cool stuff like the Tao of Ops? Then sign up for our email list and have more good stuff delivered direct to your inbox.