Python - requests.exceptions.SSLError - dh key too small Python - 请求异常.SSLError - dh密钥太小

49

我正在使用Python和requests爬取一些内部页面。我已经关闭了SSL验证和警告。

requests.packages.urllib3.disable_warnings()
page = requests.get(url, verify=False)

在某些服务器上,我遇到了一个SSL错误,我无法解决。

Traceback (most recent call last):
  File "scraper.py", line 6, in <module>
    page = requests.get(url, verify=False)
  File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/api.py", line 71, in get
    return request('get', url, params=params, **kwargs)
  File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/api.py", line 57, in request
    return session.request(method=method, url=url, **kwargs)
  File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/sessions.py", line 475, in request
    resp = self.send(prep, **send_kwargs)
  File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/sessions.py", line 585, in send
    r = adapter.send(request, **kwargs)
  File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/adapters.py", line 477, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: [SSL: SSL_NEGATIVE_LENGTH] dh key too small (_ssl.c:600)

这种问题在Cygwin内外,在Windows和OSX系统中都会出现。我的研究提示服务器上的OpenSSL版本过旧。我希望能够在客户端解决这个问题。

编辑: 通过使用一组密码,我成功地解决了这个问题。

import requests

requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL'
try:
    requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST += 'HIGH:!DH:!aNULL'
except AttributeError:
    # no pyopenssl support used / needed / available
    pass

page = requests.get(url, verify=False)

您想解决什么问题?如果SSL证书存在问题,您想使用HTTP还是继续使用HTTPS忽略这个问题? - Marcel Wilson
@MarcelWilson:这不是证书的问题。 - Steffen Ullrich
@SteffenUllrich 当然,你是对的。我应该说明一下如果“SSL”普遍存在问题的话。 - Marcel Wilson
将你的代码解决方案移动到答案中,可以帮助其他人更容易地找到解决方案。 - Larry Cai
14个回答

47

这并不是额外的答案,只是尝试将问题的解决方案代码与额外信息结合起来,以便其他人可以直接复制而不需要额外尝试。

问题不仅在于服务器端的DH密钥问题,还有很多不同的库在Python模块中不匹配。

以下代码段用于忽略那些安全问题,因为可能无法在服务器端解决。例如,如果它是内部老化服务器,则没有人想更新它。

除了针对 'HIGH:!DH:!aNULL' 的被攻击字符串外,urllib3 模块也可以被导入以禁用警告,如果它已经存在。

import requests
import urllib3

requests.packages.urllib3.disable_warnings()
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
try:
    requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
except AttributeError:
    # no pyopenssl support used / needed / available
    pass

page = requests.get(url, verify=False)

1
使用自定义的HTTPAdapter更安全,该适配器将规则添加到整个会话中,例如requests.Session().mount("https://affected.website", MySSLContextAdapter(ssl_ciphers='HIGH:!DH:!aNULL')),其中该适配器创建context = ssl.create_default_context()并设置context.set_ciphers(self.ssl_ciphers),并在HTTPAdapter.init_poolmanager中使用它。 - frost-nzcr4
2
如果您直接与套接字进行交互,可以通过上下文(在Python 3.2+和2.7.9+中)设置这些密码:context.set_ciphers('HIGH:!DH:!aNULL') - Nick K9
2
你的评论拯救了一家企业...我们用于付款的银行出现了SSL问题,而你的评论暂时解决了这个问题...给了我们足够的时间去更换并与另一家银行合作。 - Amir Heshmati
我不需要 'verify=False'。它有效。 我想说的是,对于在TLSv1.1和TLSv1.2中弃用((Diffie Hellman)),'HIGH:!DH:!aNULL'的被黑字符串没有严重的安全问题。 - F.Tamy
感谢 @Larry Cai!您的提示帮助我使用 REST API 在 Databricks 中执行 POST 方法。 - Fernando Delago
如果你正在使用 httpx,那么你可以通过 httpx._config.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' 来有效地完成同样的操作。 - Scott Stafford

29

这对我也起作用:

import requests
import urllib3
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS = 'ALL:@SECLEVEL=1'

openssl的SECLEVEL文档:https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html

目前SECLEVEL=2是openssl的默认设置,至少在我的环境中(ubuntu 20.04,openssl 1.1.1f); SECLEVEL=1会降低安全性。

安全级别旨在避免对单个密码进行调整的复杂性。

我认为我们大多数凡人不具备单个密码的安全强度/弱点方面的深入了解,我肯定没有。 安全级别似乎是一种保持对安全门限控制的好方法。

注意:我得到了一个不同的SSL错误,WRONG_SIGNATURE_TYPE而不是SSL_NEGATIVE_LENGTH,但基本问题是相同的。

错误:

