Как получить сертификат SSL-ответа от запросов в python?

Попытка получить сертификат SSL из ответа на requests.

Какой хороший способ сделать это?

Ответ 1

requests намеренно оборачивают такие вещи низкого уровня, как этот. Как правило, единственное, что вы хотите сделать, это проверить, что сертификаты действительны. Для этого просто введите verify=True. Если вы хотите использовать нестандартный пакет cacert, вы также можете пропустить это. Например:

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

Кроме того, requests - это, прежде всего, набор оболочек для других библиотек, в основном urllib3 и stdlib http.client (или, для 2.x, httplib) и ssl.

Иногда ответом является просто получить resp.raw к объектам более низкого уровня (например, resp.raw - это urllib3.response.HTTPResponse), но во многих случаях это невозможно.

И это один из таких случаев. Единственными объектами, которые когда-либо видят сертификаты, являются http.client.HTTPSConnection (или urllib3.connectionpool.VerifiedHTTPSConnection, но это только подкласс первого) и ssl.SSLSocket, и ни один из них больше не существует к моменту запроса возвращается. (Как следует из имени HTTPSConnection connectionpool, объект HTTPSConnection хранится в пуле и может быть повторно использован, как только он это SSLSocket; SSLSocket является членом HTTPSConnection.)

Итак, вам нужно что-то исправить, чтобы вы могли скопировать данные в цепочку. Это может быть так просто, как это:

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

Это не проверено, поэтому никаких гарантий; вам может понадобиться исправить это.

Кроме того, подкласс и переопределение, вероятно, будут чище, чем monkeypatching (тем более, что HTTPAdapter был разработан для HTTPAdapter подклассов).

Или, что еще лучше, разветвление urllib3 и requests, изменение вашего форка и (если вы считаете, что это законно полезно) отправка запросов на получение исходящих данных.

Во всяком случае, теперь, из вашего кода, вы можете сделать это:

resp.peercert

Это даст вам 'subjectAltName' ключами 'subject' и 'subjectAltName', возвращаемыми pyopenssl.WrappedSocket.getpeercert. Если вы хотите получить дополнительную информацию о сертификате, попробуйте вариант этого ответа Кристофа Вандепласа, который позволяет получить объект OpenSSL.crypto.X509. Если вы хотите получить всю цепочку сертификатов пэра, см. Ответ GoldenStake.

Конечно, вы также можете передать всю информацию, необходимую для проверки сертификата, но это еще проще, поскольку она уже проходит через верхний уровень.

Ответ 2

Для начала, ответ abarnert очень полный. Преследуя предложенную проблему connection-close соединения Kalkran, я обнаружил, что peercert не содержит подробной информации о SSL-сертификате.

Я углубился в информацию о соединении и сокете и извлек self.sock.connection.get_peer_certificate() которая содержит замечательные функции, такие как:

  • get_subject() для CN
  • get_notAfter() и get_notBefore() для срока годности
  • get_serial_number() и get_signature_algorithm() для крипто-связанных технических деталей
  • ...

Обратите внимание, что они доступны только в том случае, если в вашей системе установлен pyopenssl. Под капотом urllib3 использует pyopenssl если он доступен, и стандартный модуль ssl библиотеки в противном случае. self.sock.connection показанный ниже, существует только в том случае, если self.sock является urllib3.contrib.pyopenssl.WrappedSocket, а не в случае ssl.SSLSocket. Вы можете установить pyopenssl с помощью pip install pyopenssl.

Как только это будет сделано, код становится:

import requests

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.peer_certificate = self._connection.peer_certificate
    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.peer_certificate = resp.peer_certificate
    except AttributeError:
        pass
    return response
HTTPAdapter.build_response = new_HTTPAdapter_build_response

HTTPSConnection = requests.packages.urllib3.connection.HTTPSConnection
orig_HTTPSConnection_connect = HTTPSConnection.connect
def new_HTTPSConnection_connect(self):
    orig_HTTPSConnection_connect(self)
    try:
        self.peer_certificate = self.sock.connection.get_peer_certificate()
    except AttributeError:
        pass
HTTPSConnection.connect = new_HTTPSConnection_connect

Вы сможете легко получить доступ к результату:

r = requests.get('https://yourdomain.tld', timeout=0.1)
print('Expires on: {}'.format(r.peer_certificate.get_notAfter()))
print(dir(r.peer_certificate))

Если, как и я, вы хотите игнорировать предупреждения SSL-сертификатов, просто добавьте следующее в начало файла и не проверяйте SSL:

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

r = requests.get('https://yourdomain.tld', timeout=0.1, verify=False)
print(dir(r.peer_certificate))

Ответ 3

Это, хотя и совсем не совсем, работает:

import requests

req = requests.get('https://httpbin.org')
pool = req.connection.poolmanager.connection_from_url('https://httpbin.org')
conn = pool.pool.get()
# get() removes it from the pool, so put it back in
pool.pool.put(conn)
print(conn.sock.getpeercert())

Ответ 4

Чтобы начать, ответ abarnert очень полно

Но я хотел бы добавить, что в случае, когда вы ищете цепочку сертификатов peer, вам нужно будет исправить еще один фрагмент кода

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

после этого вы можете называть его очень похожим образом, как принятый ответ

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

вы получите resp.peercertchain, который содержит tuple OpenSSL.crypto.X509 objects

Ответ 5

Спасибо всем за отличные ответы.

Это помогло мне найти ответ на этот вопрос:

Как добавить пользовательский корневой сертификат CA в CA Store, используемый Python в Windows?

И соберите этот репозиторий:

https://github.com/neozenith/get-ca-py

#! /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/info/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)