域名证书检测
具体仓库地址
主要代码如下
#!/usr/bin/python3 # -*- coding: UTF-8 -*- # Author: nestealin # Created: 2021-08-21 import log import logging import conf import json import ssl import sys import socket import platform import time from datetime import datetime def check_domain_valid(domain): for symbol in conf.abnormal_symbol: if symbol in domain: logging.error("输入的域名存在异常符号,正在退出...") sys.exit(1) return domain def check_expire(domain_left_days_info_list, expire_date, shown_status=False): """ :param domain_left_days_info_list: <list> [<str>, <str>, <int>] ['域名', '远程主机', 剩余天数] :param expire_date: <int> 自定检测的剩余天数阈值 :param shown_status: <int> 显示状态(True显示执行, False隐藏) :return: <list> ['域名: xxx,远程主机: xxx, 还剩 d 天到期'] """ almost_expire_list = list() if shown_status is True: logging.info( '正在检测域名:%s, 远程主机:%s的证书过期时间...' % (domain_left_days_info_list[0], domain_left_days_info_list[1])) if domain_left_days_info_list[2] < expire_date: almost_expire_list.append( '域名:%s, 远程主机:%s, 还剩 %d 天到期' % (domain_left_days_info_list[0], domain_left_days_info_list[1], domain_left_days_info_list[2])) return almost_expire_list def local_domain_list_init(domain_list_file=conf.domain_list_file_path): logging.info('开始从%s文件读取待检测域名...' % domain_list_file) domain_list = list() for domain in open('%s' % domain_list_file, 'r').readlines(): domain = domain.strip('\n') if domain.startswith('#') or len(domain) < 3: logging.warning('该行内容 "%s" 被注释或为空,跳过检测' % domain) else: domain = check_domain_valid(domain) domain_list.append(domain) return domain_list def get_domain_remote_host(domain): try: myaddr = socket.getaddrinfo(domain, None) if myaddr: return 200, myaddr[0][4][0] else: return 1508, "域名%s解析结果为空" % domain except Exception as e: logging.error("域名%s解析远程主机异常:%s" % (domain, str(repr(e)))) return 1507, "域名%s解析存在异常" % domain def ssl_socket(servername, remote_server=None, shown_status=False, **kwargs): """ :param servername: <str> test1.baidu.com :param remote_server: <str> 可选,用于指定域名hosts访问,留空默认跟着本机域名解析执行 :param shown_status: <bool> 可选,具体域名证书内容 :param kwargs: <dict> 可选,{"line": "默认"} || {"type": "CNAME"} :return: <tuple> 正常(<int>, <list>) || 异常(<int>, <str>) """ if remote_server is None: remote_host = get_domain_remote_host(servername) else: remote_host = [200, remote_server] domain_line = kwargs.get("line", "None(直接查询无该值记录)") domain_type = kwargs.get("type", "None(直接查询无该值记录)") try: if remote_host[0] == 200: logging.info( "正在检测域名:%s, 远程主机:%s的SSL证书详情..." % (servername, remote_host[1])) ctx = ssl.create_default_context() client = socket.socket() # 设置5秒连接超时 client.settimeout(5) # 指定远程https端口为443 with ctx.wrap_socket(client, server_hostname=servername) as ssl_client: ssl_client.connect((remote_host[1], 443)) cert_info = ssl_client.getpeercert() """ # 证书KEY信息: subject issuer version serialNumber notBefore notAfter subjectAltName OCSP caIssuers crlDistributionPoints """ # 证书包含域名(SAN) subject_alt_name_list = cert_info["subjectAltName"] cert_dns_dict = dict() cert_dns_dict["DNS"] = dns_list = list() for cert_dns in subject_alt_name_list: dns_list.append(cert_dns[1]) # print(cert_dns_dict) # 证书主体详情,部分包含主体名称,个人可能只有“commonName” subject = dict(x[0] for x in cert_info["subject"]) logging.debug("证书主体详情如下:\n%s" % json.dumps(subject)) # 证书主体 # cert_subject = subject["organizationName"] # 证书主域 # issued_to = subject["commonName"] issuer = dict(x[0] for x in cert_info["issuer"]) logging.debug("证书签发机构详情如下:\n%s" % json.dumps(issuer)) # 签发机构 issued_by = issuer["organizationName"] # 签发时间 start_date = cert_info["notBefore"] # 过期时间 expire_date = cert_info["notAfter"] # 计算证书有效期剩余天数 check_datetime = datetime.now() # 切割天数的取值范围可能因时间格式不对导致无法转换,例如[-25:-5]时会出现如下报错 # ValueError("time data 'Sep 17 11:18:16 202' does not match format '%b %d %H:%M:%S %Y'") expire_datetime = datetime.strptime( expire_date[-25:-4], "%b %d %H:%M:%S %Y") left_days = (expire_datetime - check_datetime).days domain_left_days_list = list() domain_left_days_list.append(servername) domain_left_days_list.append(remote_host[1]) domain_left_days_list.append(left_days) if shown_status is True: logging.info( '域名(Domain): {servername}'.format( servername=servername)) logging.info( '线路(Line): {domain_line}'.format( domain_line=domain_line)) logging.info( '记录类型(Domain Type): {domain_type}'.format( domain_type=domain_type)) logging.info( '记录解析/值(Domain Value): {domain_value}'.format( domain_value=remote_host[1])) logging.info( '颁发时间(notBefore): {start_date}'.format( start_date=start_date)) logging.info( '过期时间(notAfter): {expire_date}'.format( expire_date=expire_date)) logging.info( '剩余时间(Days left): {left_days} 天'.format( left_days=left_days)) logging.info( '签发机构(Issuer): {issuer_name}'.format( issuer_name=issued_by)) logging.info( '证书包含域名(subjectAltName): {subjectAltName}'.format( subjectAltName=', '.join( cert_dns_dict.get("DNS")))) logging.info(conf.split_line*55) return 200, domain_left_days_list else: return 1509, remote_host[1] except socket.timeout as s_timeout: err_msg = "域名: %s, 远程主机: %s, error: %s" % ( servername, remote_host[1], s_timeout) logging.error( "域名%s到目标主机%s连接超时,详情:%s" % (servername, remote_host[1], err_msg)) return 1505, err_msg except ssl.CertificateError as cert_error: err_msg = "域名: %s, 远程主机: %s, error: %s" % ( servername, remote_host[1], cert_error.verify_message) err_no = cert_error.verify_code if err_no == 10: logging.error("域名%s证书已过期,详情:%s" % (servername, err_msg)) return 1504, err_msg elif err_no == 62: logging.error( "目标站点%s证书与域名%s不匹配,详情:%s" % (remote_host[1], servername, err_msg)) return 1503, err_msg else: return 1511, err_msg except OSError as os_err: err_msg = "域名: %s, 远程主机: %s, error: %s" % ( servername, remote_host[1], os_err.strerror) err_no = os_err.errno if err_no == 61: logging.error( "域名%s的远程主机%s连接被拒绝,详情:%s" % (servername, remote_host[1], err_msg)) return 1502, err_msg else: return 1510, err_msg # 提前兜socket或者证书报错可能会混淆具体原因 except Exception as e: err_msg = "域名: %s, 远程主机: %s, error: %s" % ( servername, remote_host[1], str(repr(e))) logging.error( "域名:%s,远程主机:%s存在其他异常: %s" % (servername, remote_host[1], err_msg)) return 1501, err_msg def detect_to_single_domain(domain: str, shown_status=False): single_domain_status_dict = conf.detect_status_dict ssl_cert_info = ssl_socket(domain, shown_status=shown_status) if ssl_cert_info[0] == 200: expire_list = check_expire(ssl_cert_info[1], conf.detect_expire_date) if expire_list: single_domain_status_dict[1500]["details"].append(expire_list[0]) else: single_domain_status_dict[200]["details"].append(domain) else: single_domain_status_dict[ssl_cert_info[0] ]["details"].append(ssl_cert_info[1]) return single_domain_status_dict def detect_from_local_domain_file(domain_list, shown_status=False): local_domain_list_status_dict = conf.detect_status_dict for domain in domain_list: ssl_cert_info = ssl_socket(domain, shown_status=shown_status) if ssl_cert_info[0] == 200: expire_list = check_expire( ssl_cert_info[1], conf.detect_expire_date) if expire_list: local_domain_list_status_dict[1500]["details"].append( expire_list[0]) else: local_domain_list_status_dict[200]["details"].append(domain) else: local_domain_list_status_dict[ssl_cert_info[0]]["details"].append( ssl_cert_info[1]) return local_domain_list_status_dict def detect_result_output(status_dict): logging.info("\n{split_line}\n检测结束, 详细结果如下:\n{split_line}".format(split_line=conf.split_line*20)) for status_code in status_dict: msg = "发现%d个域名%s, 详情如下:\n%s" % (len( status_dict[status_code]["details"]), status_dict[status_code]["description"], '\n'.join( status_dict[status_code]["details"])) if len(status_dict[status_code]["details"]) == 0: logging.info( "本次未检测到域名存在%s状态,跳过输出..." % status_dict[status_code]["description"]) elif len(status_dict[status_code]["details"]) > 0 and status_code == 200: logging.info(msg) elif len(status_dict[status_code]["details"]) > 0 and status_code == 1500: logging.warning(msg) else: logging.error(msg) def input_options(shown_status=False): welcome_msg = '''{split_line} 温馨提醒: 1) 本脚本默认检查{expire_date}天内过期域名, 如需修改阈值, 请在"conf.py"中修改"detect_expire_date"字段 2) "本地域名列表检测" 需要提前在当前目录下"{domain_list_file}"文件中提前编写, 详情可参考"domain.sample"文件 {split_line} 本脚本可提供如下检测操作: 1. 单域名检测 2. 根据本地域名列表检测 3. 退出'''.format(split_line=conf.split_line*12, expire_date=conf.detect_expire_date, domain_list_file=conf.domain_list_file_path) print(welcome_msg) option_1 = input('请输入操作序号: ') if option_1.isdigit(): if int(option_1) == 1: domain = input('请输入检测域名(例如:www.baidu.com): ').replace(" ", "") domain = check_domain_valid(domain) single_status_dict = detect_to_single_domain( domain, shown_status=shown_status) detect_result_output(single_status_dict) elif int(option_1) == 2: domain_list = local_domain_list_init() local_domain_status_dict = detect_from_local_domain_file( domain_list, shown_status=shown_status) detect_result_output(local_domain_status_dict) elif int(option_1) == 3: logging.info('正在退出...') sys.exit(0) else: logging.error('请按上述提示输入对应操作序号。') sys.exit(1) else: logging.error('请按上述提示输入对应操作序号。') sys.exit(1) if __name__ == '__main__': if platform.python_version().split('.')[0] < '3': logging.error( '温馨提示:当前版本为%s , 请使用python3环境运行此脚本。' % platform.python_version()) sys.exit() # shown_status可控制是否输出具体的证书信息 input_options(shown_status=True)
单域名检测
python3 run.py >>> ============ 温馨提醒: 1) 本脚本默认检查30天内过期域名, 如需修改阈值, 请在"conf.py"中修改"detect_expire_date"字段 2) "本地域名列表检测" 需要提前在当前目录下"./meta_domain_data/domain.conf"文件中提前编写, 详情可参考"domain.sample"文件 ============ 本脚本可提供如下检测操作: 1. 单域名检测 2. 根据本地域名列表检测 3. 退出 请输入操作序号: 1 请输入检测域名(例如:www.baidu.com): www.sina.com
输出如下:
==================== 检测结束, 详细结果如下: ==================== [2021-08-21 14:54:27.177] run.py[line:299] INFO 发现1个域名正常域名, 详情如下: www.sina.com [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在证书即将过期状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在其他错误状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在连接拒绝状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在与证书域名不匹配状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在证书已过期状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在连接超时状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在根域id获取失败状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在dns解析存在其他错误状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在域名解析异常状态,跳过输出... [2021-08-21 14:54:27.177] run.py[line:296] INFO 本次未检测到域名存在远程主机解析异常状态,跳过输出... [2021-08-21 14:54:27.178] run.py[line:296] INFO 本次未检测到域名存在远程主机SSL连接异常状态,跳过输出... [2021-08-21 14:54:27.178] run.py[line:296] INFO 本次未检测到域名存在证书存在其他异常状态,跳过输出...
详情输出: ( shown_status=True )
[2021-08-21 16:12:22.629] run.py[line:98] INFO 正在检测域名:www.sina.com, 远程主机:113.96.179.244的SSL证书详情... [2021-08-21 16:12:22.665] run.py[line:167] INFO 域名(Domain): www.sina.com [2021-08-21 16:12:22.665] run.py[line:170] INFO 线路(Line): None(直接查询无该值记录) [2021-08-21 16:12:22.665] run.py[line:173] INFO 记录类型(Domain Type): None(直接查询无该值记录) [2021-08-21 16:12:22.666] run.py[line:176] INFO 记录解析/值(Domain Value): 113.96.179.244 [2021-08-21 16:12:22.666] run.py[line:179] INFO 颁发时间(notBefore): Nov 30 00:00:00 2020 GMT [2021-08-21 16:12:22.666] run.py[line:182] INFO 过期时间(notAfter): Dec 31 23:59:59 2021 GMT [2021-08-21 16:12:22.666] run.py[line:185] INFO 剩余时间(Days left): 132 天 [2021-08-21 16:12:22.666] run.py[line:188] INFO 签发机构(Issuer): DigiCert Inc [2021-08-21 16:12:22.666] run.py[line:192] INFO 证书包含域名(subjectAltName): sina.cn, *.sina.com
本地证书内容检测
主要代码如下
#!/usr/bin/python3 # -*- coding: UTF-8 -*- # Author: nestealin # Created: 2021/08/21 import OpenSSL from dateutil import parser def get_local_cert_info(cert_path: str): cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, open(cert_path).read()) certIssue = cert.get_issuer() print("证书版本: ", cert.get_version() + 1) print("证书主体: ", cert.get_subject().O) print("证书域名: ", cert.get_subject().CN) print("证书序列号: ", hex(cert.get_serial_number())) print("证书中使用的签名算法: ", cert.get_signature_algorithm().decode("UTF-8")) print("颁发者: ", certIssue.commonName) datetime_struct = parser.parse(cert.get_notBefore().decode("UTF-8")) print("有效期从: ", datetime_struct.strftime('%Y-%m-%d %H:%M:%S')) datetime_struct = parser.parse(cert.get_notAfter().decode("UTF-8")) print("到期时间: ", datetime_struct.strftime('%Y-%m-%d %H:%M:%S')) print("证书是否已经过期: ", cert.has_expired()) print("公钥长度: ", cert.get_pubkey().bits()) print( "公钥:\n", OpenSSL.crypto.dump_publickey( OpenSSL.crypto.FILETYPE_PEM, cert.get_pubkey()).decode("utf-8")) print("证书subject代表释义:") print("CN : 通用名称 OU : 机构单元名称") print("O : 机构名 L : 地理位置") print("S : 州/省名 C : 国名") for item in certIssue.get_components(): print(item[0].decode("utf-8"), " —— ", item[1].decode("utf-8")) # 证书扩展字段,仅v3证书支持 # print("ext:") # for ext_inx_num in range(cert.get_extension_count()): # print(cert.get_extension(ext_inx_num)) if __name__ == '__main__': cert_path = "server.cer" get_local_cert_info(cert_path)
本地证书检测
python3 local_ssl_cert_check.py
输出如下:
证书版本: 3 证书主体: None 证书域名: *.nestealin.com 证书序列号: 0x4f27c30abd4458002b5ccfb3cf49aa8c6ad 证书中使用的签名算法: sha256WithRSAEncryption 颁发者: Let's Encrypt Authority X3 有效期从: 2020-09-22 14:57:01 到期时间: 2020-12-21 14:57:01 证书是否已经过期: True 公钥长度: 2048