一、哪些情况可能会遇到这个错误?

  1. 使用urllib.request.urlopen请求https资源时(不限系统)
  2. 使用requests的get或post方法请求https资源时(不限系统)
  3. MAC OS中使用上面任意库请求https资源时
  4. 使用pip install安装软件包时
  5. 使用setup.py upload 上传软件包到仓库时
  6. 使用其他python脚本或命令,内部使用了情况1或情况2的相关接口,访问https资源时。
  7. 使用Python类库请求一些https网站正常,请求另外一些https网站遇到这个错误

二、为什么会出现这个错误?这个错误说明了什么?

要想知道这个错误为什么会出现,是什么导致了这个错误,需要简单了解下SSL和证书的相关知识:

2.1 HTTPS的简要知识

在HTTPS普及之前,大多数网站都使用明文传输的HTTP协议。用户和网站的访问内容,密码等在家庭路由器、小区交换机、公网节点等都是可以被中间人看到、拦截或修改的,即主要存在泄漏、篡改、假冒三大安全问题。
为了解决上面的安全问题,就需要对传输内容进行加密,常规的加密算法包括对称加密和非对称加密,因为对称加密存在秘钥不便传输问题,HTTPS在连接的初始建立阶段使用的是非对称加密算法,加密连接创建后再切换到对称加密算法提高性能。
非对称加密的特点:

  • 存在公钥和私钥两个秘钥,一般公钥是任何人都可以拿到的。
  • 公钥加密的内容可以用私钥解密,反之亦然。
  • 用公钥或私钥加密很简单,但根据密文和公钥破解私钥很难。
    HTTPS协议使用的是RSA非对称加密技术。HTTPS通信流程如下:
  1. TCP三次握手
  2. 客户端向服务器发起HTTPS请求
  3. 服务器返回数字证书
  4. 客户端用本地内置证书验证服务器证书有效性,如果证书无效中断整个流程;证书有效则生成随机值作为对称秘钥,并使用证书加密对称秘钥;
  5. 客户端把证书加密后的对称秘钥发送给服务器
  6. 服务器用私钥解密获得对称秘钥,再用对称秘钥加密内容发给客户端
  7. 客户端收到服务器对称加密的内容,可以正确解密,至此加密连接创建完成。
    流程3的服务器证书里面存储的就是网站的公钥。我们遇到的错误就是发生在流程4中,验证证书无效。
    更多关于HTTPS的知识

2.2 客户端是如何验证服务器证书的呢?

服务器的证书是包含了网站主要信息、公钥、数字签名等信息的一个文件。
非对称加密只是确保了网络连接的不被窃听和篡改,但包含公钥的证书是谁都可以签发的,那拿到一个百度证书如何确定这个证书就是百度的证书呢?公钥一般几百上千个字符,全球十几亿个网站证书全都记录下来也不现实,所以就提出了另一个概念:CA(Certificate Authority)证书签发机构。客户端和服务器端都信任且保存了CA的证书,作为根证书;如果一个证书是由CA签发的二级证书,那我们可以用CA的证书验证它的有效性;如果服务器的证书是由验证过的二级证书签发的,那么我们也可以验证服务器的证书的有效性;这样的一个逐层验证过程即形成了证书链
根证书是直接保存在操作系统中的,凡是根证书直接或间接签发的证书都可以信任;按这样的原理,我们可以验证成千上万的网站证书是否有效;截止2020/08,MacOS内置了168个根证书,Windows内置了255个根证书,Chrome和FireFox使用自带的证书库。
需要注意的是,虽然操作系统内置了根证书,但一些软件可能并未使用系统根证书,而是在软件安装时自身带了一些证书作为根证书。(如pythonpython的requests库和pip等库使用包内置的根证书,有些浏览器也使用的内置根证书而非系统根证书),如果电脑装有everything,搜索cacert.pem可以发现很多文件,这些都是各个软件自带的根证书。
此外,**证书链可能不止一条!**计算机本地保存的根证书一般是几十年有效期,由根证书颁发的二级证书一般是几年有效期。如果你的电脑买了有两三年了,有可能出现买的时候根证书还有效,现在根证书过期的情况,从而导致一些网站无法访问。从浏览器访问网站是按浏览器的根证书库形成证书链,浏览器的根证书库由浏览器维护和更新,所以一直显示是正常的。而windows操作系统根证书库由windows更新维护,python ssl使用的是操作系统的根证书库,所以也可能出现浏览器中证书正常,python中报证书过期或无效问题。(PS:Windows10家庭版似乎没有根证书自动更新。。)
虽然服务器部署证书的时候,一般都是部署的完整证书链,但客户端不一定会使用网站提供的证书链;如果服务器证书链的根证书在客户端本地根证书库中不存在,客户端就会从本地根证书库中尝试自己构造一个可信的证书链,构造过程中可能会使用过期的证书。
这是一个中间证书过期的例子

