| import logging |
| import re |
| import socket |
| import OpenSSL |
| import time |
| import sys |
| |
| from datetime import datetime |
| from datetime import tzinfo |
| from datetime import timedelta |
| from http.client import HTTPConnection |
| from urllib.parse import urlparse |
| |
| from cryptography.hazmat._oid import ExtensionOID |
| from cryptography.hazmat.bindings._rust import ObjectIdentifier |
| from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, load_pem_private_key |
| |
| from cryptography import x509 |
| from cryptography.x509 import DNSName, ExtensionNotFound |
| |
| SEC_PER_DAY = 24 * 60 * 60 |
| |
| |
| log = logging.getLogger(__name__) |
| |
| |
| class MDCertUtil(object): |
| # Utility class for inspecting certificates in test cases |
| |
| @classmethod |
| def load_server_cert(cls, host_ip, host_port, host_name, tls=None, ciphers=None): |
| ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) |
| if tls is not None and tls != 1.0: |
| ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1) |
| if tls is not None and tls != 1.1: |
| ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1_1) |
| if tls is not None and tls != 1.2: |
| ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1_2) |
| if tls is not None and tls != 1.3 and hasattr(OpenSSL.SSL, "OP_NO_TLSv1_3"): |
| ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1_3) |
| if ciphers is not None: |
| ctx.set_cipher_list(ciphers) |
| s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| connection = OpenSSL.SSL.Connection(ctx, s) |
| connection.connect((host_ip, int(host_port))) |
| connection.setblocking(1) |
| connection.set_tlsext_host_name(host_name.encode('utf-8')) |
| connection.do_handshake() |
| ossl_cert = connection.get_peer_certificate() |
| return MDCertUtil(None, cert=ossl_cert.to_cryptography()) |
| |
| @classmethod |
| def parse_pem_cert(cls, text): |
| cert = x509.load_pem_x509_certificate(text.encode('utf-8')) |
| return MDCertUtil(None, cert=cert) |
| |
| @classmethod |
| def get_plain(cls, url, timeout): |
| server = urlparse(url) |
| try_until = time.time() + timeout |
| while time.time() < try_until: |
| # noinspection PyBroadException |
| try: |
| c = HTTPConnection(server.hostname, server.port, timeout=timeout) |
| c.request('GET', server.path) |
| resp = c.getresponse() |
| data = resp.read() |
| c.close() |
| return data |
| except IOError: |
| log.debug("connect error:", sys.exc_info()[0]) |
| time.sleep(.1) |
| except: |
| log.error("Unexpected error:", sys.exc_info()[0]) |
| log.error("Unable to contact server after %d sec" % timeout) |
| return None |
| |
| def __init__(self, cert_path, cert=None): |
| self.cert = cert |
| self.privkey = None |
| if cert_path is not None: |
| self.cert_path = cert_path |
| # load certificate and private key |
| if cert_path.startswith("http"): |
| assert False |
| try: |
| with open(cert_path) as fd: |
| cert = x509.load_pem_x509_certificate("".join(fd.readlines()).encode()) |
| except Exception as error: |
| self.error = error |
| if cert is not None: |
| self.cert = cert |
| if self.cert is None: |
| raise self.error |
| |
| def add_privkey(self, path, password=None): |
| with open(path) as fd: |
| self.privkey = load_pem_private_key("".join(fd.readlines()).encode(), password=password) |
| |
| def get_issuer(self): |
| return self.cert.get_issuer() |
| |
| def get_serial(self): |
| # the string representation of a serial number is not unique. Some |
| # add leading 0s to align with word boundaries. |
| return ("%lx" % (self.cert.serial_number)).upper() |
| |
| @staticmethod |
| def _get_serial(cert) -> int: |
| if isinstance(cert, x509.Certificate): |
| return cert.serial_number |
| if isinstance(cert, MDCertUtil): |
| return cert.cert.serial_number |
| elif isinstance(cert, str): |
| # assume a hex number |
| return int(cert, 16) |
| elif isinstance(cert, int): |
| return cert |
| assert False, f'{cert}' |
| return 0 |
| |
| def get_serial_number(self): |
| return self._get_serial(self.cert) |
| |
| def same_serial_as(self, other): |
| return self._get_serial(self.cert) == self._get_serial(other) |
| |
| def get_not_before(self): |
| try: |
| return self.cert.not_valid_before_utc |
| except AttributeError: |
| return self.cert.not_valid_before |
| |
| def get_not_after(self): |
| try: |
| return self.cert.not_valid_after_utc |
| except AttributeError: |
| return self.cert.not_valid_after |
| |
| def get_key_length(self): |
| return self.cert.public_key().key_size |
| |
| def get_san_list(self): |
| sans = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) |
| return sans.value.get_values_for_type(DNSName) |
| |
| def get_must_staple(self): |
| try: |
| self.cert.extensions.get_extension_for_oid(ExtensionOID.TLS_FEATURE) |
| return True |
| except ExtensionNotFound: |
| return False |
| |
| @classmethod |
| def validate_privkey(cls, privkey_path, passphrase=None): |
| with open(privkey_path) as fd: |
| privkey = load_pem_private_key("".join(fd.readlines()).encode(), password=passphrase) |
| return privkey is not None |