Adding a toy certificate authority. (#378)

This commit is contained in:
kevgliss 2016-06-29 09:05:39 -07:00 committed by GitHub
parent eefff8497a
commit 54b888bb08
10 changed files with 180 additions and 11 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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()

13
lemur/common/missing.py Normal file
View File

@ -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()

View File

@ -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.')

View File

@ -0,0 +1,5 @@
try:
VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version
except Exception as e:
VERSION = 'unknown'

View File

@ -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

View File

@ -0,0 +1 @@
from lemur.tests.conftest import * # noqa

View File

@ -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=[