From 54b888bb08660a3512998a71dea060bf1942f97f Mon Sep 17 00:00:00 2001 From: kevgliss Date: Wed, 29 Jun 2016 09:05:39 -0700 Subject: [PATCH] Adding a toy certificate authority. (#378) --- lemur/authorities/schemas.py | 8 +- lemur/authorities/service.py | 18 ++- lemur/certificates/schemas.py | 8 +- lemur/common/missing.py | 13 ++ lemur/common/validators.py | 3 - lemur/plugins/lemur_cryptography/__init__.py | 5 + lemur/plugins/lemur_cryptography/plugin.py | 132 ++++++++++++++++++ .../lemur_cryptography/tests/conftest.py | 1 + .../tests/test_cryptography.py | 0 setup.py | 3 +- 10 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 lemur/common/missing.py create mode 100644 lemur/plugins/lemur_cryptography/__init__.py create mode 100644 lemur/plugins/lemur_cryptography/plugin.py create mode 100644 lemur/plugins/lemur_cryptography/tests/conftest.py create mode 100644 lemur/plugins/lemur_cryptography/tests/test_cryptography.py diff --git a/lemur/authorities/schemas.py b/lemur/authorities/schemas.py index cc045504..2a816dd4 100644 --- a/lemur/authorities/schemas.py +++ b/lemur/authorities/schemas.py @@ -7,14 +7,14 @@ """ 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.exceptions import ValidationError from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema from lemur.users.schemas import UserNestedOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema -from lemur.common import validators +from lemur.common import validators, missing class AuthorityInputSchema(LemurInputSchema): @@ -60,6 +60,10 @@ class AuthorityInputSchema(LemurInputSchema): if not data.get('parent'): 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): owner = fields.Email(required=True) diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index d976b4a3..9de7b1f8 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -44,9 +44,17 @@ def mint(**kwargs): Creates the authority based on the plugin provided. """ 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) - return body, chain, roles + return body, private_key, chain, roles def create_authority_roles(roles, owner, plugin_title): @@ -88,9 +96,10 @@ def create(**kwargs): Creates a new authority. """ kwargs['creator'] = g.user.email - body, chain, roles = mint(**kwargs) + body, private_key, chain, roles = mint(**kwargs) kwargs['body'] = body + kwargs['private_key'] = private_key kwargs['chain'] = chain 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 if not g.current_user.is_admin: authority_ids = [] + for authority in g.current_user.authorities: + authority_ids.append(authority.id) + for role in g.current_user.roles: for authority in role.authorities: authority_ids.append(authority.id) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 53d02bdd..dc16e25c 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -6,7 +6,7 @@ .. moduleauthor:: Kevin Glisson """ 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 lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \ @@ -20,7 +20,7 @@ from lemur.domains.schemas import DomainNestedOutputSchema from lemur.users.schemas import UserNestedOutputSchema 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 @@ -68,6 +68,10 @@ class CertificateInputSchema(CertificateSchema): def validate_dates(self, data): validators.dates(data) + @pre_load + def ensure_dates(self, data): + return missing.dates(data) + class CertificateEditInputSchema(CertificateSchema): active = fields.Boolean() diff --git a/lemur/common/missing.py b/lemur/common/missing.py new file mode 100644 index 00000000..89cb91dc --- /dev/null +++ b/lemur/common/missing.py @@ -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() diff --git a/lemur/common/validators.py b/lemur/common/validators.py index 60cc91ed..49bae27c 100644 --- a/lemur/common/validators.py +++ b/lemur/common/validators.py @@ -94,9 +94,6 @@ def dates(data): if not data.get('validity_end') and data.get('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 not data['validity_start'] < data['validity_end']: raise ValidationError('Validity start must be before validity end.') diff --git a/lemur/plugins/lemur_cryptography/__init__.py b/lemur/plugins/lemur_cryptography/__init__.py new file mode 100644 index 00000000..8ce5a7f3 --- /dev/null +++ b/lemur/plugins/lemur_cryptography/__init__.py @@ -0,0 +1,5 @@ +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception as e: + VERSION = 'unknown' diff --git a/lemur/plugins/lemur_cryptography/plugin.py b/lemur/plugins/lemur_cryptography/plugin.py new file mode 100644 index 00000000..38d35fa1 --- /dev/null +++ b/lemur/plugins/lemur_cryptography/plugin.py @@ -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 +""" +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 diff --git a/lemur/plugins/lemur_cryptography/tests/conftest.py b/lemur/plugins/lemur_cryptography/tests/conftest.py new file mode 100644 index 00000000..0e1cd89f --- /dev/null +++ b/lemur/plugins/lemur_cryptography/tests/conftest.py @@ -0,0 +1 @@ +from lemur.tests.conftest import * # noqa diff --git a/lemur/plugins/lemur_cryptography/tests/test_cryptography.py b/lemur/plugins/lemur_cryptography/tests/test_cryptography.py new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index d03c78cb..e876070e 100644 --- a/setup.py +++ b/setup.py @@ -178,7 +178,8 @@ setup( 'java_keystore_export = lemur.plugins.lemur_java.plugin:JavaKeystoreExportPlugin', 'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin', '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=[