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

Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren

Vanaf november 2020 kan de vertrouwensketen worden geverifieerd zonder OpenSSL te bellen met Python's subprocess.

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

Tijdens het schrijven van een script om te controleren of websites correct zijn omgeleid naar 'https:/www'. Ik dacht om ook wat SSL-certificaatcontroles toe te voegen. Dit betekent dat ik SSL-certificaten die van een host zijn gedownload, moest controleren. Is het certificaat echt voor deze website? Laat me de vervaldatum zien. Is de certificaatketen correct? En kunnen we het certificaat (de certificaten) vertrouwen?

In eerste instantie kwam ik vast te zitten waar veel mensen vast kwamen te zitten door het volgende. De tussenliggende certificaten die u van een host downloadt zijn niet te vertrouwen en pyOpenSSL heeft de 'untrusted' vlag niet gebruikt bij de controle van een certificaatketen. Dit betekent dat het certificaat wel te vertrouwen is terwijl het dat niet is.

De enige manier om dit te doen was om het commando OpenSSL te verifiëren met behulp van subprocess. Niet wat we willen, maar we konden het tenminste doen. Er zijn andere manieren, maar die zijn veel complexer. Dit werd gerapporteerd en besproken in 2016. Met de release van pyOpenSSL 20.0.0 (2020-11-27) werden de volgende wijzigingen aangebracht:

  • een nieuwe methode 'load_locations()' naar X509Store om trusted certificate file bundles en/of directories in te stellen
  • een nieuwe parameter 'keten' aan X509StoreContext waar u untrusted -certificaten kunt toevoegen
  • een nieuwe methode 'get_verified_chain()' naarX509StoreContext die de volledige gevalideerde keten retourneert

Met deze veranderingen kunnen we nu eindelijk de vertrouwensketen verifiëren.

Python en cryptografie is niet eenvoudig.

Websites, diensten zijn verhuisd van HTTP naar HTTPS. Dit betekent dat wanneer u verbinding maakt met een dienst, u ook het certificaat of de certificaten moet controleren. Doet Python dit voor u? De Python requests library doet dit automatisch voor u. Je hoeft niet eens een parameter als 'verify=True' toe te voegen.

Maar hier zoek ik naar een manier om de SSL-certificaten in mijn eigen Python script te controleren. Hieronder beschrijf ik enkele manieren om dit te doen en wat Python code die ik heb geschreven om dit te onderzoeken. Deze draait op mijn Ubuntu 18.04 PC.

Relevant lezen, kijken

Misschien wilt u beginnen met het lezen van de artikelen 'Chain of Fools: An Exploration of Certificate Chain Validation Mishaps', en '[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl', en het bekijken van een mooie video 'Digital Certificates: Chain of Trust', zie onderstaande links.

De CertInfo klasse

In de onderstaande voorbeelden gebruik ik een klasse CertInfo(). Dit is een klasse die ik heb geschreven om informatie uit een certificaat te halen. De code Python staat aan het einde van dit artikel.

De certificaatketen

Een certificatenketen is een gekoppelde lijst van certificaten. In elk certificaat staan twee items die aangeven hoe ze gekoppeld zijn:

  • Subject-CN (gewone naam)
  • Issuer-CN (gewone naam)

Te beginnen met het servercertificaat, wordt het afgegeven door de Issuer-CN. Het servercertificaat wordt ook wel end-entity-certificaat, bladcertificaat of abonneecertificaat genoemd. Het volgende certificaat dat we downloaden moet een Subject-CN hebben die identiek is aan het servercertificaat Issuer-CN, enz.

Hieronder is het eerste certificaat dat gedownload is van de host host_cert[0], het tweede host_cert[1], etc. Het laatste certificaat is het root-certificaat 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

Voorbeeld#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

Aan de hand van deze gegevens kunnen we controleren of de keten die begint bij het servercertificaat eindigt bij het basiscertificaat. Als we willen controleren of de certificaten correct zijn geconfigureerd, is het essentieel dat we dit doen omdat OpenSSL deze volgorde niet controleert! Merk op dat www.badssl.com één tussenliggend certificaat heeft en two-intermediate-certs-example.org en www.example.org twee tussenliggende certificaten hebben. www.example.org is een speciaal geval, ik zal dit hieronder bespreken.

Hier is Python code om de certificaatketen te controleren:

    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

Het verkrijgen van het basiscertificaat

Om de certificaatketen te controleren hebben we ook het basiscertificaat nodig. In veel gevallen kunt u een CA root pad opnemen in een directory op uw apparaat en de verificatie-functie zal het root-certificaat automatisch opzoeken. Op mijn PC kan ik de OpenSSL CApath parameter instellen op:

/etc/ssl/certs

Maar wat als we het CA root certficaat willen krijgen voor gebruik in de certificaat ketenverificatie functie zoals hierboven beschreven? Ik heb niet gezien hoe ik dit kan doen met pyOpenSSL maar het kan op andere manieren.

Ten eerste kunnen we dit doen met OpenSSL en subprocess. Het commando OpenSSL is:

openssl  x509 -noout -issuer_hash -in cert.pem

waarbij cert.pm het laatste (tussenliggende) certificaat is dat van de host wordt opgehaald (aangenomen dat de certificaatvolgorde correct is). Er zijn enkele beperkingen, maar in de meeste gevallen zal dit werken. Dit geeft een hexadecimaal getal als:

4a6481c9

Met een '.0' als bijlage is dit een symlink voor het basiscertificaat:

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

Resultaat:

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

Als u wilt weten waarom dit zo werkt, zie dan het hulpprogramma c_rehash dat bij OpenSSL wordt geleverd. Dit bouwt een hash voor het snel opzoeken van root-certificaten.

In Python gebruik ik subprocess om het commando OpenSSL uit te voeren, en schrijf dan het rootbestand in mijn directory voor gemakkelijke toegang. Merk op dat ik STDIN gebruik om de PEM naar OpenSSL te voeren en STDOUT gebruik om het resultaat vast te leggen.

    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)

Een andere manier om het hoofdcertificaat te verkrijgen als we een hoofdcertificaatbundel op ons systeem hebben, is om deze bundel te downloaden, bijvoorbeeld van de Curl website:

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

Dit bestand bevat de CA root PEMs. Als we dit willen gebruiken moeten we de certificaten uitpakken en vervolgens een woordenboek maken met (index)sleutel Subject-CN en de waarde PEM. De eerste keer dat we dit bouwen en opslaan in een bestand (met behulp van Pickle), de volgende keer dat we dit bestand laden, indexeren we de directory en geven we de PEM van het CA root certificaat terug.

    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)

Ook hier is CertInfo() een klasse die ik heb geschreven om informatie uit een certificaat te halen. De code Python staat aan het einde van dit artikel.

Controle van de vertrouwensketen

Het is niet voldoende om te controleren of het servercertificaat gekoppeld is aan een root-certificaat op uw PC, telefoon, apparaat. Er is ook wat men noemt 'Chain of Trust'. Certificaten die we downloaden van een host zijn niet te vertrouwen. Het enige certificaat dat wel te vertrouwen is, is het root-certificaat dat op uw apparaat, PC, in een speciale directory staat, op mijn Ubuntu PC:

/etc/ssl/certs

Met behulp van het root-certificaat kunnen we controleren of we het volgende (tussenliggende) certificaat kunnen vertrouwen. Indien vertrouwd gebruiken we dit intermediaire certificaat en controleren we of het volgende certificaat te vertrouwen is. Dit doen we tot we bij het servercertificaat aankomen.

Het OpenSSL commando voor de www.badssl.com website:

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

Als we het root-certificaat hebben, kunnen we dat doen:

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

De vlag 'untrusted' vertelt OpenSSL dat 1.pem niet te vertrouwen is en moet worden vertrouwd voordat 0.pem wordt gecontroleerd. Similary, we kunnen de keten controleren voor de two-intermediate-certs-example.org website die twee tussenliggende certificaten heeft:

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

Merk op dat als u de tussenliggende certificaten 1.pem en 2.pem omwisselt voor de website two-intermediate-certs-example.org , het resultaat in beide gevallen hetzelfde is. Dit betekent dat OpenSSL eerst de volgorde van de keten probeert te vinden en vervolgens de vertrouwensoperatie start. In dit geval kunnen we ook de twee tussenliggende certificaten samenvoegen tot 12.pem en laten lopen:

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

De SSL-certificaten die ik van de host heb gedownload staan in self.host_certs en zijn ook opgeslagen in bestanden 0.pem, 1.pem, ... Met behulp van Python's subprocess is de 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

Dit is lelijk en vereist dat we eerst PEM bestanden opslaan voordat we subprocess oproepen.

Sinds pyOpenSSL 20.0.0 kunnen we dit veel eleganter doen:

    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

De verify_certificate() methode genereert een uitzondering is als de keten niet geverifieerd kan worden.

De vertrouwensketen, stap voor stap

Hierboven hebben we de Chain of Trust gecontroleerd met een enkel OpenSSL commando. Maar we kunnen dit ook op een andere manier doen. Beginnend met het vertrouwde root-certificaat op mijn PC proberen we eerst het laatste tussenliggende certificaat van de host te vertrouwen. Als dit voorbij is, gebruiken we het nu vertrouwde tussenliggende certificaat om te proberen het volgende untrusted certificaat te vertrouwen tot aan het servercertificaat. Met OpenSSL gebruiken we de vlag 'partial_chain'.

Voor de website two-intermediate-certs-example.org kunnen we bijvoorbeeld de volgende commando's geven:

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

Zelf ondertekende (tussen)certificaten

Wanneer een tussentijds certificaat zelf wordt ondertekend, stopt OpenSSL met valideren(?). Dit betekent dat we willen controleren of een certificaat dat van de host is gedownload zelf ondertekend is.

Een certificaat is zelf ondertekend wanneer:

  • de Subject-CN en de Issuer-CN overeenkomen.
  • de subjectKeyIdentifier en de authorityKeyIdentifier wedstrijd
  • het certificaat bevat een sleutelgebruiksextensie met de KU_KEY_CERT_SIGN bitreeks