2.2.1 常见的证书错误有:

证书不可信(ERR_CERT_AUTHORITY_INVALID)【最多】
由不可信的CA机构签发的证书,比如自签发的证书,或者操作系统缺失CA根证书。
证书名称不匹配(ERR_CERT_COMMON_NAME_INVALID)
证书是签发给www.baidu.com,但访问的网站是baidu.com,也会出现这个问题。
证书过期(ERR_CERT_DATE_INVALID)
证书有效期到期,或本地时间不正确(根据行业标准,网站SSL证书有效期不能超过398天)
证书被吊销(ERR_CERT_REVOKED)
虚假信息申请得到的证书,或私钥被泄漏,或签发错误等会出现这个情况
一般性SSL错误(ERR_SSL_PROTOCOL_ERROR)
证书部署错误,客户端SSL协议不匹配,证书字段缺失等问题。

三、哪些原因可能会导致证书错误?

根据上面分析,造成证书错误的原因有很多,但笼统的可以分为两类:证书无效报错,证书有效报错
判断网站证书是否有效建议通过myssl.com,输入网站域名,可以测试出网站安全等级、支持的TLS版本、证书问题、支持浏览器版本等情况。

3.1 证书无效报错

3.1.1 网站使用自签发证书

自签名的证书因为不是由根证书逐层签发的,所以任意一台电脑访问这种网站时都会弹出连接不安全的警告。
现象:这个网站在任意一台电脑访问都会报证书错误。

3.1.2 证书和域名不匹配

证书分为单域名证书和通配符证书;单域名证书需要和网站域名完全匹配才能验证通过,比如申请了www.baidu.com的单域名证书,部署在news.baidu.com上面就会出现证书名称不匹配错误。通配符证书一般是针对主域名申请,如“*.baidu.com”,可以部署在其下所有子域名网站上。

3.2 证书有效报错

3.2.1 本地计算机缺少合适的根证书

现象:其他电脑访问此网站证书有效,此电脑报证书错误。(或通过myssl.com验证网站证书有效)

3.2.2 本地计算机根证书过期未更新

现象:新电脑执行python脚本或命令正常,此电脑浏览器访问证书有效,python脚本显示证书无效。
针对Python ssl的进一步排查方法:

  1. 查看当前python使用的openssl版本:
>>> import _ssl
>>> _ssl.OPENSSL_VERSION
'OpenSSL 1.1.1g  21 Apr 2020'
  1. 找到对应Openssl程序
    可通过everything搜索,按时间找到:
    找到对应版本的Openssl
  2. 命令提示符打开,请求出错的URL,查看证书链:
openssl s_client -showcerts -connect www.baidu.com:443

openssl查看证书链
从显示的证书链来看,第二个证书R3是let’s encrypt的根证书,第一个DSR Root证书确实于2021年9月到期了,过期了两个月。
而浏览器查看网站的证书链,第一个证书的有效期是从2015年到2035年:
浏览器的let‘s 证书

3.2.3 证书链不完整

