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

Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста

С ноября 2020 года проверку Цепочки доверия можно проводить без вызова OpenSSL с Python's subprocess.

17 декабря 2020
post main image
https://unsplash.com/@ilhamfzn

Во время написания сценария, чтобы проверить, правильно ли сайты перенаправлены на 'https:/www'. Я также подумал добавить некоторые проверки SSL-сертификатов. Это означает, что я должен был проверить SSL сертификаты, загруженные с узла. Действительно ли сертификат для этого сайта? Покажите мне срок годности. Правильна ли цепочка сертификатов? И можем ли мы доверять сертификату (-ам)?

Изначально я застрял там, где многие застряли из-за следующего. Промежуточные сертификаты, которые вы загружаете с узла, нельзя доверять, а pyOpenSSL не использовал флаг 'untrusted' при проверке цепочки сертификатов. Это означает, что сертификат может быть помечен как доверенный, а не как таковой.

Единственным способом обойти это было выполнение команды OpenSSL с помощью subprocess. Не то, что мы хотим, но, по крайней мере, мы могли бы это сделать. Есть и другие способы, но они намного сложнее. Об этом сообщалось и обсуждалось в 2016 году. С выходом pyOpenSSL 20.0.0 (2020-11-27) были внесены следующие изменения:

  • новый метод 'load_locations()' в X509Store для установки пучков и/или каталогов доверенных файлов сертификатов
  • новый параметр 'chain' в X509StoreContext, куда вы можете добавить untrusted сертификаты
  • новый метод 'get_verified_chain()' toX509StoreContext, возвращающий полную подтвержденную цепочку

С этими изменениями мы наконец-то можем проверить Цепочку доверия.

Python и криптография - это нелегко.

Сайты, сервисы перешли с HTTP на HTTPS. Это означает, что когда вы подключаетесь к службе, вы также должны проверить сертификат(ы). Python делает это за вас? Python requests library делает это автоматически за вас. Вам даже не нужно добавлять параметр типа 'verify=True'.

Но здесь я ищу способ проверить SSL сертификаты в моем собственном Python скрипте. Ниже я описываю некоторые способы сделать это и некоторые Python код, который я написал, чтобы исследовать это. Он выполняется на моём Ubuntu 18.04 PC.

Соответствующее чтение, просмотр

Возможно, вы захотите начать с чтения статей 'Chain of Fools: An Exploration of Certificate Chain Validation Mishaps' и '[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl', а также просмотра хорошего видео 'Digital Certificates: Chain of Trust', смотрите ссылки ниже.

Класс CertInfo

В примерах ниже я использую класс CertInfo(). Это класс, который я написал для извлечения информации из сертификата. Код Python находится в конце этой статьи.

Цепь сертификатов

Цепочка сертификатов - это связанный список сертификатов. В каждом сертификате есть два элемента, которые указывают, как они связаны:

  • Subject-CN (общее имя)
  • Issuer-CN (общее имя)

Начиная с сертификата сервера, он выдается Issuer-CN. Сертификат сервера также называется сертификатом конечного пользователя, сертификатом листа или сертификатом абонента. Следующий загруженный нами сертификат должен иметь Subject-CN , идентичный сертификату сервера Issuer-CN и т.д.

Ниже приведен первый сертификат, загруженный с узла host_cert[0], второй host_cert[1] и т.д. Последним сертификатом является корневой сертификат 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

Пример №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

Используя эти данные, мы можем проверить, заканчивается ли цепочка, начинающаяся с сертификата сервера, на корневом сертификате. Если мы хотим проверить, правильно ли сконфигурированы сертификаты, то это необходимо сделать, потому что OpenSSL не проверяет эту последовательность! Обратите внимание, что www.badssl.com имеет один промежуточный сертификат, а two-intermediate-certs-example.org и www.example.org имеют два промежуточных сертификата. www.example.org - это особый случай, я расскажу об этом ниже.

Вот код Python для проверки цепочки сертификатов:

    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

Получение корневого сертификата

Для проверки цепочки сертификатов нам также нужен корневой сертификат. Во многих случаях вы можете включить путь CA root в директорию на вашем устройстве, и функция верификации будет автоматически искать корневой сертификат. На моем ПК я могу установить параметр OpenSSL CApath на значение CA root :

/etc/ssl/certs

Но что, если мы хотим получить CA root -сертификат для использования в функции проверки цепочки сертификатов, как описано выше? Я не видел, как это сделать с pyOpenSSL , но это можно сделать другими способами.

