域名HTTPS证书内容检测


域名证书检测

  • 具体仓库地址

  • 主要代码如下

    #!/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

文章作者: NesTeaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesTeaLin !
  目录