angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Verwendung von Pythons pyOpenSSL zur Überprüfung von SSL-Zertifikaten, die von einem Host heruntergeladen wurden

Ab November 2020 kann die Chain of Trust ohne Aufruf von OpenSSL mit Python's subprocess verifiziert werden.

17 Dezember 2020
post main image
https://unsplash.com/@ilhamfzn

Während ich ein Skript schrieb, um zu prüfen, ob Websites korrekt auf 'https:/www.' umgeleitet werden. Ich dachte daran, auch einige SSL-Zertifikatsprüfungen hinzuzufügen. Das bedeutet, dass ich SSL-Zertifikate, die ich von einem Host heruntergeladen habe, überprüfen musste. Ist das Zertifikat wirklich für diese Website? Zeigen Sie mir das Ablaufdatum. Ist die Zertifikatskette korrekt? Und können wir dem/den Zertifikat(en) vertrauen?

Anfänglich blieb ich dort stecken, wo viele Leute wegen des Folgenden stecken bleiben. Die Zwischenzertifikate, die Sie von einem Host herunterladen, sind nicht vertrauenswürdig und pyOpenSSL hat bei der Überprüfung einer Zertifikatskette nicht das Flag 'untrusted' verwendet. Das bedeutet, dass das Zertifikat als vertrauenswürdig gekennzeichnet werden kann, obwohl es das nicht ist.

Die einzige Möglichkeit zur Umgehung bestand darin, den Befehl OpenSSL verify mit subprocess auszuführen. Nicht das, was wir wollen, aber zumindest konnten wir es tun. Es gibt andere Möglichkeiten, aber sie sind viel komplexer. Dies wurde 2016 berichtet und diskutiert. Mit dem Release von pyOpenSSL 20.0.0 (2020-11-27) wurden folgende Änderungen vorgenommen:

  • eine neue Methode 'load_locations()' zu X509Store, um vertrauenswürdige Zertifikats-Dateibündel und/oder Verzeichnisse zu setzen
  • ein neuer Parameter 'chain' zu X509StoreContext, mit dem Sie untrusted -Zertifikate hinzufügen können
  • eine neue Methode 'get_verified_chain()' zuX509StoreContext, die die komplette verifizierte Kette zurückgibt

Mit diesen Änderungen können wir nun endlich die Chain of Trust verifizieren.

Python und Kryptographie ist nicht einfach

Websites, Dienste sind von HTTP auf HTTPS umgestiegen. Das bedeutet, wenn Sie sich mit einem Dienst verbinden, müssen Sie auch das/die Zertifikat(e) überprüfen. Tut Python dies für Sie? Der Python requests library tut dies automatisch für Sie. Sie müssen nicht einmal einen Parameter wie 'verify=True' hinzufügen.

Aber hier suche ich nach einer Möglichkeit, die SSL-Zertifikate in meinem eigenen Python -Skript zu überprüfen. Im Folgenden beschreibe ich einige Möglichkeiten, dies zu tun, und einen Python -Code, den ich geschrieben habe, um dies zu untersuchen. Dies läuft auf meinem Ubuntu 18.04 PC.

Relevantes Lesen, Anschauen

Vielleicht möchten Sie damit beginnen, die Artikel 'Chain of Fools: An Exploration of Certificate Chain Validation Mishaps' und '[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl' zu lesen und ein schönes Video 'Digital Certificates: Chain of Trust' anzusehen, siehe Links unten.

Die Klasse CertInfo

In den folgenden Beispielen verwende ich die Klasse CertInfo(). Dies ist eine Klasse, die ich geschrieben habe, um Informationen aus einem Zertifikat zu extrahieren. Der Python -Code befindet sich am Ende dieses Artikels.

Die Zertifikatskette

Eine Zertifikatskette ist eine verknüpfte Liste von Zertifikaten. In jedem Zertifikat gibt es zwei Elemente, die angeben, wie sie verknüpft sind:

  • Subject-CN (allgemeiner Name)
  • Issuer-CN (allgemeiner Name)

Beginnend mit dem Server-Zertifikat, wird es vom Issuer-CN ausgestellt. Das Server-Zertifikat wird auch Endteilnehmer-Zertifikat, Blatt-Zertifikat oder Teilnehmer-Zertifikat genannt. Das nächste heruntergeladene Zertifikat muss eine Subject-CN haben, die mit der Issuer-CN des Server-Zertifikats identisch ist, usw.