Во-первых, мы можем сделать это с OpenSSL и subprocess. Команда OpenSSL - это команда:

openssl  x509 -noout -issuer_hash -in cert.pem

где cert.pm является последним (промежуточным) сертификатом, полученным с узла (если порядок следования сертификатов правильный). Есть некоторые ограничения, но в большинстве случаев это будет работать. Это возвращает шестнадцатеричное число, например:

4a6481c9

С приложением '.0' это symlink к корневому сертификату:

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

Результат:

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

Если вы хотите знать, почему это работает так, смотрите утилиту c_rehash , которая идет вместе с OpenSSL. Это создает хэш для быстрого поиска корневых сертификатов.

В Python я использую subprocess для выполнения команды OpenSSL , а затем записываю корневой файл в свою директорию для легкого доступа. Обратите внимание, что я использую STDIN для подачи команды PEM на OpenSSL и использую STDOUT для получения результата.

    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)

Другим способом получения корневого сертификата, если у нас в системе есть корневой сертификат, является загрузка этого пакета, например, с веб сайта Curl :

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

Этот файл содержит CA root PEMs. Если мы хотим использовать это, мы должны извлечь сертификаты, а затем построить словарь с ключом (индексом) Subject-CN и значением PEM. При первом создании и хранении этого файла (используя Pickle), при следующей загрузке этого файла индексируем каталог и возвращаем PEM сертификата 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)

Снова CertInfo() - это класс, который я написал для извлечения информации из сертификата. Код Python находится в конце этой статьи.

Проверка цепочки доверия

Недостаточно проверить, связан ли сертификат сервера с корневым сертификатом на вашем ПК, телефоне, устройстве. Существует также то, что называется "Цепочка доверия". Сертификаты, которые мы загружаем с узла, нельзя доверять. Единственным сертификатом, которому можно доверять, является корневой сертификат, находящийся на вашем устройстве, ПК, в специальной директории, на моем Ubuntu PC:

/etc/ssl/certs

Используя корневой сертификат, мы можем проверить, можно ли доверять следующему (промежуточному) сертификату. Если доверяете, то мы используем этот промежуточный сертификат и проверяем, можно ли доверять следующему сертификату. Мы делаем это до тех пор, пока не получим сертификат сервера.

Команда OpenSSL для вебсайта www.badssl.com :

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

Если у нас есть корневой сертификат, мы можем это сделать:

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

Флаг 'untrusted' говорит OpenSSL , что 1.pem нельзя доверять и нужно доверять перед проверкой 0.pem. Симиляр, мы можем проверить цепочку для сайта two-intermediate-certs-example.org , который имеет два промежуточных сертификата:

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

Обратите внимание, что если вы поменяете промежуточные сертификаты 1.pem и 2.pem на two-intermediate-certs-example.org , то результат будет одинаковым в обоих случаях. Это означает, что OpenSSL сначала пытается найти цепной порядок, а затем начинает операцию доверия. В этом случае мы также можем скомпоновать два промежуточных сертификата в 12.pem и запустить их:

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

SSL-сертификаты, которые я скачал с узла, находятся в self.host_certs , а также хранятся в файлах 0.pem, 1.pem, ... Используя Python 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

Это уродливо и требует, чтобы мы сначала хранили файлы PEM перед вызовом subprocess.

Начиная с pyOpenSSL 20.0.0 мы можем делать это намного элегантнее:

    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

Метод verify_certificate() генерирует исключение, если цепочка не может быть проверена.

Цепочка доверия, шаг за шагом

Выше мы проверили цепочку доверия с помощью одной команды OpenSSL . Но мы можем сделать это и по-другому. Начиная с доверенного корневого сертификата на моем ПК, мы сначала пытаемся доверять последнему промежуточному сертификату с узла. Если это пройдет, мы используем теперь доверенный промежуточный сертификат, чтобы попытаться доверять следующему untrusted сертификату вплоть до сертификата сервера. С OpenSSL мы используем флаг 'partial_chain'.

Например, для веб сайта two-intermediate-certs-example.org мы можем выдать следующие команды:

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

Самоподписанные (промежуточные) сертификаты

Когда промежуточный сертификат является самоподписанным, OpenSSL останавливает валидацию(?). Это означает, что мы хотим проверить, является ли сертификат, загруженный с узла самоподписанным.

