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

Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte

A partir de novembre 2020, la chaîne de confiance peut être vérifiée sans appeler OpenSSL avec Python's subprocess.

17 décembre 2020
post main image
https://unsplash.com/@ilhamfzn

En écrivant un script pour vérifier si les sites web sont correctement redirigés vers "https:/www. J'ai pensé à ajouter quelques contrôles de certificats SSL également. Cela signifie que j'ai dû vérifier les certificats SSL téléchargés depuis un hôte. Le certificat est-il vraiment pour ce site web ? Montrez-moi la date d'expiration. La chaîne de certificats est-elle correcte ? Et peut-on faire confiance au(x) certificat(s) ?

Au départ, je me suis retrouvé coincé là où beaucoup de gens se sont retrouvés coincés pour les raisons suivantes. Les certificats intermédiaires que vous téléchargez à partir d'un hôte ne sont pas fiables et pyOpenSSL n'a pas utilisé l'indicateur "untrusted" lors de la vérification d'une chaîne de certificats. Cela signifie que le certificat peut être marqué comme fiable alors qu'il ne l'est pas.

La seule solution était d'exécuter la commande de vérification OpenSSL en utilisant subprocess. Ce n'est pas ce que nous voulons, mais au moins nous pouvons le faire. Il existe d'autres moyens, mais ils sont beaucoup plus complexes. Cela a été signalé et discuté en 2016. Avec la publication de pyOpenSSL 20.0.0 (2020-11-27), les changements suivants ont été apportés :

  • une nouvelle méthode "load_locations()" dans X509Store pour définir des paquets et/ou des répertoires de fichiers de certificats de confiance
  • un nouveau paramètre "chain" dans le X509StoreContext où vous pouvez ajouter des certificats untrusted
  • une nouvelle méthode 'get_verified_chain()' àX509StoreContext renvoyant la chaîne complète validée

Grâce à ces changements, nous pouvons enfin vérifier la chaîne de confiance.

Python et la cryptographie n'est pas facile

Les sites web et les services sont passés de HTTP à HTTPS. Cela signifie que lorsque vous vous connectez à un service, vous devez également vérifier le(s) certificat(s). Est-ce que Python le fait pour vous ? Le Python requests library le fait automatiquement pour vous. Vous n'avez même pas besoin d'ajouter un paramètre comme "verify=True".

Mais je cherche ici un moyen de vérifier les certificats SSL dans mon propre script Python . Je décris ci-dessous quelques moyens de le faire et un code Python que j'ai écrit pour étudier la question. Il fonctionne sur mon PC Ubuntu 18.04.

Lire, regarder

Vous pouvez commencer par lire les articles "Chain of Fools: An Exploration of Certificate Chain Validation Mishaps" et "[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl", et regarder une belle vidéo "Digital Certificates: Chain of Trust", voir les liens ci-dessous.

La classe CertInfo

Dans les exemples ci-dessous, j'utilise une classe CertInfo(). Il s'agit d'une classe que j'ai écrite pour extraire des informations d'un certificat. Le code Python se trouve à la fin de cet article.

La chaîne de certificats

Une chaîne de certificats est une liste liée de certificats. Chaque certificat comporte deux éléments qui précisent comment ils sont liés :

  • Subject-CN (nom commun)
  • Issuer-CN (nom commun)

A partir du certificat du serveur, il est délivré par le Issuer-CN. Le certificat de serveur est également appelé certificat d'entité finale, certificat de feuille ou certificat d'abonné. Le certificat suivant que nous avons téléchargé doit avoir un Subject-CN identique au Issuer-CN du certificat de serveur, etc.

Ci-dessous, le premier certificat téléchargé de l'hôte est host_cert[0], le second host_cert[1], etc. Le dernier certificat est le certificat racine 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

Exemple n° 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

Grâce à ces données, nous pouvons vérifier si la chaîne qui commence au certificat du serveur se termine au certificat racine. Si nous voulons vérifier si les certificats sont correctement configurés, il est essentiel que nous le fassions car OpenSSL ne vérifie pas cette séquence ! Notez que www.badssl.com a un certificat intermédiaire et que two-intermediate-certs-example.org et www.example.org ont deux certificats intermédiaires. www.example.org est un cas particulier, dont je parlerai plus loin.

Voici le code Python pour vérifier la chaîne de certificats :

    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

Obtenir le certificat racine

Pour vérifier la chaîne de certificats, nous avons également besoin du certificat racine. Dans de nombreux cas, vous pouvez inclure un chemin CA root vers un répertoire de votre appareil et la fonction de vérification recherchera automatiquement le certificat racine. Sur mon PC, je peux régler le paramètre OpenSSL CApath sur :

/etc/ssl/certs

Mais que faire si nous voulons obtenir le certificat CA root pour l'utiliser dans la fonction de vérification de la chaîne de certificats comme décrit ci-dessus ? Je n'ai pas vu comment faire avec pyOpenSSL mais cela peut se faire d'une autre manière.

Tout d'abord, nous pouvons le faire avec OpenSSL et subprocess. La commande OpenSSL est :

openssl  x509 -noout -issuer_hash -in cert.pem

où cert.pm est le dernier certificat (intermédiaire) récupéré auprès de l'hôte (en supposant que l'ordre des certificats est correct). Il existe certaines limitations, mais dans la plupart des cas, cela fonctionne. Cela renvoie un nombre hexadécimal comme :

4a6481c9

Avec un ".0" ajouté, il s'agit d'un symlink au certificat racine :

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

Résultat :

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

Si vous voulez savoir pourquoi cela fonctionne ainsi, consultez l'utilitaire c_rehash qui est fourni avec OpenSSL. Il crée un hachage pour une recherche rapide des certificats racine.

Dans Python , j'utilise subprocess pour exécuter la commande OpenSSL , puis j'écris le fichier racine dans mon répertoire pour y accéder facilement. Notez que j'utilise STDIN pour transmettre la commande PEM à OpenSSL et que j'utilise STDOUT pour saisir le résultat.

    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)

Une autre façon d'obtenir le certificat racine si nous avons un paquet de certificats racine sur notre système est de télécharger ce paquet, par exemple à partir du site web Curl :

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

Ce fichier contient le CA root PEMs. Si nous voulons l'utiliser, nous devons extraire les certificats et ensuite construire un dictionnaire avec la clé (index) Subject-CN et la valeur PEM. La première fois que nous construisons et stockons ce dictionnaire dans un fichier (en utilisant Pickle), les fois suivantes, nous chargeons ce fichier, indexons le répertoire et renvoyons la valeur PEM du certificat CA root .

    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)

Là encore, CertInfo() est une classe que j'ai écrite pour extraire des informations d'un certificat. Le code Python se trouve à la fin de cet article.

Vérification de la chaîne de confiance

Il ne suffit pas de vérifier si le certificat du serveur est lié à un certificat racine sur votre PC, votre téléphone, votre appareil. Il existe également ce que l'on appelle la "chaîne de confiance". On ne peut pas faire confiance aux certificats que l'on télécharge d'un hôte. Le seul certificat qui soit fiable est le certificat racine qui se trouve sur votre appareil, PC, dans un répertoire spécial, sur mon PC Ubuntu :

/etc/ssl/certs

Le certificat racine nous permet de vérifier si nous pouvons faire confiance au certificat suivant (intermédiaire). Si c'est le cas, nous utilisons ce certificat intermédiaire et vérifions si le certificat suivant est fiable. Nous faisons cela jusqu'à ce que nous arrivions au certificat du serveur.

La commande OpenSSL pour le site web www.badssl.com :

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

Si nous avons le certificat racine, nous pouvons le faire :

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

L'indicateur "untrusted" indique à OpenSSL que 1.pem n'est pas fiable et doit être fiable avant de vérifier 0.pem. De même, nous pouvons vérifier la chaîne pour le site web two-intermediate-certs-example.org qui possède deux certificats intermédiaires :

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

Notez que si vous échangez les certificats intermédiaires 1.pem et 2.pem contre le site web two-intermediate-certs-example.org , le résultat est le même dans les deux cas. Cela signifie que OpenSSL essaie d'abord de trouver l'ordre de la chaîne, puis lance l'opération de confiance. Dans ce cas, nous pouvons également concaténer les deux certificats intermédiaires dans 12.pem et l'exécuter :

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

Les certificats SSL que j'ai téléchargés de l'hôte se trouvent dans self.host_certs et sont également stockés dans les fichiers 0.pem, 1.pem, ... En utilisant le code Python, on obtient le code subprocess :

    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

C'est moche et cela nous oblige à stocker d'abord les fichiers PEM avant d'appeler subprocess.

Depuis pyOpenSSL 20.0.0 , nous pouvons faire cela de manière beaucoup plus élégante :

    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

La méthode verify_certificate() génère une exception si la chaîne ne peut être vérifiée.

La chaîne de confiance, étape par étape

Ci-dessus, nous avons vérifié la chaîne de confiance avec une seule commande OpenSSL . Mais nous pouvons également le faire d'une autre manière. En commençant par le certificat racine de confiance sur mon PC, nous essayons d'abord de faire confiance au dernier certificat intermédiaire de l'hôte. Si cela réussit, nous utilisons le certificat intermédiaire de confiance pour essayer de faire confiance au prochain certificat untrusted jusqu'au certificat du serveur. Avec le certificat OpenSSL , nous utilisons le drapeau "partial_chain".

Par exemple, pour le site web two-intermediate-certs-example.org , nous pouvons émettre les commandes suivantes

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

Certificats (intermédiaires) auto-signés

Lorsqu'un certificat intermédiaire est auto-signé, OpenSSL arrête la validation( ?). Cela signifie que nous voulons vérifier si un certificat téléchargé depuis l'hôte est auto-signé.

Un certificat est auto-signé quand :

  • le Subject-CN et le Issuer-CN correspondent
  • la correspondance entre subjectKeyIdentifier et authorityKeyIdentifier
  • le certificat contient une extension d'utilisation de la clé avec le jeu de bits KU_KEY_CERT_SIGN

