为什么Python SSL模块无法验证graph.facebook.com的证书?
当我在浏览器中输入 https://graph.facebook.com/me 时,我得到了一个加密的HTTPS连接,这个连接的证书链如下:
- DigiCert 高级保证 EV 根证书 (根证书)
- DigiCert 高级保证 CA-3
- *.facebook.com
所以我从 https://www.digicert.com/digicert-root-certificates.htm 下载了根证书(我也从浏览器中导出了这个证书,比较后发现它们是一样的),然后尝试用Python自带的SSL模块来验证与graph.facebook.com的连接是否真实有效。
我执行了示例代码 http://docs.python.org/library/ssl.html#client-side-operation,把ca_cert替换成"DigiCertHighAssuranceEVRootCA.crt",地址替换成graph.facebook.com。结果连接尝试失败,出现了异常:
ssl.SSLError: [Errno 1] _ssl.c:499: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
如果我用同样的代码和证书去测试ev-root.digicert.com(这是DigiCert提供的地址,用来测试客户端是否能验证他们的证书),一切都正常。在浏览器中,我可以确认这个连接使用的证书链是:
- DigiCert 高级保证 EV 根证书 (根证书)
- DigiCert 高级保证 EV CA-1
- ev-root.digicert.com
通过运行 ssl.get_server_certificate(('graph.facebook.com', 443)) 我得到了和浏览器中显示的相同的证书,标识为"*.facebook.com",这意味着Python代码和我的浏览器都得到了相同的证书来进行验证。
为什么Chrome可以用给定的根证书验证graph.facebook.com,而Python可以用同样的根证书验证另一个网站,但Python却无法验证graph.facebook.com呢?
2 个回答
如果你在使用代理服务器工作,可能需要在浏览器中打开 https://graph.facebook.com,然后查看一下那里的证书路径。你的代理服务器可能会充当网站证书的颁发者。在这种情况下,你需要找到你的代理证书,把它提取成一个pem文件,然后把这个文件的内容添加到Python的certifi cacert.pem证书里。
我从OpenSSL的邮件列表上找到了答案。看起来“DigiCert高保障EV根证书”在被自己签名之前,是由另一个证书机构签名的。所以现在有两个版本的证书。一个是和SSL实现捆绑在一起的,DigiCert提供下载的那个,它是自签名的,可以用作根证书来验证它签发的其他证书。另一个版本是在SSL握手过程中,Facebook的服务器返回的,它是由某个Entrust证书签名的。这两个证书的公钥和keyid是一样的。
NSS,也就是Firefox和Chrome的SSL实现,似乎正确地遵循了X.509的规范,忽略了服务器发送的证书链中的最后一个证书,而是使用自己信任的“DigiCert高保障EV根证书”来验证整个证书链。Python的实现是基于OpenSSL的,它使用主机提供的证书来验证“DigiCert高保障CA-3”,然后再尝试验证最后一个证书。因为这个证书是由其他证书机构签名的,而我没有提供那个证书,所以验证失败。我觉得这种行为不太对,因为既然我已经信任了证书链中间的一个证书,理论上我就不需要检查后面的证书了。
我的解决办法是给ssl模块提供能够验证“DigiCert高保障EV根证书”的Entrust证书。