Im Folgenden ist das erste vom Host heruntergeladene Zertifikat host_cert[0], das zweite host_cert[1], usw. Das letzte Zertifikat ist das Stammzertifikat root_cert.

Example#1: www.badssl.com

INFO - ------------------------------------------------------------
INFO - Dumping certs:  www.badssl.com:443 ...
INFO - * got 2 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - *  Subject-CN: *.badssl.com
INFO - *  Issuer-CN: DigiCert SHA2 Secure Server CA
INFO - host_cert[1]
INFO - *  Subject-CN: DigiCert SHA2 Secure Server CA
INFO - *  Issuer-CN: DigiCert Global Root CA
INFO - root_cert
INFO - *  Subject-CN: DigiCert Global Root CA
INFO - *  Issuer-CN: DigiCert Global Root CA

Beispiel#2: two-intermediate-certs-example.org

INFO - ------------------------------------------------------------
INFO - Dumping certs:  two-intermediate-certs-example.org:443 ...
INFO - * got 3 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - *  Subject-CN:  two-intermediate-certs-example.org
INFO - *  Issuer-CN: Sectigo RSA Domain Validation Secure Server CA
INFO - host_cert[1]
INFO - *  Subject-CN: Sectigo RSA Domain Validation Secure Server CA
INFO - *  Issuer-CN: USERTrust RSA Certification Authority
INFO - host_cert[2]
INFO - *  Subject-CN: USERTrust RSA Certification Authority
INFO - *  Issuer-CN: AAA Certificate Services
INFO - root_cert
INFO - *  Subject-CN: AAA Certificate Services
INFO - *  Issuer-CN: AAA Certificate Services

Example#3: www.example.org

INFO - ------------------------------------------------------------
INFO - Dumping certs:  www.example.org:443 ...
INFO - * got 3 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - *  Subject-CN:  www.example.org
INFO - *  Issuer-CN: DigiCert TLS RSA SHA256 2020 CA1
INFO - host_cert[1]
INFO - *  Subject-CN: DigiCert TLS RSA SHA256 2020 CA1
INFO - *  Issuer-CN: DigiCert Global Root CA
INFO - host_cert[2]
INFO - *  Subject-CN: DigiCert Global Root CA
INFO - *  Issuer-CN: DigiCert Global Root CA
INFO - root_cert
INFO - *  Subject-CN: DigiCert Global Root CA
INFO - *  Issuer-CN: DigiCert Global Root CA

Mit diesen Daten können wir überprüfen, ob die Kette, die beim Server-Zertifikat beginnt, beim Root-Zertifikat endet. Wenn wir überprüfen wollen, ob die Zertifikate korrekt konfiguriert sind, müssen wir dies unbedingt tun, da OpenSSL diese Reihenfolge nicht überprüft! Beachten Sie, dass www.badssl.com ein Zwischenzertifikat hat und two-intermediate-certs-example.org und www.example.org zwei Zwischenzertifikate haben. www.example.org ist ein Sonderfall, auf den ich weiter unten eingehen werde.

Hier ist der Code von Python , um die Zertifikatskette zu überprüfen:

    def check_chain_order(self):

        if self.root_cert is  None:
            return False

        # link issuer-subject, start with server_cert
        for i, cert in enumerate(self.host_certs):
            if i < 1:
                # we need two
                continue
            ci1 =  CertInfo(self.host_certs[i - 1])
            ci2 =  CertInfo(self.host_certs[i])
            if ci1.issuer_cn != ci2.subject_cn:
                return False

        # link issuer-subject, root_cert
        ci1 =  CertInfo(self.host_certs[-1])
        ci2 =  CertInfo(self.root_cert)
        if ci1.issuer_cn != ci2.subject_cn:
            return False

        return True

Abrufen des Stammzertifikats

Um die Zertifikatskette zu überprüfen, benötigen wir auch das Wurzelzertifikat. In vielen Fällen können Sie einen CA root -Pfad zu einem Verzeichnis auf Ihrem Gerät angeben und die Verifizierungsfunktion wird das Wurzelzertifikat automatisch nachschlagen. Auf meinem PC kann ich den Parameter OpenSSL CApath auf setzen:

/etc/ssl/certs

Aber was ist, wenn wir das CA root -Zertifikat für die Verwendung in der Zertifikatsketten-Verifizierungsfunktion wie oben beschrieben erhalten wollen? Ich habe nicht gesehen, wie man das mit pyOpenSSL machen kann, aber es kann auf andere Weise gemacht werden.

Erstens können wir dies mit OpenSSL und subprocess tun. Der Befehl OpenSSL ist:

openssl  x509 -noout -issuer_hash -in cert.pem

wobei cert.pm das letzte (Zwischen-)Zertifikat ist, das vom Host geholt wurde (vorausgesetzt, die Zertifikatsreihenfolge ist korrekt). Es gibt einige Einschränkungen, aber in den meisten Fällen wird dies funktionieren. Dies gibt eine hexadezimale Zahl wie zurück:

4a6481c9

Mit einem angehängten '.0' ist dies ein symlink für das Stammzertifikat:

ls -l /etc/ssl/certs/4a6481c9.0

Ergebnis:

lrwxrwxrwx 1 root root 27 okt 14  2017 /etc/ssl/certs/4a6481c9.0 -> GlobalSign_Root_CA_-_R2.pem

Wenn Sie wissen wollen, warum das so funktioniert, sehen Sie sich das Dienstprogramm c_rehash an, das mit OpenSSL geliefert wird. Dieses erstellt einen Hash zum schnellen Nachschlagen von Wurzelzertifikaten.

In Python verwende ich subprocess , um den OpenSSL -Befehl auszuführen, und schreibe dann die Root-Datei in mein Verzeichnis für den einfachen Zugriff. Beachten Sie, dass ich STDIN verwende, um PEM mit OpenSSL zu füttern und STDOUT verwende, um das Ergebnis zu erfassen.

    def get_root_cert(self, pem):
        cmd = ['openssl', 'x509', '-noout', '-issuer_hash']
        p =  subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = p.communicate(input=pem.encode())[0].decode('utf-8').strip()

        if p.returncode != 0:
            return  None

        root_pem_file = os.path.join(os.sep, 'etc', 'ssl', 'certs', out  +  '.0')
        if not os.path.isfile(root_pem_file):
            return  None

        with open(root_pem_file, 'r') as fh:
            root_pem = fh.read()

        with open('root.pem', 'w') as fh:
            fh.write(root_pem)
        return crypto.load_certificate(crypto.FILETYPE_PEM, root_pem)

Eine andere Möglichkeit, das Wurzelzertifikat zu erhalten, wenn wir ein Wurzelzertifikat-Bundle auf unserem System haben, besteht darin, dieses Bundle herunterzuladen, zum Beispiel von der Website Curl :

https://curl.haxx.se/docs/caextract.html

Diese Datei enthält das CA root PEMs. Wenn wir diese verwenden wollen, sollten wir die Zertifikate extrahieren und dann ein Wörterbuch mit dem (Index-)Schlüssel Subject-CN und dem Wert PEM erstellen. Beim ersten Mal erstellen und speichern wir dies in einer Datei (mit Pickle), beim nächsten Mal laden wir diese Datei, indizieren das Verzeichnis und geben die PEM des Zertifikats CA root zurück.

    def get_root_cert(self, pem):
        root_subject_cn2pems_file = 'root_subject_cn2pems.pickle'
        cacert_file = 'cacert.pem'

        cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
        ci =  CertInfo(cert)
        issuer_cn = ci.issuer_cn

        if os.path.isfile(root_subject_cn2pems_file):
            # use stored root pems
            with open(root_subject_cn2pems_file, 'rb') as fh:
                root_subject_cn2pems = pickle.load(fh)
        else:
            # create root pems and store
            with open(cacert_file, 'r') as fh:
                capems = fh.read()

            pem_begin = '-----BEGIN CERTIFICATE-----'
            pem_end = '-----END CERTIFICATE-----'

            root_subject_cn2pems = {}
            for part in capems.split(pem_begin)[1:]:
                if pem_end not in part:
                    continue
                pem_part, rem = part.split(pem_end, 1)
                pem = str(pem_begin  +  pem_part  +  pem_end)

                cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
                ci =  CertInfo(cert)
                if ci.subject_cn is  None:
                    continue
                root_subject_cn2pems[ci.subject_cn] = pem

            with open(root_subject_cn2pems_file, 'wb') as fh:
                pickle.dump(root_subject_cn2pems, fh)

        # try to get root pem
        if issuer_cn not in root_subject_cn2pems:
            return  None

        root_pem = root_subject_cn2pems[issuer_cn]
        with open('root.pem', 'w') as fh:
            fh.write(root_pem)
        return crypto.load_certificate(crypto.FILETYPE_PEM, root_pem)

Wiederum ist CertInfo() eine Klasse, die ich geschrieben habe, um Informationen aus einem Zertifikat zu extrahieren. Der Python -Code befindet sich am Ende dieses Artikels.

Überprüfen der Vertrauenskette

Es reicht nicht aus, zu prüfen, ob das Server-Zertifikat mit einem Root-Zertifikat auf Ihrem PC, Telefon, Gerät verknüpft ist. Es gibt auch die so genannte "Chain of Trust". Zertifikaten, die wir von einem Host herunterladen, kann nicht vertraut werden. Das einzige Zertifikat, dem vertraut werden kann, ist das Stammzertifikat, das sich auf Ihrem Gerät, PC, in einem speziellen Verzeichnis, auf meinem PC Ubuntu befindet:

/etc/ssl/certs

Mit dem Root-Zertifikat können wir prüfen, ob wir dem nächsten (Zwischen-)Zertifikat vertrauen können. Wenn es vertrauenswürdig ist, verwenden wir dieses Zwischenzertifikat und prüfen, ob das nächste Zertifikat vertrauenswürdig ist. Das machen wir so lange, bis wir beim Server-Zertifikat ankommen.

Der Befehl OpenSSL für die Website www.badssl.com :

openssl  verify -x509_strict -CApath /etc/ssl/certs -untrusted   1.pem   0.pem

Wenn wir das Root-Zertifikat haben, können wir das tun:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   1.pem   0.pem

Das 'untrusted'-Flag sagt OpenSSL , dass 1.pem nicht vertrauenswürdig ist und vor der Überprüfung von 0.pem vertrauenswürdig sein muss. In ähnlicher Weise können wir die Kette für die Website two-intermediate-certs-example.org prüfen, die zwei Zwischenzertifikate hat:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   2.pem  -untrusted   1.pem   0.pem

Beachten Sie, dass, wenn Sie die Zwischenzertifikate 1.pem und 2.pem für die Website two-intermediate-certs-example.org vertauschen, das Ergebnis in beiden Fällen das gleiche ist. Das bedeutet, dass OpenSSL zuerst versucht, die Kettenreihenfolge zu finden und dann die Vertrauensoperation startet. In diesem Fall können wir auch die beiden Zwischenzertifikate in 12.pem verketten und ausführen:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   12.pem   0.pem

Die SSL-Zertifikate, die ich vom Host heruntergeladen habe, sind in self.host_certs und auch in den Dateien 0.pem, 1.pem, ... Mit Pythons subprocess ist der Code:

    def chain_is_trusted(self):
        cmd = ['openssl', 'verify', '-x509_strict', '-no-CApath', '-CAfile', 'root.pem']
        for i in range(self.host_certs_len - 1, 0, -1):
            cmd.extend([
                '-untrusted',
                str(i)  +  '.pem',        
            ])
        cmd.extend([
            '0.pem',
        ])
        p =  subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = p.communicate()[0].decode('utf-8').strip()

        if p.returncode == 0:
            return True
        return False

Dies ist hässlich und erfordert, dass wir zuerst PEM -Dateien speichern, bevor wir subprocess aufrufen.

Seit pyOpenSSL 20.0.0 können wir das viel eleganter machen:

    def chain_is_trusted(self):

        store = crypto.X509Store()
        store.set_flags(crypto.X509StoreFlags.X509_STRICT)
        store.load_locations(None, capath='/etc/ssl/certs')

        # server cert
        server_cert =  self.host_certs[0]

        # intermediate certs
         untrusted_certs =  self.host_certs[1:]

        store_ctx = crypto.X509StoreContext(store, server_cert, chain=untrusted_certs)

        try:
            store_ctx.verify_certificate()
            # optional
            # certs = store_ctx.get_verified_chain()
            return True
        except crypto.X509StoreContextError as e:
            pass
        return False

Die Methode verify_certificate() erzeugt eine Exception, wenn die Kette nicht verifiziert werden kann.

Die Vertrauenskette, Schritt für Schritt

