Проверка сертификатов 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()

Ответ 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. Работали как шарм.