获取https网站证书信息

获取https网站证书信息

Posted by taomujian on January 19, 2021

前言

最近为团队内部开发的平台需要获取https网站证书信息,百度谷歌了许久,参考csdn博主墨痕诉清风的代码修改下,获取的效果还不错,用了这么久,一直没有对于原理研究过,这次更新下文章,把原理等信息加上去

原理

SSL/TLS

内容

SSL和TLS协议可以为通信双方提供识别和认证通道,从而保证通信的机密性和数据完整性.TLS协议是从Netscape SSL 3.0协议演变而来的,不过这两种协议并不兼容,SSL已经逐渐被TLS取代.TLS握手是启动HTTPS通信的过程,类似于TCP建立连接时的三次握手.在TLS握手的过程中,通信双方交换消息以相互验证,相互确认,并确立它们所要使用的加密算法以及会话密钥(用于对称加密的密钥).

SSL/TLS握手大概过程:

1
2
3
4
5
6
7
1.商定双方通信所使用的的TLS版本(例如 TLS1.0, 1.2, 1.3等等)

2.确定双方所要使用的密码组合

3.客户端通过服务器的公钥和数字证书(上篇文章已有介绍)上的数字签名验证服务端的身份

4.生成会话密钥,该密钥将用于握手结束后的对称加密

SSL/TLS握手详细过程

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1."client hello"消息:客户端通过发送"client hello"消息向服务器发起握手请求,该消息包含了客户端所支持的TLS版本和密码组合以供服务器进行选择,还有一个"client random"随机字符串.

2."server hello"消息:服务器发送"server hello"消息对客户端进行回应,该消息包含了数字证书,服务器选择的密码组合和"server random"随机字符串.

3.验证:客户端对服务器发来的证书进行验证,确保对方的合法身份,验证过程可以细化为以下几个步骤:
    (1) 检查数字签名
    (2) 验证证书链(这个概念下面会进行说明)
    (3) 检查证书的有效期
    (4) 检查证书的撤回状态(撤回代表证书已失效)

4."premaster secret"字符串:客户端向服务器发送另一个随机字符串"premaster secret (预主密钥)",这个字符串是经过服务器的公钥加密过的,只有对应的私钥才能解密.

5.使用私钥: 服务器使用私钥解密"premaster secret"

6.生成共享密钥: 客户端和服务器均使用client random,server random 和 premaster secret,并通过相同的算法生成相同的共享密钥KEY

7.客户端就绪: 客户端发送经过共享密钥KEY加密过的"finished"信号。

8.服务器就绪: 服务器发送经过共享密钥KEY加密过的"finished"信号

9.达成安全通信: 握手完成,双方使用对称加密进行安全通信

HTTPS协议不是独立协议,是HTTP协议与SSL/TLS协议加一起,SSL/TLS协议处于应用传输层和应用层之间(TCP和HTTP之间)

代码首先是与目标建立一个TCP连接,然后在这个TCP连接的基础上建立一个SSL/TLS连接.然后获取服务器端的证书,获取到证书后,再按照证书格式去解析内容.在使用浏览器访问https网站时也是先与目标建立TCP连接,然后在这个TCP连接的基础上建立SSL/TLS连接,这个过程由浏览器来完成,用户无需操作.

安装python依赖库

pip install pyasn1

pip install pyasn1_modules

pip install python-dateutil

pip install pyOpenSSL

pip install cryptography

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/env python3

import re
import os
import ssl
import json
import idna
import pyasn1
import OpenSSL
import subprocess
from OpenSSL import SSL
from socket import socket
from dateutil import parser
from subprocess import Popen, PIPE, STDOUT
from pyasn1.codec.der.decoder import decode as asn1_decoder
# Import SubjectAltName from rfc2459 module
from pyasn1_modules.rfc2459 import *
from pyasn1.codec.native.encoder import encode as nat_encoder

