web-dev-qa-db-fra.com

Comment obtenir un certificat SSL de réponse des demandes de Python?

Essayer d'obtenir le certificat SSL à partir d'une réponse dans requests .

Quel est un bon moyen de faire ça?

16
Juan Carlos Coto

requests _ enveloppe délibérément des trucs de bas niveau comme celui-ci. Normalement, la seule chose que vous voulez faire est de Vérifiez que les certs sont valides . Pour faire ça, juste passer verify=True. Si vous souhaitez utiliser un paquet cacert non standard, vous pouvez transmettre cela aussi. Par exemple:

resp = requests.get('https://example.com', verify=True, cert=['/path/to/my/ca.crt'])

En outre, requests est principalement un ensemble d'emballages autour d'autres bibliothèques, principalement urllib3 et le stdlib http.client (ou, pour 2.x, httplib) et ssl .

Parfois, la réponse est juste pour obtenir aux objets de niveau inférieur (par exemple, resp.raw est le urllib3.response.HTTPResponse), mais dans de nombreux cas, c'est impossible.

Et c'est l'un de ces cas. Les seuls objets qui jamais voir les certs sont un http.client.HTTPSConnection (ou un urllib3.connectionpool.VerifiedHTTPSConnection, mais c'est juste une sous-classe de l'ancien) et un ssl.SSLSocket et aucun de ceux qui n'existent plus avant le retour de la requête. (Comme le nom connectionpool implique, l'objet HTTPSConnection est stocké dans un pool et peut être réutilisé dès que cela sera fait; le SSLSocket est membre du HTTPSConnection.)

Donc, vous devez corriger les choses afin que vous puissiez copier les données de la chaîne. Cela peut être aussi simple que ceci:

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__
def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peercert = self._connection.sock.getpeercert()
    except AttributeError:
        pass
HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response
def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peercert = resp.peercert
    except AttributeError:
        pass
    return response
HTTPAdapter.build_response = new_HTTPAdapter_build_response

C'est non testé, donc pas de garanties; Vous devrez peut-être régler plus que cela.

De plus, le sous-classement et le remplacement seraient probablement plus propres que le skeyPatching (surtout que HTTPAdapter a été conçu pour être sous-classée).

Ou, encore mieux, forking urllib3 et requests, modifier votre fourchette et (si vous pensez que cela est légitimement utile) soumettant des demandes de traction en amont.

Quoi qu'il en soit, maintenant, de votre code, vous pouvez le faire:

resp.peercert

Cela vous donnera un dict avec 'subject' et 'subjectAltName' clés, comme retourné par pyopenssl.WrappedSocket.getpeercert. Si vous voulez plutôt plus d'informations sur le certificat, essayez Variante de Christophe Vandeplas de cette réponse qui vous permet d'obtenir un OpenSSL.crypto.X509 objet. Si vous souhaitez obtenir la chaîne de certificats de pairs entière, voir Réponse de Goldenstake .

Bien sûr, vous voudrez peut-être aussi transmettre toutes les informations nécessaires pour vérifier le certificat, mais cela est encore plus facile, car il passe déjà à travers le niveau supérieur.

20
abarnert

Merci pour les réponses impressionnantes de tout le monde.

Cela m'a aidé à faire une réponse à cette question:

Comment ajouter un certificat de racine CA personnalisé au magasin CA utilisé par Python dans Windows?

Mise à jour 2019-02-12

Veuillez jeter un oeil sur CERT Human: Certificats SSL pour l'homme Pour une réécriture impressionnante de mon https://github.com/neozenith/get-ca Projet par - Lifehackjim .

J'ai archivé le référentiel d'origine maintenant.

Stand seul Snippet

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
Get Certificates from a request and dump them.
"""

import argparse
import sys

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

"""
Inspired by the answers from this Stackoverflow question:
https://stackoverflow.com/questions/16903528/how-to-get-response-ssl-certificate-from-requests-in-python

What follows is a series of patching the low level libraries in requests.
"""

"""
https://stackoverflow.com/a/47931103/622276
"""

sock_requests = requests.packages.urllib3.contrib.pyopenssl.WrappedSocket


def new_getpeercertchain(self, *args, **kwargs):
    x509 = self.connection.get_peer_cert_chain()
    return x509


sock_requests.getpeercertchain = new_getpeercertchain

"""
https://stackoverflow.com/a/16904808/622276
"""

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__


def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peercertchain = self._connection.sock.getpeercertchain()
    except AttributeError:
        pass


HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response


def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peercertchain = resp.peercertchain
    except AttributeError:
        pass
    return response


HTTPAdapter.build_response = new_HTTPAdapter_build_response

"""
Attempt to wrap in a somewhat usable CLI
"""


def cli(args):
    parser = argparse.ArgumentParser(description="Request any URL and dump the certificate chain")
    parser.add_argument("url", metavar="URL", type=str, nargs=1, help="Valid https URL to be handled by requests")

    verify_parser = parser.add_mutually_exclusive_group(required=False)
    verify_parser.add_argument("--verify", dest="verify", action="store_true", help="Explicitly set SSL verification")
    verify_parser.add_argument(
        "--no-verify", dest="verify", action="store_false", help="Explicitly disable SSL verification"
    )
    parser.set_defaults(verify=True)

    return vars(parser.parse_args(args))


def dump_pem(cert, outfile="ca-chain.crt"):
    """Use the CN to dump certificate to PEM format"""
    PyOpenSSL = requests.packages.urllib3.contrib.pyopenssl
    pem_data = PyOpenSSL.OpenSSL.crypto.dump_certificate(PyOpenSSL.OpenSSL.crypto.FILETYPE_PEM, cert)
    issuer = cert.get_issuer().get_components()

    print(pem_data.decode("utf-8"))

    with open(outfile, "a") as output:
        for part in issuer:
            output.write(part[0].decode("utf-8"))
            output.write("=")
            output.write(part[1].decode("utf-8"))
            output.write(",\t")
        output.write("\n")
        output.write(pem_data.decode("utf-8"))


if __name__ == "__main__":
    cli_args = cli(sys.argv[1:])

    url = cli_args["url"][0]
    req = requests.get(url, verify=cli_args["verify"])
    for cert in req.peercertchain:
        dump_pem(cert)
3
Josh Peak

Pour commencer, Réponse d'Abarnert est très complet

Mais je voudrais ajouter, que dans le cas, vous recherchez la chaîne de peer Cert, vous auriez besoin de patcher encore un autre morceau de code

import requests
sock_requests = requests.packages.urllib3.contrib.pyopenssl.WrappedSocket
def new_getpeercertchain(self,*args, **kwargs):
    x509 = self.connection.get_peer_cert_chain()
    return x509
sock_requests.getpeercertchain = new_getpeercertchain

après cela, vous pouvez l'appeler de manière très similaire comme la réponse acceptée

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__
def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peercertchain = self._connection.sock.getpeercertchain()
    except AttributeError:
        pass
HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response
def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peercertchain = resp.peercertchain
    except AttributeError:
        pass
    return response
HTTPAdapter.build_response = new_HTTPAdapter_build_response

tu auras resp.peercertchain qui contient un Tuple de OpenSSL.crypto.X509 Objets

2
GoldenStake