Проверка сертификатов SSL с помощью Python
Мне нужно написать script, который соединяется с кучей сайтов в нашей корпоративной интрасети через HTTPS и проверяет, что их SSL-сертификаты действительны; что они не истекли, они выданы для правильного адреса и т.д. Мы используем наш собственный корпоративный центр сертификации для этих сайтов, поэтому у нас есть открытый ключ CA для проверки сертификатов.
Python по умолчанию просто принимает и использует SSL-сертификаты при использовании HTTPS, поэтому даже если сертификат недействителен, библиотеки Python, такие как urllib2 и Twisted, просто с удовольствием используют сертификат.
Есть ли где-нибудь хорошая библиотека, которая позволит мне подключиться к сайту через HTTPS и проверить его сертификат таким образом?
Как проверить сертификат на Python?
Ответы
Ответ 1
Из версии версии 2.7.9/3.4.3 on, 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() может быть передано параметру контекста.
https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection
Изменено в версии 3.4.3: этот класс теперь выполняет все необходимые проверки сертификата и имени хоста по умолчанию. Чтобы вернуться к предыдущему, непроверенному, поведение ssl._create_unverified_context() может быть передано параметру контекста.
Обратите внимание, что новая встроенная проверка основана на базе данных сертификатов системы. Против этого пакета requests поставляется собственный комплект сертификатов. Плюсы и минусы обоих подходов обсуждаются в разделе Trust database PEP 476.
Ответ 2
Я добавил дистрибутив в индекс пакета Python, который делает функцию match_hostname()
из пакета Python 3.2 ssl
доступной в предыдущих версиях 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:
...
Ответ 3
Вы можете использовать Twisted для проверки сертификатов. Основной API CertificateOptions, который может быть представлен как аргумент contextFactory
для различных функций, таких как listenSSL и startTLS.
К сожалению, ни Python, ни Twisted не поставляется с кучей сертификатов CA, необходимых для фактической проверки HTTPS, а также для проверки валидации HTTPS. Из-за ограничение в PyOpenSSL, вы не можете сделать это полностью правильно, но благодаря тому, что почти все сертификаты включают тему commonName, вы можете приблизиться достаточно.
Вот пример наивной выборки проверенного Twisted HTTPS-клиента, который игнорирует расширения wildcards и subjectAltName и использует сертификаты сертификатов, присутствующие в пакете ca-сертификатов в большинстве дистрибутивов 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 make sure it 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()
Ответ 4
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.
Ответ 5
Вот пример script, который демонстрирует проверку сертификата:
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()
Ответ 6
Или просто упростите свою жизнь, используя библиотеку requests:
import requests
requests.get('https://somesite.com', cert='/path/server.crt', verify=True)
Несколько слов об использовании.
Ответ 7
M2Crypto может сделать проверку. Вы также можете использовать M2Crypto с Twisted, если хотите. Настольный клиент Chandler использует Twisted для работы в сети и M2Crypto для SSL, включая проверку сертификата.
Основываясь на комментариях Glyphs, кажется, что M2Crypto выполняет проверку сертификатов по умолчанию лучше, чем то, что вы можете сделать с pyOpenSSL в настоящее время, потому что M2Crypto также проверяет поле subjectAltName.
Я также написал в блоге о том, как получить сертификаты, которые Mozilla Firefox поставляется с Python и которые можно использовать с решениями Python SSL.
Ответ 8
Jython выполняют проверку сертификата по умолчанию, поэтому используя стандартные библиотечные модули, например, httplib.HTTPSConnection и т.д., с jython проверит сертификаты и предоставит исключения для сбоев, т.е. несоответствующие идентификаторы, истекшие сертификаты и т.д.
На самом деле вам нужно сделать дополнительную работу, чтобы заставить jython вести себя как cpython, т.е. чтобы jython НЕ проверял сертификаты.
Я написал сообщение в блоге о том, как отключить проверку сертификатов на jython, потому что он может быть полезен на этапах тестирования и т.д.
Установка надежного поставщика безопасности на java и jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/
Ответ 9
Следующий код позволяет вам использовать все проверки правильности SSL (например, срок действия даты, цепочка сертификатов CA...), ЗАПРЕЩАЕТСЯ этап сменной проверки, например, для проверки имени хоста или выполнения других дополнительных шагов проверки сертификата.
from httplib import HTTPSConnection
import ssl
def create_custom_HTTPSConnection(host):
def verify_cert(cert, host):
# Write your code here
# You can certainly base yourself on ssl.match_hostname
print 'Host:', host
print 'Peer cert:', cert
class CustomHTTPSConnection(HTTPSConnection, object):
def connect(self):
super(CustomHTTPSConnection, self).connect()
cert = self.sock.getpeercert()
verify_cert(cert, host)
context = ssl.create_default_context()
context.check_hostname = False
return CustomHTTPSConnection(host=host, context=context)
if __name__ == '__main__':
# try expired.badssl.com or self-signed.badssl.com !
conn = create_custom_HTTPSConnection('badssl.com')
conn.request('GET', '/')
conn.getresponse().read()
Ответ 10
pyOpenSSL - это интерфейс к библиотеке OpenSSL. Он должен предоставить все, что вам нужно.
Ответ 11
У меня была такая же проблема, но мне хотелось свести к минимуму зависимостей сторонних разработчиков (поскольку этот одноразовый script должен выполняться многими пользователями). Моим решением было обернуть вызов curl
и убедиться, что код выхода 0
. Работали как шарм.