Проверка SSL-сертификатов с помощью Python
Мне нужно написать сценарий, который подключается к куче сайтов в нашей корпоративной интрасети по протоколу HTTPS и проверяет, что их SSL-сертификаты действительны; что они не просрочены, что они выданы по правильному адресу и т. д. Мы используем наш собственный внутренний корпоративный центр сертификации для этих сайтов, поэтому у нас есть открытый ключ центра сертификации для проверки сертификатов.
Python по умолчанию просто принимает и использует SSL-сертификаты при использовании HTTPS, поэтому даже если сертификат недопустимые, библиотеки Python, такие как urllib2 и Twisted, будут просто с радостью использовать сертификат.
Есть ли где-нибудь хорошая библиотека, которая позволит мне подключиться к сайту по HTTPS и проверить его сертификат таким образом?
Как проверить сертификат в Python?
10 ответов:
Начиная с версии 2.7.9/3.4.3, Python по умолчанию пытается выполнить проверку сертификата.
Это было предложено в PEP 467, который стоит прочитать: https://www.python.org/dev/peps/pep-0476/
Изменения затрагивают все соответствующие модули stdlib (urllib/urllib2, http, httplib).
Соответствующая документация:
Https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection
Этот класс теперь выполняет все необходимые проверки сертификатов и имен хостов по умолчанию. Вернуться к предыдущему, непроверенному, поведению ssl._create_unverified_context () можно передать в параметр context.
Https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection
Обратите внимание, что новая встроенная проверка основана на базе данных сертификатов , предоставленных системой. В противоположность этому, пакет requests отправляет свой собственный пакет сертификатов. Плюсы и минусы обоих подходов обсуждаются в база данных доверия раздел PEP 476.Изменено в версии 3.4.3: этот класс теперь выполняет все необходимые проверки сертификата и имени хоста по умолчанию. Вернуться к предыдущему, непроверенному поведению использование SSL._create_unverified_context () можно передать в параметр context.
Я добавил дистрибутив к индексу пакета Python, который делает функцию
match_hostname()
из пакета Python 3.2ssl
доступной в предыдущих версиях Python.Http://pypi.python.org/pypi/backports.ssl_match_hostname/
Вы можете установить его с помощью:
pip install backports.ssl_match_hostname
Или вы можете сделать его зависимостью, перечисленной в вашем проекте
setup.py
. В любом случае, его можно использовать следующим образом:from backports.ssl_match_hostname import match_hostname, CertificateError ... sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv3, cert_reqs=ssl.CERT_REQUIRED, ca_certs=...) try: match_hostname(sslsock.getpeercert(), hostname) except CertificateError, ce: ...
Вы можете использовать Twisted для проверки сертификатов. Основным API является CertificateOptions, который может быть предоставлен в качестве аргумента
contextFactory
различным функциям, таким как listenSSL и startTLS.К сожалению, ни Python, ни Twisted не поставляется с кучей сертификатов CA, необходимых для реальной проверки HTTPS, ни логикой проверки HTTPS. Из-за ограничения в PyOpenSSL , Вы пока не можете сделать это полностью правильно, но благодаря тому, что то, что почти все сертификаты включают в себя общее имя субъекта, вы можете получить достаточно близко.
Вот наивный пример реализации проверяющего скрученного HTTPS-клиента, который игнорирует подстановочные знаки и расширения subjectAltName и использует сертификаты центра сертификации, присутствующие в пакете "ca-certificates" в большинстве дистрибутивов Ubuntu. Попробуйте это с вашими любимыми действительными и недействительными сайтами сертификатов:).
import os import glob from OpenSSL.SSL import Context, TLSv1_METHOD, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, OP_NO_SSLv2 from OpenSSL.crypto import load_certificate, FILETYPE_PEM from twisted.python.urlpath import URLPath from twisted.internet.ssl import ContextFactory from twisted.internet import reactor from twisted.web.client import getPage certificateAuthorityMap = {} for certFileName in glob.glob("/etc/ssl/certs/*.pem"): # There might be some dead symlinks in there, so let's make sure it's real. if os.path.exists(certFileName): data = open(certFileName).read() x509 = load_certificate(FILETYPE_PEM, data) digest = x509.digest('sha1') # Now, de-duplicate in case the same cert has multiple names. certificateAuthorityMap[digest] = x509 class HTTPSVerifyingContextFactory(ContextFactory): def __init__(self, hostname): self.hostname = hostname isClient = True def getContext(self): ctx = Context(TLSv1_METHOD) store = ctx.get_cert_store() for value in certificateAuthorityMap.values(): store.add_cert(value) ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname) ctx.set_options(OP_NO_SSLv2) return ctx def verifyHostname(self, connection, x509, errno, depth, preverifyOK): if preverifyOK: if self.hostname != x509.get_subject().commonName: return False return preverifyOK def secureGet(url): return getPage(url, HTTPSVerifyingContextFactory(URLPath.fromString(url).netloc)) def done(result): print 'Done!', len(result) secureGet("https://google.com/").addCallback(done) reactor.run()
PycURL делает это прекрасно.
Ниже приведен краткий пример. Он броситpycurl.error
, если что-то подозрительно, где вы получите кортеж с кодом ошибки и читаемым человеком сообщением.import pycurl curl = pycurl.Curl() curl.setopt(pycurl.CAINFO, "myFineCA.crt") curl.setopt(pycurl.SSL_VERIFYPEER, 1) curl.setopt(pycurl.SSL_VERIFYHOST, 2) curl.setopt(pycurl.URL, "https://internal.stuff/") curl.perform()
Вы, вероятно, захотите настроить дополнительные параметры, например, где хранить результаты и т. д. Но не нужно загромождать пример несущественными вещами.
Пример того, какие исключения могут быть вызваны:
(60, 'Peer certificate cannot be authenticated with known CA certificates') (51, "common name 'CN=something.else.stuff,O=Example Corp,C=SE' does not match 'internal.stuff'")
Некоторые ссылки, которые я нашел полезными, являются libcurl-docs для setopt и getinfo.
Вот пример скрипта, который демонстрирует проверку сертификата:
import httplib import re import socket import sys import urllib2 import ssl class InvalidCertificateException(httplib.HTTPException, urllib2.URLError): def __init__(self, host, cert, reason): httplib.HTTPException.__init__(self) self.host = host self.cert = cert self.reason = reason def __str__(self): return ('Host %s returned an invalid certificate (%s) %s\n' % (self.host, self.reason, self.cert)) class CertValidatingHTTPSConnection(httplib.HTTPConnection): default_port = httplib.HTTPS_PORT def __init__(self, host, port=None, key_file=None, cert_file=None, ca_certs=None, strict=None, **kwargs): httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs) self.key_file = key_file self.cert_file = cert_file self.ca_certs = ca_certs if self.ca_certs: self.cert_reqs = ssl.CERT_REQUIRED else: self.cert_reqs = ssl.CERT_NONE def _GetValidHostsForCert(self, cert): if 'subjectAltName' in cert: return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns'] else: return [x[0][1] for x in cert['subject'] if x[0][0].lower() == 'commonname'] def _ValidateCertificateHostname(self, cert, hostname): hosts = self._GetValidHostsForCert(cert) for host in hosts: host_re = host.replace('.', '\.').replace('*', '[^.]*') if re.search('^%s$' % (host_re,), hostname, re.I): return True return False def connect(self): sock = socket.create_connection((self.host, self.port)) self.sock = ssl.wrap_socket(sock, keyfile=self.key_file, certfile=self.cert_file, cert_reqs=self.cert_reqs, ca_certs=self.ca_certs) if self.cert_reqs & ssl.CERT_REQUIRED: cert = self.sock.getpeercert() hostname = self.host.split(':', 0)[0] if not self._ValidateCertificateHostname(cert, hostname): raise InvalidCertificateException(hostname, cert, 'hostname mismatch') class VerifiedHTTPSHandler(urllib2.HTTPSHandler): def __init__(self, **kwargs): urllib2.AbstractHTTPHandler.__init__(self) self._connection_args = kwargs def https_open(self, req): def http_class_wrapper(host, **kwargs): full_kwargs = dict(self._connection_args) full_kwargs.update(kwargs) return CertValidatingHTTPSConnection(host, **full_kwargs) try: return self.do_open(http_class_wrapper, req) except urllib2.URLError, e: if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: raise InvalidCertificateException(req.host, '', e.reason.args[1]) raise https_request = urllib2.HTTPSHandler.do_request_ if __name__ == "__main__": if len(sys.argv) != 3: print "usage: python %s CA_CERT URL" % sys.argv[0] exit(2) handler = VerifiedHTTPSHandler(ca_certs = sys.argv[1]) opener = urllib2.build_opener(handler) print opener.open(sys.argv[2]).read()
Или просто сделайте свою жизнь проще, используя библиотеку запросов:
import requests requests.get('https://somesite.com', cert='/path/server.crt', verify=True)
M2Crypto может выполнить проверку. Вы также можете использовать M2Crypto с Twisted , Если хотите. Клиент Chandler desktop использует Twisted для работы в сети и M2Crypto для SSL, включая проверку сертификатов.
Основываясь на комментариях глифов, кажется, что M2Crypto по умолчанию лучше проверяет сертификат, чем то, что вы можете сделать с pyOpenSSL в настоящее время, потому что M2Crypto также проверяет поле subjectAltName.
Я также написал в блоге о том, как получить сертификаты Mozilla Firefox поставляется с Python и может использоваться с решениями Python SSL.
Jython выполняет проверку сертификатов по умолчанию, поэтому использует стандартные библиотечные модули, например httplib.HTTPSConnection и т. д., с языка Jython будет проверять сертификаты и даем исключения отказов, т. е. несоответствие удостоверений, сертификатов истек, и т. д.
Фактически, вы должны сделать некоторую дополнительную работу, чтобы заставить jython вести себя как cpython, то есть заставить jython не проверять сертификаты.
Я написал сообщение в блоге о том, как отключить проверку сертификатов на jython, потому что это может быть полезно в фазах тестирования и т. д.
Установка полностью доверяющего поставщика безопасности на java и jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/
PyOpenSSL - это интерфейс к библиотеке OpenSSL. Он должен обеспечить все, что вам нужно.
У меня была та же проблема, но я хотел минимизировать зависимости от сторонних разработчиков (потому что этот одноразовый скрипт должен был выполняться многими пользователями). Мое решение состояло в том, чтобы обернуть вызов
curl
и убедиться, что код выхода был0
. Сработало как заклинание.