Oben haben wir die Chain of Trust mit einem einzigen OpenSSL -Befehl überprüft. Wir können dies aber auch auf andere Weise tun. Ausgehend von dem vertrauenswürdigen Root-Zertifikat auf meinem PC versuchen wir zunächst, dem letzten Zwischenzertifikat des Hosts zu vertrauen. Wenn dies gelingt, verwenden wir das nun vertrauenswürdige Zwischenzertifikat, um zu versuchen, dem nächsten untrusted -Zertifikat zu vertrauen, bis hin zum Server-Zertifikat. Bei OpenSSL verwenden wir das Flag 'partial_chain'.

Zum Beispiel für die Website two-intermediate-certs-example.org können wir folgende Befehle ausgeben:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -partial_chain   2.pem

openssl  verify -x509_strict -no-CApath -CAfile  2.pem  -partial_chain   1.pem

openssl  verify -x509_strict -no-CApath -CAfile  1.pem  -partial_chain   0.pem

Selbstsignierte (Zwischen-)Zertifikate

Wenn ein Zwischenzertifikat selbstsigniert ist, stoppt OpenSSL die Validierung(?). Das heißt, wir wollen prüfen, ob ein vom Host heruntergeladenes Zertifikat selbstsigniert ist.

Ein Zertifikat ist selbstsigniert, wenn:

  • die Subject-CN und die Issuer-CN übereinstimmen
  • die subjectKeyIdentifier und die authorityKeyIdentifier übereinstimmen
  • das Zertifikat enthält eine Schlüsselverwendungserweiterung mit dem gesetzten Bit KU_KEY_CERT_SIGN

Ich habe nur die ersten beiden Punkte implementiert, keine Ahnung, wie ich den dritten Punkt umsetzen soll.

Beim Testen fand ich für self-signed.badssl.com:

subjectKeyIdentifier:  None
authorityKeyIdentifier:  None

aber für www.example.org:

subjectKeyIdentifier: 03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
authorityKeyIdentifier: keyid:03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55

Ich nehme an, dass ich hier das führende 'keyid:' entfernen muss, bevor ich das subjectKeyIdentifier mit dem authorityKeyIdentifier vergleiche.

    def cert_is_self_signed(self, cert):
        ci =  CertInfo(cert)

        # strip optional keyid from authority_key_identifier
        keyid = 'keyid:'
        keyid_len = len(keyid)
        extension_authority_key_identifier = ci.extension_authority_key_identifier
        if extension_authority_key_identifier is not  None:
            if extension_authority_key_identifier[:keyid_len] == keyid:
                extension_authority_key_identifier = extension_authority_key_identifier[keyid_len:]

        # subject_key_identifier, authority_key_identifier: both  None  or match
        if (ci.subject_cn == ci.issuer_cn) and \
            ((ci.extension_subject_key_identifier is  None  and ci.extension_authority_key_identifier is  None) or \
            (ci.extension_subject_key_identifier == extension_authority_key_identifier)):
                return True
        return False

Mehr über selbstsignierte Zertifikate finden Sie im Artikel 'How to know if certificate is self-signed', siehe Links unten.

Hier wird auch auf RFC 3280 verwiesen:

'Ein Zertifikat ist selbstausgestellt, wenn die DNs, die im Subject- und Issuer-Feld erscheinen, identisch sind und nicht leer sind. Im Allgemeinen sind der Aussteller und der Betreff der Zertifikate, die einen Pfad bilden, für jedes Zertifikat unterschiedlich. Eine CA kann jedoch ein Zertifikat für sich selbst ausstellen, um Key-Rollover oder Änderungen in den Zertifikatsrichtlinien zu unterstützen. Diese selbst ausgestellten Zertifikate werden bei der Auswertung von Pfadlängen- oder Namensbeschränkungen nicht mitgezählt.'

Das keyIdentifier-Feld der authorityKeyIdentifier -Erweiterung MUSS in allen von konformen CAs generierten Zertifikaten enthalten sein, um den Aufbau von Zertifizierungspfaden zu erleichtern. Es gibt eine Ausnahme: Wenn eine CA ihren öffentlichen Schlüssel in Form eines "selbstsignierten" Zertifikats ausgibt, KANN die Kennung des Behördenschlüssels weggelassen werden. Die Signatur eines selbstsignierten Zertifikats wird mit dem privaten Schlüssel erzeugt, der mit dem öffentlichen Schlüssel des Zertifikats verknüpft ist. (Dies beweist, dass der Aussteller sowohl den öffentlichen als auch den privaten Schlüssel besitzt.) In diesem Fall wären die Subject- und Authority-Key-Identifikatoren identisch, aber nur der Subject-Key-Identifikator wird für die Erstellung des Zertifizierungspfads benötigt.'

