lemur/common/defaults.py (145 lines of code) (raw):

import re import unicodedata from cryptography import x509 from cryptography.hazmat.primitives.serialization import Encoding from flask import current_app from sentry_sdk import capture_exception from lemur.common.utils import is_selfsigned from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE def text_to_slug(value, joiner="-"): """ Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters. A series of non-alphanumeric characters is replaced with the joiner character. """ if len(value) > 10_000: raise ValueError("Input value is too long.") # Strip all character accents: decompose Unicode characters and then drop combining chars. value = "".join( c for c in unicodedata.normalize("NFKD", value) if not unicodedata.combining(c) ) # Replace all remaining non-alphanumeric characters with joiner string. Multiple characters get collapsed into a # single joiner. Except, keep 'xn--' used in IDNA domain names as is. value = re.sub(r"[^A-Za-z0-9.]+(?<!xn--)", joiner, value) # '-' in the beginning or end of string looks ugly. return value.strip(joiner) def certificate_name(common_name, issuer, not_before, not_after, san, domains=[]): """ Create a name for our certificate. A naming standard is based on a series of templates. The name includes useful information such as Common Name, Validation dates, and Issuer. :param common_name: :param not_after: :param issuer: :param not_before: :param san: :param domains: :rtype: str :return: """ if san: t = SAN_NAMING_TEMPLATE else: t = DEFAULT_NAMING_TEMPLATE if common_name and common_name.strip(): subject = common_name elif len(domains): subject = domains[0].name temp = t.format( subject=subject, issuer=issuer.replace(" ", ""), not_before=not_before.strftime("%Y%m%d"), not_after=not_after.strftime("%Y%m%d"), ) temp = temp.replace("*", "WILDCARD") return text_to_slug(temp) def signing_algorithm(cert): return cert.signature_hash_algorithm.name def common_name(cert): """ Attempts to get a sane common name from a given certificate. :param cert: :return: Common name or None """ try: subject_oid = cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME) if len(subject_oid) > 0: return subject_oid[0].value.strip() return None except Exception as e: capture_exception() current_app.logger.error( { "message": "Unable to get common name", "error": e, "public_key": cert.public_bytes(Encoding.PEM).decode("utf-8") }, exc_info=True ) def organization(cert): """ Attempt to get the organization name from a given certificate. :param cert: :return: """ try: o = cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME) if not o: return None return o[0].value.strip() except Exception as e: capture_exception() current_app.logger.error(f"Unable to get organization! {e}") def organizational_unit(cert): """ Attempt to get the organization unit from a given certificate. :param cert: :return: """ try: ou = cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATIONAL_UNIT_NAME) if not ou: return None return ou[0].value.strip() except Exception as e: capture_exception() current_app.logger.error(f"Unable to get organizational unit! {e}") def country(cert): """ Attempt to get the country from a given certificate. :param cert: :return: """ try: c = cert.subject.get_attributes_for_oid(x509.OID_COUNTRY_NAME) if not c: return None return c[0].value.strip() except Exception as e: capture_exception() current_app.logger.error(f"Unable to get country! {e}") def state(cert): """ Attempt to get the from a given certificate. :param cert: :return: """ try: s = cert.subject.get_attributes_for_oid(x509.OID_STATE_OR_PROVINCE_NAME) if not s: return None return s[0].value.strip() except Exception as e: capture_exception() current_app.logger.error(f"Unable to get state! {e}") def location(cert): """ Attempt to get the location name from a given certificate. :param cert: :return: """ try: loc = cert.subject.get_attributes_for_oid(x509.OID_LOCALITY_NAME) if not loc: return None return loc[0].value.strip() except Exception as e: capture_exception() current_app.logger.error(f"Unable to get location! {e}") def domains(cert): """ Attempts to get an domains listed in a certificate. If 'subjectAltName' extension is not available we simply return the common name. :param cert: :return: List of domains """ domains = [] try: ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME) entries = ext.value.get_values_for_type(x509.DNSName) for entry in entries: domains.append(entry) entries = ext.value.get_values_for_type(x509.IPAddress) for entry in entries: domains.append(str(entry)) except x509.ExtensionNotFound: if current_app.config.get("LOG_SSL_SUBJ_ALT_NAME_ERRORS", True): capture_exception() except Exception as e: capture_exception() return domains def serial(cert): """ Fetch the serial number from the certificate. :param cert: :return: serial number """ return cert.serial_number def san(cert): """ Determines if a given certificate is a SAN certificate. SAN certificates are simply certificates that cover multiple domains. :param cert: :return: Bool """ if len(domains(cert)) > 1: return True def is_wildcard(cert): """ Determines if certificate is a wildcard certificate. :param cert: :return: Bool """ d = domains(cert) if len(d) == 1 and d[0][0:1] == "*": return True if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*": return True def bitstrength(cert): """ Calculates a certificates public key bit length. :param cert: :return: Integer """ try: return cert.public_key().key_size except AttributeError: capture_exception() current_app.logger.debug("Unable to get bitstrength.") def issuer(cert): """ Gets a sane issuer slug from a given certificate, stripping non-alphanumeric characters. For self-signed certificates, the special value '<selfsigned>' is returned. If issuer cannot be determined, '<unknown>' is returned. :param cert: Parsed certificate object :return: Issuer slug """ # If certificate is self-signed, we return a special value -- there really is no distinct "issuer" for it if is_selfsigned(cert): return "<selfsigned>" # Try Common Name or fall back to Organization name attrs = cert.issuer.get_attributes_for_oid( x509.OID_COMMON_NAME ) or cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME) if not attrs: current_app.logger.error( f"Unable to get issuer! Cert serial {cert.serial_number:x}" ) return "<unknown>" return text_to_slug(attrs[0].value, "") def not_before(cert): """ Gets the naive datetime of the certificates 'not_before' field. This field denotes the first date in time which the given certificate is valid. :param cert: :return: Datetime """ return cert.not_valid_before_utc def not_after(cert): """ Gets the naive datetime of the certificates 'not_after' field. This field denotes the last date in time which the given certificate is valid. :return: Datetime """ return cert.not_valid_after_utc