Überprüfen Sie SSL-Zertifikate mit Python

85

Ich muss ein Skript schreiben, das über HTTPS eine Verbindung zu einer Reihe von Websites in unserem Unternehmensintranet herstellt und überprüft, ob deren SSL-Zertifikate gültig sind. dass sie nicht abgelaufen sind, dass sie für die richtige Adresse ausgestellt wurden usw. Wir verwenden für diese Websites unsere eigene interne Unternehmenszertifizierungsstelle, sodass wir über den öffentlichen Schlüssel der Zertifizierungsstelle verfügen, um die Zertifikate zu überprüfen.

Python akzeptiert und verwendet standardmäßig nur SSL-Zertifikate, wenn HTTPS verwendet wird. Selbst wenn ein Zertifikat ungültig ist, verwenden Python-Bibliotheken wie urllib2 und Twisted das Zertifikat nur gerne.

Gibt es irgendwo eine gute Bibliothek, mit der ich über HTTPS eine Verbindung zu einer Site herstellen und deren Zertifikat auf diese Weise überprüfen kann?

Wie überprüfe ich ein Zertifikat in Python?

Eli Courtwright
quelle
10
Ihr Kommentar zu Twisted ist falsch: Twisted verwendet pyopenssl und nicht die in Python integrierte SSL-Unterstützung. Während HTTPS-Zertifikate in seinem HTTP-Client standardmäßig nicht validiert werden, können Sie mit dem Argument "contextFactory" getPage und downloadPage eine validierende Kontextfactory erstellen. Meines Wissens kann das integrierte "ssl" -Modul nicht davon überzeugt werden, eine Zertifikatsvalidierung durchzuführen.
Glyphe
4
Mit dem SSL-Modul in Python 2.6 und höher können Sie Ihren eigenen Zertifikatvalidator schreiben. Nicht optimal, aber machbar.
Heikki Toivonen
3
Die Situation hat sich geändert. Python validiert jetzt standardmäßig Zertifikate. Ich habe unten eine neue Antwort hinzugefügt.
Dr. Jan-Philip Gehrcke
Die Situation änderte sich auch für Twisted (tatsächlich etwas früher als für Python); Wenn Sie treqoder twisted.web.client.Agentseit Version 14.0 verwenden, überprüft Twisted standardmäßig Zertifikate.
Glyphe

Antworten:

19

Ab Release 2.7.9 / 3.4.3 versucht Python standardmäßig , eine Zertifikatüberprüfung durchzuführen.

Dies wurde in PEP 467 vorgeschlagen, das eine Lektüre wert ist: https://www.python.org/dev/peps/pep-0476/

Die Änderungen betreffen alle relevanten stdlib-Module (urllib / urllib2, http, httplib).

Relevante Dokumentation:

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

Diese Klasse führt jetzt standardmäßig alle erforderlichen Zertifikats- und Hostnamenprüfungen durch. Um zum vorherigen, nicht verifizierten Verhalten ssl._create_unverified_context () zurückzukehren, kann es an den Kontextparameter übergeben werden.

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

In Version 3.4.3 geändert: Diese Klasse führt jetzt standardmäßig alle erforderlichen Zertifikats- und Hostnamenprüfungen durch. Um zum vorherigen, nicht verifizierten Verhalten ssl._create_unverified_context () zurückzukehren, kann es an den Kontextparameter übergeben werden.

Beachten Sie, dass die neue integrierte Überprüfung auf der vom System bereitgestellten Zertifikatdatenbank basiert . Im Gegensatz dazu liefert das Anforderungspaket ein eigenes Zertifikatspaket. Vor- und Nachteile beider Ansätze werden im Abschnitt Vertrauensdatenbank von PEP 476 erörtert .

Dr. Jan-Philip Gehrcke
quelle
Gibt es Lösungen, um die Überprüfung des Zertifikats für die vorherige Version von Python sicherzustellen? Man kann die Version von Python nicht immer aktualisieren.
Vaab
widerrufene Zertifikate werden nicht validiert. ZB revoked.badssl.com
Raz
Ist es obligatorisch, HTTPSConnectionUnterricht zu benutzen ? Ich habe benutzt SSLSocket. Wie kann ich eine Validierung durchführen SSLSocket? Muss ich die Verwendung pyopensslwie hier erläutert explizit validieren ?
Anir
31

Ich habe dem Python-Paketindex eine Distribution hinzugefügt, die die match_hostname()Funktion aus dem Python 3.2- sslPaket in früheren Versionen von Python verfügbar macht .

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

Sie können es installieren mit:

pip install backports.ssl_match_hostname