Ik heb alleen de eerste twee geïmplementeerd, geen idee op het moment dat ik het derde punt moet doen.

Tijdens het testen vond ik voor self-signed.badssl.com:

subjectKeyIdentifier:  None
authorityKeyIdentifier:  None

maar voor 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

Ik neem aan dat ik hier de leidende 'keyid:' moet verwijderen voordat ik de subjectKeyIdentifier vergelijk met de 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

Meer over zelf-ondertekende certificaten is te vinden in het artikel 'How to know if certificate is self-signed', zie onderstaande links.

Hier wordt ook verwezen naar RFC 3280:

Een certificaat wordt zelf uitgegeven als de DN's die in het onderwerp en het veld van de emittent staan, identiek zijn en niet leeg zijn. In het algemeen zijn de emittent en het onderwerp van de certificaten die een pad vormen voor elk certificaat verschillend. Een CA kan echter een certificaat aan zichzelf uitgeven ter ondersteuning van key rollover of wijzigingen in het certificaatbeleid. Deze zelf uitgegeven certificaten worden niet meegeteld bij de evaluatie van de padlengte of de naambeperkingen.

Het veld keyIdentifier van de uitbreiding authorityKeyIdentifier MOET worden opgenomen in alle certificaten die worden gegenereerd door conforme CA's om de constructie van het certificeringspad te vergemakkelijken. Er is één uitzondering; wanneer een CA zijn openbare sleutel verspreidt in de vorm van een "zelf ondertekend" certificaat, mag de sleutelidentificatiecode van de autoriteit worden weggelaten. De handtekening op een zelfondertekend certificaat wordt gegenereerd met de privésleutel die bij de openbare sleutel van het certificaat hoort. (Dit bewijst dat de emittent zowel de openbare als de particuliere sleutel bezit.) In dit geval zouden het onderwerp en de autoriteitssleutel identiek zijn, maar alleen het onderwerpssleutelsymbool is nodig voor de opbouw van het certificeringspad.

Verwarring met example.com, example.org

Tijdens de tests heb ik ook example.com en example.org gebruikt. De gastheer gaf drie certificaten terug. Ik controleerde het resultaat door certificaten te vervangen en te kijken wat er gebeurt.

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

Het was een puinhoop. Ik kon tussenliggende certificaten 2.pem en OpenSSL nog verwijderen: OK. Ik kon 1.pem vervangen door een ander willekeurig certificaat en OpenSSL zei nog: OK. Wat was hier aan de hand?

Toen ik naar de certificaten keek die de host terugstuurde, vond ik dat het derde certificaat, 2.pem, zelf ondertekend was en identiek was aan het basiscertificaat op mijn PC. Dit betekent dat deze domeinen mij een hoofdcertificaat sturen? OpenSSL klaagt niet, browsers klagen niet.

Ik heb dit niet verder onderzocht, maar ik dacht dit te vermelden om hoofdpijn te voorkomen ... :-(

De verkeerde host check

De verkeerde gastheercontrole moet ervoor zorgen dat het certificaat voor deze gastheer is. Ik zag niet hoe dit met pyOpenSSL kan worden gedaan, in feite zag ik geen manier om dit met OpenSSL te doen. OpenSSL waarschuwt u niet als het certificaat voor een andere host is.

We kunnen Wget gebruiken:

wget  wrong.host.badssl.com

Resultaat:

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

Of we kunnen Curl gebruiken:

curl -L  wrong.host.badssl.com

Resultaat:

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

In Python kunnen we dit doen met de Python requests library. Een verkeerde host geeft de uitzondering:

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

Resultaat:

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

Merk op dat de uitzondering 'hostname ... doesn't match' alleen verschijnt als andere tests slagen. Als er ergens een probleem is met de certificaten krijgt u een Exception 'certificate verify failed'.

De klasse CertInfo()

Ik heb wat Python code geschreven om hiermee te spelen. Het belangrijkste is de CertInfo() klasse. Hier decodeer ik het certificaat.

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

Niet alle eigenschappen van het certificaat zijn beschikbaar. Ik heb bijvoorbeeld geen gemakkelijke manier gevonden om de Signature te krijgen.

Om het certificaat af te drukken:

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

Samenvatting

Dit was een lange reis. Ik was op zoek naar een Python pyOpenSSL enige oplossing om de Chain of Trust te verifiëren zonder subprocess en kwam de hele tijd pagina's tegen die me vertelden dat dit niet mogelijk was met alleen pyOpenSSL . Hierna controleerde ik de pyOpenSSL repository op Github.com en er stond dat dit zeer recentelijk was toegevoegd. De changelog toonde de parameters en methoden en na het lezen van de documenten was het eenvoudig te implementeren.

Ik heb ook manieren gevonden om de certificaatketen te controleren, of een certificaat zelf ondertekend is, verlopen is en een manier om te controleren op een verkeerde host. Nu op naar het uitbreiden van mijn server check script ...

Links / credits

[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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen (2)

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.

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?