@
一、哪些情況可能會遇到這個錯誤?
- 使用urllib.request.urlopen請求https資源時(不限系統)
- 使用requests的get或post方法請求https資源時(不限系統)
- MAC OS中使用上面任意庫請求https資源時
- 使用pip install安裝軟件包時
- 使用setup.py upload 上傳軟件包到倉庫時
- 使用其他python腳本或命令,內部使用了情況1或情況2的相關接口,訪問https資源時。
- 使用Python類庫請求一些https網站正常,請求另外一些https網站遇到這個錯誤
二、為什么會出現這個錯誤?這個錯誤說明了什么?
要想知道這個錯誤為什么會出現,是什么導致了這個錯誤,需要簡單了解下SSL和證書的相關知識:
2.1 HTTPS的簡要知識
在HTTPS普及之前,大多數網站都使用明文傳輸的HTTP協議。用戶和網站的訪問內容,密碼等在家庭路由器、小區交換機、公網節點等都是可以被中間人看到、攔截或修改的,即主要存在泄漏、篡改、假冒三大安全問題。
為了解決上面的安全問題,就需要對傳輸內容進行加密,常規的加密算法包括對稱加密和非對稱加密,因為對稱加密存在秘鑰不便傳輸問題,HTTPS在連接的初始建立階段使用的是非對稱加密算法,加密連接創建后再切換到對稱加密算法提高性能。
非對稱加密的特點:
- 存在公鑰和私鑰兩個秘鑰,一般公鑰是任何人都可以拿到的。
- 公鑰加密的內容可以用私鑰解密,反之亦然。
- 用公鑰或私鑰加密很簡單,但根據密文和公鑰破解私鑰很難。
HTTPS協議使用的是RSA非對稱加密技術。HTTPS通信流程如下:
- TCP三次握手
- 客戶端向服務器發起HTTPS請求
- 服務器返回數字證書
- 客戶端用本地內置證書驗證服務器證書有效性,如果證書無效中斷整個流程;證書有效則生成隨機值作為對稱秘鑰,並使用證書加密對稱秘鑰;
- 客戶端把證書加密后的對稱秘鑰發送給服務器
- 服務器用私鑰解密獲得對稱秘鑰,再用對稱秘鑰加密內容發給客戶端
- 客戶端收到服務器對稱加密的內容,可以正確解密,至此加密連接創建完成。
流程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的進一步排查方法:
- 查看當前python使用的openssl版本:
>>> import _ssl
>>> _ssl.OPENSSL_VERSION
'OpenSSL 1.1.1g 21 Apr 2020'
- 找到對應Openssl程序
可通過everything搜索,按時間找到:
- 命令提示符打開,請求出錯的URL,查看證書鏈:
openssl s_client -showcerts -connect www.baidu.com:443
從顯示的證書鏈來看,第二個證書R3是let's encrypt的根證書,第一個DSR Root證書確實於2021年9月到期了,過期了兩個月。
而瀏覽器查看網站的證書鏈,第一個證書的有效期是從2015年到2035年:
3.2.3 證書鏈不完整
現象:myssl.com驗證網站證書有效(提示證書鏈不完整),此電腦瀏覽器訪問網站證書有效,部分軟件請求網站證書錯誤。
3.2.4 MacOS上python3沒有證書列表
現象:python腳本訪問任何https網站均報證書錯誤。
3.2.5 解析證書的客戶端軟件問題
現象:通過myssl.com驗證網站證書有效,此電腦瀏覽器訪問網站證書有效,但python urlopen或requests訪問網站證書錯誤。
四、如何解決證書錯誤?
4.1 證書無效,替換有效證書
如果是自簽名證書或證書過期等情況,建議申請個有效的證書部署。
收費證書很多,可以找權威機構購買;免費證書可以考慮:
- 申請let's encrypt的三個月通配符證書;到期前需要重新申請並部署。
- 阿里雲的一年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 更新操作系統根證書庫
此方法可以解決部分證書錯誤,一些軟件包未使用系統根證書導致的錯誤仍然無法解決。
- 生成更新根證書列表文件:
D:/
certutil.exe -generateSSTFromWU roots.sst
- 從SST文件導入所有根證書,注意以管理員身份運行Powser Shell:
$sstStore = ( Get-ChildItem -Path D:\roots.sst )
$sstStore | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root
- 重新進入【管理計算機證書】,查看根證書數量已從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/