Verwechslung mit example.com, example.org

Bei Tests habe ich auch example.com und example.org verwendet. Der Host gab drei Zertifikate zurück. Ich überprüfte das Ergebnis, indem ich die Zertifikate ersetzte und sah, was passiert.

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   2.pem  -untrusted   1.pem   0.pem

Es war ein Durcheinander. Ich konnte das Zwischenzertifikat 2.pem entfernen und OpenSSL sagte immer noch: OK. Ich konnte 1.pem durch ein anderes zufälliges Zertifikat ersetzen und OpenSSL sagte immer noch: OK. Was war hier los?

Dann schaute ich mir die vom Host zurückgegebenen Zertifikate an und stellte fest, dass das dritte Zertifikat, 2.pem, selbstsigniert und mit dem Stammzertifikat auf meinem PC identisch war. Das bedeutet, dass diese Domänen mir ein Root-Zertifikat senden? OpenSSL beschwert sich nicht, die Browser beschweren sich auch nicht.

Ich habe das nicht weiter untersucht, aber ich dachte, ich sollte das erwähnen, um Kopfschmerzen zu vermeiden ... :-(

Die Falsche-Host-Prüfung

Die Falsche-Host-Prüfung muss sicherstellen, dass das Zertifikat für diesen Host ist. Ich habe nicht gesehen, wie dies mit pyOpenSSL gemacht werden kann, in der Tat habe ich keine Möglichkeit gesehen, dies mit OpenSSL zu tun. OpenSSL warnt Sie nicht, wenn das Zertifikat für einen anderen Host ist.

Wir können Wget verwenden:

wget  wrong.host.badssl.com

Ergebnis:

ERROR: no certificate subject alternative name matches requested host name ‘wrong.host.badssl.com’.

Oder wir können Curl verwenden:

curl -L  wrong.host.badssl.com

Ergebnis:

curl: (51) SSL: no alternative certificate subject name matches target host name 'wrong.host.badssl.com'

In Python können wir das mit der Python requests library machen. Ein falscher Host gibt die Ausnahme:

    ...
    request.get(...)
    ...

Ergebnis:

HTTPSConnectionPool(host='wrong.host.badssl.com', port=443): Max retries exceeded with url: / (Caused by SSLError(CertificateError("hostname 'wrong.host.badssl.com' doesn't match either of '*.badssl.com', 'badssl.com'",),))

Beachten Sie, dass die Exception 'hostname ... doesn't match' nur erscheint, wenn die anderen Tests erfolgreich sind. Wenn es irgendwo ein Problem mit den Zertifikaten gibt, erhalten Sie eine Exception 'certificate verify failed'.

Die Klasse CertInfo()

Ich habe etwas Python -Code geschrieben, um damit zu spielen. Am wichtigsten ist die Klasse CertInfo(). Hier dekodiere ich das Zertifikat.

# cert_info.py

import  datetime
from  OpenSSL  import crypto


class  CertInfo:
    
    def __init__(
        self,
        cert=None,
        ):
        self.cert = cert

    def decode_x509name_obj(self, o):
        parts = []
        for c in o.get_components():
            parts.append(c[0].decode('utf-8')  +  '='  +  c[1].decode('utf-8'))
        return ', '.join(parts)

    def cert_date_to_gmt_date(self, d):
        return  datetime.datetime.strptime(d.decode('ascii'), '%Y%m%d%H%M%SZ')

    def cert_date_to_gmt_date_string(self, d):
        return self.cert_date_to_gmt_date(d).strftime("%Y-%m-%d %H:%M:%S GMT")

    def get_item(self, item, extension=None, return_as=None, algo=None):
        try:
            if item == 'subject':
                return self.decode_x509name_obj(self.cert.get_subject())

            elif item == 'subject_o':
                return self.cert.get_subject().O.strip()

            elif item == 'subject_cn':
                return self.cert.get_subject().CN.strip()

            elif item == 'extensions':
                ext_count = self.cert.get_extension_count()
                if extension is  None:
                    ext_infos = []
                    for i in range (0, ext_count):
                        ext = self.cert.get_extension(i)
                        ext_infos.append(ext.get_short_name().decode('utf-8'))
                    return ext_infos

                for i in range (0, ext_count):
                    ext = self.cert.get_extension(i)
                    if extension in str(ext.get_short_name()):
                        return ext.__str__().strip()
                return  None

            elif item == 'version':
                return self.cert.get_version()

            elif item == 'pubkey_type':
                pk_type = self.cert.get_pubkey().type()
                if pk_type == crypto.TYPE_RSA:
                    return 'RSA'
                elif pk_type == crypto.TYPE_DSA:
                    return 'DSA'
                return 'Unknown'

            elif item == 'pubkey_pem':
                return crypto.dump_publickey(crypto.FILETYPE_PEM, self.cert.get_pubkey()).decode('utf-8')

            elif item == 'serial_number':
                return self.cert.get_serial_number()

            elif item == 'not_before':
                not_before = self.cert.get_notBefore()
                if return_as == 'string':
                    return self.cert_date_to_gmt_date_string(not_before)
                return self.cert_date_to_gmt_date(not_before)

            elif item == 'not_after':
                not_after = self.cert.get_notAfter()
                if return_as == 'string':
                    return self.cert_date_to_gmt_date_string(not_after)
                return self.cert_date_to_gmt_date(not_after)

            elif item == 'has_expired':
                return self.cert.has_expired()

            elif item == 'issuer':
                return self.decode_x509name_obj(self.cert.get_issuer())

            elif item == 'issuer_o':
                return self.cert.get_issuer().O.strip()

            elif item == 'issuer_cn':
                return self.cert.get_issuer().CN.strip()

            elif item == 'signature_algorithm':
                return self.cert.get_signature_algorithm().decode('utf-8')

            elif item == 'digest':
                # ['md5', 'sha1', 'sha256', 'sha512']
                return self.cert.digest(algo)

            elif item == 'pem':
                return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert).decode('utf-8')

            else:
                return  None

        except Exception as e:
            logger.error('item = {}, exception, e = {}'.format(item, e))
            return  None

    @property
    def subject(self):
        return self.get_item('subject')

    @property
    def subject_o(self):
        return self.get_item('subject_o')

    @property
    def subject_cn(self):
        return self.get_item('subject_cn')

    @property
    def subject_name_hash(self):
        return self.get_item('subject_name_hash')

    @property
    def extension_count(self):
        return self.get_item('extension_count')

    @property
    def extensions(self):
        return self.get_item('extensions')

    @property
    def extension_basic_constraints(self):
        return self.get_item('extensions', extension='basicConstraints')

    @property
    def extension_subject_key_identifier(self):
        return self.get_item('extensions', extension='subjectKeyIdentifier')

    @property
    def extension_authority_key_identifier(self):
        return self.get_item('extensions', extension='authorityKeyIdentifier')

    @property
    def extension_subject_alt_name(self):
        return self.get_item('extensions', extension='subjectAltName')

    @property
    def version(self):
        return self.get_item('version')

    @property
    def pubkey_type(self):
        return self.get_item('pubkey_type')

    @property
    def pubkey_pem(self):
        return self.get_item('pubkey_pem')

    @property
    def serial_number(self):
        return self.get_item('serial_number')

    @property
    def not_before(self):
        return self.get_item('not_before')

    @property
    def not_before_s(self):
        return self.get_item('not_before', return_as='string')

    @property
    def not_after(self):
        return self.get_item('not_after')

    @property
    def not_after_s(self):
        return self.get_item('not_after', return_as='string')

    @property
    def has_expired(self):
        return self.get_item('has_expired')

    @property
    def issuer(self):
        return self.get_item('issuer')

    @property
    def issuer_o(self):
        return self.get_item('issuer_o')

    @property
    def issuer_cn(self):
        return self.get_item('issuer_cn')

    @property
    def signature_algorithm(self):
        return self.get_item('signature_algorithm')

    @property
    def digest_sha256(self):
        return self.get_item('digest', algo='sha256')

    @property
    def pem(self):
        return self.get_item('pem')

