Valider les certificats SSL avec Python

85

J'ai besoin d'écrire un script qui se connecte à un tas 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, nous avons donc la clé publique de l'autorité de certification pour vérifier les certificats.

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

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

Comment vérifier un certificat en Python?

Eli Courtwright
la source
10
Votre commentaire sur Twisted est incorrect: Twisted utilise pyopenssl, pas le support SSL intégré de Python. Bien qu'il ne valide pas les certificats HTTPS par défaut dans son client HTTP, vous pouvez utiliser l'argument "contextFactory" pour getPage et downloadPage pour construire une fabrique de contexte de validation. En revanche, à ma connaissance, il n'y a aucun moyen que le module intégré "ssl" puisse être convaincu de faire la validation de certificat.
Glyph du
4
Avec le module SSL de Python 2.6 et versions ultérieures, vous pouvez écrire votre propre validateur de certificat. Pas optimal, mais faisable.
Heikki Toivonen
3
La situation a changé, Python valide désormais par défaut les certificats. J'ai ajouté une nouvelle réponse ci-dessous.
-Philip Gehrcke
La situation a également changé pour Twisted (un peu avant pour Python, en fait); Si vous utilisez treqou twisted.web.client.Agentdepuis la version 14.0, Twisted vérifie les certificats par défaut.
Glyph

Réponses:

19

À partir de la version 2.7.9 / 3.4.3, Python tente par défaut d'effectuer la validation du certificat.

Cela a été proposé dans PEP 467, qui vaut la peine d'être lu: 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. Contrairement à cela, le package de requêtes contient son propre ensemble de certificats. Les avantages et les inconvénients des deux approches sont examinés dans la section de la base de données Trust du PEP 476 .

Dr Jan-Philip Gehrcke
la source
des solutions pour assurer les vérifications du certificat pour la version précédente de python? On ne peut pas toujours mettre à jour la version de python.
vaab
il ne valide pas les certificats révoqués. Par exemple revoked.badssl.com
Raz
Est-il obligatoire d'utiliser la HTTPSConnectionclasse? J'utilisais SSLSocket. Comment puis-je faire la validation avec SSLSocket? Dois-je valider explicitement l'utilisation pyopensslcomme expliqué ici ?
anir
31

J'ai ajouté une distribution à l'index des packages Python qui rend la match_hostname()fonction du sslpackage Python 3.2 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 vous pouvez en faire une dépendance répertoriée dans votre projet setup.py. Dans tous les cas, 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:
    ...
Brandon Rhodes
la source
1
Il me manque quelque chose ... pouvez-vous s'il vous plaît remplir les espaces ci-dessus ou fournir un exemple complet (pour un site comme Google)?
smholloway
L'exemple sera différent en fonction de la bibliothèque que vous utilisez pour accéder à Google, car différentes bibliothèques placent le socket SSL à différents endroits, et c'est le socket SSL qui a besoin de sa getpeercert()méthode appelée pour que la sortie puisse être transmise match_hostname().
Brandon Rhodes
12
Je suis gêné au nom de Python que tout le monde doive utiliser cela. Les bibliothèques SSL HTTPS intégrées de Python ne vérifiant pas les certificats prêts à l'emploi par défaut sont complètement insensées, et il est difficile d'imaginer combien de systèmes non sécurisés sont actuellement disponibles en conséquence.
Glenn Maynard
26

Vous pouvez utiliser Twisted pour vérifier les certificats. L'API principale est CertificateOptions , qui peut être fournie comme contextFactoryargument de diverses fonctions telles que listenSSL et startTLS .

Malheureusement, ni Python ni Twisted ne sont fournis avec la pile de certificats CA requis pour effectuer réellement la validation HTTPS, ni la logique de validation HTTPS. En raison d' une limitation dans PyOpenSSL , vous ne pouvez pas encore le faire complètement correctement, 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ïf d'un client HTTPS Twisted vérifiant 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' dans 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()
Glyphe
la source
pouvez-vous le rendre non bloquant?
sean riley le
Merci; J'ai une note maintenant que j'ai lu et compris ceci: les rappels de vérification doivent retourner True lorsqu'il n'y a pas d'erreur et False quand il y en a. Votre code renvoie essentiellement une erreur lorsque le commonName n'est pas localhost. Je ne sais pas si c'est ce que vous vouliez, même s'il serait logique de le faire dans certains cas. J'ai juste pensé que je laisserais un commentaire à ce sujet pour le bénéfice des futurs lecteurs de cette réponse.
Eli Courtwright
"self.hostname" dans ce cas n'est pas "localhost"; notez le URLPath(url).netloc: cela signifie la partie hôte de l'URL transmise à secureGet. En d'autres termes, il vérifie que le commonName du sujet est le même que celui demandé par l'appelant.
Glyph
J'ai exécuté une version de ce code de test et j'ai utilisé Firefox, wget et Chrome pour lancer un test du serveur HTTPS. Dans mon test, je constate que le rappel verifyHostname est appelé 3-4 fois à chaque connexion. Pourquoi ne fonctionne-t-il pas qu'une seule fois?
themaestro
2
URLPath (bla) .netloc est toujours localhost: URLPath .__ init__ prend des composants d'url individuels, vous passez une URL entière en tant que "schéma" et obtenez le netloc par défaut de 'localhost' pour aller avec. Vous vouliez probablement utiliser URLPath.fromString (url) .netloc. Malheureusement, cela expose l'archivage de verifyHostName à l'envers: il commence à rejeter https://www.google.com/car l'un des sujets est «www.google.com», ce qui fait que la fonction renvoie False. Cela signifiait probablement retourner True (accepté) si les noms correspondent et False si ce n'est pas le cas?
mzz
25

