# coding: utf-8 """ ASN.1 type classes for X.509 certificates. Exports the following items: - Attributes() - Certificate() - Extensions() - GeneralName() - GeneralNames() - Name() Other type classes are defined that help compose the types listed above. """ from __future__ import unicode_literals, division, absolute_import, print_function from contextlib import contextmanager from encodings import idna # noqa import hashlib import re import socket import stringprep import sys import unicodedata from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri from ._ordereddict import OrderedDict from ._types import type_name, str_cls, bytes_to_list from .algos import AlgorithmIdentifier, AnyAlgorithmIdentifier, DigestAlgorithm, SignedDigestAlgorithm from .core import ( Any, BitString, BMPString, Boolean, Choice, Concat, Enumerated, GeneralizedTime, GeneralString, IA5String, Integer, Null, NumericString, ObjectIdentifier, OctetBitString, OctetString, ParsableOctetString, PrintableString, Sequence, SequenceOf, Set, SetOf, TeletexString, UniversalString, UTCTime, UTF8String, VisibleString, VOID, ) from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 # and a few other supplementary sources, mostly due to extra supported # extension and name OIDs class DNSName(IA5String): _encoding = 'idna' _bad_tag = (12, 19) def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.2 :param other: Another DNSName object :return: A boolean """ if not isinstance(other, DNSName): return False return self.__unicode__().lower() == other.__unicode__().lower() def set(self, value): """ Sets the value of the DNS name :param value: A unicode string """ if not isinstance(value, str_cls): raise TypeError(unwrap( ''' %s value must be a unicode string, not %s ''', type_name(self), type_name(value) )) if value.startswith('.'): encoded_value = b'.' + value[1:].encode(self._encoding) else: encoded_value = value.encode(self._encoding) self._unicode = value self.contents = encoded_value self._header = None if self._trailer != b'': self._trailer = b'' class URI(IA5String): def set(self, value): """ Sets the value of the string :param value: A unicode string """ if not isinstance(value, str_cls): raise TypeError(unwrap( ''' %s value must be a unicode string, not %s ''', type_name(self), type_name(value) )) self._unicode = value self.contents = iri_to_uri(value) self._header = None if self._trailer != b'': self._trailer = b'' def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.4 :param other: Another URI object :return: A boolean """ if not isinstance(other, URI): return False return iri_to_uri(self.native, True) == iri_to_uri(other.native, True) def __unicode__(self): """ :return: A unicode string """ if self.contents is None: return '' if self._unicode is None: self._unicode = uri_to_iri(self._merge_chunks()) return self._unicode class EmailAddress(IA5String): _contents = None # If the value has gone through the .set() method, thus normalizing it _normalized = False # In the wild we've seen this encoded as a UTF8String and PrintableString _bad_tag = (12, 19) @property def contents(self): """ :return: A byte string of the DER-encoded contents of the sequence """ return self._contents @contents.setter def contents(self, value): """ :param value: A byte string of the DER-encoded contents of the sequence """ self._normalized = False self._contents = value def set(self, value): """ Sets the value of the string :param value: A unicode string """ if not isinstance(value, str_cls): raise TypeError(unwrap( ''' %s value must be a unicode string, not %s ''', type_name(self), type_name(value) )) if value.find('@') != -1: mailbox, hostname = value.rsplit('@', 1) encoded_value = mailbox.encode('ascii') + b'@' + hostname.encode('idna') else: encoded_value = value.encode('ascii') self._normalized = True self._unicode = value self.contents = encoded_value self._header = None if self._trailer != b'': self._trailer = b'' def __unicode__(self): """ :return: A unicode string """ # We've seen this in the wild as a PrintableString, and since ascii is a # subset of cp1252, we use the later for decoding to be more user friendly if self._unicode is None: contents = self._merge_chunks() if contents.find(b'@') == -1: self._unicode = contents.decode('cp1252') else: mailbox, hostname = contents.rsplit(b'@', 1) self._unicode = mailbox.decode('cp1252') + '@' + hostname.decode('idna') return self._unicode def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.5 :param other: Another EmailAddress object :return: A boolean """ if not isinstance(other, EmailAddress): return False if not self._normalized: self.set(self.native) if not other._normalized: other.set(other.native) if self._contents.find(b'@') == -1 or other._contents.find(b'@') == -1: return self._contents == other._contents other_mailbox, other_hostname = other._contents.rsplit(b'@', 1) mailbox, hostname = self._contents.rsplit(b'@', 1) if mailbox != other_mailbox: return False if hostname.lower() != other_hostname.lower(): return False return True class IPAddress(OctetString): def parse(self, spec=None, spec_params=None): """ This method is not applicable to IP addresses """ raise ValueError(unwrap( ''' IP address values can not be parsed ''' )) def set(self, value): """ Sets the value of the object :param value: A unicode string containing an IPv4 address, IPv4 address with CIDR, an IPv6 address or IPv6 address with CIDR """ if not isinstance(value, str_cls): raise TypeError(unwrap( ''' %s value must be a unicode string, not %s ''', type_name(self), type_name(value) )) original_value = value has_cidr = value.find('/') != -1 cidr = 0 if has_cidr: parts = value.split('/', 1) value = parts[0] cidr = int(parts[1]) if cidr < 0: raise ValueError(unwrap( ''' %s value contains a CIDR range less than 0 ''', type_name(self) )) if value.find(':') != -1: family = socket.AF_INET6 if cidr > 128: raise ValueError(unwrap( ''' %s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address ''', type_name(self) )) cidr_size = 128 else: family = socket.AF_INET if cidr > 32: raise ValueError(unwrap( ''' %s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address ''', type_name(self) )) cidr_size = 32 cidr_bytes = b'' if has_cidr: cidr_mask = '1' * cidr cidr_mask += '0' * (cidr_size - len(cidr_mask)) cidr_bytes = int_to_bytes(int(cidr_mask, 2)) cidr_bytes = (b'\x00' * ((cidr_size // 8) - len(cidr_bytes))) + cidr_bytes self._native = original_value self.contents = inet_pton(family, value) + cidr_bytes self._bytes = self.contents self._header = None if self._trailer != b'': self._trailer = b'' @property def native(self): """ The native Python datatype representation of this value :return: A unicode string or None """ if self.contents is None: return None if self._native is None: byte_string = self.__bytes__() byte_len = len(byte_string) value = None cidr_int = None if byte_len in set([32, 16]): value = inet_ntop(socket.AF_INET6, byte_string[0:16]) if byte_len > 16: cidr_int = int_from_bytes(byte_string[16:]) elif byte_len in set([8, 4]): value = inet_ntop(socket.AF_INET, byte_string[0:4]) if byte_len > 4: cidr_int = int_from_bytes(byte_string[4:]) if cidr_int is not None: cidr_bits = '{0:b}'.format(cidr_int) cidr = len(cidr_bits.rstrip('0')) value = value + '/' + str_cls(cidr) self._native = value return self._native def __ne__(self, other): return not self == other def __eq__(self, other): """ :param other: Another IPAddress object :return: A boolean """ if not isinstance(other, IPAddress): return False return self.__bytes__() == other.__bytes__() class Attribute(Sequence): _fields = [ ('type', ObjectIdentifier), ('values', SetOf, {'spec': Any}), ] class Attributes(SequenceOf): _child_spec = Attribute class KeyUsage(BitString): _map = { 0: 'digital_signature', 1: 'non_repudiation', 2: 'key_encipherment', 3: 'data_encipherment', 4: 'key_agreement', 5: 'key_cert_sign', 6: 'crl_sign', 7: 'encipher_only', 8: 'decipher_only', } class PrivateKeyUsagePeriod(Sequence): _fields = [ ('not_before', GeneralizedTime, {'implicit': 0, 'optional': True}), ('not_after', GeneralizedTime, {'implicit': 1, 'optional': True}), ] class NotReallyTeletexString(TeletexString): """ OpenSSL (and probably some other libraries) puts ISO-8859-1 into TeletexString instead of ITU T.61. We use Windows-1252 when decoding since it is a superset of ISO-8859-1, and less likely to cause encoding issues, but we stay strict with encoding to prevent us from creating bad data. """ _decoding_encoding = 'cp1252' def __unicode__(self): """ :return: A unicode string """ if self.contents is None: return '' if self._unicode is None: self._unicode = self._merge_chunks().decode(self._decoding_encoding) return self._unicode @contextmanager def strict_teletex(): try: NotReallyTeletexString._decoding_encoding = 'teletex' yield finally: NotReallyTeletexString._decoding_encoding = 'cp1252' class DirectoryString(Choice): _alternatives = [ ('teletex_string', NotReallyTeletexString), ('printable_string', PrintableString), ('universal_string', UniversalString), ('utf8_string', UTF8String), ('bmp_string', BMPString), # This is an invalid/bad alternative, but some broken certs use it ('ia5_string', IA5String), ] class NameType(ObjectIdentifier): _map = { '2.5.4.3': 'common_name', '2.5.4.4': 'surname', '2.5.4.5': 'serial_number', '2.5.4.6': 'country_name', '2.5.4.7': 'locality_name', '2.5.4.8': 'state_or_province_name', '2.5.4.9': 'street_address', '2.5.4.10': 'organization_name', '2.5.4.11': 'organizational_unit_name', '2.5.4.12': 'title', '2.5.4.15': 'business_category', '2.5.4.17': 'postal_code', '2.5.4.20': 'telephone_number', '2.5.4.41': 'name', '2.5.4.42': 'given_name', '2.5.4.43': 'initials', '2.5.4.44': 'generation_qualifier', '2.5.4.45': 'unique_identifier', '2.5.4.46': 'dn_qualifier', '2.5.4.65': 'pseudonym', '2.5.4.97': 'organization_identifier', # https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf '2.23.133.2.1': 'tpm_manufacturer', '2.23.133.2.2': 'tpm_model', '2.23.133.2.3': 'tpm_version', '2.23.133.2.4': 'platform_manufacturer', '2.23.133.2.5': 'platform_model', '2.23.133.2.6': 'platform_version', # https://tools.ietf.org/html/rfc2985#page-26 '1.2.840.113549.1.9.1': 'email_address', # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf '1.3.6.1.4.1.311.60.2.1.1': 'incorporation_locality', '1.3.6.1.4.1.311.60.2.1.2': 'incorporation_state_or_province', '1.3.6.1.4.1.311.60.2.1.3': 'incorporation_country', # https://tools.ietf.org/html/rfc4519#section-2.39 '0.9.2342.19200300.100.1.1': 'user_id', # https://tools.ietf.org/html/rfc2247#section-4 '0.9.2342.19200300.100.1.25': 'domain_component', # http://www.alvestrand.no/objectid/0.2.262.1.10.7.20.html '0.2.262.1.10.7.20': 'name_distinguisher', } # This order is largely based on observed order seen in EV certs from # Symantec and DigiCert. Some of the uncommon name-related fields are # just placed in what seems like a reasonable order. preferred_order = [ 'incorporation_country', 'incorporation_state_or_province', 'incorporation_locality', 'business_category', 'serial_number', 'country_name', 'postal_code', 'state_or_province_name', 'locality_name', 'street_address', 'organization_name', 'organizational_unit_name', 'title', 'common_name', 'user_id', 'initials', 'generation_qualifier', 'surname', 'given_name', 'name', 'pseudonym', 'dn_qualifier', 'telephone_number', 'email_address', 'domain_component', 'name_distinguisher', 'organization_identifier', 'tpm_manufacturer', 'tpm_model', 'tpm_version', 'platform_manufacturer', 'platform_model', 'platform_version', ] @classmethod def preferred_ordinal(cls, attr_name): """ Returns an ordering value for a particular attribute key. Unrecognized attributes and OIDs will be sorted lexically at the end. :return: An orderable value. """ attr_name = cls.map(attr_name) if attr_name in cls.preferred_order: ordinal = cls.preferred_order.index(attr_name) else: ordinal = len(cls.preferred_order) return (ordinal, attr_name) @property def human_friendly(self): """ :return: A human-friendly unicode string to display to users """ return { 'common_name': 'Common Name', 'surname': 'Surname', 'serial_number': 'Serial Number', 'country_name': 'Country', 'locality_name': 'Locality', 'state_or_province_name': 'State/Province', 'street_address': 'Street Address', 'organization_name': 'Organization', 'organizational_unit_name': 'Organizational Unit', 'title': 'Title', 'business_category': 'Business Category', 'postal_code': 'Postal Code', 'telephone_number': 'Telephone Number', 'name': 'Name', 'given_name': 'Given Name', 'initials': 'Initials', 'generation_qualifier': 'Generation Qualifier', 'unique_identifier': 'Unique Identifier', 'dn_qualifier': 'DN Qualifier', 'pseudonym': 'Pseudonym', 'email_address': 'Email Address', 'incorporation_locality': 'Incorporation Locality', 'incorporation_state_or_province': 'Incorporation State/Province', 'incorporation_country': 'Incorporation Country', 'domain_component': 'Domain Component', 'name_distinguisher': 'Name Distinguisher', 'organization_identifier': 'Organization Identifier', 'tpm_manufacturer': 'TPM Manufacturer', 'tpm_model': 'TPM Model', 'tpm_version': 'TPM Version', 'platform_manufacturer': 'Platform Manufacturer', 'platform_model': 'Platform Model', 'platform_version': 'Platform Version', 'user_id': 'User ID', }.get(self.native, self.native) class NameTypeAndValue(Sequence): _fields = [ ('type', NameType), ('value', Any), ] _oid_pair = ('type', 'value') _oid_specs = { 'common_name': DirectoryString, 'surname': DirectoryString, 'serial_number': DirectoryString, 'country_name': DirectoryString, 'locality_name': DirectoryString, 'state_or_province_name': DirectoryString, 'street_address': DirectoryString, 'organization_name': DirectoryString, 'organizational_unit_name': DirectoryString, 'title': DirectoryString, 'business_category': DirectoryString, 'postal_code': DirectoryString, 'telephone_number': PrintableString, 'name': DirectoryString, 'given_name': DirectoryString, 'initials': DirectoryString, 'generation_qualifier': DirectoryString, 'unique_identifier': OctetBitString, 'dn_qualifier': DirectoryString, 'pseudonym': DirectoryString, # https://tools.ietf.org/html/rfc2985#page-26 'email_address': EmailAddress, # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf 'incorporation_locality': DirectoryString, 'incorporation_state_or_province': DirectoryString, 'incorporation_country': DirectoryString, 'domain_component': DNSName, 'name_distinguisher': DirectoryString, 'organization_identifier': DirectoryString, 'tpm_manufacturer': UTF8String, 'tpm_model': UTF8String, 'tpm_version': UTF8String, 'platform_manufacturer': UTF8String, 'platform_model': UTF8String, 'platform_version': UTF8String, 'user_id': DirectoryString, } _prepped = None @property def prepped_value(self): """ Returns the value after being processed by the internationalized string preparation as specified by RFC 5280 :return: A unicode string """ if self._prepped is None: self._prepped = self._ldap_string_prep(self['value'].native) return self._prepped def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 :param other: Another NameTypeAndValue object :return: A boolean """ if not isinstance(other, NameTypeAndValue): return False if other['type'].native != self['type'].native: return False return other.prepped_value == self.prepped_value def _ldap_string_prep(self, string): """ Implements the internationalized string preparation algorithm from RFC 4518. https://tools.ietf.org/html/rfc4518#section-2 :param string: A unicode string to prepare :return: A prepared unicode string, ready for comparison """ # Map step string = re.sub('[\u00ad\u1806\u034f\u180b-\u180d\ufe0f-\uff00\ufffc]+', '', string) string = re.sub('[\u0009\u000a\u000b\u000c\u000d\u0085]', ' ', string) if sys.maxunicode == 0xffff: # Some installs of Python 2.7 don't support 8-digit unicode escape # ranges, so we have to break them into pieces # Original was: \U0001D173-\U0001D17A and \U000E0020-\U000E007F string = re.sub('\ud834[\udd73-\udd7a]|\udb40[\udc20-\udc7f]|\U000e0001', '', string) else: string = re.sub('[\U0001D173-\U0001D17A\U000E0020-\U000E007F\U000e0001]', '', string) string = re.sub( '[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f' '\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]+', '', string ) string = string.replace('\u200b', '') string = re.sub('[\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000]', ' ', string) string = ''.join(map(stringprep.map_table_b2, string)) # Normalize step string = unicodedata.normalize('NFKC', string) # Prohibit step for char in string: if stringprep.in_table_a1(char): raise ValueError(unwrap( ''' X.509 Name objects may not contain unassigned code points ''' )) if stringprep.in_table_c8(char): raise ValueError(unwrap( ''' X.509 Name objects may not contain change display or zzzzdeprecated characters ''' )) if stringprep.in_table_c3(char): raise ValueError(unwrap( ''' X.509 Name objects may not contain private use characters ''' )) if stringprep.in_table_c4(char): raise ValueError(unwrap( ''' X.509 Name objects may not contain non-character code points ''' )) if stringprep.in_table_c5(char): raise ValueError(unwrap( ''' X.509 Name objects may not contain surrogate code points ''' )) if char == '\ufffd': raise ValueError(unwrap( ''' X.509 Name objects may not contain the replacement character ''' )) # Check bidirectional step - here we ensure that we are not mixing # left-to-right and right-to-left text in the string has_r_and_al_cat = False has_l_cat = False for char in string: if stringprep.in_table_d1(char): has_r_and_al_cat = True elif stringprep.in_table_d2(char): has_l_cat = True if has_r_and_al_cat: first_is_r_and_al = stringprep.in_table_d1(string[0]) last_is_r_and_al = stringprep.in_table_d1(string[-1]) if has_l_cat or not first_is_r_and_al or not last_is_r_and_al: raise ValueError(unwrap( ''' X.509 Name object contains a malformed bidirectional sequence ''' )) # Insignificant space handling step string = ' ' + re.sub(' +', ' ', string).strip() + ' ' return string class RelativeDistinguishedName(SetOf): _child_spec = NameTypeAndValue @property def hashable(self): """ :return: A unicode string that can be used as a dict key or in a set """ output = [] values = self._get_values(self) for key in sorted(values.keys()): output.append('%s: %s' % (key, values[key])) # Unit separator is used here since the normalization process for # values moves any such character, and the keys are all dotted integers # or under_score_words return '\x1F'.join(output) def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 :param other: Another RelativeDistinguishedName object :return: A boolean """ if not isinstance(other, RelativeDistinguishedName): return False if len(self) != len(other): return False self_types = self._get_types(self) other_types = self._get_types(other) if self_types != other_types: return False self_values = self._get_values(self) other_values = self._get_values(other) for type_name_ in self_types: if self_values[type_name_] != other_values[type_name_]: return False return True def _get_types(self, rdn): """ Returns a set of types contained in an RDN :param rdn: A RelativeDistinguishedName object :return: A set object with unicode strings of NameTypeAndValue type field values """ return set([ntv['type'].native for ntv in rdn]) def _get_values(self, rdn): """ Returns a dict of prepped values contained in an RDN :param rdn: A RelativeDistinguishedName object :return: A dict object with unicode strings of NameTypeAndValue value field values that have been prepped for comparison """ output = {} [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn] return output class RDNSequence(SequenceOf): _child_spec = RelativeDistinguishedName @property def hashable(self): """ :return: A unicode string that can be used as a dict key or in a set """ # Record separator is used here since the normalization process for # values moves any such character, and the keys are all dotted integers # or under_score_words return '\x1E'.join(rdn.hashable for rdn in self) def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 :param other: Another RDNSequence object :return: A boolean """ if not isinstance(other, RDNSequence): return False if len(self) != len(other): return False for index, self_rdn in enumerate(self): if other[index] != self_rdn: return False return True class Name(Choice): _alternatives = [ ('', RDNSequence), ] _human_friendly = None _sha1 = None _sha256 = None @classmethod def build(cls, name_dict, use_printable=False): """ Creates a Name object from a dict of unicode string keys and values. The keys should be from NameType._map, or a dotted-integer OID unicode string. :param name_dict: A dict of name information, e.g. {"common_name": "Will Bond", "country_name": "US", "organization": "Codex Non Sufficit LC"} :param use_printable: A bool - if PrintableString should be used for encoding instead of UTF8String. This is for backwards compatibility with old software. :return: An x509.Name object """ rdns = [] if not use_printable: encoding_name = 'utf8_string' encoding_class = UTF8String else: encoding_name = 'printable_string' encoding_class = PrintableString # Sort the attributes according to NameType.preferred_order name_dict = OrderedDict( sorted( name_dict.items(), key=lambda item: NameType.preferred_ordinal(item[0]) ) ) for attribute_name, attribute_value in name_dict.items(): attribute_name = NameType.map(attribute_name) if attribute_name == 'email_address': value = EmailAddress(attribute_value) elif attribute_name == 'domain_component': value = DNSName(attribute_value) elif attribute_name in set(['dn_qualifier', 'country_name', 'serial_number']): value = DirectoryString( name='printable_string', value=PrintableString(attribute_value) ) else: value = DirectoryString( name=encoding_name, value=encoding_class(attribute_value) ) rdns.append(RelativeDistinguishedName([ NameTypeAndValue({ 'type': attribute_name, 'value': value }) ])) return cls(name='', value=RDNSequence(rdns)) @property def hashable(self): """ :return: A unicode string that can be used as a dict key or in a set """ return self.chosen.hashable def __len__(self): return len(self.chosen) def __ne__(self, other): return not self == other def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 :param other: Another Name object :return: A boolean """ if not isinstance(other, Name): return False return self.chosen == other.chosen @property def native(self): if self._native is None: self._native = OrderedDict() for rdn in self.chosen.native: for type_val in rdn: field_name = type_val['type'] if field_name in self._native: existing = self._native[field_name] if not isinstance(existing, list): existing = self._native[field_name] = [existing] existing.append(type_val['value']) else: self._native[field_name] = type_val['value'] return self._native @property def human_friendly(self): """ :return: A human-friendly unicode string containing the parts of the name """ if self._human_friendly is None: data = OrderedDict() last_field = None for rdn in self.chosen: for type_val in rdn: field_name = type_val['type'].human_friendly last_field = field_name if field_name in data: data[field_name] = [data[field_name]] data[field_name].append(type_val['value']) else: data[field_name] = type_val['value'] to_join = [] keys = data.keys() if last_field == 'Country': keys = reversed(list(keys)) for key in keys: value = data[key] native_value = self._recursive_humanize(value) to_join.append('%s: %s' % (key, native_value)) has_comma = False for element in to_join: if element.find(',') != -1: has_comma = True break separator = ', ' if not has_comma else '; ' self._human_friendly = separator.join(to_join[::-1]) return self._human_friendly def _recursive_humanize(self, value): """ Recursively serializes data compiled from the RDNSequence :param value: An Asn1Value object, or a list of Asn1Value objects :return: A unicode string """ if isinstance(value, list): return ', '.join( reversed([self._recursive_humanize(sub_value) for sub_value in value]) ) return value.native @property def sha1(self): """ :return: The SHA1 hash of the DER-encoded bytes of this name """ if self._sha1 is None: self._sha1 = hashlib.sha1(self.dump()).digest() return self._sha1 @property def sha256(self): """ :return: The SHA-256 hash of the DER-encoded bytes of this name """ if self._sha256 is None: self._sha256 = hashlib.sha256(self.dump()).digest() return self._sha256 class AnotherName(Sequence): _fields = [ ('type_id', ObjectIdentifier), ('value', Any, {'explicit': 0}), ] class CountryName(Choice): class_ = 1 tag = 1 _alternatives = [ ('x121_dcc_code', NumericString), ('iso_3166_alpha2_code', PrintableString), ] class AdministrationDomainName(Choice): class_ = 1 tag = 2 _alternatives = [ ('numeric', NumericString), ('printable', PrintableString), ] class PrivateDomainName(Choice): _alternatives = [ ('numeric', NumericString), ('printable', PrintableString), ] class PersonalName(Set): _fields = [ ('surname', PrintableString, {'implicit': 0}), ('given_name', PrintableString, {'implicit': 1, 'optional': True}), ('initials', PrintableString, {'implicit': 2, 'optional': True}), ('generation_qualifier', PrintableString, {'implicit': 3, 'optional': True}), ] class TeletexPersonalName(Set): _fields = [ ('surname', TeletexString, {'implicit': 0}), ('given_name', TeletexString, {'implicit': 1, 'optional': True}), ('initials', TeletexString, {'implicit': 2, 'optional': True}), ('generation_qualifier', TeletexString, {'implicit': 3, 'optional': True}), ] class OrganizationalUnitNames(SequenceOf): _child_spec = PrintableString class TeletexOrganizationalUnitNames(SequenceOf): _child_spec = TeletexString class BuiltInStandardAttributes(Sequence): _fields = [ ('country_name', CountryName, {'optional': True}), ('administration_domain_name', AdministrationDomainName, {'optional': True}), ('network_address', NumericString, {'implicit': 0, 'optional': True}), ('terminal_identifier', PrintableString, {'implicit': 1, 'optional': True}), ('private_domain_name', PrivateDomainName, {'explicit': 2, 'optional': True}), ('organization_name', PrintableString, {'implicit': 3, 'optional': True}), ('numeric_user_identifier', NumericString, {'implicit': 4, 'optional': True}), ('personal_name', PersonalName, {'implicit': 5, 'optional': True}), ('organizational_unit_names', OrganizationalUnitNames, {'implicit': 6, 'optional': True}), ] class BuiltInDomainDefinedAttribute(Sequence): _fields = [ ('type', PrintableString), ('value', PrintableString), ] class BuiltInDomainDefinedAttributes(SequenceOf): _child_spec = BuiltInDomainDefinedAttribute class TeletexDomainDefinedAttribute(Sequence): _fields = [ ('type', TeletexString), ('value', TeletexString), ] class TeletexDomainDefinedAttributes(SequenceOf): _child_spec = TeletexDomainDefinedAttribute class PhysicalDeliveryCountryName(Choice): _alternatives = [ ('x121_dcc_code', NumericString), ('iso_3166_alpha2_code', PrintableString), ] class PostalCode(Choice): _alternatives = [ ('numeric_code', NumericString), ('printable_code', PrintableString), ] class PDSParameter(Set): _fields = [ ('printable_string', PrintableString, {'optional': True}), ('teletex_string', TeletexString, {'optional': True}), ] class PrintableAddress(SequenceOf): _child_spec = PrintableString class UnformattedPostalAddress(Set): _fields = [ ('printable_address', PrintableAddress, {'optional': True}), ('teletex_string', TeletexString, {'optional': True}), ] class E1634Address(Sequence): _fields = [ ('number', NumericString, {'implicit': 0}), ('sub_address', NumericString, {'implicit': 1, 'optional': True}), ] class NAddresses(SetOf): _child_spec = OctetString class PresentationAddress(Sequence): _fields = [ ('p_selector', OctetString, {'explicit': 0, 'optional': True}), ('s_selector', OctetString, {'explicit': 1, 'optional': True}), ('t_selector', OctetString, {'explicit': 2, 'optional': True}), ('n_addresses', NAddresses, {'explicit': 3}), ] class ExtendedNetworkAddress(Choice): _alternatives = [ ('e163_4_address', E1634Address), ('psap_address', PresentationAddress, {'implicit': 0}) ] class TerminalType(Integer): _map = { 3: 'telex', 4: 'teletex', 5: 'g3_facsimile', 6: 'g4_facsimile', 7: 'ia5_terminal', 8: 'videotex', } class ExtensionAttributeType(Integer): _map = { 1: 'common_name', 2: 'teletex_common_name', 3: 'teletex_organization_name', 4: 'teletex_personal_name', 5: 'teletex_organization_unit_names', 6: 'teletex_domain_defined_attributes', 7: 'pds_name', 8: 'physical_delivery_country_name', 9: 'postal_code', 10: 'physical_delivery_office_name', 11: 'physical_delivery_office_number', 12: 'extension_of_address_components', 13: 'physical_delivery_personal_name', 14: 'physical_delivery_organization_name', 15: 'extension_physical_delivery_address_components', 16: 'unformatted_postal_address', 17: 'street_address', 18: 'post_office_box_address', 19: 'poste_restante_address', 20: 'unique_postal_name', 21: 'local_postal_attributes', 22: 'extended_network_address', 23: 'terminal_type', } class ExtensionAttribute(Sequence): _fields = [ ('extension_attribute_type', ExtensionAttributeType, {'implicit': 0}), ('extension_attribute_value', Any, {'explicit': 1}), ] _oid_pair = ('extension_attribute_type', 'extension_attribute_value') _oid_specs = { 'common_name': PrintableString, 'teletex_common_name': TeletexString, 'teletex_organization_name': TeletexString, 'teletex_personal_name': TeletexPersonalName, 'teletex_organization_unit_names': TeletexOrganizationalUnitNames, 'teletex_domain_defined_attributes': TeletexDomainDefinedAttributes, 'pds_name': PrintableString, 'physical_delivery_country_name': PhysicalDeliveryCountryName, 'postal_code': PostalCode, 'physical_delivery_office_name': PDSParameter, 'physical_delivery_office_number': PDSParameter, 'extension_of_address_components': PDSParameter, 'physical_delivery_personal_name': PDSParameter, 'physical_delivery_organization_name': PDSParameter, 'extension_physical_delivery_address_components': PDSParameter, 'unformatted_postal_address': UnformattedPostalAddress, 'street_address': PDSParameter, 'post_office_box_address': PDSParameter, 'poste_restante_address': PDSParameter, 'unique_postal_name': PDSParameter, 'local_postal_attributes': PDSParameter, 'extended_network_address': ExtendedNetworkAddress, 'terminal_type': TerminalType, } class ExtensionAttributes(SequenceOf): _child_spec = ExtensionAttribute class ORAddress(Sequence): _fields = [ ('built_in_standard_attributes', BuiltInStandardAttributes), ('built_in_domain_defined_attributes', BuiltInDomainDefinedAttributes, {'optional': True}), ('extension_attributes', ExtensionAttributes, {'optional': True}), ] class EDIPartyName(Sequence): _fields = [ ('name_assigner', DirectoryString, {'implicit': 0, 'optional': True}), ('party_name', DirectoryString, {'implicit': 1}), ] class GeneralName(Choice): _alternatives = [ ('other_name', AnotherName, {'implicit': 0}), ('rfc822_name', EmailAddress, {'implicit': 1}), ('dns_name', DNSName, {'implicit': 2}), ('x400_address', ORAddress, {'implicit': 3}), ('directory_name', Name, {'explicit': 4}), ('edi_party_name', EDIPartyName, {'implicit': 5}), ('uniform_resource_identifier', URI, {'implicit': 6}), ('ip_address', IPAddress, {'implicit': 7}), ('registered_id', ObjectIdentifier, {'implicit': 8}), ] def __ne__(self, other): return not self == other def __eq__(self, other): """ Does not support other_name, x400_address or edi_party_name :param other: The other GeneralName to compare to :return: A boolean """ if self.name in ('other_name', 'x400_address', 'edi_party_name'): raise ValueError(unwrap( ''' Comparison is not supported for GeneralName objects of choice %s ''', self.name )) if other.name in ('other_name', 'x400_address', 'edi_party_name'): raise ValueError(unwrap( ''' Comparison is not supported for GeneralName objects of choice %s''', other.name )) if self.name != other.name: return False return self.chosen == other.chosen class GeneralNames(SequenceOf): _child_spec = GeneralName class Time(Choice): _alternatives = [ ('utc_time', UTCTime), ('general_time', GeneralizedTime), ] class Validity(Sequence): _fields = [ ('not_before', Time), ('not_after', Time), ] class BasicConstraints(Sequence): _fields = [ ('ca', Boolean, {'default': False}), ('path_len_constraint', Integer, {'optional': True}), ] class AuthorityKeyIdentifier(Sequence): _fields = [ ('key_identifier', OctetString, {'implicit': 0, 'optional': True}), ('authority_cert_issuer', GeneralNames, {'implicit': 1, 'optional': True}), ('authority_cert_serial_number', Integer, {'implicit': 2, 'optional': True}), ] class DistributionPointName(Choice): _alternatives = [ ('full_name', GeneralNames, {'implicit': 0}), ('name_relative_to_crl_issuer', RelativeDistinguishedName, {'implicit': 1}), ] class ReasonFlags(BitString): _map = { 0: 'unused', 1: 'key_compromise', 2: 'ca_compromise', 3: 'affiliation_changed', 4: 'superseded', 5: 'cessation_of_operation', 6: 'certificate_hold', 7: 'privilege_withdrawn', 8: 'aa_compromise', } class GeneralSubtree(Sequence): _fields = [ ('base', GeneralName), ('minimum', Integer, {'implicit': 0, 'default': 0}), ('maximum', Integer, {'implicit': 1, 'optional': True}), ] class GeneralSubtrees(SequenceOf): _child_spec = GeneralSubtree class NameConstraints(Sequence): _fields = [ ('permitted_subtrees', GeneralSubtrees, {'implicit': 0, 'optional': True}), ('excluded_subtrees', GeneralSubtrees, {'implicit': 1, 'optional': True}), ] class DistributionPoint(Sequence): _fields = [ ('distribution_point', DistributionPointName, {'explicit': 0, 'optional': True}), ('reasons', ReasonFlags, {'implicit': 1, 'optional': True}), ('crl_issuer', GeneralNames, {'implicit': 2, 'optional': True}), ] _url = False @property def url(self): """ :return: None or a unicode string of the distribution point's URL """ if self._url is False: self._url = None name = self['distribution_point'] if name.name != 'full_name': raise ValueError(unwrap( ''' CRL distribution points that are relative to the issuer are not supported ''' )) for general_name in name.chosen: if general_name.name == 'uniform_resource_identifier': url = general_name.native if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')): self._url = url break return self._url class CRLDistributionPoints(SequenceOf): _child_spec = DistributionPoint class DisplayText(Choice): _alternatives = [ ('ia5_string', IA5String), ('visible_string', VisibleString), ('bmp_string', BMPString), ('utf8_string', UTF8String), ] class NoticeNumbers(SequenceOf): _child_spec = Integer class NoticeReference(Sequence): _fields = [ ('organization', DisplayText), ('notice_numbers', NoticeNumbers), ] class UserNotice(Sequence): _fields = [ ('notice_ref', NoticeReference, {'optional': True}), ('explicit_text', DisplayText, {'optional': True}), ] class PolicyQualifierId(ObjectIdentifier): _map = { '1.3.6.1.5.5.7.2.1': 'certification_practice_statement', '1.3.6.1.5.5.7.2.2': 'user_notice', } class PolicyQualifierInfo(Sequence): _fields = [ ('policy_qualifier_id', PolicyQualifierId), ('qualifier', Any), ] _oid_pair = ('policy_qualifier_id', 'qualifier') _oid_specs = { 'certification_practice_statement': IA5String, 'user_notice': UserNotice, } class PolicyQualifierInfos(SequenceOf): _child_spec = PolicyQualifierInfo class PolicyIdentifier(ObjectIdentifier): _map = { '2.5.29.32.0': 'any_policy', } class PolicyInformation(Sequence): _fields = [ ('policy_identifier', PolicyIdentifier), ('policy_qualifiers', PolicyQualifierInfos, {'optional': True}) ] class CertificatePolicies(SequenceOf): _child_spec = PolicyInformation class PolicyMapping(Sequence): _fields = [ ('issuer_domain_policy', PolicyIdentifier), ('subject_domain_policy', PolicyIdentifier), ] class PolicyMappings(SequenceOf): _child_spec = PolicyMapping class PolicyConstraints(Sequence): _fields = [ ('require_explicit_policy', Integer, {'implicit': 0, 'optional': True}), ('inhibit_policy_mapping', Integer, {'implicit': 1, 'optional': True}), ] class KeyPurposeId(ObjectIdentifier): _map = { # https://tools.ietf.org/html/rfc5280#page-45 '2.5.29.37.0': 'any_extended_key_usage', '1.3.6.1.5.5.7.3.1': 'server_auth', '1.3.6.1.5.5.7.3.2': 'client_auth', '1.3.6.1.5.5.7.3.3': 'code_signing', '1.3.6.1.5.5.7.3.4': 'email_protection', '1.3.6.1.5.5.7.3.5': 'ipsec_end_system', '1.3.6.1.5.5.7.3.6': 'ipsec_tunnel', '1.3.6.1.5.5.7.3.7': 'ipsec_user', '1.3.6.1.5.5.7.3.8': 'time_stamping', '1.3.6.1.5.5.7.3.9': 'ocsp_signing', # http://tools.ietf.org/html/rfc3029.html#page-9 '1.3.6.1.5.5.7.3.10': 'dvcs', # http://tools.ietf.org/html/rfc6268.html#page-16 '1.3.6.1.5.5.7.3.13': 'eap_over_ppp', '1.3.6.1.5.5.7.3.14': 'eap_over_lan', # https://tools.ietf.org/html/rfc5055#page-76 '1.3.6.1.5.5.7.3.15': 'scvp_server', '1.3.6.1.5.5.7.3.16': 'scvp_client', # https://tools.ietf.org/html/rfc4945#page-31 '1.3.6.1.5.5.7.3.17': 'ipsec_ike', # https://tools.ietf.org/html/rfc5415#page-38 '1.3.6.1.5.5.7.3.18': 'capwap_ac', '1.3.6.1.5.5.7.3.19': 'capwap_wtp', # https://tools.ietf.org/html/rfc5924#page-8 '1.3.6.1.5.5.7.3.20': 'sip_domain', # https://tools.ietf.org/html/rfc6187#page-7 '1.3.6.1.5.5.7.3.21': 'secure_shell_client', '1.3.6.1.5.5.7.3.22': 'secure_shell_server', # https://tools.ietf.org/html/rfc6494#page-7 '1.3.6.1.5.5.7.3.23': 'send_router', '1.3.6.1.5.5.7.3.24': 'send_proxied_router', '1.3.6.1.5.5.7.3.25': 'send_owner', '1.3.6.1.5.5.7.3.26': 'send_proxied_owner', # https://tools.ietf.org/html/rfc6402#page-10 '1.3.6.1.5.5.7.3.27': 'cmc_ca', '1.3.6.1.5.5.7.3.28': 'cmc_ra', '1.3.6.1.5.5.7.3.29': 'cmc_archive', # https://tools.ietf.org/html/draft-ietf-sidr-bgpsec-pki-profiles-15#page-6 '1.3.6.1.5.5.7.3.30': 'bgpspec_router', # https://www.ietf.org/proceedings/44/I-D/draft-ietf-ipsec-pki-req-01.txt '1.3.6.1.5.5.8.2.2': 'ike_intermediate', # https://msdn.microsoft.com/en-us/library/windows/desktop/aa378132(v=vs.85).aspx # and https://support.microsoft.com/en-us/kb/287547 '1.3.6.1.4.1.311.10.3.1': 'microsoft_trust_list_signing', '1.3.6.1.4.1.311.10.3.2': 'microsoft_time_stamp_signing', '1.3.6.1.4.1.311.10.3.3': 'microsoft_server_gated', '1.3.6.1.4.1.311.10.3.3.1': 'microsoft_serialized', '1.3.6.1.4.1.311.10.3.4': 'microsoft_efs', '1.3.6.1.4.1.311.10.3.4.1': 'microsoft_efs_recovery', '1.3.6.1.4.1.311.10.3.5': 'microsoft_whql', '1.3.6.1.4.1.311.10.3.6': 'microsoft_nt5', '1.3.6.1.4.1.311.10.3.7': 'microsoft_oem_whql', '1.3.6.1.4.1.311.10.3.8': 'microsoft_embedded_nt', '1.3.6.1.4.1.311.10.3.9': 'microsoft_root_list_signer', '1.3.6.1.4.1.311.10.3.10': 'microsoft_qualified_subordination', '1.3.6.1.4.1.311.10.3.11': 'microsoft_key_recovery', '1.3.6.1.4.1.311.10.3.12': 'microsoft_document_signing', '1.3.6.1.4.1.311.10.3.13': 'microsoft_lifetime_signing', '1.3.6.1.4.1.311.10.3.14': 'microsoft_mobile_device_software', # https://support.microsoft.com/en-us/help/287547/object-ids-associated-with-microsoft-cryptography '1.3.6.1.4.1.311.20.2.2': 'microsoft_smart_card_logon', # https://opensource.apple.com/source # - /Security/Security-57031.40.6/Security/libsecurity_keychain/lib/SecPolicy.cpp # - /libsecurity_cssm/libsecurity_cssm-36064/lib/oidsalg.c '1.2.840.113635.100.1.2': 'apple_x509_basic', '1.2.840.113635.100.1.3': 'apple_ssl', '1.2.840.113635.100.1.4': 'apple_local_cert_gen', '1.2.840.113635.100.1.5': 'apple_csr_gen', '1.2.840.113635.100.1.6': 'apple_revocation_crl', '1.2.840.113635.100.1.7': 'apple_revocation_ocsp', '1.2.840.113635.100.1.8': 'apple_smime', '1.2.840.113635.100.1.9': 'apple_eap', '1.2.840.113635.100.1.10': 'apple_software_update_signing', '1.2.840.113635.100.1.11': 'apple_ipsec', '1.2.840.113635.100.1.12': 'apple_ichat', '1.2.840.113635.100.1.13': 'apple_resource_signing', '1.2.840.113635.100.1.14': 'apple_pkinit_client', '1.2.840.113635.100.1.15': 'apple_pkinit_server', '1.2.840.113635.100.1.16': 'apple_code_signing', '1.2.840.113635.100.1.17': 'apple_package_signing', '1.2.840.113635.100.1.18': 'apple_id_validation', '1.2.840.113635.100.1.20': 'apple_time_stamping', '1.2.840.113635.100.1.21': 'apple_revocation', '1.2.840.113635.100.1.22': 'apple_passbook_signing', '1.2.840.113635.100.1.23': 'apple_mobile_store', '1.2.840.113635.100.1.24': 'apple_escrow_service', '1.2.840.113635.100.1.25': 'apple_profile_signer', '1.2.840.113635.100.1.26': 'apple_qa_profile_signer', '1.2.840.113635.100.1.27': 'apple_test_mobile_store', '1.2.840.113635.100.1.28': 'apple_otapki_signer', '1.2.840.113635.100.1.29': 'apple_test_otapki_signer', '1.2.840.113625.100.1.30': 'apple_id_validation_record_signing_policy', '1.2.840.113625.100.1.31': 'apple_smp_encryption', '1.2.840.113625.100.1.32': 'apple_test_smp_encryption', '1.2.840.113635.100.1.33': 'apple_server_authentication', '1.2.840.113635.100.1.34': 'apple_pcs_escrow_service', # http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.201-2.pdf '2.16.840.1.101.3.6.8': 'piv_card_authentication', '2.16.840.1.101.3.6.7': 'piv_content_signing', # https://tools.ietf.org/html/rfc4556.html '1.3.6.1.5.2.3.4': 'pkinit_kpclientauth', '1.3.6.1.5.2.3.5': 'pkinit_kpkdc', # https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html '1.2.840.113583.1.1.5': 'adobe_authentic_documents_trust', # https://www.idmanagement.gov/wp-content/uploads/sites/1171/uploads/fpki-pivi-cert-profiles.pdf '2.16.840.1.101.3.8.7': 'fpki_pivi_content_signing' } class ExtKeyUsageSyntax(SequenceOf): _child_spec = KeyPurposeId class AccessMethod(ObjectIdentifier): _map = { '1.3.6.1.5.5.7.48.1': 'ocsp', '1.3.6.1.5.5.7.48.2': 'ca_issuers', '1.3.6.1.5.5.7.48.3': 'time_stamping', '1.3.6.1.5.5.7.48.5': 'ca_repository', } class AccessDescription(Sequence): _fields = [ ('access_method', AccessMethod), ('access_location', GeneralName), ] class AuthorityInfoAccessSyntax(SequenceOf): _child_spec = AccessDescription class SubjectInfoAccessSyntax(SequenceOf): _child_spec = AccessDescription # https://tools.ietf.org/html/rfc7633 class Features(SequenceOf): _child_spec = Integer class EntrustVersionInfo(Sequence): _fields = [ ('entrust_vers', GeneralString), ('entrust_info_flags', BitString) ] class NetscapeCertificateType(BitString): _map = { 0: 'ssl_client', 1: 'ssl_server', 2: 'email', 3: 'object_signing', 4: 'reserved', 5: 'ssl_ca', 6: 'email_ca', 7: 'object_signing_ca', } class Version(Integer): _map = { 0: 'v1', 1: 'v2', 2: 'v3', } class TPMSpecification(Sequence): _fields = [ ('family', UTF8String), ('level', Integer), ('revision', Integer), ] class SetOfTPMSpecification(SetOf): _child_spec = TPMSpecification class TCGSpecificationVersion(Sequence): _fields = [ ('major_version', Integer), ('minor_version', Integer), ('revision', Integer), ] class TCGPlatformSpecification(Sequence): _fields = [ ('version', TCGSpecificationVersion), ('platform_class', OctetString), ] class SetOfTCGPlatformSpecification(SetOf): _child_spec = TCGPlatformSpecification class EKGenerationType(Enumerated): _map = { 0: 'internal', 1: 'injected', 2: 'internal_revocable', 3: 'injected_revocable', } class EKGenerationLocation(Enumerated): _map = { 0: 'tpm_manufacturer', 1: 'platform_manufacturer', 2: 'ek_cert_signer', } class EKCertificateGenerationLocation(Enumerated): _map = { 0: 'tpm_manufacturer', 1: 'platform_manufacturer', 2: 'ek_cert_signer', } class EvaluationAssuranceLevel(Enumerated): _map = { 1: 'level1', 2: 'level2', 3: 'level3', 4: 'level4', 5: 'level5', 6: 'level6', 7: 'level7', } class EvaluationStatus(Enumerated): _map = { 0: 'designed_to_meet', 1: 'evaluation_in_progress', 2: 'evaluation_completed', } class StrengthOfFunction(Enumerated): _map = { 0: 'basic', 1: 'medium', 2: 'high', } class URIReference(Sequence): _fields = [ ('uniform_resource_identifier', IA5String), ('hash_algorithm', DigestAlgorithm, {'optional': True}), ('hash_value', BitString, {'optional': True}), ] class CommonCriteriaMeasures(Sequence): _fields = [ ('version', IA5String), ('assurance_level', EvaluationAssuranceLevel), ('evaluation_status', EvaluationStatus), ('plus', Boolean, {'default': False}), ('strengh_of_function', StrengthOfFunction, {'implicit': 0, 'optional': True}), ('profile_oid', ObjectIdentifier, {'implicit': 1, 'optional': True}), ('profile_url', URIReference, {'implicit': 2, 'optional': True}), ('target_oid', ObjectIdentifier, {'implicit': 3, 'optional': True}), ('target_uri', URIReference, {'implicit': 4, 'optional': True}), ] class SecurityLevel(Enumerated): _map = { 1: 'level1', 2: 'level2', 3: 'level3', 4: 'level4', } class FIPSLevel(Sequence): _fields = [ ('version', IA5String), ('level', SecurityLevel), ('plus', Boolean, {'default': False}), ] class TPMSecurityAssertions(Sequence): _fields = [ ('version', Version, {'default': 'v1'}), ('field_upgradable', Boolean, {'default': False}), ('ek_generation_type', EKGenerationType, {'implicit': 0, 'optional': True}), ('ek_generation_location', EKGenerationLocation, {'implicit': 1, 'optional': True}), ('ek_certificate_generation_location', EKCertificateGenerationLocation, {'implicit': 2, 'optional': True}), ('cc_info', CommonCriteriaMeasures, {'implicit': 3, 'optional': True}), ('fips_level', FIPSLevel, {'implicit': 4, 'optional': True}), ('iso_9000_certified', Boolean, {'implicit': 5, 'default': False}), ('iso_9000_uri', IA5String, {'optional': True}), ] class SetOfTPMSecurityAssertions(SetOf): _child_spec = TPMSecurityAssertions class SubjectDirectoryAttributeId(ObjectIdentifier): _map = { # https://tools.ietf.org/html/rfc2256#page-11 '2.5.4.52': 'supported_algorithms', # https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf '2.23.133.2.16': 'tpm_specification', '2.23.133.2.17': 'tcg_platform_specification', '2.23.133.2.18': 'tpm_security_assertions', # https://tools.ietf.org/html/rfc3739#page-18 '1.3.6.1.5.5.7.9.1': 'pda_date_of_birth', '1.3.6.1.5.5.7.9.2': 'pda_place_of_birth', '1.3.6.1.5.5.7.9.3': 'pda_gender', '1.3.6.1.5.5.7.9.4': 'pda_country_of_citizenship', '1.3.6.1.5.5.7.9.5': 'pda_country_of_residence', # https://holtstrom.com/michael/tools/asn1decoder.php '1.2.840.113533.7.68.29': 'entrust_user_role', } class SetOfGeneralizedTime(SetOf): _child_spec = GeneralizedTime class SetOfDirectoryString(SetOf): _child_spec = DirectoryString class SetOfPrintableString(SetOf): _child_spec = PrintableString class SupportedAlgorithm(Sequence): _fields = [ ('algorithm_identifier', AnyAlgorithmIdentifier), ('intended_usage', KeyUsage, {'explicit': 0, 'optional': True}), ('intended_certificate_policies', CertificatePolicies, {'explicit': 1, 'optional': True}), ] class SetOfSupportedAlgorithm(SetOf): _child_spec = SupportedAlgorithm class SubjectDirectoryAttribute(Sequence): _fields = [ ('type', SubjectDirectoryAttributeId), ('values', Any), ] _oid_pair = ('type', 'values') _oid_specs = { 'supported_algorithms': SetOfSupportedAlgorithm, 'tpm_specification': SetOfTPMSpecification, 'tcg_platform_specification': SetOfTCGPlatformSpecification, 'tpm_security_assertions': SetOfTPMSecurityAssertions, 'pda_date_of_birth': SetOfGeneralizedTime, 'pda_place_of_birth': SetOfDirectoryString, 'pda_gender': SetOfPrintableString, 'pda_country_of_citizenship': SetOfPrintableString, 'pda_country_of_residence': SetOfPrintableString, } def _values_spec(self): type_ = self['type'].native if type_ in self._oid_specs: return self._oid_specs[type_] return SetOf _spec_callbacks = { 'values': _values_spec } class SubjectDirectoryAttributes(SequenceOf): _child_spec = SubjectDirectoryAttribute class ExtensionId(ObjectIdentifier): _map = { '2.5.29.9': 'subject_directory_attributes', '2.5.29.14': 'key_identifier', '2.5.29.15': 'key_usage', '2.5.29.16': 'private_key_usage_period', '2.5.29.17': 'subject_alt_name', '2.5.29.18': 'issuer_alt_name', '2.5.29.19': 'basic_constraints', '2.5.29.30': 'name_constraints', '2.5.29.31': 'crl_distribution_points', '2.5.29.32': 'certificate_policies', '2.5.29.33': 'policy_mappings', '2.5.29.35': 'authority_key_identifier', '2.5.29.36': 'policy_constraints', '2.5.29.37': 'extended_key_usage', '2.5.29.46': 'freshest_crl', '2.5.29.54': 'inhibit_any_policy', '1.3.6.1.5.5.7.1.1': 'authority_information_access', '1.3.6.1.5.5.7.1.11': 'subject_information_access', # https://tools.ietf.org/html/rfc7633 '1.3.6.1.5.5.7.1.24': 'tls_feature', '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', '1.2.840.113533.7.65.0': 'entrust_version_extension', '2.16.840.1.113730.1.1': 'netscape_certificate_type', # https://tools.ietf.org/html/rfc6962.html#page-14 '1.3.6.1.4.1.11129.2.4.2': 'signed_certificate_timestamp_list', } class Extension(Sequence): _fields = [ ('extn_id', ExtensionId), ('critical', Boolean, {'default': False}), ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') _oid_specs = { 'subject_directory_attributes': SubjectDirectoryAttributes, 'key_identifier': OctetString, 'key_usage': KeyUsage, 'private_key_usage_period': PrivateKeyUsagePeriod, 'subject_alt_name': GeneralNames, 'issuer_alt_name': GeneralNames, 'basic_constraints': BasicConstraints, 'name_constraints': NameConstraints, 'crl_distribution_points': CRLDistributionPoints, 'certificate_policies': CertificatePolicies, 'policy_mappings': PolicyMappings, 'authority_key_identifier': AuthorityKeyIdentifier, 'policy_constraints': PolicyConstraints, 'extended_key_usage': ExtKeyUsageSyntax, 'freshest_crl': CRLDistributionPoints, 'inhibit_any_policy': Integer, 'authority_information_access': AuthorityInfoAccessSyntax, 'subject_information_access': SubjectInfoAccessSyntax, 'tls_feature': Features, 'ocsp_no_check': Null, 'entrust_version_extension': EntrustVersionInfo, 'netscape_certificate_type': NetscapeCertificateType, 'signed_certificate_timestamp_list': OctetString, } class Extensions(SequenceOf): _child_spec = Extension class TbsCertificate(Sequence): _fields = [ ('version', Version, {'explicit': 0, 'default': 'v1'}), ('serial_number', Integer), ('signature', SignedDigestAlgorithm), ('issuer', Name), ('validity', Validity), ('subject', Name), ('subject_public_key_info', PublicKeyInfo), ('issuer_unique_id', OctetBitString, {'implicit': 1, 'optional': True}), ('subject_unique_id', OctetBitString, {'implicit': 2, 'optional': True}), ('extensions', Extensions, {'explicit': 3, 'optional': True}), ] class Certificate(Sequence): _fields = [ ('tbs_certificate', TbsCertificate), ('signature_algorithm', SignedDigestAlgorithm), ('signature_value', OctetBitString), ] _processed_extensions = False _critical_extensions = None _subject_directory_attributes_value = None _key_identifier_value = None _key_usage_value = None _subject_alt_name_value = None _issuer_alt_name_value = None _basic_constraints_value = None _name_constraints_value = None _crl_distribution_points_value = None _certificate_policies_value = None _policy_mappings_value = None _authority_key_identifier_value = None _policy_constraints_value = None _freshest_crl_value = None _inhibit_any_policy_value = None _extended_key_usage_value = None _authority_information_access_value = None _subject_information_access_value = None _private_key_usage_period_value = None _tls_feature_value = None _ocsp_no_check_value = None _issuer_serial = None _authority_issuer_serial = False _crl_distribution_points = None _delta_crl_distribution_points = None _valid_domains = None _valid_ips = None _self_issued = None _self_signed = None _sha1 = None _sha256 = None def _set_extensions(self): """ Sets common named extensions to private attributes and creates a list of critical extensions """ self._critical_extensions = set() for extension in self['tbs_certificate']['extensions']: name = extension['extn_id'].native attribute_name = '_%s_value' % name if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: A set of unicode strings """ if not self._processed_extensions: self._set_extensions() return self._critical_extensions @property def private_key_usage_period_value(self): """ This extension is used to constrain the period over which the subject private key may be used :return: None or a PrivateKeyUsagePeriod object """ if not self._processed_extensions: self._set_extensions() return self._private_key_usage_period_value @property def subject_directory_attributes_value(self): """ This extension is used to contain additional identification attributes about the subject. :return: None or a SubjectDirectoryAttributes object """ if not self._processed_extensions: self._set_extensions() return self._subject_directory_attributes_value @property def key_identifier_value(self): """ This extension is used to help in creating certificate validation paths. It contains an identifier that should generally, but is not guaranteed to, be unique. :return: None or an OctetString object """ if not self._processed_extensions: self._set_extensions() return self._key_identifier_value @property def key_usage_value(self): """ This extension is used to define the purpose of the public key contained within the certificate. :return: None or a KeyUsage """ if not self._processed_extensions: self._set_extensions() return self._key_usage_value @property def subject_alt_name_value(self): """ This extension allows for additional names to be associate with the subject of the certificate. While it may contain a whole host of possible names, it is usually used to allow certificates to be used with multiple different domain names. :return: None or a GeneralNames object """ if not self._processed_extensions: self._set_extensions() return self._subject_alt_name_value @property def issuer_alt_name_value(self): """ This extension allows associating one or more alternative names with the issuer of the certificate. :return: None or an x509.GeneralNames object """ if not self._processed_extensions: self._set_extensions() return self._issuer_alt_name_value @property def basic_constraints_value(self): """ This extension is used to determine if the subject of the certificate is a CA, and if so, what the maximum number of intermediate CA certs after this are, before an end-entity certificate is found. :return: None or a BasicConstraints object """ if not self._processed_extensions: self._set_extensions() return self._basic_constraints_value @property def name_constraints_value(self): """ This extension is used in CA certificates, and is used to limit the possible names of certificates issued. :return: None or a NameConstraints object """ if not self._processed_extensions: self._set_extensions() return self._name_constraints_value @property def crl_distribution_points_value(self): """ This extension is used to help in locating the CRL for this certificate. :return: None or a CRLDistributionPoints object extension """ if not self._processed_extensions: self._set_extensions() return self._crl_distribution_points_value @property def certificate_policies_value(self): """ This extension defines policies in CA certificates under which certificates may be issued. In end-entity certificates, the inclusion of a policy indicates the issuance of the certificate follows the policy. :return: None or a CertificatePolicies object """ if not self._processed_extensions: self._set_extensions() return self._certificate_policies_value @property def policy_mappings_value(self): """ This extension allows mapping policy OIDs to other OIDs. This is used to allow different policies to be treated as equivalent in the process of validation. :return: None or a PolicyMappings object """ if not self._processed_extensions: self._set_extensions() return self._policy_mappings_value @property def authority_key_identifier_value(self): """ This extension helps in identifying the public key with which to validate the authenticity of the certificate. :return: None or an AuthorityKeyIdentifier object """ if not self._processed_extensions: self._set_extensions() return self._authority_key_identifier_value @property def policy_constraints_value(self): """ This extension is used to control if policy mapping is allowed and when policies are required. :return: None or a PolicyConstraints object """ if not self._processed_extensions: self._set_extensions() return self._policy_constraints_value @property def freshest_crl_value(self): """ This extension is used to help locate any available delta CRLs :return: None or an CRLDistributionPoints object """ if not self._processed_extensions: self._set_extensions() return self._freshest_crl_value @property def inhibit_any_policy_value(self): """ This extension is used to prevent mapping of the any policy to specific requirements :return: None or a Integer object """ if not self._processed_extensions: self._set_extensions() return self._inhibit_any_policy_value @property def extended_key_usage_value(self): """ This extension is used to define additional purposes for the public key beyond what is contained in the basic constraints. :return: None or an ExtKeyUsageSyntax object """ if not self._processed_extensions: self._set_extensions() return self._extended_key_usage_value @property def authority_information_access_value(self): """ This extension is used to locate the CA certificate used to sign this certificate, or the OCSP responder for this certificate. :return: None or an AuthorityInfoAccessSyntax object """ if not self._processed_extensions: self._set_extensions() return self._authority_information_access_value @property def subject_information_access_value(self): """ This extension is used to access information about the subject of this certificate. :return: None or a SubjectInfoAccessSyntax object """ if not self._processed_extensions: self._set_extensions() return self._subject_information_access_value @property def tls_feature_value(self): """ This extension is used to list the TLS features a server must respond with if a client initiates a request supporting them. :return: None or a Features object """ if not self._processed_extensions: self._set_extensions() return self._tls_feature_value @property def ocsp_no_check_value(self): """ This extension is used on certificates of OCSP responders, indicating that revocation information for the certificate should never need to be verified, thus preventing possible loops in path validation. :return: None or a Null object (if present) """ if not self._processed_extensions: self._set_extensions() return self._ocsp_no_check_value @property def signature(self): """ :return: A byte string of the signature """ return self['signature_value'].native @property def signature_algo(self): """ :return: A unicode string of "rsassa_pkcs1v15", "rsassa_pss", "dsa", "ecdsa" """ return self['signature_algorithm'].signature_algo @property def hash_algo(self): """ :return: A unicode string of "md2", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "sha512_224", "sha512_256" """ return self['signature_algorithm'].hash_algo @property def public_key(self): """ :return: The PublicKeyInfo object for this certificate """ return self['tbs_certificate']['subject_public_key_info'] @property def subject(self): """ :return: The Name object for the subject of this certificate """ return self['tbs_certificate']['subject'] @property def issuer(self): """ :return: The Name object for the issuer of this certificate """ return self['tbs_certificate']['issuer'] @property def serial_number(self): """ :return: An integer of the certificate's serial number """ return self['tbs_certificate']['serial_number'].native @property def key_identifier(self): """ :return: None or a byte string of the certificate's key identifier from the key identifier extension """ if not self.key_identifier_value: return None return self.key_identifier_value.native @property def issuer_serial(self): """ :return: A byte string of the SHA-256 hash of the issuer concatenated with the ascii character ":", concatenated with the serial number as an ascii string """ if self._issuer_serial is None: self._issuer_serial = self.issuer.sha256 + b':' + str_cls(self.serial_number).encode('ascii') return self._issuer_serial @property def not_valid_after(self): """ :return: A datetime of latest time when the certificate is still valid """ return self['tbs_certificate']['validity']['not_after'].native @property def not_valid_before(self): """ :return: A datetime of the earliest time when the certificate is valid """ return self['tbs_certificate']['validity']['not_before'].native @property def authority_key_identifier(self): """ :return: None or a byte string of the key_identifier from the authority key identifier extension """ if not self.authority_key_identifier_value: return None return self.authority_key_identifier_value['key_identifier'].native @property def authority_issuer_serial(self): """ :return: None or a byte string of the SHA-256 hash of the isser from the authority key identifier extension concatenated with the ascii character ":", concatenated with the serial number from the authority key identifier extension as an ascii string """ if self._authority_issuer_serial is False: akiv = self.authority_key_identifier_value if akiv and akiv['authority_cert_issuer'].native: issuer = self.authority_key_identifier_value['authority_cert_issuer'][0].chosen # We untag the element since it is tagged via being a choice from GeneralName issuer = issuer.untag() authority_serial = self.authority_key_identifier_value['authority_cert_serial_number'].native self._authority_issuer_serial = issuer.sha256 + b':' + str_cls(authority_serial).encode('ascii') else: self._authority_issuer_serial = None return self._authority_issuer_serial @property def crl_distribution_points(self): """ Returns complete CRL URLs - does not include delta CRLs :return: A list of zero or more DistributionPoint objects """ if self._crl_distribution_points is None: self._crl_distribution_points = self._get_http_crl_distribution_points(self.crl_distribution_points_value) return self._crl_distribution_points @property def delta_crl_distribution_points(self): """ Returns delta CRL URLs - does not include complete CRLs :return: A list of zero or more DistributionPoint objects """ if self._delta_crl_distribution_points is None: self._delta_crl_distribution_points = self._get_http_crl_distribution_points(self.freshest_crl_value) return self._delta_crl_distribution_points def _get_http_crl_distribution_points(self, crl_distribution_points): """ Fetches the DistributionPoint object for non-relative, HTTP CRLs referenced by the certificate :param crl_distribution_points: A CRLDistributionPoints object to grab the DistributionPoints from :return: A list of zero or more DistributionPoint objects """ output = [] if crl_distribution_points is None: return [] for distribution_point in crl_distribution_points: distribution_point_name = distribution_point['distribution_point'] if distribution_point_name is VOID: continue # RFC 5280 indicates conforming CA should not use the relative form if distribution_point_name.name == 'name_relative_to_crl_issuer': continue # This library is currently only concerned with HTTP-based CRLs for general_name in distribution_point_name.chosen: if general_name.name == 'uniform_resource_identifier': output.append(distribution_point) return output @property def ocsp_urls(self): """ :return: A list of zero or more unicode strings of the OCSP URLs for this cert """ if not self.authority_information_access_value: return [] output = [] for entry in self.authority_information_access_value: if entry['access_method'].native == 'ocsp': location = entry['access_location'] if location.name != 'uniform_resource_identifier': continue url = location.native if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')): output.append(url) return output @property def valid_domains(self): """ :return: A list of unicode strings of valid domain names for the certificate. Wildcard certificates will have a domain in the form: *.example.com """ if self._valid_domains is None: self._valid_domains = [] # For the subject alt name extension, we can look at the name of # the choice selected since it distinguishes between domain names, # email addresses, IPs, etc if self.subject_alt_name_value: for general_name in self.subject_alt_name_value: if general_name.name == 'dns_name' and general_name.native not in self._valid_domains: self._valid_domains.append(general_name.native) # If there was no subject alt name extension, and the common name # in the subject looks like a domain, that is considered the valid # list. This is done because according to # https://tools.ietf.org/html/rfc6125#section-6.4.4, the common # name should not be used if the subject alt name is present. else: pattern = re.compile('^(\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') for rdn in self.subject.chosen: for name_type_value in rdn: if name_type_value['type'].native == 'common_name': value = name_type_value['value'].native if pattern.match(value): self._valid_domains.append(value) return self._valid_domains @property def valid_ips(self): """ :return: A list of unicode strings of valid IP addresses for the certificate """ if self._valid_ips is None: self._valid_ips = [] if self.subject_alt_name_value: for general_name in self.subject_alt_name_value: if general_name.name == 'ip_address': self._valid_ips.append(general_name.native) return self._valid_ips @property def ca(self): """ :return; A boolean - if the certificate is marked as a CA """ return self.basic_constraints_value and self.basic_constraints_value['ca'].native @property def max_path_length(self): """ :return; None or an integer of the maximum path length """ if not self.ca: return None return self.basic_constraints_value['path_len_constraint'].native @property def self_issued(self): """ :return: A boolean - if the certificate is self-issued, as defined by RFC 5280 """ if self._self_issued is None: self._self_issued = self.subject == self.issuer return self._self_issued @property def self_signed(self): """ :return: A unicode string of "no" or "maybe". The "maybe" result will be returned if the certificate issuer and subject are the same. If a key identifier and authority key identifier are present, they will need to match otherwise "no" will be returned. To verify is a certificate is truly self-signed, the signature will need to be verified. See the certvalidator package for one possible solution. """ if self._self_signed is None: self._self_signed = 'no' if self.self_issued: if self.key_identifier: if not self.authority_key_identifier: self._self_signed = 'maybe' elif self.authority_key_identifier == self.key_identifier: self._self_signed = 'maybe' else: self._self_signed = 'maybe' return self._self_signed @property def sha1(self): """ :return: The SHA-1 hash of the DER-encoded bytes of this complete certificate """ if self._sha1 is None: self._sha1 = hashlib.sha1(self.dump()).digest() return self._sha1 @property def sha1_fingerprint(self): """ :return: A unicode string of the SHA-1 hash, formatted using hex encoding with a space between each pair of characters, all uppercase """ return ' '.join('%02X' % c for c in bytes_to_list(self.sha1)) @property def sha256(self): """ :return: The SHA-256 hash of the DER-encoded bytes of this complete certificate """ if self._sha256 is None: self._sha256 = hashlib.sha256(self.dump()).digest() return self._sha256 @property def sha256_fingerprint(self): """ :return: A unicode string of the SHA-256 hash, formatted using hex encoding with a space between each pair of characters, all uppercase """ return ' '.join('%02X' % c for c in bytes_to_list(self.sha256)) def is_valid_domain_ip(self, domain_ip): """ Check if a domain name or IP address is valid according to the certificate :param domain_ip: A unicode string of a domain name or IP address :return: A boolean - if the domain or IP is valid for the certificate """ if not isinstance(domain_ip, str_cls): raise TypeError(unwrap( ''' domain_ip must be a unicode string, not %s ''', type_name(domain_ip) )) encoded_domain_ip = domain_ip.encode('idna').decode('ascii').lower() is_ipv6 = encoded_domain_ip.find(':') != -1 is_ipv4 = not is_ipv6 and re.match('^\\d+\\.\\d+\\.\\d+\\.\\d+$', encoded_domain_ip) is_domain = not is_ipv6 and not is_ipv4 # Handle domain name checks if is_domain: if not self.valid_domains: return False domain_labels = encoded_domain_ip.split('.') for valid_domain in self.valid_domains: encoded_valid_domain = valid_domain.encode('idna').decode('ascii').lower() valid_domain_labels = encoded_valid_domain.split('.') # The domain must be equal in label length to match if len(valid_domain_labels) != len(domain_labels): continue if valid_domain_labels == domain_labels: return True is_wildcard = self._is_wildcard_domain(encoded_valid_domain) if is_wildcard and self._is_wildcard_match(domain_labels, valid_domain_labels): return True return False # Handle IP address checks if not self.valid_ips: return False family = socket.AF_INET if is_ipv4 else socket.AF_INET6 normalized_ip = inet_pton(family, encoded_domain_ip) for valid_ip in self.valid_ips: valid_family = socket.AF_INET if valid_ip.find('.') != -1 else socket.AF_INET6 normalized_valid_ip = inet_pton(valid_family, valid_ip) if normalized_valid_ip == normalized_ip: return True return False def _is_wildcard_domain(self, domain): """ Checks if a domain is a valid wildcard according to https://tools.ietf.org/html/rfc6125#section-6.4.3 :param domain: A unicode string of the domain name, where any U-labels from an IDN have been converted to A-labels :return: A boolean - if the domain is a valid wildcard domain """ # The * character must be present for a wildcard match, and if there is # most than one, it is an invalid wildcard specification if domain.count('*') != 1: return False labels = domain.lower().split('.') if not labels: return False # Wildcards may only appear in the left-most label if labels[0].find('*') == -1: return False # Wildcards may not be embedded in an A-label from an IDN if labels[0][0:4] == 'xn--': return False return True def _is_wildcard_match(self, domain_labels, valid_domain_labels): """ Determines if the labels in a domain are a match for labels from a wildcard valid domain name :param domain_labels: A list of unicode strings, with A-label form for IDNs, of the labels in the domain name to check :param valid_domain_labels: A list of unicode strings, with A-label form for IDNs, of the labels in a wildcard domain pattern :return: A boolean - if the domain matches the valid domain """ first_domain_label = domain_labels[0] other_domain_labels = domain_labels[1:] wildcard_label = valid_domain_labels[0] other_valid_domain_labels = valid_domain_labels[1:] # The wildcard is only allowed in the first label, so if # The subsequent labels are not equal, there is no match if other_domain_labels != other_valid_domain_labels: return False if wildcard_label == '*': return True wildcard_regex = re.compile('^' + wildcard_label.replace('*', '.*') + '$') if wildcard_regex.match(first_domain_label): return True return False # The structures are taken from the OpenSSL source file x_x509a.c, and specify # extra information that is added to X.509 certificates to store trust # information about the certificate. class KeyPurposeIdentifiers(SequenceOf): _child_spec = KeyPurposeId class SequenceOfAlgorithmIdentifiers(SequenceOf): _child_spec = AlgorithmIdentifier class CertificateAux(Sequence): _fields = [ ('trust', KeyPurposeIdentifiers, {'optional': True}), ('reject', KeyPurposeIdentifiers, {'implicit': 0, 'optional': True}), ('alias', UTF8String, {'optional': True}), ('keyid', OctetString, {'optional': True}), ('other', SequenceOfAlgorithmIdentifiers, {'implicit': 1, 'optional': True}), ] class TrustedCertificate(Concat): _child_specs = [Certificate, CertificateAux]