Nicht alle Eigenschaften des Zertifikats sind verfügbar. Zum Beispiel habe ich keinen einfachen Weg gefunden, um die Signatur zu erhalten.

Um die Elemente des Zertifikats zu drucken:

    def print_cert_items(self, cert_id, cert):
            
        def format_cert_items(m):
            return '{}: {}'.format(m[0], m[1])

        ci =  CertInfo(cert)

        cert_items = [
            ('Subject', ci.subject),
            ('Subject-CN', ci.subject_cn),
            ('Subject name hash', ci.subject_name_hash),
            ('Issuer', ci.issuer),
            ('Issuer-CN', ci.issuer_cn),
            ('Extensions', ci.extensions),
            ('Extension-basicConstraints', ci.extension_basic_constraints),
            ('Extension-subjectKeyIdentifier', ci.extension_subject_key_identifier),
            ('Extension-authorityKeyIdentifier', ci.extension_authority_key_identifier),
            ('Extension-subjectAltName (SAN)', ci.extension_subject_alt_name),
            ('Version', ci.version),
            ('Serial_number', ci.serial_number),
            ('Public key-type', ci.pubkey_type),
            ('Public key-pem', ci.pubkey_pem),
            ('Not before', ci.not_before_s),
            ('Not after', ci.not_after_s),
            ('Has expired', ci.has_expired),
            ('Signature algortihm', ci.signature_algorithm),
            ('Digest-sha256', ci.digest_sha256),
            ('PEM', ci.pem),
        ]

        print('{}'.format(cert_id))
        cert_item_lines = map(format_cert_items, cert_items)
        print('{}'.format('\n'.join(cert_item_lines)))