PycURL fait cela à merveille.

Voici un court exemple. Il lancera un pycurl.errorsi quelque chose est louche, où 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, comme l'emplacement de stockage des résultats, etc. Mais pas besoin d'encombrer l'exemple avec des éléments non essentiels.

Exemple des 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'")

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

plundra
la source
15

Ou simplifiez-vous simplement la vie en utilisant la bibliothèque de requêtes :

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

Quelques mots de plus sur son utilisation.

OVNI
la source
10
L' certargument est le certificat côté client, pas un certificat de serveur à vérifier. Vous souhaitez utiliser l' verifyargument.
Paŭlo Ebermann
2
requêtes valide par défaut . Pas besoin d'utiliser l' verifyargument, sauf pour être plus explicite ou désactiver la vérification.
Dr.Jan-Philip Gehrcke
1
Ce n'est pas un module interne. Vous devez exécuter les demandes d'installation de pip
Robert Townley
14

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 __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()
Eli Courtwright
la source
@tonfa: bonne prise; J'ai fini par ajouter la vérification du nom d'hôte également, et j'ai modifié ma réponse pour inclure le code que j'ai utilisé.
Eli Courtwright
Je ne parviens pas à accéder au lien d'origine (c'est-à-dire «cette page»). At-il bougé?
Matt Ball le
@Matt: Je suppose que oui, mais FWIW, le lien d'origine n'est pas nécessaire, car mon programme de test est un exemple de travail complet et autonome. J'ai lié à la page qui m'a aidé à écrire ce code car cela semblait être la chose décente de fournir une attribution. Mais comme il n'existe plus, je modifierai mon message pour supprimer le lien, merci de l'avoir signalé.
Eli Courtwright le
Cela ne fonctionne pas avec des gestionnaires supplémentaires comme les gestionnaires de proxy en raison de la connexion manuelle de socket dans CertValidatingHTTPSConnection.connect. Consultez cette pull request pour plus de détails (et un correctif).
schlamar
2
Voici une solution nettoyée et fonctionnelle avec backports.ssl_match_hostname.
schlamar
8

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

Sur la base du commentaire de Glyphs, il semble que M2Crypto effectue une meilleure vérification de certificat par défaut que ce que vous pouvez faire avec pyOpenSSL actuellement, car M2Crypto vérifie également le champ subjectAltName.

J'ai également blogué sur la façon d' obtenir les certificats fournis par Mozilla Firefox en Python et utilisables avec les solutions SSL Python.

Heikki Toivonen
la source
4

Jython effectue la vérification des certificats par défaut, donc en utilisant des modules de bibliothèque standard, par exemple httplib.HTTPSConnection, etc., avec jython vérifiera les certificats et donnera des exceptions pour les échecs, c'est-à-dire 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 certificats.

J'ai écrit un article de blog sur la façon de désactiver la vérification des certificats sur jython, car cela peut être utile dans les phases de test, etc.

Installation d'un fournisseur de sécurité de confiance totale sur java et jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

Alan Kennedy
la source
2

Le code suivant vous permet de bénéficier de tous les contrôles de validation SSL (ex: validité de la date, chaîne de certificats CA ...) SAUF une étape de vérification enfichable par ex. Pour vérifier le nom d'hôte ou faire 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 __name__ == '__main__':
    # try expired.badssl.com or self-signed.badssl.com !
    conn = create_custom_HTTPSConnection('badssl.com')
    conn.request('GET', '/')
    conn.getresponse().read()
Carl D'Halluin
la source
-1

pyOpenSSL est une interface avec la bibliothèque OpenSSL. Il devrait fournir tout ce dont vous avez besoin.

DéplacéAussie
la source
OpenSSL n'effectue pas de correspondance de nom d'hôte. Son prévu pour OpenSSL 1.1.0.
jww
-1

J'avais le même problème mais je voulais minimiser les dépendances tierces (car ce script unique devait être exécuté par de nombreux utilisateurs). Ma solution était de boucler un curlappel et de m'assurer que le code de sortie était 0. A travaillé comme un charme.

Ztyx
la source
Je dirais que stackoverflow.com/a/1921551/1228491 en utilisant pycurl est une bien meilleure solution alors.
Marian