Сертификат является самоподписанным когда:

  • совпадение Subject-CN и Issuer-CN
  • совпадение subjectKeyIdentifier и authorityKeyIdentifier
  • сертификат содержит расширение использования ключа с набором битов KU_KEY_CERT_SIGN

Я реализовал только первые два, не имея представления о том, как сделать третий пункт.

Во время тестирования я нашел для self-signed.badssl.com:

subjectKeyIdentifier:  None
authorityKeyIdentifier:  None

но для 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

Полагаю, что перед сравнением subjectKeyIdentifier с authorityKeyIdentifier я должен удалить здесь ведущий 'keyid:'.

    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

Подробнее о самоподписных сертификатах можно найти в статье 'How to know if certificate is self-signed', смотрите ссылки ниже.

Здесь они также относятся к RFC 3280:

'Сертификат выдается самостоятельно, если DN, которые появляются в полях субъекта и эмитента, идентичны и не пусты. В общем случае, эмитент и субъект сертификатов, составляющих путь, различаются для каждого сертификата. Однако ЦС может выпустить сертификат для поддержки переноса ключей или изменений в политиках сертификатов. Эти самоиздаваемые сертификаты не учитываются при оценке длины пути или ограничений имени".

Поле keyIdentifier расширения authorityKeyIdentifier ДОЛЖНО быть включено во все сертификаты, генерируемые соответствующими CA для упрощения построения пути сертификации. Есть одно исключение: если УЦ распространяет свой открытый ключ в виде "самоподписанного" сертификата, то идентификатор ключа центра сертификации MAY опускается. Подпись на самоподписанном сертификате генерируется частным ключом, связанным с публичным ключом субъекта сертификата. (Это доказывает, что эмитент обладает как публичным, так и закрытым ключами.) В этом случае идентификаторы ключей субъекта и ключа центра будут идентичны, но для построения пути сертификации необходим только идентификатор ключа субъекта".

Путаница с example.com, example.org

Во время тестов я также использовал example.com и example.org. Хост вернул три сертификата. Я проверял результат путем замены сертификатов и посмотрел, что произойдет.

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

Был беспорядок. Я мог удалить промежуточный сертификат 2.pem и OpenSSL , но все равно сказал: ХОРОШО. Я мог заменить 1.pem другим случайным сертификатом и OpenSSL все равно сказал: OK: OK. Что здесь происходило?

Затем я посмотрел на сертификаты, возвращенные узлом, и обнаружил, что третий сертификат, 2.pem, был самоподписанным и идентичен корневому сертификату на моем ПК. Это означает, что эти домены прислали мне корневой сертификат? OpenSSL не жалуется, браузеры не жалуются.

Я не стал углубляться в это, но подумал, что стоит упомянуть об этом, чтобы избежать головной боли ... :-(

Не та проверка хоста

Неправильная проверка узла должна убедиться, что сертификат предназначен для этого узла. Я не видел, как это можно сделать с pyOpenSSL, на самом деле я не видел способа сделать это с OpenSSL. OpenSSL не предупреждает вас, если сертификат предназначен для другого узла.

Мы можем использовать Wget:

wget  wrong.host.badssl.com

Результат:

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

Или мы можем использовать Curl:

curl -L  wrong.host.badssl.com

Результат:

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

В Python мы можем использовать Python requests library. Неправильный хост дает исключение:

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

Результат:

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

Обратите внимание, что исключение 'hostname ... doesn't match' появляется только при прохождении других тестов. Если где-то есть проблема с сертификатами, вы получаете Исключение 'certificate verify failed'.

Класс CertInfo()

Я написал код Python , чтобы поиграть с этим. Самым важным является класс CertInfo(). Здесь я декодирую сертификат.

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

Не все свойства сертификата доступны. Например, я не нашел легкого способа получения Подписи.

Чтобы распечатать элементы сертификата:

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

Резюме

Это был долгий путь. Я искал только решение Python pyOpenSSL для проверки цепочки доверия без subprocess и все время натыкался на страницы, говорящие мне, что это невозможно сделать только с pyOpenSSL . После этого я проверил репозиторий pyOpenSSL на Github.com и там сказано, что это было добавлено совсем недавно. changelog показала параметры и методы, и после прочтения документов это было легко реализовать.

Я также нашел способы проверки цепочки сертификатов, является ли сертификат самоподписанным сертификатом, истек ли срок действия и способ проверки на неправильный узел. Теперь перейдем к расширению скрипта проверки сервера ...

Ссылки / кредиты

[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

Подробнее

Cryptography pyOpenSSL

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии (2)

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.

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?