Oder Sie können es zu einer Abhängigkeit machen, die in Ihrem Projekt aufgeführt ist setup.py. In beiden Fällen kann es folgendermaßen verwendet werden:

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
quelle
1
Mir fehlt etwas ... können Sie bitte die obigen Lücken ausfüllen oder ein vollständiges Beispiel angeben (für eine Website wie Google)?
Smholloway
Das Beispiel sieht unterschiedlich aus, je nachdem, welche Bibliothek Sie für den Zugriff auf Google verwenden, da verschiedene Bibliotheken den SSL-Socket an verschiedenen Stellen platzieren und der SSL-Socket seine getpeercert()Methode aufrufen muss, damit die Ausgabe übergeben werden kann match_hostname().
Brandon Rhodes
12
Es ist mir in Pythons Namen peinlich, dass jemand dies verwenden muss. Die in Python integrierten SSL-HTTPS-Bibliotheken, die standardmäßig keine Zertifikate überprüfen, sind völlig verrückt, und es ist schmerzhaft, sich vorzustellen, wie viele unsichere Systeme derzeit verfügbar sind.
Glenn Maynard
26

Mit Twisted können Sie Zertifikate überprüfen. Die Haupt-API ist CertificateOptions , die als contextFactoryArgument für verschiedene Funktionen wie listenSSL und startTLS bereitgestellt werden kann .

Leider werden weder Python noch Twisted mit dem Stapel von CA-Zertifikaten geliefert, die für die eigentliche HTTPS-Validierung erforderlich sind, noch mit der HTTPS-Validierungslogik. Aufgrund einer Einschränkung in PyOpenSSL können Sie dies noch nicht vollständig korrekt ausführen. Dank der Tatsache, dass fast alle Zertifikate einen Betreff commonName enthalten, können Sie jedoch nahe genug heranrücken.

Hier ist eine naive Beispielimplementierung eines verifizierenden Twisted HTTPS-Clients, der Platzhalter und subjectAltName-Erweiterungen ignoriert und die Zertifizierungsstellenzertifikate verwendet, die im Paket 'ca-certificates' in den meisten Ubuntu-Distributionen enthalten sind. Versuchen Sie es mit Ihren bevorzugten gültigen und ungültigen Zertifikatseiten :).

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
quelle
Kannst du es blockierungsfrei machen?
Sean Riley
Vielen Dank; Ich habe jetzt einen Hinweis, den ich gelesen und verstanden habe: Überprüfen Sie, ob Rückrufe True zurückgeben sollten, wenn kein Fehler vorliegt, und False, wenn ein Fehler vorliegt. Ihr Code gibt grundsätzlich einen Fehler zurück, wenn der commonName nicht localhost ist. Ich bin mir nicht sicher, ob Sie das beabsichtigt haben, obwohl es in einigen Fällen sinnvoll wäre, dies zu tun. Ich dachte nur, ich würde einen Kommentar dazu hinterlassen, um zukünftigen Lesern dieser Antwort zu helfen.
Eli Courtwright
"self.hostname" ist in diesem Fall nicht "localhost"; Beachten Sie URLPath(url).netlocFolgendes: Dies bedeutet, dass der Host-Teil der URL an SecureGet übergeben wird. Mit anderen Worten, es wird überprüft, ob der gemeinsame Name des Betreffs mit dem vom Anrufer angeforderten übereinstimmt.
Glyphe
Ich habe eine Version dieses Testcodes ausgeführt und Firefox, wget und Chrome verwendet, um einen Test-HTTPS-Server zu starten. In meinen Testläufen sehe ich jedoch, dass der Rückruf verifyHostname bei jeder Verbindung 3-4 Mal aufgerufen wird. Warum läuft es nicht nur einmal?
Themaestro
2
URLPath (blah) .netloc ist immer localhost: URLPath .__ init__ verwendet einzelne URL-Komponenten. Sie übergeben eine gesamte URL als "Schema" und erhalten das dazugehörige Standard-Netloc "localhost". Sie wollten wahrscheinlich URLPath.fromString (url) .netloc verwenden. Leider wird dadurch das Einchecken von verifyHostName rückwärts angezeigt: Es https://www.google.com/wird abgelehnt, da einer der Betreffs "www.google.com" ist und die Funktion "False" zurückgibt. Es bedeutete wahrscheinlich, True (akzeptiert) zurückzugeben, wenn die Namen übereinstimmen, und False, wenn sie nicht übereinstimmen?
Mzz
25

PycURL macht das wunderbar.

Unten ist ein kurzes Beispiel. Es wird ein werfen , pycurl.errorwenn etwas faul ist, wo Sie ein Tupel mit dem Fehlercode und einer für Menschen lesbaren Nachricht erhalten.

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()

Möglicherweise möchten Sie weitere Optionen konfigurieren, z. B. wo die Ergebnisse gespeichert werden sollen usw. Sie müssen das Beispiel jedoch nicht mit nicht wesentlichen Elementen überladen.

Beispiel für mögliche Ausnahmen:

(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'")

Einige Links, die ich nützlich fand, sind die libcurl-docs für setopt und getinfo.

Plundra
quelle
15

Oder erleichtern Sie einfach Ihr Leben, indem Sie die Anforderungsbibliothek verwenden:

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

Noch ein paar Worte zu seiner Verwendung.

UFO
quelle
10
Das certArgument ist das clientseitige Zertifikat, kein Serverzertifikat, gegen das geprüft werden muss. Sie möchten das verifyArgument verwenden.
Paŭlo Ebermann
2
Anforderungen werden standardmäßig validiert . Das verifyArgument muss nicht verwendet werden, außer um expliziter zu sein oder die Überprüfung zu deaktivieren.
Dr. Jan-Philip Gehrcke
1
Es ist kein internes Modul. Sie müssen Pip-Installationsanforderungen ausführen
Robert Townley
14

Hier ist ein Beispielskript, das die Zertifikatvalidierung demonstriert:

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
quelle
@tonfa: Guter Fang; Am Ende habe ich auch die Überprüfung des Hostnamens hinzugefügt und meine Antwort so bearbeitet, dass sie den von mir verwendeten Code enthält.
Eli Courtwright
Ich kann den ursprünglichen Link (dh 'diese Seite') nicht erreichen. Hat es sich bewegt?
Matt Ball
@Matt: Ich denke schon, aber FWIW der ursprüngliche Link ist nicht notwendig, da mein Testprogramm ein vollständiges, in sich geschlossenes Arbeitsbeispiel ist. Ich habe auf die Seite verlinkt, die mir beim Schreiben dieses Codes geholfen hat, da es anständig schien, eine Zuordnung vorzunehmen. Da es aber nicht mehr existiert, werde ich meinen Beitrag bearbeiten, um den Link zu entfernen. Vielen Dank, dass Sie darauf hingewiesen haben.
Eli Courtwright
Dies funktioniert bei zusätzlichen Handlern wie Proxy-Handlern aufgrund der manuellen Socket-Verbindung nicht CertValidatingHTTPSConnection.connect. Weitere Informationen (und eine Korrektur) finden Sie in dieser Pull-Anfrage .
Schlamar
2
Hier ist ein gereinigter und Arbeitslösung mit backports.ssl_match_hostname.
Schlamar
8

M2Crypto kann die Validierung durchführen . Sie können M2Crypto auch mit Twisted verwenden, wenn Sie möchten . Der Chandler-Desktop-Client verwendet Twisted für das Netzwerk und M2Crypto für SSL , einschließlich der Zertifikatsüberprüfung.

Basierend auf dem Glyphenkommentar scheint M2Crypto standardmäßig eine bessere Zertifikatsüberprüfung durchzuführen als das, was Sie derzeit mit pyOpenSSL tun können, da M2Crypto auch das Feld subjectAltName überprüft.

Ich habe auch darüber gebloggt, wie man die Zertifikate erhält, mit denen Mozilla Firefox in Python ausgeliefert und mit Python-SSL-Lösungen verwendet werden kann.

Heikki Toivonen
quelle
4

Jython führt standardmäßig eine Zertifikatsüberprüfung durch. Wenn Sie also Standardbibliotheksmodule wie z. B. httplib.HTTPSConnection usw. mit jython verwenden, werden Zertifikate überprüft und Ausnahmen für Fehler, dh nicht übereinstimmende Identitäten, abgelaufene Zertifikate usw., angegeben.

Tatsächlich müssen Sie einige zusätzliche Arbeiten ausführen, damit sich jython wie cpython verhält, dh damit jython KEINE Zertifikate überprüft.

Ich habe einen Blog-Beitrag darüber geschrieben, wie die Zertifikatsprüfung in Jython deaktiviert wird, da dies in Testphasen usw. hilfreich sein kann.

Installieren eines vertrauenswürdigen Sicherheitsanbieters unter Java und Jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

Alan Kennedy
quelle
2

Mit dem folgenden Code können Sie von allen SSL-Validierungsprüfungen profitieren (z. B. Datumsgültigkeit, CA-Zertifikatkette ...), AUSSER einem steckbaren Überprüfungsschritt, z. B. um den Hostnamen zu überprüfen oder andere zusätzliche Schritte zur Zertifikatsüberprüfung durchzuführen.

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
quelle
-1

pyOpenSSL ist eine Schnittstelle zur OpenSSL-Bibliothek. Es sollte alles bieten, was Sie brauchen.

DisplacedAussie
quelle
OpenSSL führt keinen Hostnamenabgleich durch. Es ist für OpenSSL 1.1.0 geplant.
JWW
-1

Ich hatte das gleiche Problem, wollte aber die Abhängigkeiten von Drittanbietern minimieren (da dieses einmalige Skript von vielen Benutzern ausgeführt werden sollte). Meine Lösung bestand darin, einen curlAnruf zu beenden und sicherzustellen, dass der Exit-Code war 0. Lief wie am Schnürchen.

Ztyx
quelle
Ich würde sagen, stackoverflow.com/a/1921551/1228491 mit pycurl ist dann eine viel bessere Lösung.
Marian