# 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 = 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) == iri_to_uri(other.native)
|
|
|
|
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
|
|
|
|
@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
|
|
"""
|
|
|
|
if self._unicode is None:
|
|
contents = self._merge_chunks()
|
|
if contents.find(b'@') == -1:
|
|
self._unicode = contents.decode('ascii')
|
|
else:
|
|
mailbox, hostname = contents.rsplit(b'@', 1)
|
|
self._unicode = mailbox.decode('ascii') + '@' + 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 a 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)
|
|
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/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',
|
|
'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',
|
|
}.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,
|
|
}
|
|
|
|
_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://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 = 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
|
|
|
|
@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 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]
|