Pregunta

Tengo que escribir un script que se conecta a un montón de sitios en nuestra intranet corporativa a través de HTTPS y verifica que sus certificados SSL son válidos; de que no se cumplan, que se emiten por la dirección correcta, etc. Utilizamos nuestra propia autoridad interna corporativa Certificado de estos sitios, por lo que tenemos la clave pública de la CA para verificar los certificados en contra.

Python por defecto solo acepta y utiliza certificados SSL cuando utiliza HTTPS, por lo que incluso si un certificado no es válido, bibliotecas de Python como urllib2 y Twisted acaba de usar sin problemas el certificado.

¿Hay una buena biblioteca en algún lugar que me permita conectarse a un sitio a través de HTTPS y verificar su certificado de esta manera?

¿Cómo se verifica un certificado en Python?

¿Fue útil?

Solución

A partir de versión 2.7.9 / 3.4.3 en adelante, Python por defecto los intentos para llevar a cabo la validación de certificados.

Esto ha sido propuesto en PEP 467, que es digno de una lectura: https: //www.python. org / dev / PEP / PEP-0476 /

Los cambios afectan a todos los módulos stdlib pertinentes (urllib / urllib2, http, httplib).

La documentación pertinente:

https://docs.python.org/2/library/httplib.html#httplib .HTTPSConnection

  

Esta clase ahora realiza todos los certificados y controles necesarios de nombre de host por defecto. Para volver a la ssl._create_unverified_context anterior, no verificada, el comportamiento () puede ser pasado al parámetro de contexto.

https://docs.python.org/3/library/http .client.html # http.client.HTTPSConnection

  

cambiado en la versión 3.4.3: Esta clase ahora realiza todos los certificados y controles necesarios de nombre de host por defecto. Para volver a la ssl._create_unverified_context anterior, no verificada, el comportamiento () puede ser pasado al parámetro de contexto.

Tenga en cuenta que la nueva verificación integrado se basa en la proporcionado por el sistema base de datos de certificados. En oposición a esto, el solicita paquete se envía a su propio paquete certificado. Ventajas y desventajas de ambos enfoques se discuten en el base de datos Confianza de PEP 476 .

Otros consejos

He añadido una distribución del índice de paquetes de Python que hace la función de match_hostname() del paquete 3.2 ssl Python disponibles en versiones anteriores de Python.

http://pypi.python.org/pypi/backports.ssl_match_hostname/

Puede instalarlo con:

pip install backports.ssl_match_hostname

O puede que sea una dependencia enumerados en setup.py de su proyecto. De cualquier manera, se puede utilizar la siguiente manera:

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:
    ...

Puede utilizar trenzado para verificar los certificados. El API principal es CertificateOptions , que puede ser proporcionado como argumento contextFactory a varias funciones tales como listenSSL y StartTLS .

Por desgracia, ni Python ni torcida viene con un montón de certificados de CA requerida para hacer realidad HTTPS validación, ni la lógica de validación HTTPS. Debido a una limitación en pyOpenSSL , no puede hacerlo correctamente por completo por el momento, pero gracias al hecho de que casi todos los certificados incluyen un tema commonName, puede acercarse lo suficiente.

Aquí hay un ejemplo de implementación ingenua de un cliente Twisted HTTPS verificar que ignora los comodines y extensiones subjectAltName, y utiliza los certificados de autoridad de certificado presentes en el paquete '' ca-certificados en la mayoría de las distribuciones de Ubuntu. Prueba con sus sitios de certificados válidos y no válidos favoritos:.)

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 hace esto muy bien.

A continuación se muestra un ejemplo corto. Se lanzará una pycurl.error si algo es a pescado, donde se obtiene una tupla con código de error y un mensaje legible por humanos.

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

Es probable que desee configurar más opciones, como dónde almacenar los resultados, etc. Pero no hay necesidad de saturar el ejemplo con lo no esencial.

Ejemplo de lo excepciones que podrían plantearse:

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

Algunos enlaces que he encontrado útiles son los libcurl-docs para setopt y getinfo.

Aquí hay un script de ejemplo que demuestra la validación del certificado:

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

M2Crypto puede hacer la validación . También puede utilizar M2Crypto con Twisted si lo desea. El cliente de escritorio Chandler utiliza torcida para la creación de redes y M2Crypto para SSL, incluyendo la validación de certificados.

Sobre la base de Glifos comentar que parece que le va mejor M2Crypto la verificación del certificado por defecto de lo que puede hacer con pyOpenSSL Actualmente, debido M2Crypto comprueba campo subjectAltName también.

También he escribió en su blog sobre cómo obtener los certificados barcos Mozilla Firefox con en Python y se puede usar con soluciones SSL Python.

Jython HACE llevar a cabo la verificación del certificado de forma predeterminada, por lo que el uso de módulos de biblioteca estándar, por ejemplo httplib.HTTPSConnection, etc, con jython se verificar los certificados y dar excepciones para los fallos, es decir, identidades que no coinciden, expirado certs, etc.

De hecho, usted tiene que hacer un trabajo extra para conseguir jython a comportarse como CPython, es decir, para llegar a jython NO verificar los CERT.

He escrito una entrada de blog sobre cómo desactivar la verificación de certificados de jython, ya que puede ser útil en fases de pruebas, etc.

La instalación de un proveedor de seguridad de todo confiando en Java y jython.
http: //jython.xhaus. com / instalación-un-todo-confiando-security-proveedor-en-java-y-jython /

El siguiente código le permite beneficiarse de todas las comprobaciones de validación SSL (por ejemplo, fecha de validez, CA cadena de certificados ...) excepto una etapa de verificación enchufable, por ejemplo, para verificar el nombre de host o hacer otras etapas de verificación del certificado adicional.

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
        # Raise ssl.CertificateError if verification fails
        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()

pyOpenSSL es una interfaz a la biblioteca OpenSSL. Se debe proporcionar todo lo necesario.

Yo estaba teniendo el mismo problema, pero quería minimizar las dependencias de 3 ª parte (porque esta secuencia de comandos de una sola vez iba a ser ejecutado por muchos usuarios). Mi solución fue para envolver una llamada curl y asegurarse de que el código de salida se 0. Trabajado como un encanto.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top