Validar certificados SSL com Python

85

Preciso escrever um script que se conecte a vários sites em nossa intranet corporativa por HTTPS e verifique se seus certificados SSL são válidos; que não tenham expirado, que tenham sido emitidos para o endereço correto etc. Usamos nossa própria Autoridade de Certificação corporativa interna para esses sites, portanto, temos a chave pública da CA para verificar os certificados.

Por padrão, o Python apenas aceita e usa certificados SSL ao usar HTTPS, portanto, mesmo se um certificado for inválido, as bibliotecas Python, como urllib2 e Twisted, usarão o certificado com prazer.

Existe uma boa biblioteca em algum lugar que me permita conectar a um site por HTTPS e verificar seu certificado dessa forma?

Como faço para verificar um certificado em Python?

Eli Courtwright
fonte
10
Seu comentário sobre o Twisted está incorreto: o Twisted usa pyopenssl, não o suporte SSL integrado do Python. Embora não valide certificados HTTPS por padrão em seu cliente HTTP, você pode usar o argumento "contextFactory" para getPage e downloadPage para construir uma fábrica de contexto de validação. Em contraste, que eu saiba, não há como o módulo "ssl" embutido ser convencido a fazer a validação do certificado.
Glyph
4
Com o módulo SSL no Python 2.6 e posterior, você pode escrever seu próprio validador de certificado. Não é ideal, mas factível.
Heikki Toivonen
3
A situação mudou, o Python agora valida os certificados por padrão. Eu adicionei uma nova resposta abaixo.
Dr. Jan-Philip Gehrcke
A situação também mudou para o Twisted (um pouco antes do Python, na verdade); Se você usa treqou twisted.web.client.Agentdesde a versão 14.0, Twisted verifica os certificados por padrão.
Glifo

Respostas:

19

A partir da versão 2.7.9 / 3.4.3, o Python, por padrão, tenta realizar a validação do certificado.

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

As mudanças afetam todos os módulos stdlib relevantes (urllib / urllib2, http, httplib).

Documentação relevante:

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

Esta classe agora executa todas as verificações de certificado e nome de host necessárias por padrão. Para reverter para o comportamento anterior não verificado, ssl._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 agora executa todas as verificações de certificado e nome de host necessárias por padrão. Para reverter ao comportamento anterior, não verificado, ssl._create_unverified_context () pode ser passado para o parâmetro de contexto.

Observe que a nova verificação integrada é baseada no banco de dados de certificados fornecido pelo sistema . Oposto a isso, o pacote de solicitações envia seu próprio pacote de certificados. Prós e contras de ambas as abordagens são discutidos na seção de banco de dados de confiança do PEP 476 .

Dr. Jan-Philip Gehrcke
fonte
alguma solução para garantir verificações de certificado para a versão anterior do python? Nem sempre é possível atualizar a versão do python.
vaab
não valida certificados revogados. Por exemplo, revoked.badssl.com
Raz
É obrigatório usar a HTTPSConnectionclasse? Eu estava usando SSLSocket. Como posso fazer a validação com SSLSocket? Devo validar explicitamente usando pyopensslcomo explicado aqui ?
anir
31

Eu adicionei uma distribuição ao Python Package Index que disponibiliza a match_hostname()função do sslpacote Python 3.2 em versões anteriores do Python.

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

Você pode instalá-lo com:

pip install backports.ssl_match_hostname

Ou você pode torná-lo uma dependência listada no do seu projeto setup.py. De qualquer forma, ele 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:
    ...
Brandon Rhodes
fonte
1
Estou faltando algo ... você pode preencher os espaços em branco acima ou fornecer um exemplo completo (para um site como o Google)?
smholloway
O exemplo terá uma aparência diferente dependendo de qual biblioteca você está usando para acessar o Google, já que diferentes bibliotecas colocam o soquete SSL em diferentes lugares, e é o soquete SSL que precisa de seu getpeercert()método chamado para que a saída possa ser passada match_hostname().
Brandon Rhodes
12
Estou envergonhado em nome do Python que alguém tenha que usar isso. As bibliotecas SSL HTTPS integradas do Python que não verificam certificados prontos para uso por padrão são completamente insanas e é doloroso imaginar quantos sistemas inseguros existem agora como resultado.
Glenn Maynard
26

Você pode usar Twisted para verificar os certificados. A API principal é CertificateOptions , que pode ser fornecida como contextFactoryargumento para várias funções, como listenSSL e startTLS .

Infelizmente, nem Python nem Twisted vêm com a pilha de certificados CA necessários para realmente fazer a validação HTTPS, nem a lógica de validação HTTPS. Devido a uma limitação no PyOpenSSL , você não pode fazer isso completamente correto ainda, mas graças ao fato de que quase todos os certificados incluem um assunto commonName, você pode chegar perto o suficiente.

