Python调用使用自颁发证书的https接口

使用keytools或者openssl生成p12格式的KeyStore(包含SSL证书),并使用该证书和SpringBoot搭建了服务端的https接口,搭建过程参考HTTPS相关知识点介绍

接下来介绍如何在Pyhon中使用requests工具包调用服务端的https接口。

第1次尝试-直接调用

import requests
if __name__ == '__main__':
    LOCAL_SERVICE_URL = 'https://localhost:1443/api/anno/success'
    REMOTE_SERVICE_URL = 'https://www.baidu.com/'
    print('\n\n-----request remote https-----')
    res = requests.get(REMOTE_SERVICE_URL)
    print(res.status_code)
    print('\n\n-----request local https-----')
    res = requests.get(LOCAL_SERVICE_URL)
    print(res.status_code)
复制代码

运行结果:调用REMOTE_SERVICE_URL成功,调用LOCAL_SERVICE_URL抛出异常:ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1131)

结果分析:requests工具包进行SSL验证时依赖certifi工具包,certifi包安装目录下的cacert.pem文件中预置了可信任的根证书颁布机构清单,默认情况下只有清单中机构及其下层机构颁布的SSL证书可以通过验证。REMOTE_SERVICE_URL使用的SSL证书是正式CA机构颁布的,其根证书机构为:GlobalSign Root CA,该机构就在certifi包可信任的根证书CA机构清单中,所以调用REMOTE_SERVICE_URL成功。而LOCAL_SERVICE_URL使用的自颁布SSL证书,不在可信任机构清单中,所以验证失败抛出异常。

解决办法:一种是跳过SSL证书验证,一种是将自颁发证书的机构设置为可信任的。

第2次尝试-跳过SSL验证

根据requests的官方文档,虽然不推荐,但是可以通过设置请求参数verify=False来跳过SSL验证。

import requests
from requests.packages import urllib3
if __name__ == '__main__':
    LOCAL_SERVICE_URL = 'https://localhost:1443/api/anno/success'
    REMOTE_SERVICE_URL = 'https://www.baidu.com/'
    # 屏蔽requests关闭SSL证书验证(verify参数设置为False)时的告警信息
    urllib3.disable_warnings()
    print('\n\n-----request remote https-----')
    res = requests.get(REMOTE_SERVICE_URL)
    print(res.status_code)
    print('\n\n-----request local https-----')
    res = requests.get(LOCAL_SERVICE_URL, verify=False)
    print(res.status_code)
复制代码

第3次尝试-将自颁发证书的机构设置为可信任-临时设置

根据requests的官方文档,可以将请求参数verify设置为 CA_BUNDLE 文件的路径或者包含可信任 CA 证书文件的文件夹路径(如果 verify 设为文件夹路径,文件夹必须通过 OpenSSL 提供的 c_rehash 工具处理),这样也可以通过SSL证书验证。

根据博客What is a CA Bundle and Where to Find It?的说明,CA Bundle是包含根证书和中间证书的一个文件,由于自颁发SSL证书只有一层,所以自颁发的SSL证书就是CA Boundle文件。

requestsverify参数要求CA Bundle文件是文本格式,二进制的无法读取。下面介绍获取文本格式SSL证书的两种方法:

  1. 由于p12格式的KeyStore包含了SSL证书和私钥,所以需要从中提取SSL证书,采用openssl的python工具包将p12格式转换为pem格式
import os
from OpenSSL import crypto
​
def p12_to_pem(cert_name, pwd):
    pem_file_path = cert_name + '.pem'
    pem_file = open(pem_file_path, 'wb')
    p12_file_path = cert_name + '.p12'
    p12_file = crypto.load_pkcs12(open(p12_file_path, 'rb').read(), pwd)
​
    print(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12_file.get_privatekey()))
    print(crypto.dump_certificate(crypto.FILETYPE_PEM, p12_file.get_certificate()))
​
    pem_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12_file.get_privatekey()))
    pem_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, p12_file.get_certificate()))
    ca = p12_file.get_ca_certificates()
    if ca is not None:
        for cert in ca:
            pem_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
    pem_file.close()
    return pem_file_path
​
if __name__ == '__main__':
    root_path = 'E:\dataPython\data\cert'
    cert_pem_file_path = p12_to_pem(os.path.join(root_path, 'baeldung'), b'****')
复制代码

生成pem格式的SSL证书文件baeldung.pem内容如下:其中PRIVATE KEY其实不是SSL证书的内容,删除后也不影响证书验证。

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCN1bHqnX4kceSh
wSlb4s8X7Hz+581Kyq2tPDBSwqe6b9SmC5Hq0m8EbsChy/OwM9FrZZF9bOdPHHUM
sCtl3EEmc0fHylHqBT9/JWM=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDgjCCAmqgAwIBAgIEZacX2zANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJD
TjESMBAGA1UECBMJZ3Vhbmdkb25nMREwDwYDVQQHEwhzaGVuemhlbjENMAsGA1UE
9o2PulnUgwok+63gcbkCA1In7tz+qylx/bhz8qT0eI6WpsPYc0Y=
-----END CERTIFICATE-----
复制代码
  1. 除了使用openssl将p12转换为pem格式的SSL证书外,可以使用浏览器功能导出证书。在浏览器访问服务端的https接口后,按以下步骤即可导出cer格式的SSL证书。

image-20220405185251963.png

导出的SSL证书文件baeldung.cer内容如下:只有证书内容,没有PRIVATE KEY。

-----BEGIN CERTIFICATE-----
MIIDgjCCAmqgAwIBAgIEZacX2zANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJD
TjESMBAGA1UECBMJZ3Vhbmdkb25nMREwDwYDVQQHEwhzaGVuemhlbjENMAsGA1UE
9o2PulnUgwok+63gcbkCA1In7tz+qylx/bhz8qT0eI6WpsPYc0Y=
-----END CERTIFICATE-----
复制代码

最后设置requestsverify为CA BUNDLE文件路径来解决证书验证不通过的问题,用pem或cer格式的都可以。

import requests
from requests.packages import urllib3
if __name__ == '__main__':
    LOCAL_SERVICE_URL = 'https://localhost:1443/api/anno/success'
    REMOTE_SERVICE_URL = 'https://www.baidu.com/'
    print('\n-----request remote https-----')
    res = requests.get(REMOTE_SERVICE_URL)
    print(res.status_code)
    print('\n-----request local https-----')
    res = requests.get(LOCAL_SERVICE_URL, verify='E:/dataPython/data/cert/baeldung.pem')
    print(res.status_code)
    res = requests.get(LOCAL_SERVICE_URL, verify='E:/dataPython/data/cert/baeldung.cer')
    print(res.status_code)
复制代码

注:对于https://www.baidu.com/其SSL证书有3层,所以仅仅导出某一层的SSL证书是不能作为CA BUNDLE文件的,否则反而会报错。必须将3层证书按最底层到最高层(ROOT)的顺序拷贝到1个文件后,该文件才能作为CA BUNDLE文件。即按照博客What is a CA Bundle and Where to Find It?所说的操作。

第4次尝试-将自颁发证书的机构设置为可信任-永久设置

根据第1次尝试的结果分析,证书校验时从certifi包安装目录下的cacert.pem文件获取可信任的根证书颁发机构清单,那我们可以把我们自颁发的证书内容追加到cacert.pem文件来一次性解决SSL证书验证问题。追加内容后的cacert.pem文件如下:

# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
# Label: "GlobalSign Root CA"
# Serial: 4835703278459707669005204
# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a
# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c
# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99
-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
-----END CERTIFICATE-----
​
# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA
# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA
# Label: "HARICA TLS ECC Root CA 2021"
# Serial: 137515985548005187474074462014555733966
# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0
# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48
# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01
-----BEGIN CERTIFICATE-----
MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw
nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps
-----END CERTIFICATE-----
​
#省略其他ROOT CA
​
# SelfSigned 
-----BEGIN CERTIFICATE-----
MIIDgjCCAmqgAwIBAgIEZacX2zANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJD
TjESMBAGA1UECBMJZ3Vhbmdkb25nMREwDwYDVQQHEwhzaGVuemhlbjENMAsGA1UE
9o2PulnUgwok+63gcbkCA1In7tz+qylx/bhz8qT0eI6WpsPYc0Y=
-----END CERTIFICATE-----
​
复制代码

再次运行第1次尝试的代码来调用https借口,访问都正常。

import requests
if __name__ == '__main__':
    LOCAL_SERVICE_URL = 'https://localhost:1443/api/anno/success'
    REMOTE_SERVICE_URL = 'https://www.baidu.com/'
    print('\n\n-----request remote https-----')
    res = requests.get(REMOTE_SERVICE_URL)
    print(res.status_code)
    print('\n\n-----request local https-----')
    res = requests.get(LOCAL_SERVICE_URL)
    print(res.status_code)
复制代码

总结

第2、3、4次尝试中使用的三种解决方法,推荐采用第3次尝试使用的设置verify参数为CA BUNDLE文件路径,安全性更好。

猜你喜欢

转载自juejin.im/post/7083080207904702478