Wednesday, November 10, 2010

Validating SSL server certificate with Python 2.x

SSL stands for Secure Sockets Layer and is designed to create secure connection between client and server. Secure means that connection is encrypted and therefore protected from eavesdropping. It also allows to validate site identity when connecting with HTTPS protocol.


However, there is a bug in ssl module from standard library of Python 2.x, that allows successful MITM attack using valid certificate from other site. Basically, module checks when connecting that server certificate is valid and correctly signed by root certificate, but it does not check that certificate actually belongs to the site, i.e. that site name matches the name specified in certificate.


It is still possible to validate server identity in Python 2.6 manually. Let's start with illustration of the vulnerability. The following snippet should fail - it replaces HOST "www.google.com" to connect to with its IP address. If you try to use this IP in Chrome like https://74.125.232.50 - it will show an error, but ssl library will not throw exception.

import socket
import ssl

HOST = "www.google.com"
PORT = 443

# replace HOST name with IP, this should fail connection attempt,
# but it doesn't in Python 2.x
HOST = socket.getaddrinfo(HOST, PORT)[0][4][0]
print(HOST)

# create socket and connect to server
# server address is specified later in connect() method
sock = socket.socket()
sock.connect((HOST, PORT))

# wrap socket to add SSL support
sock = ssl.wrap_socket(sock,
# flag that certificate from the other side of connection is
# required and should be validated when wrapping
cert_reqs=ssl.CERT_REQUIRED,
# file with root certificates
ca_certs="cacert.pem"
)



You will need cacert.pem file with root certificates. Just grab the latest version from http://curl.haxx.se/ca/cacert.pem This code above won't give you any error. Replace HOST value with https://www.debian-administration.org/ to check that certificate validation actually works. This site's certificate is not signed by any root certificates from "cacerts.txt", so you get an error.

To validate that a certificate matches requested site, you need to check commonName field in the subject of the certificate. This information can be accessed with getpeercert() method of wrapped socket.


import socket
import ssl

HOST = "www.google.com"
PORT = 443

# replace HOST name with IP, this should fail connection attempt
HOST = socket.getaddrinfo(HOST, PORT)[0][4][0]
print(HOST)

# create socket and connect to server
# server address is specified later in connect() method
sock = socket.socket()
sock.connect((HOST, PORT))

# wrap socket to add SSL support
sock = ssl.wrap_socket(sock,
# flag that certificate from the other side of connection is
# required and should be validated when wrapping
cert_reqs=ssl.CERT_REQUIRED,
# file with root certificates
ca_certs="cacerts.txt"
)

# manual check of hostname
cert = sock.getpeercert()
for field in cert['subject']:
if field[0][0] == 'commonName':
certhost = field[0][1]
if certhost != HOST:
raise ssl.SSLError(
"Host name '%s' doesn't match certificate host '%s'"
% (HOST, certhost))


That's it. I put my findings to http://wiki.python.org/moin/SSL - you may want check it for updates.