Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
С ноября 2020 года проверку Цепочки доверия можно проводить без вызова OpenSSL с Python's subprocess.
Во время написания сценария, чтобы проверить, правильно ли сайты перенаправлены на '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)
Оставьте ответ
Ответьте анонимно или войдите в систему, чтобы ответить.
Hello,
Very interesting. Could you share full script/class? It looks truncated.
Regards!
This example is not complete. So I re-iterate the previous comment. Please could you share the full script/class?
Недавний
- Использование Ingress для доступа к RabbitMQ на кластере Microk8s
- Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery
- Базовое планирование заданий с помощью APScheduler
- Коммутатор базы данных с HAProxy и HAProxy Runtime API
- Docker Swarm rolling updates
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
Большинство просмотренных
- Использование PyInstaller и Cython для создания исполняемого файла Python
- Уменьшение времени отклика на запросы на странице Flask SQLAlchemy веб-сайта
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Подключение к службе на хосте Docker из контейнера Docker
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов