diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 94c841f9..e802f343 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -9,6 +9,7 @@ import arrow from flask import current_app +from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import rsa from sqlalchemy.orm import relationship @@ -229,16 +230,39 @@ class Certificate(db.Model): @property def extensions(self): - # TODO pull the OU, O, CN, etc + other extensions. - names = [{'name_type': 'DNSName', 'value': x.name} for x in self.domains] + return_extensions = {} + cert = lemur.common.utils.parse_certificate(self.body) + for extension in cert.extensions: + if isinstance(extension, x509.BasicConstraints): + return_extensions['basic_constraints'] = extension + elif isinstance(extension, x509.SubjectAlternativeName): + return_extensions['sub_alt_names'] = extension + elif isinstance(extension, x509.ExtendedKeyUsage): + return_extensions['extended_key_usage'] = extension + elif isinstance(extension, x509.KeyUsage): + return_extensions['key_usage'] = extension + elif isinstance(extension, x509.SubjectKeyIdentifier): + return_extensions['subject_key_identifier'] = {'include_ski': True} + elif isinstance(extension, x509.AuthorityInformationAccess): + return_extensions['certificate_info_access'] = {'include_aia': True} + elif isinstance(extension, x509.AuthorityKeyIdentifier): + aki = { + 'use_key_identifier': False, + 'use_authority_cert': False + } + if extension.key_identifier: + aki['use_key_identifier'] = True + if extension.authority_cert_issuer: + aki['use_authority_cert'] = True + return_extensions['authority_key_identifier'] = aki + elif isinstance(extension, x509.CRLDistributionPoints): + # FIXME: Don't support CRLDistributionPoints yet https://github.com/Netflix/lemur/issues/662 + pass + else: + # FIXME: Not supporting custom OIDs yet. https://github.com/Netflix/lemur/issues/665 + pass - extensions = { - 'sub_alt_names': { - 'names': names - } - } - - return extensions + return return_extensions def get_arn(self, account_number): """ diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 2e4b5ddc..e03d67a0 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -345,65 +345,23 @@ def create_csr(**csr_config): x509.NameAttribute(x509.OID_EMAIL_ADDRESS, csr_config['owner']) ])) - builder = builder.add_extension( - x509.BasicConstraints(ca=False, path_length=None), critical=True, - ) + extensions = csr_config.get('extensions', {}) + critical_extensions = ['basic_constraints', 'sub_alt_names', 'key_usage'] + noncritical_extensions = ['extended_key_usage'] + for k, v in extensions.items(): + if k in critical_extensions and v: + current_app.logger.debug("Add CExt: {0} {1}".format(k, v)) + builder = builder.add_extension(v, critical=True) + if k in noncritical_extensions and v: + current_app.logger.debug("Add Ext: {0} {1}".format(k, v)) + builder = builder.add_extension(v, critical=False) - if csr_config.get('extensions'): - for k, v in csr_config.get('extensions', {}).items(): - if k == 'sub_alt_names': - # map types to their x509 objects - general_names = [] - for name in v['names']: - if name['name_type'] == 'DNSName': - general_names.append(x509.DNSName(name['value'])) - - builder = builder.add_extension( - x509.SubjectAlternativeName(general_names), critical=True - ) - - # TODO support more CSR options, none of the authority plugins currently support these options - # builder.add_extension( - # x509.KeyUsage( - # digital_signature=digital_signature, - # content_commitment=content_commitment, - # key_encipherment=key_enipherment, - # data_encipherment=data_encipherment, - # key_agreement=key_agreement, - # key_cert_sign=key_cert_sign, - # crl_sign=crl_sign, - # encipher_only=enchipher_only, - # decipher_only=decipher_only - # ), critical=True - # ) - # - # # we must maintain our own list of OIDs here - # builder.add_extension( - # x509.ExtendedKeyUsage( - # server_authentication=server_authentication, - # email= - # ) - # ) - # - # builder.add_extension( - # x509.AuthorityInformationAccess() - # ) - # - # builder.add_extension( - # x509.AuthorityKeyIdentifier() - # ) - # - # builder.add_extension( - # x509.SubjectKeyIdentifier() - # ) - # - # builder.add_extension( - # x509.CRLDistributionPoints() - # ) - # - # builder.add_extension( - # x509.ObjectIdentifier(oid) - # ) + ski = extensions.get('subject_key_identifier', {}) + if ski.get('include_ski', False): + builder = builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()), + critical=False + ) request = builder.sign( private_key, hashes.SHA256(), default_backend() @@ -500,14 +458,6 @@ def get_certificate_primitives(certificate): certificate via `create`. """ start, end = calculate_reissue_range(certificate.not_before, certificate.not_after) - names = [{'name_type': 'DNSName', 'value': x.name} for x in certificate.domains] - - # TODO pull additional extensions - extensions = { - 'sub_alt_names': { - 'names': names - } - } return dict( authority=certificate.authority, @@ -517,7 +467,7 @@ def get_certificate_primitives(certificate): validity_end=end, destinations=certificate.destinations, roles=certificate.roles, - extensions=extensions, + extensions=certificate.extensions, owner=certificate.owner, organization=certificate.organization, organizational_unit=certificate.organizational_unit, diff --git a/lemur/common/fields.py b/lemur/common/fields.py index 3030cb0b..c15c160c 100644 --- a/lemur/common/fields.py +++ b/lemur/common/fields.py @@ -3,6 +3,9 @@ import warnings from datetime import datetime as dt from marshmallow.fields import Field from marshmallow import utils +from cryptography import x509 +from marshmallow.exceptions import ValidationError +import ipaddress class ArrowDateTime(Field): @@ -91,3 +94,240 @@ class ArrowDateTime(Field): warnings.warn('It is recommended that you install python-dateutil ' 'for improved datetime deserialization.') raise self.fail('invalid') + + +class KeyUsageExtension(Field): + """An x509.KeyUsage ExtensionType object + + Dict of KeyUsage names/values are deserialized into an x509.KeyUsage object + and back. + + :param kwargs: The same keyword arguments that :class:`Field` receives. + + """ + + def _serialize(self, value, attr, obj): + return { + 'useDigitalSignature': value.digital_signature, + 'useNonRepudiation': value.content_commitment, + 'useKeyEncipherment': value.key_encipherment, + 'useDataEncipherment': value.data_encipherment, + 'useKeyAgreement': value.key_agreement, + 'useKeyCertSign': value.key_cert_sign, + 'useCrlSign': value.crl_sign, + 'useEncipherOnly': value._encipher_only, + 'useDecipherOnly': value._decipher_only + } + + def _deserialize(self, value, attr, data): + keyusages = { + 'digital_signature': False, + 'content_commitment': False, + 'key_encipherment': False, + 'data_encipherment': False, + 'key_agreement': False, + 'key_cert_sign': False, + 'crl_sign': False, + 'encipher_only': False, + 'decipher_only': False + } + for k, v in value.items(): + if k == 'useDigitalSignature': + keyusages['digital_signature'] = v + if k == 'useNonRepudiation': + keyusages['content_commitment'] = v + if k == 'useKeyEncipherment': + keyusages['key_encipherment'] = v + if k == 'useDataEncipherment': + keyusages['data_encipherment'] = v + if k == 'useKeyCertSign': + keyusages['key_cert_sign'] = v + if k == 'useCrlSign': + keyusages['crl_sign'] = v + if k == 'useEncipherOnly' and v: + keyusages['encipher_only'] = True + keyusages['key_agreement'] = True + if k == 'useDecipherOnly' and v: + keyusages['decipher_only'] = True + keyusages['key_agreement'] = True + + if keyusages['encipher_only'] and keyusages['decipher_only']: + raise ValidationError('A certificate cannot have both Encipher Only and Decipher Only Extended Key Usages.') + + return x509.KeyUsage( + digital_signature=keyusages['digital_signature'], + content_commitment=keyusages['content_commitment'], + key_encipherment=keyusages['key_encipherment'], + data_encipherment=keyusages['data_encipherment'], + key_agreement=keyusages['key_agreement'], + key_cert_sign=keyusages['key_cert_sign'], + crl_sign=keyusages['crl_sign'], + encipher_only=keyusages['encipher_only'], + decipher_only=keyusages['decipher_only'] + ) + + +class ExtendedKeyUsageExtension(Field): + """An x509.ExtendedKeyUsage ExtensionType object + + Dict of ExtendedKeyUsage names/values are deserialized into an x509.ExtendedKeyUsage object + and back. + + :param kwargs: The same keyword arguments that :class:`Field` receives. + + """ + + def _serialize(self, value, attr, obj): + usages = value._usages + usage_list = {} + for usage in usages: + if usage.dotted_string == x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH: + usage_list["useClientAuthentication"] = True + if usage.dotted_string == x509.oid.ExtendedKeyUsageOID.SERVER_AUTH: + usage_list["useServerAuthentication"] = True + if usage.dotted_string == x509.oid.ExtendedKeyUsageOID.CODE_SIGNING: + usage_list["useCodeSigning"] = True + if usage.dotted_string == x509.oid.ExtendedKeyUsageOID.EMAIL_PROTECTION: + usage_list["useEmailProtection"] = True + if usage.dotted_string == x509.oid.ExtendedKeyUsageOID.TIME_STAMPING: + usage_list["useTimestamping"] = True + if usage.dotted_string == x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING: + usage_list["useOCSPSigning"] = True + if usage.dotted_string == "1.3.6.1.5.5.7.3.14": + usage_list["useEapOverLAN"] = True + if usage.dotted_string == "1.3.6.1.5.5.7.3.13": + usage_list["useEapOverPPP"] = True + if usage.dotted_string == "1.3.6.1.4.1.311.20.2.2": + usage_list["useSmartCardLogon"] = True + + return usage_list + + def _deserialize(self, value, attr, data): + usage_oids = [] + for k, v in value.items(): + if k == 'useClientAuthentication' and v: + usage_oids.append(x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH) + if k == 'useServerAuthentication' and v: + usage_oids.append(x509.oid.ExtendedKeyUsageOID.SERVER_AUTH) + if k == 'useCodeSigning' and v: + usage_oids.append(x509.oid.ExtendedKeyUsageOID.CODE_SIGNING) + if k == 'useEmailProtection' and v: + usage_oids.append(x509.oid.ExtendedKeyUsageOID.EMAIL_PROTECTION) + if k == 'useTimestamping' and v: + usage_oids.append(x509.oid.ExtendedKeyUsageOID.TIME_STAMPING) + if k == 'useOCSPSigning' and v: + usage_oids.append(x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING) + if k == 'useEapOverLAN' and v: + usage_oids.append(x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.3.14")) + if k == 'useEapOverPPP' and v: + usage_oids.append(x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.3.13")) + if k == 'useSmartCardLogon' and v: + usage_oids.append(x509.oid.ObjectIdentifier("1.3.6.1.4.1.311.20.2.2")) + + return x509.ExtendedKeyUsage(usage_oids) + + +class BasicConstraintsExtension(Field): + """An x509.BasicConstraints ExtensionType object + + Dict of CA boolean and a path_length integer names/values are deserialized into an x509.BasicConstraints object + and back. + + :param kwargs: The same keyword arguments that :class:`Field` receives. + + """ + + def _serialize(self, value, attr, obj): + return {'ca': value.ca(), 'path_length': value.path_length()} + + def _deserialize(self, value, attr, data): + ca = value.get('ca', False) + path_length = value.get('path_length', None) + + if ca: + if not isinstance(path_length, (type(None), int)): + raise ValidationError('A CA certificate path_length (for BasicConstraints) must be None or an integer.') + return x509.BasicConstraints(ca=True, path_length=path_length) + else: + return x509.BasicConstraints(ca=False, path_length=None) + + +class SubjectAlternativeNameExtension(Field): + """An x509.SubjectAlternativeName ExtensionType object + + Dict of CA boolean and a path_length integer names/values are deserialized into an x509.BasicConstraints object + and back. + + :param kwargs: The same keyword arguments that :class:`Field` receives. + + """ + + def _serialize(self, value, attr, obj): + general_names = [] + for name in value._general_names: + value = name.value() + if isinstance(name, x509.DNSName): + name_type = 'DNSName' + if isinstance(name, x509.IPAddress): + name_type = 'IPAddress' + value = str(value) + if isinstance(name, x509.UniformResourceIdentifier): + name_type = 'uniformResourceIdentifier' + if isinstance(name, x509.DirectoryName): + name_type = 'directoryName' + if isinstance(name, x509.RFC822Name): + name_type = 'rfc822Name' + if isinstance(name, x509.RegisteredID): + name_type = 'registeredID' + value = value.dotted_string + general_names.append({'nameType': name_type, 'value': value}) + + return general_names + + def _deserialize(self, value, attr, data): + general_names = [] + for name in value.get('names', []): + if name['nameType'] == 'DNSName': + general_names.append(x509.DNSName(name['value'])) + if name['nameType'] == 'IPAddress': + general_names.append(x509.IPAddress(ipaddress.ip_address(name['value']))) + if name['nameType'] == 'IPNetwork': + general_names.append(x509.IPAddress(ipaddress.ip_network(name['value']))) + if name['nameType'] == 'uniformResourceIdentifier': + general_names.append(x509.UniformResourceIdentifier(name['value'])) + if name['nameType'] == 'directoryName': + # FIXME: Need to parse a string in name['value'] like: + # 'CN=Common Name, O=Org Name, OU=OrgUnit Name, C=US, ST=ST, L=City/emailAddress=person@example.com' + # or + # 'CN=Common Name/O=Org Name/OU=OrgUnit Name/C=US/ST=NH/L=City/emailAddress=person@example.com' + # and turn it into something like: + # x509.Name([ + # x509.NameAttribute(x509.OID_COMMON_NAME, "Common Name"), + # x509.NameAttribute(x509.OID_ORGANIZATION_NAME, "Org Name"), + # x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, "OrgUnit Name"), + # x509.NameAttribute(x509.OID_COUNTRY_NAME, "US"), + # x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, "NH"), + # x509.NameAttribute(x509.OID_LOCALITY_NAME, "City"), + # x509.NameAttribute(x509.OID_EMAIL_ADDRESS, "person@example.com") + # ] + # general_names.append(x509.DirectoryName(x509.Name(BLAH)))) + pass + if name['nameType'] == 'rfc822Name': + general_names.append(x509.RFC822Name(name['value'])) + if name['nameType'] == 'registeredID': + general_names.append(x509.RegisteredID(x509.ObjectIdentifier(name['value']))) + if name['nameType'] == 'otherName': + # This has two inputs (type and value), so it doesn't fit the mold of the rest of these GeneralName entities. + # general_names.append(x509.OtherName(name['type'], bytes(name['value']), 'utf-8')) + pass + if name['nameType'] == 'x400Address': + # The Python Cryptography library doesn't support x400Address types (yet?) + pass + if name['nameType'] == 'EDIPartyName': + # The Python Cryptography library doesn't support EDIPartyName types (yet?) + pass + + if general_names: + return x509.SubjectAlternativeName(general_names) + else: + return None diff --git a/lemur/schemas.py b/lemur/schemas.py index d6ece2e2..9eff45e9 100644 --- a/lemur/schemas.py +++ b/lemur/schemas.py @@ -9,11 +9,12 @@ """ from sqlalchemy.orm.exc import NoResultFound -from marshmallow import fields, post_load, pre_load, post_dump, validates_schema +from marshmallow import fields, post_load, pre_load, post_dump from marshmallow.exceptions import ValidationError from lemur.common import validators from lemur.common.schema import LemurSchema, LemurInputSchema, LemurOutputSchema +from lemur.common.fields import KeyUsageExtension, ExtendedKeyUsageExtension, BasicConstraintsExtension, SubjectAlternativeNameExtension from lemur.plugins import plugins from lemur.roles.models import Role @@ -166,10 +167,6 @@ class BaseExtensionSchema(LemurSchema): return data -class BasicConstraintsSchema(BaseExtensionSchema): - pass - - class AuthorityKeyIdentifierSchema(BaseExtensionSchema): use_key_identifier = fields.Boolean() use_authority_cert = fields.Boolean() @@ -183,30 +180,6 @@ class CertificateInfoAccessSchema(BaseExtensionSchema): return {'includeAIA': data['include_aia']} -class KeyUsageSchema(BaseExtensionSchema): - use_crl_sign = fields.Boolean() - use_data_encipherment = fields.Boolean() - use_decipher_only = fields.Boolean() - use_encipher_only = fields.Boolean() - use_key_encipherment = fields.Boolean() - use_digital_signature = fields.Boolean() - use_non_repudiation = fields.Boolean() - use_key_agreement = fields.Boolean() - use_key_cert_sign = fields.Boolean() - - -class ExtendedKeyUsageSchema(BaseExtensionSchema): - use_server_authentication = fields.Boolean() - use_client_authentication = fields.Boolean() - use_eap_over_lan = fields.Boolean() - use_eap_over_ppp = fields.Boolean() - use_ocsp_signing = fields.Boolean() - use_smart_card_logon = fields.Boolean() - use_timestamping = fields.Boolean() - use_code_signing = fields.Boolean() - use_email_protection = fields.Boolean() - - class SubjectKeyIdentifierSchema(BaseExtensionSchema): include_ski = fields.Boolean() @@ -215,20 +188,6 @@ class SubjectKeyIdentifierSchema(BaseExtensionSchema): return {'includeSKI': data['include_ski']} -class SubAltNameSchema(BaseExtensionSchema): - name_type = fields.String(validate=validators.sub_alt_type) - value = fields.String() - - @validates_schema - def check_sensitive(self, data): - if data.get('name_type') == 'DNSName': - validators.sensitive_domain(data['value']) - - -class SubAltNamesSchema(BaseExtensionSchema): - names = fields.Nested(SubAltNameSchema, many=True) - - class CustomOIDSchema(BaseExtensionSchema): oid = fields.String() encoding = fields.String(validate=validators.encoding) @@ -237,13 +196,15 @@ class CustomOIDSchema(BaseExtensionSchema): class ExtensionSchema(BaseExtensionSchema): - basic_constraints = fields.Nested(BasicConstraintsSchema) - key_usage = fields.Nested(KeyUsageSchema) - extended_key_usage = fields.Nested(ExtendedKeyUsageSchema) + basic_constraints = BasicConstraintsExtension() + key_usage = KeyUsageExtension() + extended_key_usage = ExtendedKeyUsageExtension() subject_key_identifier = fields.Nested(SubjectKeyIdentifierSchema) - sub_alt_names = fields.Nested(SubAltNamesSchema) + sub_alt_names = SubjectAlternativeNameExtension() authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema) certificate_info_access = fields.Nested(CertificateInfoAccessSchema) + # FIXME: Convert custom OIDs to a custom field in fields.py like other Extensions + # FIXME: Remove support in UI for Critical custom extensions https://github.com/Netflix/lemur/issues/665 custom = fields.List(fields.Nested(CustomOIDSchema)) diff --git a/lemur/static/app/angular/authorities/authority/extensions.tpl.html b/lemur/static/app/angular/authorities/authority/extensions.tpl.html index 140eb33e..d5f65ee4 100644 --- a/lemur/static/app/angular/authorities/authority/extensions.tpl.html +++ b/lemur/static/app/angular/authorities/authority/extensions.tpl.html @@ -4,7 +4,7 @@ Subject Alternate Names