现象:myssl.com验证网站证书有效(提示证书链不完整),此电脑浏览器访问网站证书有效,部分软件请求网站证书错误。

3.2.4 MacOS上python3没有证书列表

现象:python脚本访问任何https网站均报证书错误。

3.2.5 解析证书的客户端软件问题

现象:通过myssl.com验证网站证书有效,此电脑浏览器访问网站证书有效,但python urlopen或requests访问网站证书错误。

四、如何解决证书错误?

4.1 证书无效,替换有效证书

如果是自签名证书或证书过期等情况,建议申请个有效的证书部署。
收费证书很多,可以找权威机构购买;免费证书可以考虑:

  1. 申请let’s encrypt的三个月通配符证书;到期前需要重新申请并部署。
  2. 阿里云的一年SSL单域名证书,一年最多申请20个;过期前需要重新申请部署。

4.2 证书有效报证书验证错误的几种解决方案【重要】

4.2.1 MAC OS安装python根CA证书列表

仅针对苹果系统有效。

pip install --upgrade certifi

然后执行文件:/Applications/Python\ 3.6/Install\ Certificates.command

4.2.2 补全证书链

上面我们提到,网站的证书是否有效是通过操作系统根证书逐层验证的,如果系统缺失了对应的根证书,就可能会出现网站在一些浏览器访问正常,在其他客户端(python脚本、微信浏览器等)报证书错误的现象。
如果myssl.com提示证书错误,可以按上面的修复工具连接,修复证书链不全问题。
证书链修复传送门
一般修复前证书链有2个或三个证书,修复后会多一个;直接将修复后的证书链重新部署到服务器即可。

4.2.3 更新操作系统根证书库

此方法可以解决部分证书错误,一些软件包未使用系统根证书导致的错误仍然无法解决。

  1. 生成更新根证书列表文件:
D:/
certutil.exe -generateSSTFromWU roots.sst
  1. 从SST文件导入所有根证书,注意以管理员身份运行Powser Shell:
$sstStore = ( Get-ChildItem -Path D:\roots.sst )
$sstStore | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root
  1. 重新进入【管理计算机证书】,查看根证书数量已从200+变成400+
    更多关于更新windows证书的介绍

4.2.4 手动将证书添加到根CA证书列表【推荐】

此方法可解决几乎全部使用python ssl包导致的证书错误问题。
此方法适用于客户端较少的情况,如果很多客户端都报证书错误,建议使用下一个方案。
查看python默认使用的根证书文件:

import ssl
ssl.get_default_verify_paths()

> DefaultVerifyPaths(cafile='C:\\Program Files\\Common Files\\SSL/cert.pem', capath=None, openssl_cafile_env='SSL_CERT_FILE', openssl_cafile='C:\\Program Files\\Common Files\\SSL/cert.pem', openssl_capath_env='SSL_CERT_DIR', openssl_capath='C:\\Program Files\\Common Files\\SSL/certs')

第一个cafile即当前使用的根证书列表文件,可以去看下这个文件是否存在。我们把证书内容追加到这个文件末尾即可。

如果此文件不存在,可以使用everything搜索“cacert.pem”,找个文件作为默认根证书文件(一般最大的是比较全的),我这里是:C:\Users\lenovo\AppData\Local.certifi\cacert.pem,然后添加环境变量:SSL_CERT_FILE=新的根证书文件绝对路径。
关闭命令提示符重新打开,再执行上面命令,看到cafile变成了我们修改的值即可。

浏览器打开网站,点击地址栏的锁图标,查看证书窗口,切换到证书路径tab,点击第一个根证书,查看证书,打开新证书窗口。
然后点击详细信息,复制到文件,导出证书文件。选择Base64编码X.509格式导出即可。
找到导出的文件,复制全部内容。打开cafile路径,黏贴到最后面,保存即可。

如果添加第一个证书后依然报证书错误,可以继续添加第二个(即二级证书),第三个。我是添加第二个证书后不报错了。

4.2.5 手动指定根证书文件【推荐】

