Adding a toy certificate authority. (#378)
This commit is contained in:
parent
eefff8497a
commit
54b888bb08
|
@ -7,14 +7,14 @@
|
||||||
"""
|
"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from marshmallow import fields, validates_schema
|
from marshmallow import fields, validates_schema, pre_load
|
||||||
from marshmallow import validate
|
from marshmallow import validate
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema
|
from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema
|
||||||
from lemur.users.schemas import UserNestedOutputSchema
|
from lemur.users.schemas import UserNestedOutputSchema
|
||||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||||
from lemur.common import validators
|
from lemur.common import validators, missing
|
||||||
|
|
||||||
|
|
||||||
class AuthorityInputSchema(LemurInputSchema):
|
class AuthorityInputSchema(LemurInputSchema):
|
||||||
|
@ -60,6 +60,10 @@ class AuthorityInputSchema(LemurInputSchema):
|
||||||
if not data.get('parent'):
|
if not data.get('parent'):
|
||||||
raise ValidationError("If generating a subca parent 'authority' must be specified.")
|
raise ValidationError("If generating a subca parent 'authority' must be specified.")
|
||||||
|
|
||||||
|
@pre_load
|
||||||
|
def ensure_dates(self, data):
|
||||||
|
return missing.dates(data)
|
||||||
|
|
||||||
|
|
||||||
class AuthorityUpdateSchema(LemurInputSchema):
|
class AuthorityUpdateSchema(LemurInputSchema):
|
||||||
owner = fields.Email(required=True)
|
owner = fields.Email(required=True)
|
||||||
|
|
|
@ -44,9 +44,17 @@ def mint(**kwargs):
|
||||||
Creates the authority based on the plugin provided.
|
Creates the authority based on the plugin provided.
|
||||||
"""
|
"""
|
||||||
issuer = kwargs['plugin']['plugin_object']
|
issuer = kwargs['plugin']['plugin_object']
|
||||||
body, chain, roles = issuer.create_authority(kwargs)
|
values = issuer.create_authority(kwargs)
|
||||||
|
|
||||||
|
# support older plugins
|
||||||
|
if len(values) == 3:
|
||||||
|
body, chain, roles = values
|
||||||
|
private_key = None
|
||||||
|
elif len(values) == 4:
|
||||||
|
body, private_key, chain, roles = values
|
||||||
|
|
||||||
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title)
|
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title)
|
||||||
return body, chain, roles
|
return body, private_key, chain, roles
|
||||||
|
|
||||||
|
|
||||||
def create_authority_roles(roles, owner, plugin_title):
|
def create_authority_roles(roles, owner, plugin_title):
|
||||||
|
@ -88,9 +96,10 @@ def create(**kwargs):
|
||||||
Creates a new authority.
|
Creates a new authority.
|
||||||
"""
|
"""
|
||||||
kwargs['creator'] = g.user.email
|
kwargs['creator'] = g.user.email
|
||||||
body, chain, roles = mint(**kwargs)
|
body, private_key, chain, roles = mint(**kwargs)
|
||||||
|
|
||||||
kwargs['body'] = body
|
kwargs['body'] = body
|
||||||
|
kwargs['private_key'] = private_key
|
||||||
kwargs['chain'] = chain
|
kwargs['chain'] = chain
|
||||||
|
|
||||||
if kwargs.get('roles'):
|
if kwargs.get('roles'):
|
||||||
|
@ -172,6 +181,9 @@ def render(args):
|
||||||
# we make sure that a user can only use an authority they either own are are a member of - admins can see all
|
# we make sure that a user can only use an authority they either own are are a member of - admins can see all
|
||||||
if not g.current_user.is_admin:
|
if not g.current_user.is_admin:
|
||||||
authority_ids = []
|
authority_ids = []
|
||||||
|
for authority in g.current_user.authorities:
|
||||||
|
authority_ids.append(authority.id)
|
||||||
|
|
||||||
for role in g.current_user.roles:
|
for role in g.current_user.roles:
|
||||||
for authority in role.authorities:
|
for authority in role.authorities:
|
||||||
authority_ids.append(authority.id)
|
authority_ids.append(authority.id)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from marshmallow import fields, validates_schema, post_load
|
from marshmallow import fields, validates_schema, post_load, pre_load
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
||||||
|
@ -20,7 +20,7 @@ from lemur.domains.schemas import DomainNestedOutputSchema
|
||||||
from lemur.users.schemas import UserNestedOutputSchema
|
from lemur.users.schemas import UserNestedOutputSchema
|
||||||
|
|
||||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||||
from lemur.common import validators
|
from lemur.common import validators, missing
|
||||||
from lemur.notifications import service as notification_service
|
from lemur.notifications import service as notification_service
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +68,10 @@ class CertificateInputSchema(CertificateSchema):
|
||||||
def validate_dates(self, data):
|
def validate_dates(self, data):
|
||||||
validators.dates(data)
|
validators.dates(data)
|
||||||
|
|
||||||
|
@pre_load
|
||||||
|
def ensure_dates(self, data):
|
||||||
|
return missing.dates(data)
|
||||||
|
|
||||||
|
|
||||||
class CertificateEditInputSchema(CertificateSchema):
|
class CertificateEditInputSchema(CertificateSchema):
|
||||||
active = fields.Boolean()
|
active = fields.Boolean()
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
|
||||||
|
def dates(data):
|
||||||
|
# ensure that validity_start and validity_end are always set
|
||||||
|
if not(data.get('validity_start') and data.get('validity_end')):
|
||||||
|
if data.get('validity_years'):
|
||||||
|
num_years = data['validity_years']
|
||||||
|
now = arrow.utcnow()
|
||||||
|
then = now.replace(years=+int(num_years))
|
||||||
|
|
||||||
|
data['validity_start'] = now.isoformat()
|
||||||
|
data['validity_end'] = then.isoformat()
|
|
@ -94,9 +94,6 @@ def dates(data):
|
||||||
if not data.get('validity_end') and data.get('validity_start'):
|
if not data.get('validity_end') and data.get('validity_start'):
|
||||||
raise ValidationError('If validity end is specified so must validity start.')
|
raise ValidationError('If validity end is specified so must validity start.')
|
||||||
|
|
||||||
if data.get('validity_end') and data.get('validity_years'):
|
|
||||||
raise ValidationError('Cannot specify both validity end and validity years.')
|
|
||||||
|
|
||||||
if data.get('validity_start') and data.get('validity_end'):
|
if data.get('validity_start') and data.get('validity_end'):
|
||||||
if not data['validity_start'] < data['validity_end']:
|
if not data['validity_start'] < data['validity_end']:
|
||||||
raise ValidationError('Validity start must be before validity end.')
|
raise ValidationError('Validity start must be before validity end.')
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
try:
|
||||||
|
VERSION = __import__('pkg_resources') \
|
||||||
|
.get_distribution(__name__).version
|
||||||
|
except Exception as e:
|
||||||
|
VERSION = 'unknown'
|
|
@ -0,0 +1,132 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.plugins.lemur_cryptography.plugin
|
||||||
|
:platform: Unix
|
||||||
|
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
|
|
||||||
|
from lemur.plugins.bases import IssuerPlugin
|
||||||
|
from lemur.plugins import lemur_cryptography as cryptography_issuer
|
||||||
|
|
||||||
|
|
||||||
|
def build_root_certificate(options):
|
||||||
|
private_key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=2048,
|
||||||
|
backend=default_backend()
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = issuer = x509.Name([
|
||||||
|
x509.NameAttribute(x509.OID_COUNTRY_NAME, options['country']),
|
||||||
|
x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, options['state']),
|
||||||
|
x509.NameAttribute(x509.OID_LOCALITY_NAME, options['location']),
|
||||||
|
x509.NameAttribute(x509.OID_ORGANIZATION_NAME, options['organization']),
|
||||||
|
x509.NameAttribute(x509.OID_COMMON_NAME, options['organizational_unit'])
|
||||||
|
])
|
||||||
|
|
||||||
|
builder = x509.CertificateBuilder(
|
||||||
|
subject_name=subject,
|
||||||
|
issuer_name=issuer,
|
||||||
|
public_key=private_key.public_key(),
|
||||||
|
not_valid_after=options['validity_end'],
|
||||||
|
not_valid_before=options['validity_start'],
|
||||||
|
serial_number=options['first_serial']
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.add_extension(x509.SubjectAlternativeName([x509.DNSName(options['common_name'])]), critical=False)
|
||||||
|
|
||||||
|
cert = builder.sign(private_key, hashes.SHA256(), default_backend())
|
||||||
|
|
||||||
|
cert_pem = cert.public_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM
|
||||||
|
)
|
||||||
|
|
||||||
|
private_key_pem = private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it
|
||||||
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
|
)
|
||||||
|
|
||||||
|
return cert_pem, private_key_pem
|
||||||
|
|
||||||
|
|
||||||
|
def issue_certificate(csr, options):
|
||||||
|
csr = x509.load_pem_x509_csr(csr, default_backend())
|
||||||
|
|
||||||
|
builder = x509.CertificateBuilder(
|
||||||
|
issuer_name=x509.Name([
|
||||||
|
x509.NameAttribute(
|
||||||
|
x509.OID_ISSUER_ALTERNATIVE_NAME,
|
||||||
|
options['authority'].authority_certificate.issuer
|
||||||
|
)]
|
||||||
|
),
|
||||||
|
subject_name=csr.subject,
|
||||||
|
public_key=csr.public_key(),
|
||||||
|
not_valid_before=options['validity_start'],
|
||||||
|
not_valid_after=options['validity_end'],
|
||||||
|
extensions=csr.extensions)
|
||||||
|
|
||||||
|
# TODO figure out a better way to increment serial
|
||||||
|
builder = builder.serial_number(int(uuid.uuid4()))
|
||||||
|
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
options['authority'].authority_certificate.private_key,
|
||||||
|
password=None,
|
||||||
|
backend=default_backend()
|
||||||
|
)
|
||||||
|
|
||||||
|
cert = builder.sign(private_key, hashes.SHA256(), default_backend())
|
||||||
|
|
||||||
|
return cert.public_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CryptographyIssuerPlugin(IssuerPlugin):
|
||||||
|
title = 'Cryptography'
|
||||||
|
slug = 'cryptography-issuer'
|
||||||
|
description = 'Enables the creation and signing of self-signed certificates'
|
||||||
|
version = cryptography_issuer.VERSION
|
||||||
|
|
||||||
|
author = 'Kevin Glisson'
|
||||||
|
author_url = 'https://github.com/netflix/lemur.git'
|
||||||
|
|
||||||
|
def create_certificate(self, csr, options):
|
||||||
|
"""
|
||||||
|
Creates a certificate.
|
||||||
|
|
||||||
|
:param csr:
|
||||||
|
:param options:
|
||||||
|
:return: :raise Exception:
|
||||||
|
"""
|
||||||
|
current_app.logger.debug("Issuing new cryptography certificate with options: {0}".format(options))
|
||||||
|
cert = issue_certificate(csr, options)
|
||||||
|
return cert, ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_authority(options):
|
||||||
|
"""
|
||||||
|
Creates an authority, this authority is then used by Lemur to allow a user
|
||||||
|
to specify which Certificate Authority they want to sign their certificate.
|
||||||
|
|
||||||
|
:param options:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
current_app.logger.debug("Issuing new cryptography authority with options: {0}".format(options))
|
||||||
|
cert, private_key = build_root_certificate(options)
|
||||||
|
roles = [
|
||||||
|
{'username': '', 'password': '', 'name': options['name'] + '_admin'},
|
||||||
|
{'username': '', 'password': '', 'name': options['name'] + '_operator'}
|
||||||
|
]
|
||||||
|
return cert, private_key, "", roles
|
|
@ -0,0 +1 @@
|
||||||
|
from lemur.tests.conftest import * # noqa
|
3
setup.py
3
setup.py
|
@ -178,7 +178,8 @@ setup(
|
||||||
'java_keystore_export = lemur.plugins.lemur_java.plugin:JavaKeystoreExportPlugin',
|
'java_keystore_export = lemur.plugins.lemur_java.plugin:JavaKeystoreExportPlugin',
|
||||||
'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin',
|
'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin',
|
||||||
'atlas_metric = lemur.plugins.lemur_atlas.plugin:AtlasMetricPlugin',
|
'atlas_metric = lemur.plugins.lemur_atlas.plugin:AtlasMetricPlugin',
|
||||||
'kubernetes_destination = lemur.plugins.lemur_kubernetes.plugin:KubernetesDestinationPlugin'
|
'kubernetes_destination = lemur.plugins.lemur_kubernetes.plugin:KubernetesDestinationPlugin',
|
||||||
|
'cryptography_issuer = lemur.plugins.lemur_cryptography.plugin:CryptographyIssuerPlugin',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
Loading…
Reference in New Issue