Zusammenfassung

Dies war ein langer Weg. Ich suchte nach einer Python pyOpenSSL only Lösung, um die Chain of Trust ohne subprocess zu verifizieren und stieß die ganze Zeit auf Seiten, die mir sagten, dass dies nicht mit pyOpenSSL only möglich ist. Daraufhin überprüfte ich das pyOpenSSL -Repository auf Github.com und es hieß, dass dies erst kürzlich hinzugefügt worden sei. Die changelog zeigte die Parameter und Methoden und nach dem Lesen der Docs war es einfach zu implementieren.

Ich fand auch Möglichkeiten, die Zertifikatskette zu prüfen, ob ein Zertifikat selbstsigniert oder abgelaufen ist und eine Möglichkeit, auf einen falschen Host zu prüfen. Jetzt geht es an die Erweiterung meines Server-Check-Skripts ...

Links / Impressum

[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl
https://mail.badssl.com/pipermail/cryptography-dev/2016-August/000676.html

certvalidator
https://github.com/wbond/certvalidator

Chain of Fools: An Exploration of Certificate Chain Validation Mishaps
https://duo.com/labs/research/chain-of-fools

Cheat Sheet - OpenSSL
https://megamorf.gitlab.io/cheat-sheets/openssl/

Digital Certificates: Chain of Trust
https://www.youtube.com/watch?v=heacxYUnFHA

Get or build PEM certificate chain in Python
https://stackoverflow.com/questions/51039393/get-or-build-pem-certificate-chain-in-python

Get your certificate chain right
https://medium.com/@superseb/get-your-certificate-chain-right-4b117a9c0fce

How to know if certificate is self-signed
https://security.stackexchange.com/questions/93162/how-to-know-if-certificate-is-self-signed/93163

How to validate / verify an X509 Certificate chain of trust in Python?
https://stackoverflow.com/questions/30700348/how-to-validate-verify-an-x509-certificate-chain-of-trust-in-python

PyOpenSSL - how can I get SAN(Subject Alternative Names) list
https://stackoverflow.com/questions/49491732/pyopenssl-how-can-i-get-sansubject-alternative-names-list

ssl-check.py
https://gist.github.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c

Use openssl to individually verify components of a certificate chain
https://security.stackexchange.com/questions/118062/use-openssl-to-individually-verify-components-of-a-certificate-chain

Verify a certificate chain using openssl verify
https://stackoverflow.com/questions/25482199/verify-a-certificate-chain-using-openssl-verify

X509StoreContext objects
https://www.pyopenssl.org/en/stable/api/crypto.html#x509storecontext-objects

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare (2)

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.

avatar

Hello,
Very interesting. Could you share full script/class? It looks truncated.
Regards!

avatar

This example is not complete. So I re-iterate the previous comment. Please could you share the full script/class?