web-dev-qa-db-fra.com

Valider les certificats SSL avec Python

J'ai besoin d'écrire un script qui se connecte à un ensemble de sites sur notre intranet d'entreprise via HTTPS et vérifie la validité de leurs certificats SSL. 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. Nous disposons donc de la clé publique de l'autorité de certification pour vérifier les certificats.

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

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?

81
Eli Courtwright

Depuis la version 2.7.9/3.4.3, Python ) tente par défaut de valider le certificat.

Cela a été proposé dans le PEP 467, qui mérite une lecture: https://www.python.org/dev/peps/pep-0476/

Les modifications affectent tous les modules stdlib pertinents (urllib/urllib2, http, httplib).

Documentation pertinente:

https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection

Cette classe effectue maintenant toutes les vérifications de certificat et de nom d’hôte nécessaires par défaut. Pour revenir au comportement précédent, non vérifié, ssl._create_unverified_context () peut être passé au paramètre context.

https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection

Modifié dans la version 3.4.3: cette classe effectue désormais toutes les vérifications de certificat et de nom d'hôte nécessaires par défaut. Pour revenir au comportement précédent, non vérifié, ssl._create_unverified_context () peut être passé au paramètre context.

Notez que la nouvelle vérification intégrée est basée sur la base de données de certificats fournie par le système. À l’opposé, le paquet request envoie son propre paquet de certificats. Les avantages et les inconvénients des deux approches sont examinés dans la section Base de données de confiance du PEP 476 .

18
Jan-Philip Gehrcke

J'ai ajouté une distribution à l'index du package Python qui crée la fonction match_hostname()) à partir du package 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

Vous pouvez également en faire une dépendance répertoriée dans le setup.py De votre projet. De toute façon, il peut être utilisé 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:
    ...
30
Brandon Rhodes

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

Malheureusement, ni Python ni Twisted ne contient la pile de certificats d’autorité de certification nécessaire pour effectuer la validation HTTPS, ni la logique de validation HTTPS. En raison de ne limitation dans PyOpenSSL , vous ne pouvez pas le faire complètement correctement pour l'instant, mais grâce au fait que presque tous les certificats incluent un sujet CommonName, vous pouvez vous en approcher suffisamment.

Voici un exemple d'implémentation naïve d'un client Twisted HTTPS en cours de vérification, qui ignore les caractères génériques et les extensions subjectAltName, et utilise les certificats d'autorité de certification présents dans le package '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()
26
Glyph

PycURL le fait magnifiquement.

Ci-dessous un exemple court. Il va lancer un pycurl.error si quelque chose est louche, vous obtenez un tuple avec un 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 plus d'options, telles que l'emplacement où stocker les résultats, etc. Mais vous n'avez pas besoin d'encombrer l'exemple avec des éléments non essentiels.

Exemple de quelles exceptions peuvent être levé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'")

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

25
plundra

Ou simplement vous simplifier la vie en utilisant la bibliothèque request :

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

Quelques mots sur son utilisation.

14
ufo

Voici un exemple de script qui illustre 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 __== "__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()
14
Eli Courtwright

M2Crypto peut faire la validation . Vous pouvez également utiliser M2Crypto avec Twisted si vous le souhaitez. Le client de bureau Chandler tilise Twisted pour la mise en réseau et M2Crypto pour SSL , y compris la validation du certificat.

D'après les commentaires de Glyphs, il semble que M2Crypto effectue par défaut une meilleure vérification des certificats que ce que vous pouvez actuellement faire avec pyOpenSSL, car M2Crypto vérifie également le champ subjectAltName.

J'ai également blogué sur la façon de obtenir les certificats Mozilla Firefox est livré avec Python et utilisable avec Python solutions SSL.

8
Heikki Toivonen

Jython effectue la vérification de certificat par défaut. Vous devez donc utiliser des modules de bibliothèque standard, par exemple. httplib.HTTPSConnection, etc., avec jython vérifiera les certificats et donnera des exceptions pour les échecs, à savoir les identités incompatibles, les certificats expirés, etc.

En fait, vous devez faire un travail supplémentaire pour que jython se comporte comme cpython, c’est-à-dire pour que jython ne vérifie PAS les certs.

J'ai écrit un article sur la désactivation de la vérification des certificats sur jython, car cela peut être utile lors des phases de test, etc.

Installation d’un fournisseur de sécurité totalement fiable sur Java et jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-Java-and-jython/

4
Alan Kennedy

Le code suivant vous permet de bénéficier de toutes les vérifications de validation SSL (par exemple, la validité de la date, la chaîne de certificats de l'autorité de certification, etc.) SAUF une étape de vérification enfichable, par exemple. pour vérifier le nom d'hôte ou effectuer d'autres étapes de vérification de certificat supplémentaires.

from httplib import HTTPSConnection
import ssl


def create_custom_HTTPSConnection(Host):

    def verify_cert(cert, Host):
        # Write your code here
        # You can certainly base yourself on ssl.match_hostname
        # Raise ssl.CertificateError if verification fails
        print 'Host:', Host
        print 'Peer cert:', cert

    class CustomHTTPSConnection(HTTPSConnection, object):
        def connect(self):
            super(CustomHTTPSConnection, self).connect()
            cert = self.sock.getpeercert()
            verify_cert(cert, Host)

    context = ssl.create_default_context()
    context.check_hostname = False
    return CustomHTTPSConnection(Host=host, context=context)


if __== '__main__':
    # try expired.badssl.com or self-signed.badssl.com !
    conn = create_custom_HTTPSConnection('badssl.com')
    conn.request('GET', '/')
    conn.getresponse().read()
1
Carl D'Halluin