Traceback (most recent call last):
  [...]
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 581, in post
    return self.request('POST', url, data=data, json=json, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 533, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 646, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/adapters.py", line 514, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: HTTPSConnectionPool(host='somehost.com', port=443): Max retries exceeded with url: myurl (Caused by SSLError(SSLError(1, '[SSL: WRONG_SIGNATURE_TYPE] wrong signature type (_ssl.c:1108)')))

21

禁用警告或证书验证并不能解决问题。底层问题是服务器使用了一个弱DH密钥,可以在Logjam攻击中被滥用。

要解决这个问题,您需要选择一种密码套件,它不使用Diffie Hellman密钥交换,并且因此不会受到弱DH密钥的影响。而且这种密码套件必须得到服务器的支持。目前不清楚服务器支持哪种密码套件,但您可以尝试使用密码套件AES128-SHA或密码套件集合HIGH:!DH:!aNULL

使用带有自定义密码套件的请求有点棘手。请参考为什么Python Requests忽略验证参数?以获取示例。


谢谢您的建议,我能够使用提供的密码集和文章克服这些错误。 - Feocco
我对此并不很熟练,您能告诉我如何使用您的后续建议尝试使用urllib2进行另一种密码吗?请看一下我的问题:https://stackoverflow.com/questions/52440129/urllib2-ssl3-check-cert-and-algorithm-dh-key-too-small - steff_bdh
除了告诉客户端不要使用DH外,另一个选项是配置服务器使用更多比特位,使用dhparam可以查看指针 https://dev59.com/RLroa4cB1Zd3GeqPlHut#64581683/ - sparrowt
@sparrowt:OP明确表示:“最好是客户端修复。” - Steffen Ullrich
确实,因此不将此作为答案发布——但是有些人会找到这个问题,他们拥有服务器访问权限,在这种情况下,我认为提供如何在服务器端修复它的上下文(当我来到这里时会对我有帮助),而不是绕过它并留下使用不安全的DH配置的服务器,其他客户端也必须避免。 - sparrowt

14

我遇到了同样的问题。

通过注释,问题得以解决。

CipherString = DEFAULT@SECLEVEL=2

位于 /etc/ssl/openssl.cnf 中的一行。


1
我所做的是将2设置为1,而不是注释整行代码,这也起作用了...有一定程度的安全性总比没有好,对吧? - Juan Ignacio Avendaño Huergo

5

urllib3 的版本 2 或者 requests 的版本 2.30 开始,DEFAULT_CIPHERS 属性已经被移除,大部分之前提出的解决方案已经不再适用。相反,你需要创建一个 SSLContext

from urllib3.util import create_urllib3_context
from urllib3 import PoolManager

ctx = create_urllib3_context(ciphers=":HIGH:!DH:!aNULL")
http = PoolManager(ssl_context=ctx)
http.request("GET", ...)

如果您正在使用requests,则必须子类化自己的{{link1:HTTPAdapter}},该适配器初始化一个PoolManager,并将其挂载到Session中:
from urllib3.util import create_urllib3_context
from urllib3 import PoolManager
from requests.adapters import HTTPAdapter
from requests import Session()

class AddedCipherAdapter(HTTPAdapter):
  def init_poolmanager(self, conntections, maxsize, block=False):
    ctx = create_urllib3_context(ciphers=":HIGH:!DH:!aNULL")
    self.poolmanager = PoolManager(
      num_pools=connections,
      maxsize=maxsize,
      block=block,
      ssl_context=ctx
    )

s = Session()
s.mount("https://example.org", AddedCipherAdapter())
s.get("https://example.org/path")

4

requests库核心开发团队的某个成员已经记录了一种方法,使更改仅限于一个或少数几个服务器:

https://lukasa.co.uk/2017/02/Configuring_TLS_With_Requests/

如果您的代码与多台服务器交互,则有意义的是不要降低所有连接的安全要求,因为其中一台服务器具有问题的配置。

该代码对我来说可以直接使用。也就是说,使用我的值CIPHERS'ALL:@SECLEVEL=1'


3

也许最好不要覆盖默认的全局密码,而是创建具有特定会话所需密码的自定义HTTPAdapter:

import ssl
from typing import Any

import requests

class ContextAdapter(requests.adapters.HTTPAdapter):
    """Allows to override the default context."""

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self.ssl_context: ssl.SSLContext|None = kwargs.pop("ssl_context", None)

        super().__init__(*args, **kwargs)

    def init_poolmanager(self, *args: Any, **kwargs: Any) -> Any:
        # See available keys in urllib3.poolmanager.SSL_KEYWORDS
        kwargs.setdefault("ssl_context", self.ssl_context)

        return super().init_poolmanager(*args, **kwargs)

然后,您需要创建自定义上下文,例如:
import ssl

def create_context(
    ciphers: str, minimum_version: int, verify: bool
) -> ssl.SSLContext:
    """See https://peps.python.org/pep-0543/."""
    ctx = ssl.create_default_context()

    # Allow to use untrusted certificates.
    if not verify:
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE

    # Just for example.
    if minimum_version == ssl.TLSVersion.TLSv1:
        ctx.options &= (
            ~getattr(ssl, "OP_NO_TLSv1_3", 0)
            & ~ssl.OP_NO_TLSv1_2
            & ~ssl.OP_NO_TLSv1_1
        )
        ctx.minimum_version = minimum_version

    ctx.set_ciphers(ciphers)

    return ctx

然后您需要使用自定义上下文规则为每个网站进行配置:

session = requests.Session()
session.mount(
    "https://dh.affected-website.com",
    ContextAdapter(
        ssl_context=create_context(
            ciphers="HIGH:!DH:!aNULL"
        ),
    ),
)
session.mount(
    "https://only-elliptic.modern-website.com",
    ContextAdapter(
        ssl_context=create_context(
            ciphers="ECDHE+AESGCM"
        ),
    ),
)
session.mount(
    "https://only-tls-v1.old-website.com",
    ContextAdapter(
        ssl_context=create_context(
            ciphers="DEFAULT:@SECLEVEL=1",
            minimum_version=ssl.TLSVersion.TLSv1,
        ),
    ),
)

result = session.get("https://only-tls-v1.old-website.com/object")

阅读所有答案后,我可以说@bgoeman的答案接近于我的观点,您可以跟随他们的链接了解更多信息。


请注意,如果您正在使用 pip-system-certs,它会热补丁HTTPAdapter基类,这意味着 super().init_poolmanager 的行为与您预期的不同。我曾经吃过这个亏。 - David A

3
根据用户bgoeman的回答,以下代码可以保留默认密码并增加安全级别。
import requests
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += '@SECLEVEL=1'

2

我在从Ubuntu 18.04升级到20.04后遇到了这个问题,下面的命令对我有效。

pip install --ignore-installed pyOpenSSL --upgrade

我有Ubuntu 20.04,但它没有帮助我。 - F.Tamy
它搞乱了我所有现有的Python包,而且无法安装任何包。 - HERAwais

2

我将在这里提供我的解决方案。我不得不修改Python SSL库,因为我在Docker容器中运行代码,但这可能不是你想要做的事情。

  1. 获取服务器支持的密码套件。在我的情况下,是第三方电子邮件服务器,我使用了描述列出SSL/TLS密码套件的脚本。

check_supported_ciphers.sh

#!/usr/bin/env bash

# OpenSSL requires the port number.
SERVER=$1
DELAY=1
ciphers=$(openssl ciphers 'ALL:eNULL' | sed -e 's/:/ /g')

echo Obtaining cipher list from $(openssl version).

for cipher in ${ciphers[@]}
do
echo -n Testing $cipher...
result=$(echo -n | openssl s_client -cipher "$cipher" -connect $SERVER 2>&1)
if [[ "$result" =~ ":error:" ]] ; then
  error=$(echo -n $result | cut -d':' -f6)
  echo NO \($error\)
else
  if [[ "$result" =~ "Cipher is ${cipher}" || "$result" =~ "Cipher    :" ]] ; then
    echo YES
  else
    echo UNKNOWN RESPONSE
    echo $result
  fi
fi
sleep $DELAY
done

授予权限:

chmod +x check_supported_ciphers.sh

并执行它:

./check_supported_ciphers.sh myremoteserver.example.com | grep OK

几秒钟后,你会看到一个类似于下面的输出结果:
Testing AES128-SHA...YES (AES128-SHA_set_cipher_list)

因此,我们将使用“AES128-SHA”作为SSL密码。

  1. 在你的代码中强制引发错误:

    Traceback (most recent call last): File "my_custom_script.py", line 52, in imap = IMAP4_SSL(imap_host) File "/usr/lib/python2.7/imaplib.py", line 1169, in init IMAP4.init(self, host, port) File "/usr/lib/python2.7/imaplib.py", line 174, in init self.open(host, port) File "/usr/lib/python2.7/imaplib.py", line 1181, in open self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile) File "/usr/lib/python2.7/ssl.py", line 931, in wrap_socket ciphers=ciphers) File "/usr/lib/python2.7/ssl.py", line 599, in init self.do_handshake() File "/usr/lib/python2.7/ssl.py", line 828, in do_handshake self._sslobj.do_handshake() ssl.SSLError: [SSL: DH_KEY_TOO_SMALL] dh key too small (_ssl.c:727)

  2. 获取Python SSL库路径,如下所示:

    /usr/lib/python2.7/ssl.py

  3. 编辑它:

    cp /usr/lib/python2.7/ssl.py /usr/lib/python2.7/ssl.py.bak

    vim /usr/lib/python2.7/ssl.py

并将以下内容替换为:

_DEFAULT_CIPHERS = (
    'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:'
    'ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:'
    '!aNULL:!eNULL:!MD5:!3DES'
    )

通过:

_DEFAULT_CIPHERS = (
    'AES128-SHA'
    )

No "YES" at all - thetaprime

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接