87 votes

Valider les certificats SSL avec Python

J'ai besoin d'écrire un script qui se connecte à un groupe de sites sur notre intranet d'entreprise via HTTPS et vérifie que leurs certificats SSL sont valides ; qu'ils ne sont pas expirés, qu'ils sont émis pour la bonne adresse, etc. Nous utilisons notre propre autorité de certification interne pour ces sites, et nous disposons donc de la clé publique de l'autorité de certification pour vérifier les certificats.

Par défaut, Python accepte et utilise les certificats SSL lors de l'utilisation de HTTPS. Ainsi, même si un certificat n'est pas valide, les bibliothèques Python telles que urllib2 et Twisted utiliseront volontiers le certificat.

Existe-t-il une bonne bibliothèque quelque part qui me permette de me connecter à un site via HTTPS et de vérifier son certificat de cette manière ?

Comment vérifier un certificat en Python ?

31voto

Brandon Rhodes Points 21188

J'ai ajouté une distribution à l'index des paquets Python qui fait de la match_hostname() de la fonction Python 3.2 ssl disponible sur les versions précédentes de Python.

http://pypi.python.org/pypi/backports.ssl_match_hostname/

Vous pouvez l'installer avec :

pip install backports.ssl_match_hostname

Ou bien vous pouvez en faire une dépendance listée dans le fichier setup.py . Dans tous les cas, on peut l'utiliser comme ceci :

from backports.ssl_match_hostname import match_hostname, CertificateError
...
sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv3,
                      cert_reqs=ssl.CERT_REQUIRED, ca_certs=...)
try:
    match_hostname(sslsock.getpeercert(), hostname)
except CertificateError, ce:
    ...

26voto

Glyph Points 17756

Vous pouvez utiliser Twisted pour vérifier les certificats. L'API principale est CertificateOptions qui peut être fourni en tant que contextFactory à diverses fonctions telles que listenSSL et startTLS .

Malheureusement, ni Python ni Twisted ne sont livrés avec la pile de certificats d'autorité de certification nécessaires pour effectuer la validation HTTPS, ni avec la logique de validation HTTPS. En raison de une limitation dans PyOpenSSL Vous ne pouvez pas encore le faire correctement, mais grâce au fait que presque tous les certificats comprennent un nom commun d'objet, vous pouvez vous en approcher.

Voici un exemple d'implémentation naïve d'un client Twisted HTTPS vérifiant qui ignore les caractères génériques et les extensions subjectAltName, et utilise les certificats d'autorité présents dans le paquet 'ca-certificates' de la plupart des distributions Ubuntu. Essayez-le avec vos sites de certificats valides et invalides préférés :).

import os
import glob
from OpenSSL.SSL import Context, TLSv1_METHOD, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, OP_NO_SSLv2
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
from twisted.python.urlpath import URLPath
from twisted.internet.ssl import ContextFactory
from twisted.internet import reactor
from twisted.web.client import getPage
certificateAuthorityMap = {}
for certFileName in glob.glob("/etc/ssl/certs/*.pem"):
    # There might be some dead symlinks in there, so let's make sure it's real.
    if os.path.exists(certFileName):
        data = open(certFileName).read()
        x509 = load_certificate(FILETYPE_PEM, data)
        digest = x509.digest('sha1')
        # Now, de-duplicate in case the same cert has multiple names.
        certificateAuthorityMap[digest] = x509
class HTTPSVerifyingContextFactory(ContextFactory):
    def __init__(self, hostname):
        self.hostname = hostname
    isClient = True
    def getContext(self):
        ctx = Context(TLSv1_METHOD)
        store = ctx.get_cert_store()
        for value in certificateAuthorityMap.values():
            store.add_cert(value)
        ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
        ctx.set_options(OP_NO_SSLv2)
        return ctx
    def verifyHostname(self, connection, x509, errno, depth, preverifyOK):
        if preverifyOK:
            if self.hostname != x509.get_subject().commonName:
                return False
        return preverifyOK
def secureGet(url):
    return getPage(url, HTTPSVerifyingContextFactory(URLPath.fromString(url).netloc))
def done(result):
    print 'Done!', len(result)
secureGet("https://google.com/").addCallback(done)
reactor.run()

25voto

plundra Points 4607

PycURL fait cela à merveille.

