Validar certificados SSL con Python

Necesito escribir un script que se conecte a un grupo de sitios en nuestra intranet corporativa a través de HTTPS y verifique que sus certificados SSL sean válidos; que no están vencidos, que se emiten para la dirección correcta, etc. Usamos nuestra propia Autoridad de Certificación corporativa interna para estos sitios, por lo que tenemos la clave pública de la CA para verificar los certificados.

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

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

¿Cómo verifico un certificado en Python?

    A partir de la versión 2.7.9 / 3.4.3, Python intenta realizar la validación de certificados de forma predeterminada .

    Esto se ha propuesto en PEP 467, que vale la pena leer: https://www.python.org/dev/peps/pep-0476/

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

    Documentación relevante:

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

    Esta clase ahora realiza todas las comprobaciones necesarias de certificados y nombres de host de forma predeterminada. Para volver al comportamiento anterior, no verificado, ssl._create_unverified_context () se puede pasar al parámetro de contexto.

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

    Modificado en la versión 3.4.3: esta clase ahora realiza todas las comprobaciones necesarias de certificados y nombres de host de forma predeterminada. Para volver al comportamiento anterior, no verificado, ssl._create_unverified_context () se puede pasar al parámetro de contexto.

    Tenga en cuenta que la nueva verificación integrada se basa en la base de datos de certificados provista por el sistema . Opuesto a eso, el paquete de solicitudes envía su propio paquete de certificados. Las ventajas y desventajas de ambos enfoques se analizan en la sección de la base de datos de Trust del PEP 476 .

    He agregado una distribución al Índice de Paquetes de Python que hace que la función match_hostname() del paquete Python 3.2 ssl esté disponible en versiones anteriores de Python.

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

    Puedes instalarlo con:

     pip install backports.ssl_match_hostname 

    O puede convertirla en una dependencia que se encuentra en el setup.py su proyecto. De cualquier manera, se puede usar así:

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

    Puedes usar Twisted para verificar certificados. La API principal es CertificateOptions , que se puede proporcionar como el argumento contextFactory para varias funciones, como listenSSL y startTLS .

    Desafortunadamente, ni Python ni Twisted vienen con la stack de certificados de CA necesarios para realizar la validación de HTTPS, ni la lógica de validación de HTTPS. Debido a una limitación en PyOpenSSL , todavía no puede hacerlo completamente correctamente, pero gracias al hecho de que casi todos los certificados incluyen un sujeto nombre común, puede acercarse lo suficiente.

    Aquí hay una implementación ingenua de ejemplo de un cliente HTTPS Twisted verificador que ignora los comodines y las extensiones subjectAltName, y utiliza los certificados de autoridad de certificado presentes en el paquete de “certificados de ca” en la mayoría de las distribuciones de Ubuntu. Pruébalo con tus sitios favoritos de certificados válidos e inválidos :).

     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 breve ejemplo. pycurl.error un pycurl.error si algo es sospechoso, donde obtendrás una tupla con un código de error y un mensaje legible.

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

    Probablemente querrá configurar más opciones, como dónde almacenar los resultados, etc. Pero no hay necesidad de saturar el ejemplo con elementos no esenciales.

    Ejemplo de qué excepciones se pueden plantear:

     (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 me parecen útiles son los documentos libcurl para setopt y getinfo.

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

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

    O simplemente haz tu vida más fácil usando la biblioteca de solicitudes :

     import requests requests.get('https://somesite.com', cert='/path/server.crt', verify=True) 

    Unas palabras más sobre su uso.

    M2Crypto puede hacer la validación . También puedes usar M2Crypto con Twisted si quieres. El cliente de escritorio de Chandler utiliza Twisted para redes y M2Crypto para SSL , incluida la validación de certificados.

    Según el comentario de Glyphs, parece que M2Crypto realiza una mejor verificación de certificados de forma predeterminada que lo que puede hacer con pyOpenSSL actualmente, porque M2Crypto también comprueba el campo subjectAltName.

    También escribí en el blog sobre cómo obtener los certificados con los que Mozilla Firefox se entrega en Python y se pueden usar con las soluciones SSL de Python.

    Jython lleva a cabo la verificación de certificados de manera predeterminada, por lo que el uso de módulos de biblioteca estándar, por ejemplo, httplib.HTTPSConnection, etc., con jython verificará los certificados y dará excepciones en caso de fallas, es decir, identidades no coincidentes, certificados caducados, etc.

    De hecho, tienes que hacer un trabajo extra para que jython se comporte como cpython, es decir, para que jython NO verifique los certificados.

    He escrito una publicación de blog sobre cómo deshabilitar la verificación de certificados en jython, porque puede ser útil en las fases de prueba, etc.

    Instalación de un proveedor de seguridad de confianza en java y jython.
    http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

    El siguiente código le permite beneficiarse de todas las comprobaciones de validación de SSL (por ejemplo, validez de fecha, cadena de certificados de CA …) EXCEPTO un paso de verificación conectable, por ejemplo, para verificar el nombre de host o hacer otros pasos de verificación de certificado adicionales.

     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 para la biblioteca OpenSSL. Debe proporcionar todo lo que necesita.

    Estaba teniendo el mismo problema pero quería minimizar las dependencias de terceros (porque muchos usuarios ejecutaban este script único). Mi solución fue envolver una llamada de curl y asegurarme de que el código de salida fuera 0 . Trabajado como un encanto.