# -*- coding: utf-8 -*- """ @File : cert_common.py @Date : 2022-11-08 @Author : Peng Shiyu """ from __future__ import print_function, unicode_literals, absolute_import, division import socket from datetime import datetime import OpenSSL from dateutil import parser from domain_admin.utils import time_util from domain_admin.utils.cert_util import cert_consts # 证书品牌 cert_brands = [ 'JoySSL', 'Sectigo', 'Positive', 'CFCA', 'Certum', 'GlobalSign', 'TrustAsia', 'WoTrus', 'vTrus', 'BaiduTrust', 'DigiCert', 'Symantec', 'GeoTrust', 'Rapid', 'Entrust', 'Thawte', 'Sectigo', "Let's Encrypt", ] # 证书类型 class CertTypeByVerifyWayEnum(object): DV = 'DV' OV = 'OV' EV = 'EV' # 域名数量 class CertDomainTypeEnum(object): # 单域名 Single = 'single' # 多域名 Multiple = 'multiple' # 泛域名 Wildcard = 'wildcard' # 混合域名 Mixed = 'mixed' certTypeByDomainCountMap = { CertDomainTypeEnum.Single: '单域名', CertDomainTypeEnum.Multiple: '多域名', CertDomainTypeEnum.Wildcard: '泛域名', CertDomainTypeEnum.Mixed: '混合域名', } CertTypeByVerifyWayEnumMap = { CertTypeByVerifyWayEnum.DV: '域名型', CertTypeByVerifyWayEnum.OV: '企业型', CertTypeByVerifyWayEnum.EV: '增强型', } def get_cert_type_by_domain_count(cert): """ 获取证书域名数量类型 :param cert: :return: """ # 单个 if len(cert.subjectAltName) == 1: domain = cert.subjectAltName[0] if '*' in domain: return CertDomainTypeEnum.Wildcard else: return CertDomainTypeEnum.Single # 多个 for domain in cert.subjectAltName: if "*" in domain: return CertDomainTypeEnum.Mixed return CertDomainTypeEnum.Multiple def get_cert_brand(cert): org = cert.issuer.get('O') for cert_brand in cert_brands: if cert_brand in org: return cert_brand def is_extended_validation(cn): if 'Extended Validation' in cn: return True else: return False def get_cert_type_by_verify_type(cert): """ :param cert: :return: """ org = cert.issuer.get('CN') cert_types = [ CertTypeByVerifyWayEnum.OV, CertTypeByVerifyWayEnum.DV, CertTypeByVerifyWayEnum.EV, ] for cert_type in cert_types: if cert_type in org: return cert_type if is_extended_validation(org): return CertTypeByVerifyWayEnum.EV if cert.subject.get('O'): return CertTypeByVerifyWayEnum.OV return CertTypeByVerifyWayEnum.DV def parse_time(time_str): """ 解析并格式化时间 :param time_str: str :return: str """ return parser.parse(time_str).astimezone().strftime(cert_consts.DATETIME_FORMAT) def parse_datetime(time_str): """ 解析时间 :param time_str: str :return: datetime """ return parser.parse(time_str).astimezone().replace(tzinfo=None) def parse_domain_with_port(domain_with_port): """ 解析域名,允许携带端口号 :param domain_with_port: str 例如: www.domain.com www.domain.com:8888 :return: dict """ if ':' in domain_with_port: domain, port = domain_with_port.split(':') else: domain = domain_with_port port = cert_consts.SSL_DEFAULT_PORT if not isinstance(port, int): port = int(port) return { 'domain': domain, 'port': port, } def short_name_convert(data): """ 名字转换 :param data: dict :return: dict """ name_map = { 'C': 'countryName', 'CN': 'commonName', 'O': 'organizationName', 'OU': 'organizationalUnitName', 'L': 'localityName', 'ST': 'stateOrProvinceName' } dct = {} for key, value in name_map.items(): dct[key] = data.get(value, '') return dct class X509Item(object): version = '' subject = dict() issuer = dict() notAfter = '' notBefore = '' serialNumber = '' signatureAlgorithm = '' hasExpired = None subjectAltName = [] def to_dict(self): return { 'subject': self.subject, 'version': self.version, 'issuer': self.issuer, 'notAfter': self.notAfter, 'notBefore': self.notBefore, 'serialNumber': self.serialNumber, 'signatureAlgorithm': self.signatureAlgorithm, 'subjectAltName': self.subjectAltName, 'hasExpired': self.hasExpired, 'totalDays': self.totalDays, 'expireDays': self.expireDays, 'certBrand': self.certBrand, 'certTypeByVerifyWay': self.certTypeByVerifyWay, 'certTypeByVerifyWayLabel': self.certTypeByVerifyWayLabel, 'certTypeByDomainCount': self.certTypeByDomainCount, 'certTypeByDomainCountLabel': self.certTypeByDomainCountLabel, } @property def totalDays(self): return time_util.get_diff_days(self.notBefore, self.notAfter) @property def expireDays(self): return time_util.get_diff_days(datetime.now(), self.notAfter) @property def certBrand(self): """ 证书品牌 :return: """ return get_cert_brand(self) @property def certTypeByDomainCountLabel(self): """ 域名数量 :return: """ return certTypeByDomainCountMap.get(self.certTypeByDomainCount) @property def certTypeByDomainCount(self): """ 域名数量 :return: """ return get_cert_type_by_domain_count(self) @property def certTypeByVerifyWay(self): """ 证书类型 :return: """ return get_cert_type_by_verify_type(self) @property def certTypeByVerifyWayLabel(self): """ 证书类型 :return: """ return CertTypeByVerifyWayEnumMap.get(self.certTypeByVerifyWay) def dump_certificate_to_text(ssl_cert): """ 将证书对象转为字符串text形式 :param ssl_cert: :return: str """ return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, ssl_cert).decode('utf-8') def dump_certificate_to_pem(ssl_cert): """ 将证书对象转为字符串pem形式 :param ssl_cert: :return: str """ return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ssl_cert).decode('utf-8') def parse_cert(ssl_cert): """ 解析证书 :param ssl_cert: :return: """ item = X509Item() issuer = {} for [name, value] in ssl_cert.get_issuer().get_components(): issuer[name.decode()] = value.decode() item.issuer = issuer subject = {} for [name, value] in ssl_cert.get_subject().get_components(): subject[name.decode()] = value.decode() item.subject = subject item.version = ssl_cert.get_version() item.hasExpired = ssl_cert.has_expired() item.notAfter = time_util.parse_time(ssl_cert.get_notAfter().decode()) item.notBefore = time_util.parse_time(ssl_cert.get_notBefore().decode()) # ref: https://stackoverflow.com/questions/39286805/x-509-certificate-serial-number-to-hex-conversion item.serialNumber = '{0:x}'.format(ssl_cert.get_serial_number()).upper() item.signatureAlgorithm = ssl_cert.get_signature_algorithm().decode() item.subjectAltName = get_certificate_san(ssl_cert) return item def get_certificate_san(x509cert): """ 获取SAN域名列表 ref: https://cloud.tencent.com/developer/ask/sof/141600 :param x509cert: :return: """ dns_names = [] ext_count = x509cert.get_extension_count() for i in range(0, ext_count): ext = x509cert.get_extension(i) if 'subjectAltName' in str(ext.get_short_name()): for item in str(ext).split(', '): # add IP Address support if item.startswith('DNS:') or item.startswith('IP Address:'): key, value = item.split(':') dns_names.append(value.strip()) return dns_names def parse_public_cert(public_cert): """ 解析证书信息 :param public_cert: :return: """ cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, public_cert.encode()) return parse_cert(cert)