Validar os certificados de SSL com o Python

Preciso de escrever um script que se conecte a um monte de sites na nossa intranet corporativa sobre HTTPS e verifique se os seus certificados SSL são válidos; que eles não estão expirados, que eles são emitidos para o endereço correto, etc. Usamos a nossa própria autoridade interna de certificação corporativa para estes sites, por isso temos a chave pública do CA para verificar os certificados contra.

Python por omissão apenas aceita e usa certificados SSL ao usar HTTPS, por isso mesmo que um certificado é inválido, bibliotecas Python como o urllib2 e o Twisted irão usar o certificado de bom grado.

existe uma boa biblioteca em algum lugar que me permita conectar a um site sobre HTTPS e verificar seu certificado desta forma?

Como posso verificar um certificado em Python?

Author: jww, 2009-07-06

10 answers

Da versão 2.7.9 / 3.4.3 ligada, o Python por omissão tenta efectuar a validação do certificado.

Isto foi proposto no PEP 467, que vale a pena ler: https://www.python.org/dev/peps/pep-0476/

As alterações afectam todos os módulos stdlib relevantes (urllib/urlllib2, http, httplib).

Documentação relevante:

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

Esta aula agora executa todas as verificações necessárias do certificado e do nome da máquina por omissão. Para reverter para o ssl de comportamento anterior, não verificado._create_unverified_context () pode ser passado para o parâmetro de contexto.

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

Alterado na versão 3.4.3: esta classe executa agora todas as verificações necessárias do certificado e do nome da máquina por omissão. Para reverter ao comportamento anterior, não verificado ssl._create_unverified_context () pode ser passado para o parâmetro de contexto.

Note que a nova verificação incorporada baseia-se na base de dados de certificados fornecida pelo sistema . Ao contrário disso, os pedidos embalam os seus próprios pacotes de certificados. Os prós e os contras de ambas as abordagens são discutidos na base de dados de confiança secção do PEP 476 .

 12
Author: Jan-Philip Gehrcke, 2015-02-04 19:15:18

Adicionei uma distribuição ao índice de pacotes Python que torna a função match_hostname() do pacote Python 3.2 ssl disponível nas versões anteriores do Python.

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

Pode instalá-lo com:

pip install backports.ssl_match_hostname

Ou pode torná-lo uma dependência listada no seu projecto setup.py. De qualquer forma, pode ser usado assim:

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:
    ...
 29
Author: Brandon Rhodes, 2010-10-15 23:06:57

Pode usar o Twisted para verificar os certificados. O principal API é CertificateOptions, que pode ser fornecido como contextFactory argumento para várias funções, tais como listenSSL e startTLS.

Infelizmente, nem Python nem Twisted vêm com uma pilha de certificados da AC necessários para fazer a validação de HTTPS, nem com a lógica de validação de HTTPS. Devido a uma limitação em PyOpenSSL , Você não pode fazê-lo completamente corretamente ainda, mas graças a o fato de que quase todos os certificados incluem um nome de assunto, você pode chegar perto o suficiente.

Aqui está uma implementação ingênua de uma amostra de um cliente de HTTPS que verifica Twisted, que ignora caracteres especiais e extensões de subjetaltname, e usa os certificados de Autoridade de certificado presentes no pacote 'ca-certificates' na maioria das distribuições Ubuntu. Tente com as suas páginas de certificados válidas e inválidas favoritas:).

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()
 26
Author: Glyph, 2011-12-10 17:43:46

PycURL faz isto lindamente.

Abaixo está um pequeno exemplo. Ele vai lançar um {[[2]} se algo é suspeito, onde você obtém uma tupla com código de erro e uma mensagem legível humana.

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

Você provavelmente vai querer configurar Mais opções, como onde armazenar os resultados, etc. Mas não há necessidade de confundir o exemplo com coisas não essenciais.

Exemplo das excepções que podem ser levantadas:

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

Algumas ligações que achei úteis são as libcurl-docs para setopt e getinfo.

 25
Author: plundra, 2013-06-13 22:57:28

Aqui está um programa de exemplo que demonstra a validação do 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()
 14
Author: Eli Courtwright, 2011-12-01 21:05:50

Ou simplesmente tornar a sua vida mais fácil usando a biblioteca dos pedidos:

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

Mais algumas palavras sobre o seu uso.

 12
Author: ufo, 2015-07-21 10:22:43

M2Crypto can do the validation . Você também pode usar M2Crypto com Twisted Se quiser. O cliente de secretária do Chandler usa o Twisted para a ligação em rede e o M2Crypto para o SSL, incluindo a validação do certificado.

Com base no comentário dos glifos, parece que o M2Crypto faz uma melhor verificação do certificado por omissão do que o que pode fazer com o pyOpenSSL de momento, porque o M2Crypto também verifica o campo subjetaltname.

Eu também blogei sobre como obter os certificados os navios Mozilla Firefox com em Python e utilizáveis com soluções SSL em Python.
 8
Author: Heikki Toivonen, 2018-10-03 21:18:49

O Jython efectua a verificação do certificado por defeito, utilizando assim módulos de biblioteca normalizados, por exemplo, httplib.HTTPSConnection, etc, com jython irá verificar certificados e dar exceções para falhas, ou seja, identidades trocadas, certs expirados, etc.

Na verdade, você tem que fazer algum trabalho extra para que jython se comporte como cpython, ou seja, para que jython não verifique certs.

Escrevi um post no blog sobre como desactivar a verificação do certificado no jython, porque pode ser útil em fases de ensaio, etc.

Instalar um fornecedor de segurança confiável em java e jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/
 4
Author: Alan Kennedy, 2011-05-17 09:21:43

O PyOpenSSL é uma interface para a biblioteca OpenSSL. Deve fornecer tudo o que precisa.

 -1
Author: DisplacedAussie, 2009-07-06 14:52:32

Eu estava tendo o mesmo problema ,mas queria minimizar as dependências de terceiros (porque este script único era para ser executado por muitos usuários). A minha solução era embrulhar uma chamada e certificar-me de que o código de saída era 0. Funcionou lindamente.

 -1
Author: Ztyx, 2013-12-11 11:18:58