Je n'ai mis en œuvre que les deux premiers, sans savoir pour l'instant comment faire le troisième point.

En testant, j'ai trouvé pour self-signed.badssl.com :

subjectKeyIdentifier:  None
authorityKeyIdentifier:  None

mais pour 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

Je suppose que je dois supprimer ici le "keyid" de tête avant de comparer le subjectKeyIdentifier avec le authorityKeyIdentifier.

    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

Pour en savoir plus sur les certificats auto-signés, consultez l'article "How to know if certificate is self-signed", voir les liens ci-dessous.

Ils se réfèrent également à l'article RFC 3280 :

Un certificat est auto-émis si les DN qui apparaissent dans les champs "Objet" et "Émetteur" sont identiques et ne sont pas vides. En général, l'émetteur et le sujet des certificats qui constituent un chemin d'accès sont différents pour chaque certificat. Toutefois, une AC peut se délivrer un certificat à elle-même pour permettre la reconduction de la clé ou la modification des politiques de certificat. Ces certificats auto-émis ne sont pas pris en compte lors de l'évaluation des contraintes de longueur de chemin ou de nom".

Le champ keyIdentifier de l'extension authorityKeyIdentifier DOIT être inclus dans tous les certificats générés par les AC conformes afin de faciliter la construction du chemin de certification. Il y a une exception : lorsqu'une AC distribue sa clé publique sous la forme d'un certificat "auto-signé", l'identificateur de clé d'autorité PEUT être omis. La signature sur un certificat auto-signé est générée avec la clé privée associée à la clé publique objet du certificat. (Cela prouve que l'émetteur possède à la fois la clé publique et la clé privée.) Dans ce cas, les identificateurs de la clé publique et de la clé d'autorité seraient identiques, mais seul l'identificateur de la clé publique est nécessaire pour la construction du chemin de certification".

Confusion avec example.com, example.org

Lors des tests, j'ai également utilisé les questions example.com et example.org. L'hôte m'a renvoyé trois certificats. Je vérifiais le résultat en remplaçant les certificats et je voyais ce qui se passait.

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

C'était le bordel. Je pouvais supprimer le certificat intermédiaire 2.pem et OpenSSL disait encore : OK. Je pouvais remplacer 1.pem par un autre certificat aléatoire et OpenSSL disait encore : OK : OK. Que se passait-il ici ?

J'ai ensuite regardé les certificats renvoyés par l'hôte et j'ai constaté que le troisième certificat, 2.pem, était auto-signé et était identique au certificat racine de mon PC. Cela signifie que ces domaines m'envoient un certificat racine ? OpenSSL ne se plaint pas, les navigateurs ne se plaignent pas.

Je n'ai pas approfondi cette question, mais j'ai pensé à la mentionner pour éviter les maux de tête... :-(

Le mauvais contrôle d'hôte

Le mauvais contrôle d'hôte doit s'assurer que le certificat est pour cet hôte. Je n'ai pas vu comment cela peut être fait avec pyOpenSSL, en fait je n'ai pas vu comment le faire avec OpenSSL. OpenSSL ne vous avertit pas si le certificat est destiné à un autre hôte.

Nous pouvons utiliser Wget :

wget  wrong.host.badssl.com

Résultat :

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

Ou, nous pouvons utiliser Curl :

curl -L  wrong.host.badssl.com

Résultat : Ou nous pouvons utiliser Curl : Résultat :

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

Dans Python , nous pouvons le faire avec le Python requests library. Un mauvais hôte donne l'exception :

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

Résultat :

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

Notez que l'exception "hostname ... doesn't match" n'apparaît que si les autres tests sont réussis. S'il y a un problème avec les certificats quelque part, vous obtenez l'exception "certificate verify failed".

La classe CertInfo()

J'ai écrit un code Python pour jouer avec ça. Le plus important est la classe CertInfo(). Ici, je décode le certificat.

# 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')

Toutes les propriétés du certificat ne sont pas disponibles. Par exemple, je n'ai pas trouvé de moyen facile d'obtenir la signature.

Pour imprimer les éléments du certificat :

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

Résumé

Ce fut un long voyage. Je cherchais une solution pour vérifier la chaîne de confiance sans Python pyOpenSSL seulement et je me suis heurté à des pages me disant que cela ne pouvait pas être fait avec pyOpenSSL seulement. Après cela, j'ai vérifié le dépôt pyOpenSSL sur Github.com et il m'a dit que cela avait été ajouté très récemment. Le dépôt changelog indiquait les paramètres et les méthodes et, après lecture des documents, il était facile à mettre en œuvre.

J'ai également trouvé des moyens de vérifier la chaîne de certificats, si un certificat est auto-signé, s'il a expiré et un moyen de vérifier s'il s'agit d'un mauvais hôte. J'en viens maintenant à l'extension de mon script de vérification de serveur ...

Liens / crédits

[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

En savoir plus...

Cryptography pyOpenSSL

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires (2)

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.

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?