如果客户端代码可以修改,可通过指定根证书的方式验证指定网站的证书:

# urllib
import urllib
urllib.request.urlopen("https://example.com/some/info", cafile="ca.pem")

# requests
requests.get('https://www.baidu.com', cert='ca.pem')

4.2.6 从其他证书提供机构重新申请证书

证书有效,但报证书错误一般都是因为根证书或中间证书的缺失或过期。所以从其他证书机构重新申请证书的话,对应的根证书和中间证书会发生变化,自然可解决证书错误问题。
一般来说,越权威的证书机构,出现证书错误的可能性越小。

4.3 屏蔽证书验证【不推荐】

适用于:证书有效报错,证书无效报错。
既然是证书验证失败导致的错误,当然可以跳过验证环境,直接默认所有证书都有效,但这相当于恢复了HTTP,存在很大安全风险,如果可通过其他方法解决,尽量不要使用此方法。
屏蔽证书验证也可以有很多级别,操作系统全局屏蔽,本进程屏蔽,本次请求屏蔽等。
屏蔽本次请求的证书验证

# 针对urllib
import ssl
context = ssl._create_unverified_context()
urllib.urlopen('https://www.baidu.com', context=context)

# 针对python3 urllib
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
urllib.urlopen('https://www.baidu.com', context=ctx)

# 针对requests
requests.get('https://www.baidu.com', verify=False)

屏蔽本进程的证书验证(一般用在进程启动时)

import ssl
ssl._create_default_https_context = ssl._create_unverified_context

屏蔽操作系统所有python进程的证书验证(不推荐,疑似仅对python2生效)
设置环境变量:PYTHONHTTPSVERIFY=0
测试对python3.6, 3.8均不生效,疑似仅对python2生效。

五、常见错误情况如何解决?

上面提出了一些解决方法,但具体到各种场景,可能需要使用不同的方法。如果已确认证书有效,依然报证书错误,下面几种情况可以考虑用对应的方法:

5.1 使用urllib或requests类库,可以修改代码

建议使用手动指定根证书文件方法,简单快速。

5.2 pip install安装软件时

可以通过pip install --trusted-host pypi.python.org [packagename] 的方式指定为信任站点
也可能是pip版本太低,pypi.python.org不支持TLS1.0和TLS1.1协议,也可以尝试升级pip版本

5.3 setup.py upload上传软件包时

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1123)
如果部署了私有pip仓库,通过https访问,上传软件包时出现这个错误。因为内部是setuptools调用的urllib,无法修改源代码。建议使用手动将证书添加到根CA证书列表的方法。
我就是这种情况。部署的pip仓库,浏览器访问证书有效,通过let’s encrypt申请的通配符证书。但使用的conda python3.8环境,上传软件包时报证书错误。测试了在python3.7 3.8 3.9 3.10中均会报错,在3.6中不报错;搜索一些文档说3.6的ssl验证会更宽松一些。
我使用urllib单独请求我的pip站点也复现了证书错误,但请求baidu.com证书没问题;通过添加ISRG ROOT X1和R3的证书到根证书文件后这个错误没有了。

5.4 其他错误如何解决

具体错误情况不同,可能的原因和解决方案也不相同,可以按是否能修改源代码等情况,逐个尝试上面的解决方法。
如果上面的方法都无法解决,建议优先搜索Stack Overflow或者google上的相关资源。
百度上的相关文章大都类似,且不够系统专业,常见问题还好,一些不常见问题很难找到解决方案。

六、更多问题

系统中过期的证书可以删除么?
永远不要删除过期证书,系统使用过期证书来保证向后兼容性,参考
免费证书和收费证书的区别
请看这篇文章

参考文章:
https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error
https://xiu2.net/it/details/6137680a61da421d645b8f01
https://coderedirect.com/questions/154225/certifacte-verify-failed-certificate-has-expired-ssl-c1108
http://woshub.com/updating-trusted-root-certificates-in-windows-10/

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