Aqui está um exemplo ingênuo de implementação de um cliente HTTPS Twisted de verificação que ignora curingas e extensões subjectAltName e usa os certificados de autoridade de certificação presentes no pacote 'ca-certificates' na maioria das distribuições Ubuntu. Experimente com seus sites 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()
Glifo
fonte
você pode torná-lo não bloqueador?
sean riley
Obrigado; Tenho uma observação que li e entendi: verificar se os retornos de chamada devem retornar True quando não há erro e False quando há. Seu código basicamente retorna um erro quando o commonName não é localhost. Não tenho certeza se é isso que você pretendia, embora faça sentido em alguns casos. Eu apenas decidi deixar um comentário sobre isso para o benefício dos futuros leitores desta resposta.
Eli Courtwright
"self.hostname" nesse caso não é "localhost"; observe URLPath(url).netlocque: isso significa a parte do host da URL passada para secureGet. Em outras palavras, ele está verificando se o commonName do assunto é o mesmo que está sendo solicitado pelo chamador.
Glyph
Estou executando uma versão deste código de teste e usei Firefox, wget e Chrome para acessar um servidor HTTPS de teste. No entanto, em meus testes, estou vendo que o retorno de chamada verifyHostname está sendo chamado de 3 a 4 vezes a cada conexão. Por que não está funcionando apenas uma vez?
themaestro
2
URLPath (blah) .netloc é sempre localhost: URLPath .__ init__ pega componentes de url individuais, você está passando um url inteiro como "esquema" e obtendo o netloc padrão de 'localhost' para acompanhá-lo. Você provavelmente pretendia usar URLPath.fromString (url) .netloc. Infelizmente, isso expõe a verificação em verifyHostName ao contrário: ela começa a rejeitar https://www.google.com/porque um dos assuntos é 'www.google.com', fazendo com que a função retorne False. Provavelmente significava retornar True (aceito) se os nomes corresponderem e False se não corresponderem?
mzz
25

PycURL faz isso lindamente.

Abaixo está um pequeno exemplo. Ele irá lançar um pycurl.errorif something is fish, onde você obtém uma tupla com código de erro e uma mensagem legível 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()

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

Exemplo de quais exceções 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'")

Alguns links que achei úteis são libcurl-docs para setopt e getinfo.

plundra
fonte
15

Ou simplesmente torne sua vida mais fácil usando a biblioteca de solicitações :

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

Mais algumas palavras sobre seu uso.

ufo
fonte
10
O certargumento é o certificado do cliente, não um certificado do servidor para verificar. Você quer usar o verifyargumento.
Paŭlo Ebermann
2
solicitações valida por padrão . Não há necessidade de usar o verifyargumento, exceto para ser mais explícito ou desabilitar a verificação.
Dr. Jan-Philip Gehrcke
1
Não é um módulo interno. Você precisa executar as solicitações de instalação do pip
Robert Townley,
14

Aqui está um exemplo de script 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()
Eli Courtwright
fonte
@tonfa: Boa pegada; Acabei adicionando a verificação do nome do host também e editei minha resposta para incluir o código que usei.
Eli Courtwright,
Não consigo acessar o link original (ou seja, 'esta página'). Ele mudou?
Matt Ball,
@Matt: Acho que sim, mas FWIW o link original não é necessário, já que meu programa de teste é um exemplo funcional completo, autônomo. Eu criei um link para a página que me ajudou a escrever esse código, pois parecia a coisa decente para fornecer atribuição. Mas como ele não existe mais, vou editar minha postagem para remover o link, obrigado por apontar isso.
Eli Courtwright
Isso não funciona com manipuladores adicionais, como manipuladores de proxy, devido à conexão de soquete manual em CertValidatingHTTPSConnection.connect. Consulte esta solicitação de pull para obter detalhes (e uma correção).
schlamar
2
Aqui está uma solução limpa e funcional com backports.ssl_match_hostname.
schlamar
8

M2Crypto pode fazer a validação . Você também pode usar M2Crypto com Twisted, se desejar. O cliente de desktop Chandler usa Twisted para rede e M2Crypto para SSL , incluindo validação de certificado.

Com base no comentário do Glyphs, parece que M2Crypto faz melhor verificação de certificado por padrão do que o que você pode fazer com pyOpenSSL atualmente, porque M2Crypto verifica o campo subjectAltName também.

Também fiz um blog sobre como obter os certificados com os quais o Mozilla Firefox vem em Python e utilizáveis ​​com soluções SSL em Python.

Heikki Toivonen
fonte
4

O Jython FAZ a verificação do certificado por padrão, portanto, usando módulos de biblioteca padrão, por exemplo, httplib.HTTPSConnection, etc, o jython irá verificar os certificados e dar exceções para falhas, ou seja, identidades incompatíveis, certificados expirados, etc.

Na verdade, você tem que fazer algum trabalho extra para fazer o jython se comportar como cpython, ou seja, fazer com que o jython NÃO verifique certificados.

Eu escrevi uma postagem no blog sobre como desativar a verificação de certificado no jython, porque pode ser útil nas fases de teste, etc.

Instalando um provedor de segurança totalmente confiável em java e jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

Alan Kennedy
fonte
2

O código a seguir permite que você se beneficie de todas as verificações de validação de SSL (por exemplo, validade de data, cadeia de certificados CA ...) EXCETO uma etapa de verificação conectável, por exemplo, para verificar o nome do host ou realizar outras etapas adicionais de verificação de certificado.

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()
Carl D'Halluin
fonte
-1

pyOpenSSL é uma interface para a biblioteca OpenSSL. Deve fornecer tudo que você precisa.

DisplacedAussie
fonte
OpenSSL não executa correspondência de nome de host. Está planejado para OpenSSL 1.1.0.
jww
-1

Eu estava tendo o mesmo problema, mas queria minimizar as dependências de terceiros (porque esse script único deveria ser executado por muitos usuários). Minha solução foi encerrar uma curlchamada e verificar se o código de saída era 0. Funcionou como um encanto.

Ztyx
fonte
Eu diria que stackoverflow.com/a/1921551/1228491 usar pycurl é uma solução muito melhor então.
Marian,