Попытка получить сертификат SSL из ответа на requests
.
Какой хороший способ сделать это?
Попытка получить сертификат SSL из ответа на requests
.
Какой хороший способ сделать это?
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.
Конечно, вы также можете передать всю информацию, необходимую для проверки сертификата, но это еще проще, поскольку она уже проходит через верхний уровень.
Для начала, ответ abarnert очень полный. Преследуя предложенную проблему connection-close
соединения Kalkran, я обнаружил, что peercert
не содержит подробной информации о SSL-сертификате.
Я углубился в информацию о соединении и сокете и извлек self.sock.connection.get_peer_certificate()
которая содержит замечательные функции, такие как:
get_subject()
для CNget_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))
Это, хотя и совсем не совсем, работает:
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())
Чтобы начать, ответ 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
Спасибо всем за отличные ответы.
Это помогло мне найти ответ на этот вопрос:
Как добавить пользовательский корневой сертификат 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)