Voici un petit exemple. Il lancera un pycurl.error si quelque chose est louche, où vous obtenez un tuple avec le code d'erreur et un message lisible par l'homme.

import pycurl

curl = pycurl.Curl()
curl.setopt(pycurl.CAINFO, "myFineCA.crt")
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
curl.setopt(pycurl.URL, "https://internal.stuff/")

curl.perform()

Vous voudrez probablement configurer d'autres options, comme l'endroit où stocker les résultats, etc. Mais il n'est pas nécessaire d'encombrer l'exemple avec des éléments non essentiels.

Exemple d'exceptions qui pourraient être soulevées :

(60, 'Peer certificate cannot be authenticated with known CA certificates')
(51, "common name 'CN=something.else.stuff,O=Example Corp,C=SE' does not match 'internal.stuff'")

Quelques liens que j'ai trouvé utiles sont les libcurl-docs pour setopt et getinfo.

15voto

ufo Points 711

Ou simplement vous faciliter la vie en utilisant le demande bibliothèque :

import requests
requests.get('https://somesite.com', cert='/path/server.crt')

Quelques mots encore sur son utilisation.

14voto

Eli Courtwright Points 53071

Voici un exemple script qui démontre la validation du certificat :

import httplib
import re
import socket
import sys
import urllib2
import ssl

class InvalidCertificateException(httplib.HTTPException, urllib2.URLError):
    def __init__(self, host, cert, reason):
        httplib.HTTPException.__init__(self)
        self.host = host
        self.cert = cert
        self.reason = reason

    def __str__(self):
        return ('Host %s returned an invalid certificate (%s) %s\n' %
                (self.host, self.reason, self.cert))

class CertValidatingHTTPSConnection(httplib.HTTPConnection):
    default_port = httplib.HTTPS_PORT

    def __init__(self, host, port=None, key_file=None, cert_file=None,
                             ca_certs=None, strict=None, **kwargs):
        httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs)
        self.key_file = key_file
        self.cert_file = cert_file
        self.ca_certs = ca_certs
        if self.ca_certs:
            self.cert_reqs = ssl.CERT_REQUIRED
        else:
            self.cert_reqs = ssl.CERT_NONE

    def _GetValidHostsForCert(self, cert):
        if 'subjectAltName' in cert:
            return [x[1] for x in cert['subjectAltName']
                         if x[0].lower() == 'dns']
        else:
            return [x[0][1] for x in cert['subject']
                            if x[0][0].lower() == 'commonname']

    def _ValidateCertificateHostname(self, cert, hostname):
        hosts = self._GetValidHostsForCert(cert)
        for host in hosts:
            host_re = host.replace('.', '\.').replace('*', '[^.]*')
            if re.search('^%s$' % (host_re,), hostname, re.I):
                return True
        return False

    def connect(self):
        sock = socket.create_connection((self.host, self.port))
        self.sock = ssl.wrap_socket(sock, keyfile=self.key_file,
                                          certfile=self.cert_file,
                                          cert_reqs=self.cert_reqs,
                                          ca_certs=self.ca_certs)
        if self.cert_reqs & ssl.CERT_REQUIRED:
            cert = self.sock.getpeercert()
            hostname = self.host.split(':', 0)[0]
            if not self._ValidateCertificateHostname(cert, hostname):
                raise InvalidCertificateException(hostname, cert,
                                                  'hostname mismatch')

class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
    def __init__(self, **kwargs):
        urllib2.AbstractHTTPHandler.__init__(self)
        self._connection_args = kwargs

    def https_open(self, req):
        def http_class_wrapper(host, **kwargs):
            full_kwargs = dict(self._connection_args)
            full_kwargs.update(kwargs)
            return CertValidatingHTTPSConnection(host, **full_kwargs)

        try:
            return self.do_open(http_class_wrapper, req)
        except urllib2.URLError, e:
            if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
                raise InvalidCertificateException(req.host, '',
                                                  e.reason.args[1])
            raise

    https_request = urllib2.HTTPSHandler.do_request_

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print "usage: python %s CA_CERT URL" % sys.argv[0]
        exit(2)

    handler = VerifiedHTTPSHandler(ca_certs = sys.argv[1])
    opener = urllib2.build_opener(handler)
    print opener.open(sys.argv[2]).read()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X