Necesito escribir un script que se conecte a varios 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 de forma predeterminada solo acepta y usa certificados SSL cuando se usa HTTPS, por lo que incluso si un certificado no es válido, las bibliotecas de Python como urllib2 y Twisted simplemente usarán felizmente el certificado.
¿Existe 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?
fuente
treq
otwisted.web.client.Agent
desde la versión 14.0, Twisted verifica los certificados de forma predeterminada.Respuestas:
Desde la versión de lanzamiento 2.7.9 / 3.4.3 en adelante, Python intenta de forma predeterminada realizar la validación del certificado.
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
https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection
Tenga en cuenta que la nueva verificación incorporada se basa en la base de datos de certificados proporcionada por el sistema . En oposición a eso, el paquete de solicitudes envía su propio paquete de certificados. Los pros y los contras de ambos enfoques se analizan en la sección de la base de datos Trust de PEP 476 .
fuente
HTTPSConnection
clase? Estaba usandoSSLSocket
. ¿Cómo puedo hacer la validación conSSLSocket
? ¿Tengo que validar explícitamente el usopyopenssl
como se explica aquí ?He agregado una distribución al índice de paquetes de Python que hace que la
match_hostname()
función delssl
paquete de Python 3.2 esté disponible en versiones anteriores de Python.http://pypi.python.org/pypi/backports.ssl_match_hostname/
Puedes instalarlo con:
O puede convertirlo en una dependencia enumerada en el archivo
setup.py
. 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: ...
fuente
getpeercert()
llame a su método para que se pueda pasar la salidamatch_hostname()
.Puede utilizar Twisted para verificar certificados. La API principal es CertificateOptions , que se puede proporcionar como
contextFactory
argumento para varias funciones como listenSSL y startTLS .Desafortunadamente, ni Python ni Twisted vienen con el montón de certificados de CA necesarios para realizar la validación HTTPS, ni la lógica de validación 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 commonName, puede acercarse lo suficiente.
Aquí hay una implementación de muestra ingenua de un cliente HTTPS Twisted de verificación que ignora los comodines y las extensiones subjectAltName, y usa los certificados de autoridad de certificación presentes en el paquete 'ca -ificates' en la mayoría de las distribuciones de Ubuntu. Pruébelo con sus sitios favoritos de certificados válidos y no vá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()
fuente
URLPath(url).netloc
: eso significa que la parte del host de la URL pasada a secureGet. En otras palabras, está comprobando que el commonName del sujeto es el mismo que el que está solicitando la persona que llama.https://www.google.com/
porque uno de los sujetos es 'www.google.com', lo que hace que la función devuelva False. Probablemente pretendía devolver Verdadero (aceptado) si los nombres coinciden, y Falso si no lo hacen.PycURL hace esto maravillosamente.
A continuación se muestra un breve ejemplo. Arrojará un
pycurl.error
si algo está sospechoso, donde obtendrá una tupla con un 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()
Probablemente desee configurar más opciones, como dónde almacenar los resultados, etc. Pero no es necesario saturar el ejemplo con cosas que no son esenciales.
Ejemplo de las excepciones que se pueden generar:
(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 encontré útiles son libcurl-docs para setopt y getinfo.
fuente
O simplemente hazle la vida más fácil usando la biblioteca de solicitudes :
import requests requests.get('https://somesite.com', cert='/path/server.crt', verify=True)
Algunas palabras más sobre su uso.
fuente
cert
argumento es el certificado del lado del cliente, no un certificado de servidor para verificar. Quieres usar elverify
argumento.verify
argumento, excepto para ser más explícito o deshabilitar la verificación.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()
fuente
CertValidatingHTTPSConnection.connect
. Consulte esta solicitud de extracción para obtener más detalles (y una solución).backports.ssl_match_hostname
.M2Crypto puede hacer la validación . También puede usar M2Crypto con Twisted si lo desea. El cliente de escritorio de Chandler usa 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 verifica el campo subjectAltName.
También escribí en un blog sobre cómo obtener los certificados con los que se envía Mozilla Firefox en Python y que se pueden usar con las soluciones SSL de Python.
fuente
Jython SÍ lleva a cabo la verificación del certificado de forma predeterminada, por lo que el uso de módulos de biblioteca estándar, por ejemplo, http: http: // www. HTTPSConnection, etc., con jython verificará los certificados y dará excepciones para fallas, es decir, identidades no coincidentes, certificados vencidos, etc.
De hecho, tienes que hacer un trabajo adicional para que jython se comporte como cpython, es decir, para que jython NO verifique los certificados.
Escribí 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/
fuente
El siguiente código le permite beneficiarse de todas las comprobaciones de validación 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 realizar otros pasos de verificación de certificados 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()
fuente
pyOpenSSL es una interfaz para la biblioteca OpenSSL. Debe proporcionar todo lo que necesita.
fuente
Tenía el mismo problema pero quería minimizar las dependencias de terceros (porque muchos usuarios iban a ejecutar este script único). Mi solución fue terminar una
curl
llamada y asegurarme de que el código de salida fuera0
. Trabajado como un encanto.fuente