class CertInfo:
    def __init__(self, domain):
        self.domain_head = domain
        self.domain = domain.lstrip("https:").strip("/")
        self.cert = None
        self.certs = None
        self.results = {}
        self.certfile_path = os.getcwd() + "/tmp/certfile.crt"

    def get_openssl_command(self, cmds, regular):
        result = {}

        for key, cmd in cmds.items():
            try:
                cmd = '{0} {1}'.format(cmd, self.certfile_path)
                proc = subprocess.Popen(cmd, shell=True, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
                buff = proc.communicate()[0].decode()
                stritem = re.findall(regular, buff)
                if len(stritem) > 1:
                    result[key] = []
                    for i in range(len(stritem)):
                        result[key].append(stritem[i])
                elif len(stritem) == 1:
                    result[key] = stritem[0].replace(":", "")
            except Exception as e:
                print("Get openssl command error:{0}, line number:{1}".format(str(e), e.__traceback__.tb_lineno))
                return False
        self.results['指纹'] = {}
        self.results['指纹'].update(result)
        return True

    def get_cert_fingerprint(self):
        try:
            with open(self.certfile_path, 'w+') as output_file:
                output_file.write((OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, self.certs[0]).decode('utf-8')))
            cmds = {
                "MD5": "openssl x509 -fingerprint -md5 -in",
                "SHA1": "openssl x509 -fingerprint -sha1 -in",
                "SHA-256": "openssl x509 -fingerprint -sha256 -in"
            }
            regular = "Fingerprint=(.*)\n"
            if not self.get_openssl_command(cmds, regular):
                return False
            os.remove(self.certfile_path)
            return True
        except Exception as e:
            # logger.error("Get cert fingerprint error:{0}, line number:{1}".format(str(e), e.__traceback__.tb_lineno))
            print("Get cert fingerprint error:{0}, line number:{1}".format(str(e), e.__traceback__.tb_lineno))
            return False
    
    def get_cert_info(self):
        subject = self.cert.get_subject()
        issuer = self.cert.get_issuer()
        
        self.results['主题名称'] = {}
        for item in subject.get_components():
            if item[0].decode().strip() == 'C':
                self.results['主题名称']['国家或地区'] = item[1].decode()
            if item[0].decode().strip() == 'ST':
                self.results['主题名称']['省/市/自治区'] = item[1].decode()
            if item[0].decode().strip() == 'OU':
                self.results['主题名称']['组织单位'] = item[1].decode()
            if item[0].decode().strip() == 'O':
                self.results['主题名称']['组织'] = item[1].decode()
            if item[0].decode().strip() == 'CN':
                self.results['主题名称']['常用名称'] = item[1].decode()
            if item[0].decode().strip() == 'L':
                self.results['主题名称']['局部名称'] = item[1].decode()
        if subject.emailAddress:
            self.results['主题名称']['邮件地址'] = subject.emailAddress
        
        self.results['签发者名称'] = {}
        for item in issuer.get_components():
            if item[0].decode().strip() == 'C':
                self.results['签发者名称']['国家或地区'] = item[1].decode()
            if item[0].decode().strip() == 'ST':
                self.results['签发者名称']['省/市/自治区'] = item[1].decode()
            if item[0].decode().strip() == 'OU':
                self.results['签发者名称']['组织单位'] = item[1].decode()
            if item[0].decode().strip() == 'O':
                self.results['签发者名称']['组织'] = item[1].decode()
            if item[0].decode().strip() == 'CN':
                self.results['签发者名称']['常用名称'] = item[1].decode()
                if self.cert.get_subject().CN == item[1].decode():
                    self.results["CA"] = False
                else:
                    self.results["CA"] = True
            if item[0].decode().strip() == 'L':
                self.results['签发者名称']['局部名称'] = item[1].decode()
        if issuer.emailAddress:
            self.results['签发者名称']['邮件地址'] = subject.emailAddress

        self.results["版本"] = str(self.cert.get_version() + 1)
        self.results["序列号"] = str(self.cert.get_serial_number())
        self.results["16进制序列号"] = hex(self.cert.get_serial_number())
        self.results["签名算法"] = self.cert.get_signature_algorithm().decode("UTF-8")

        datetime_struct = parser.parse(self.cert.get_notBefore().decode("UTF-8"))
        self.results["在此之前无效"] = str(datetime_struct.strftime('%Y-%m-%d %H:%M:%S')) + " UTC"
        datetime_struct = parser.parse(self.cert.get_notAfter().decode("UTF-8"))
        self.results["在此之后无效"] = str(datetime_struct.strftime('%Y-%m-%d %H:%M:%S')) + " UTC"
        if self.cert.has_expired():
            self.results["证书是否过期"] = '是'
        else:
            self.results["证书是否过期"] = '否'

        self.results["密钥长度"] = self.cert.get_pubkey().bits()
        self.results["RSA公钥"] = OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_PEM, self.cert.get_pubkey()).decode("utf-8")
        for i in range(self.cert.get_extension_count()):
            if self.cert.get_extension(i).get_short_name().decode() == 'subjectAltName':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = SubjectAltName())
                py_alt_names = nat_encoder(decoded_alt_names)
                subject_alt_names = [ x['dNSName'].decode('utf-8') for x in py_alt_names]
                self.results['主题备用名称'] = '\n'.join(subject_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'authorityKeyIdentifier':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = AuthorityKeyIdentifier())
                self.results['授权密钥标识符'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'certificatePolicies':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = CertificatePolicies())
                self.results['证书策略'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'basicConstraints':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = BasicConstraints())
                self.results['基本约束'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'authorityInfoAccess':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = AuthorityInfoAccessSyntax())
                self.results['证书颁发机构信息访问'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'crlDistributionPoints':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = CRLDistPointsSyntax())
                self.results['CRL分发点'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'extendedKeyUsage':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = ExtKeyUsageSyntax())
                self.results['扩展密钥使用'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'keyUsage':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = KeyUsage())
                self.results['密钥使用'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'subjectKeyIdentifier':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = SubjectKeyIdentifier())
                self.results['主题密钥标识符'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'issuerAltName':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = IssuerAltName())
                self.results['主题密钥标识符'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'privateKeyUsagePeriod':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = PrivateKeyUsagePeriod())
                self.results['私钥使用时间'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'nameConstraints':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = NameConstraints())
                self.results['名称约束'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'policyConstraints':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = PolicyConstraints())
                self.results['策略约束'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'subjectDirectoryAttributes':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = SubjectDirectoryAttributes())
                self.results['主题目录属性约束'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'cRLNumber':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = univ.Integer())
                self.results['CRL号码'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'deltaCRLIndicator':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = BaseCRLNumber())
                self.results['基本CRL号码'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'policyMappings':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = PolicyMappings())
                self.results['策略映射'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'issuingDistributionPoint':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = IssuingDistributionPoint())
                self.results['签发分配点'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'cRLReasons':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = CRLReason())
                self.results['cRL原因'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'holdInstructionCode':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = univ.ObjectIdentifier())
                self.results['持有指令码'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'invalidityDate':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = useful.GeneralizedTime())
                self.results['无效日期'] = str(decoded_alt_names)
            if self.cert.get_extension(i).get_short_name().decode() == 'certificateIssuer':
                decoded_alt_names, _ = asn1_decoder(self.cert.get_extension(i).get_data(), asn1Spec = GeneralNames())
                self.results['证书签发者'] = str(decoded_alt_names)
        
    def run(self, port = 443):
        try:
            sock = socket()
            sock.setblocking(True)  # 关键
            sock.connect((self.domain, port), )
            ctx = SSL.Context(SSL.SSLv23_METHOD)
            ctx.check_hostname = False
            ctx.verify_mode = SSL.VERIFY_NONE

            sock_ssl = SSL.Connection(ctx, sock)
            sock_ssl.set_tlsext_host_name(idna.encode(self.domain))  # 关键: 对应不同域名的证书
            sock_ssl.set_connect_state()
            sock_ssl.do_handshake()

            self.cert = sock_ssl.get_peer_certificate()
            self.certs = sock_ssl.get_peer_cert_chain()  # 下载证书
            sock_ssl.close()
            sock.close()
        except Exception as e:
            print(e)
            return None
        if not self.cert:
            return None
        
        self.get_cert_fingerprint()
        self.get_cert_info()

if __name__ == '__main__':
    certinfo = CertInfo("www.baidu.com")
    certinfo.run()
    print(certinfo.results)