X509 extensions issue#646 (#666)
* Allowing that create_csr can be called with an additional flag in the csr_config to adjust the BasicConstraints for a CA. * If there are no SANs, skip adding a blank list of SANs. * Adding handling for all the extended key usage, key usage, and subject key identifier extensions. * Fixing lint checks. I was overly verbose. * This implements marshalling of the certificate extensions into x509 ExtensionType objects in the schema validation code. * Will create x509 ExtensionType objects in the schema validation stage * Allows errors parsing incoming options to bubble up to the requestor as ValidationErrors. * Cleans up create_csr a lot in the certificates/service.py * Makes BasicConstraints _just another extension_, rather than a hard-coded one * Adds BasicConstraints option for path_length to the UI for creating an authority * Removes SAN types which cannot be handled from the UI for authorities and certificates. * Fixes Certificate() object model so that it doesn't just hard-code only SAN records in the extensions property and actually returns the extensions how you expect to see them. Since Lemur is focused on using these data in the "CSR" phase of things, extensions that don't get populated until signing will be in dict() form.* Trying out schema validation of extensions
This commit is contained in:
parent
4af871f408
commit
f13a3505f3
|
@ -9,6 +9,7 @@ import arrow
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
@ -229,16 +230,39 @@ class Certificate(db.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extensions(self):
|
def extensions(self):
|
||||||
# TODO pull the OU, O, CN, etc + other extensions.
|
return_extensions = {}
|
||||||
names = [{'name_type': 'DNSName', 'value': x.name} for x in self.domains]
|
cert = lemur.common.utils.parse_certificate(self.body)
|
||||||
|
for extension in cert.extensions:
|
||||||
extensions = {
|
if isinstance(extension, x509.BasicConstraints):
|
||||||
'sub_alt_names': {
|
return_extensions['basic_constraints'] = extension
|
||||||
'names': names
|
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
|
||||||
|
|
||||||
return extensions
|
return return_extensions
|
||||||
|
|
||||||
def get_arn(self, account_number):
|
def get_arn(self, account_number):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -345,66 +345,24 @@ def create_csr(**csr_config):
|
||||||
x509.NameAttribute(x509.OID_EMAIL_ADDRESS, csr_config['owner'])
|
x509.NameAttribute(x509.OID_EMAIL_ADDRESS, csr_config['owner'])
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
ski = extensions.get('subject_key_identifier', {})
|
||||||
|
if ski.get('include_ski', False):
|
||||||
builder = builder.add_extension(
|
builder = builder.add_extension(
|
||||||
x509.BasicConstraints(ca=False, path_length=None), critical=True,
|
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||||
|
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)
|
|
||||||
# )
|
|
||||||
|
|
||||||
request = builder.sign(
|
request = builder.sign(
|
||||||
private_key, hashes.SHA256(), default_backend()
|
private_key, hashes.SHA256(), default_backend()
|
||||||
)
|
)
|
||||||
|
@ -500,14 +458,6 @@ def get_certificate_primitives(certificate):
|
||||||
certificate via `create`.
|
certificate via `create`.
|
||||||
"""
|
"""
|
||||||
start, end = calculate_reissue_range(certificate.not_before, certificate.not_after)
|
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(
|
return dict(
|
||||||
authority=certificate.authority,
|
authority=certificate.authority,
|
||||||
|
@ -517,7 +467,7 @@ def get_certificate_primitives(certificate):
|
||||||
validity_end=end,
|
validity_end=end,
|
||||||
destinations=certificate.destinations,
|
destinations=certificate.destinations,
|
||||||
roles=certificate.roles,
|
roles=certificate.roles,
|
||||||
extensions=extensions,
|
extensions=certificate.extensions,
|
||||||
owner=certificate.owner,
|
owner=certificate.owner,
|
||||||
organization=certificate.organization,
|
organization=certificate.organization,
|
||||||
organizational_unit=certificate.organizational_unit,
|
organizational_unit=certificate.organizational_unit,
|
||||||
|
|
|
@ -3,6 +3,9 @@ import warnings
|
||||||
from datetime import datetime as dt
|
from datetime import datetime as dt
|
||||||
from marshmallow.fields import Field
|
from marshmallow.fields import Field
|
||||||
from marshmallow import utils
|
from marshmallow import utils
|
||||||
|
from cryptography import x509
|
||||||
|
from marshmallow.exceptions import ValidationError
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
|
||||||
class ArrowDateTime(Field):
|
class ArrowDateTime(Field):
|
||||||
|
@ -91,3 +94,240 @@ class ArrowDateTime(Field):
|
||||||
warnings.warn('It is recommended that you install python-dateutil '
|
warnings.warn('It is recommended that you install python-dateutil '
|
||||||
'for improved datetime deserialization.')
|
'for improved datetime deserialization.')
|
||||||
raise self.fail('invalid')
|
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
|
||||||
|
|
|
@ -9,11 +9,12 @@
|
||||||
"""
|
"""
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
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 marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.common import validators
|
from lemur.common import validators
|
||||||
from lemur.common.schema import LemurSchema, LemurInputSchema, LemurOutputSchema
|
from lemur.common.schema import LemurSchema, LemurInputSchema, LemurOutputSchema
|
||||||
|
from lemur.common.fields import KeyUsageExtension, ExtendedKeyUsageExtension, BasicConstraintsExtension, SubjectAlternativeNameExtension
|
||||||
|
|
||||||
from lemur.plugins import plugins
|
from lemur.plugins import plugins
|
||||||
from lemur.roles.models import Role
|
from lemur.roles.models import Role
|
||||||
|
@ -166,10 +167,6 @@ class BaseExtensionSchema(LemurSchema):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class BasicConstraintsSchema(BaseExtensionSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorityKeyIdentifierSchema(BaseExtensionSchema):
|
class AuthorityKeyIdentifierSchema(BaseExtensionSchema):
|
||||||
use_key_identifier = fields.Boolean()
|
use_key_identifier = fields.Boolean()
|
||||||
use_authority_cert = fields.Boolean()
|
use_authority_cert = fields.Boolean()
|
||||||
|
@ -183,30 +180,6 @@ class CertificateInfoAccessSchema(BaseExtensionSchema):
|
||||||
return {'includeAIA': data['include_aia']}
|
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):
|
class SubjectKeyIdentifierSchema(BaseExtensionSchema):
|
||||||
include_ski = fields.Boolean()
|
include_ski = fields.Boolean()
|
||||||
|
|
||||||
|
@ -215,20 +188,6 @@ class SubjectKeyIdentifierSchema(BaseExtensionSchema):
|
||||||
return {'includeSKI': data['include_ski']}
|
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):
|
class CustomOIDSchema(BaseExtensionSchema):
|
||||||
oid = fields.String()
|
oid = fields.String()
|
||||||
encoding = fields.String(validate=validators.encoding)
|
encoding = fields.String(validate=validators.encoding)
|
||||||
|
@ -237,13 +196,15 @@ class CustomOIDSchema(BaseExtensionSchema):
|
||||||
|
|
||||||
|
|
||||||
class ExtensionSchema(BaseExtensionSchema):
|
class ExtensionSchema(BaseExtensionSchema):
|
||||||
basic_constraints = fields.Nested(BasicConstraintsSchema)
|
basic_constraints = BasicConstraintsExtension()
|
||||||
key_usage = fields.Nested(KeyUsageSchema)
|
key_usage = KeyUsageExtension()
|
||||||
extended_key_usage = fields.Nested(ExtendedKeyUsageSchema)
|
extended_key_usage = ExtendedKeyUsageExtension()
|
||||||
subject_key_identifier = fields.Nested(SubjectKeyIdentifierSchema)
|
subject_key_identifier = fields.Nested(SubjectKeyIdentifierSchema)
|
||||||
sub_alt_names = fields.Nested(SubAltNamesSchema)
|
sub_alt_names = SubjectAlternativeNameExtension()
|
||||||
authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
|
authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
|
||||||
certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
|
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))
|
custom = fields.List(fields.Nested(CustomOIDSchema))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
Subject Alternate Names
|
Subject Alternate Names
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<select class="form-control" ng-model="authority.subAltType" ng-init="null" ng-options="item for item in ['DNSName', 'IPAddress', 'uniformResourceIdentifier', 'directoryName','rfc822Name', 'registeredID', 'otherName', 'x400Address', 'EDIPartyName']"></select>
|
<select class="form-control" ng-model="authority.subAltType" ng-init="null" ng-options="item for item in ['DNSName', 'IPAddress', 'IPNetwork', 'uniformResourceIdentifier', 'directoryName','rfc822Name', 'registeredID']"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
@ -185,6 +185,16 @@
|
||||||
<select class="form-control" ng-model="authority.extensions.cRLDistributionPoints.includeCRLDP" ng-options="item for item in ['yes', 'no', 'default']"></select>
|
<select class="form-control" ng-model="authority.extensions.cRLDistributionPoints.includeCRLDP" ng-options="item for item in ['yes', 'no', 'default']"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Basic Constraints
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<label tooltip-trigger="mouseenter" tooltip-placement="top" uib-tooltip="Maximum number of non-self-issued intermediate certificates that may follow this certificate in a valid certification path." >
|
||||||
|
Path Length: <select class="form-control" ng-model="authority.extensions.basicConstraints.path_length" ng-options="item for item in ['None', 0, 1, 2, 3, 4, 5]"></select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-sm-2">
|
<label class="control-label col-sm-2">
|
||||||
Custom
|
Custom
|
||||||
|
|
|
@ -94,6 +94,15 @@ angular.module('lemur')
|
||||||
AuthorityService.create = function (authority) {
|
AuthorityService.create = function (authority) {
|
||||||
authority.attachSubAltName();
|
authority.attachSubAltName();
|
||||||
authority.attachCustom();
|
authority.attachCustom();
|
||||||
|
|
||||||
|
if (authority.extensions.basicConstraints === undefined) {
|
||||||
|
authority.extensions.basicConstraints = { 'path_length': null};
|
||||||
|
}
|
||||||
|
authority.extensions.basicConstraints.ca = true;
|
||||||
|
if (authority.extensions.basicConstraints.path_length === 'None') {
|
||||||
|
authority.extensions.basicConstraints.path_length = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (authority.validityYears === '') { // if a user de-selects validity years we ignore it
|
if (authority.validityYears === '') { // if a user de-selects validity years we ignore it
|
||||||
delete authority.validityYears;
|
delete authority.validityYears;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<select class="form-control" ng-model="certificate.subAltType"
|
<select class="form-control" ng-model="certificate.subAltType"
|
||||||
ng-options="item for item in ['DNSName', 'IPAddress', 'uniformResourceIdentifier', 'directoryName','rfc822Name', 'registeredID', 'otherName', 'x400Address', 'EDIPartyName']"></select>
|
ng-options="item for item in ['DNSName', 'IPAddress', 'IPNetwork', 'uniformResourceIdentifier', 'directoryName','rfc822Name', 'registeredID']"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|
|
@ -6,6 +6,7 @@ import datetime
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
from cryptography import x509
|
||||||
|
|
||||||
from lemur.certificates.views import * # noqa
|
from lemur.certificates.views import * # noqa
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ def test_get_or_increase_name(session, certificate):
|
||||||
def test_get_certificate_primitives(certificate):
|
def test_get_certificate_primitives(certificate):
|
||||||
from lemur.certificates.service import get_certificate_primitives
|
from lemur.certificates.service import get_certificate_primitives
|
||||||
|
|
||||||
names = [{'name_type': 'DNSName', 'value': x.name} for x in certificate.domains]
|
names = [x509.DNSName(x.name) for x in certificate.domains]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'common_name': certificate.cn,
|
'common_name': certificate.cn,
|
||||||
|
@ -37,9 +38,7 @@ def test_get_certificate_primitives(certificate):
|
||||||
'authority': certificate.authority,
|
'authority': certificate.authority,
|
||||||
'description': certificate.description,
|
'description': certificate.description,
|
||||||
'extensions': {
|
'extensions': {
|
||||||
'sub_alt_names': {
|
'sub_alt_names': x509.SubjectAlternativeName(names)
|
||||||
'names': names
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'destinations': [],
|
'destinations': [],
|
||||||
'roles': [],
|
'roles': [],
|
||||||
|
@ -171,10 +170,10 @@ def test_certificate_input_with_extensions(client, authority):
|
||||||
'description': 'testtestest',
|
'description': 'testtestest',
|
||||||
'extensions': {
|
'extensions': {
|
||||||
'keyUsage': {
|
'keyUsage': {
|
||||||
'useKeyEncipherment': True,
|
'digital_signature': True
|
||||||
'useDigitalSignature': True
|
|
||||||
},
|
},
|
||||||
'extendedKeyUsage': {
|
'extendedKeyUsage': {
|
||||||
|
'useClientAuthentication': True,
|
||||||
'useServerAuthentication': True
|
'useServerAuthentication': True
|
||||||
},
|
},
|
||||||
'subjectKeyIdentifier': {
|
'subjectKeyIdentifier': {
|
||||||
|
@ -245,90 +244,6 @@ def test_certificate_valid_dates(client, authority):
|
||||||
assert not errors
|
assert not errors
|
||||||
|
|
||||||
|
|
||||||
def test_sub_alt_name_schema(session):
|
|
||||||
from lemur.schemas import SubAltNameSchema # SubAltNamesSchema
|
|
||||||
input_data = {'nameType': 'DNSName', 'value': 'test.example.com'}
|
|
||||||
|
|
||||||
data, errors = SubAltNameSchema().load(input_data)
|
|
||||||
assert not errors
|
|
||||||
assert data == {'name_type': 'DNSName', 'value': 'test.example.com'}
|
|
||||||
|
|
||||||
data, errors = SubAltNameSchema().dumps(data)
|
|
||||||
assert not errors
|
|
||||||
|
|
||||||
input_datas = {'names': [input_data]}
|
|
||||||
|
|
||||||
# data, errors = SubAltNamesSchema().load(input_datas)
|
|
||||||
# assert not errors
|
|
||||||
# assert data == {'names': [{'name_type': 'DNSName', 'value': 'test.example.com'}]}
|
|
||||||
|
|
||||||
# data, errors = SubAltNamesSchema().dumps(data)
|
|
||||||
# assert data == json.dumps(input_datas)
|
|
||||||
# assert not errors
|
|
||||||
|
|
||||||
input_data = {'nameType': 'CNAME', 'value': 'test.example.com'}
|
|
||||||
data, errors = SubAltNameSchema().load(input_data)
|
|
||||||
assert errors
|
|
||||||
|
|
||||||
|
|
||||||
def test_key_usage_schema():
|
|
||||||
from lemur.schemas import KeyUsageSchema
|
|
||||||
|
|
||||||
input_data = {
|
|
||||||
'useCRLSign': True,
|
|
||||||
'useDataEncipherment': True,
|
|
||||||
'useDecipherOnly': True,
|
|
||||||
'useEncipherOnly': True,
|
|
||||||
'useKeyEncipherment': True,
|
|
||||||
'useDigitalSignature': True,
|
|
||||||
'useNonRepudiation': True
|
|
||||||
}
|
|
||||||
|
|
||||||
data, errors = KeyUsageSchema().load(input_data)
|
|
||||||
|
|
||||||
assert not errors
|
|
||||||
assert data == {
|
|
||||||
'use_crl_sign': True,
|
|
||||||
'use_data_encipherment': True,
|
|
||||||
'use_decipher_only': True,
|
|
||||||
'use_encipher_only': True,
|
|
||||||
'use_key_encipherment': True,
|
|
||||||
'use_digital_signature': True,
|
|
||||||
'use_non_repudiation': True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_extended_key_usage_schema():
|
|
||||||
from lemur.schemas import ExtendedKeyUsageSchema
|
|
||||||
|
|
||||||
input_data = {
|
|
||||||
'useServerAuthentication': True,
|
|
||||||
'useClientAuthentication': True,
|
|
||||||
'useEapOverLAN': True,
|
|
||||||
'useEapOverPPP': True,
|
|
||||||
'useOCSPSigning': True,
|
|
||||||
'useSmartCardLogon': True,
|
|
||||||
'useTimestamping': True,
|
|
||||||
'useCodeSigning': True,
|
|
||||||
'useEmailProtection': True
|
|
||||||
}
|
|
||||||
|
|
||||||
data, errors = ExtendedKeyUsageSchema().load(input_data)
|
|
||||||
|
|
||||||
assert not errors
|
|
||||||
assert data == {
|
|
||||||
'use_server_authentication': True,
|
|
||||||
'use_client_authentication': True,
|
|
||||||
'use_eap_over_lan': True,
|
|
||||||
'use_eap_over_ppp': True,
|
|
||||||
'use_ocsp_signing': True,
|
|
||||||
'use_smart_card_logon': True,
|
|
||||||
'use_timestamping': True,
|
|
||||||
'use_code_signing': True,
|
|
||||||
'use_email_protection': True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_basic_csr(client):
|
def test_create_basic_csr(client):
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
@ -342,7 +257,7 @@ def test_create_basic_csr(client):
|
||||||
location='A place',
|
location='A place',
|
||||||
owner='joe@example.com',
|
owner='joe@example.com',
|
||||||
key_type='RSA2048',
|
key_type='RSA2048',
|
||||||
extensions=dict(names=dict(sub_alt_names=['test.example.com', 'test2.example.com']))
|
extensions=dict(names=dict(sub_alt_names=x509.SubjectAlternativeName([x509.DNSName('test.example.com'), x509.DNSName('test2.example.com')])))
|
||||||
)
|
)
|
||||||
csr, pem = create_csr(**csr_config)
|
csr, pem = create_csr(**csr_config)
|
||||||
|
|
||||||
|
@ -395,7 +310,7 @@ def test_create_csr():
|
||||||
assert csr
|
assert csr
|
||||||
assert private_key
|
assert private_key
|
||||||
|
|
||||||
extensions = {'sub_alt_names': {'names': [{'name_type': 'DNSName', 'value': 'AnotherCommonName'}]}}
|
extensions = {'sub_alt_names': x509.SubjectAlternativeName([x509.DNSName('AnotherCommonName')])}
|
||||||
csr, private_key = create_csr(owner='joe@example.com', common_name='ACommonName', organization='test', organizational_unit='Meters', country='US',
|
csr, private_key = create_csr(owner='joe@example.com', common_name='ACommonName', organization='test', organizational_unit='Meters', country='US',
|
||||||
state='CA', location='Here', extensions=extensions, key_type='RSA2048')
|
state='CA', location='Here', extensions=extensions, key_type='RSA2048')
|
||||||
assert csr
|
assert csr
|
||||||
|
|
Loading…
Reference in New Issue