diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index b1f19519..5659eab9 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -77,22 +77,18 @@ def find_duplicates(cert_body): return Certificate.query.filter_by(body=cert_body).all() -def export(cert_id, export_options): +def export(cert, export_plugin): """ Exports a certificate to the requested format. This format may be a binary format. - :param export_options: - :param cert_id: + + :param export_plugin: + :param cert: :return: """ - cert = get(cert_id) - export_plugin = plugins.get(export_options['slug']) + plugin = plugins.get(export_plugin['slug']) - data = export_plugin.export(cert.body, cert.key) - if export_options.get('encrypted'): - pass - - return data + return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions']) def update(cert_id, owner, description, active, destinations, notifications, replaces): diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index bc01a861..4cba53ae 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -775,10 +775,10 @@ class CertificatesReplacementsList(AuthenticatedResource): return service.get(certificate_id).replaces -class CertficiateExport(AuthenticatedResource): +class CertificateExport(AuthenticatedResource): def __init__(self): self.reqparse = reqparse.RequestParser() - super(CertficiateExport, self).__init__() + super(CertificateExport, self).__init__() def post(self, certificate_id): """ @@ -808,6 +808,7 @@ class CertficiateExport(AuthenticatedResource): :statuscode 200: no error :statuscode 403: unauthenticated """ + self.reqparse.add_argument('export', type=dict, required=True, location='json') args = self.reqparse.parse_args() cert = service.get(certificate_id) @@ -816,7 +817,7 @@ class CertficiateExport(AuthenticatedResource): permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) if permission.can(): - data = service.export(certificate_id) + passphrase, data = service.export(cert, args['export']['plugin']) response = make_response(data) response.headers['content-type'] = 'application/octet-stream' return response @@ -829,5 +830,6 @@ api.add_resource(Certificates, '/certificates/', endpoint='c api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload') api.add_resource(CertificatePrivateKey, '/certificates//key', endpoint='privateKeyCertificates') +api.add_resource(CertificateExport, '/certificates//export', endpoint='exportCertificate') api.add_resource(NotificationCertificatesList, '/notifications//certificates', endpoint='notificationCertificates') api.add_resource(CertificatesReplacementsList, '/certificates//replacements', endpoint='replacements') diff --git a/lemur/plugins/base/v1.py b/lemur/plugins/base/v1.py index ec1e74ed..9b90e7c0 100644 --- a/lemur/plugins/base/v1.py +++ b/lemur/plugins/base/v1.py @@ -108,10 +108,11 @@ class IPlugin(local): """ return self.resource_links - def get_option(self, name, options): + @staticmethod + def get_option(name, options): for o in options: if o.get('name') == name: - return o['value'] + return o.get('value') class Plugin(IPlugin): diff --git a/lemur/plugins/bases/export.py b/lemur/plugins/bases/export.py index 70c40054..2c504615 100644 --- a/lemur/plugins/bases/export.py +++ b/lemur/plugins/bases/export.py @@ -10,6 +10,10 @@ from lemur.plugins.base import Plugin class ExportPlugin(Plugin): + """ + This is the base class from which all supported + exporters will inherit from. + """ type = 'export' def export(self): diff --git a/lemur/plugins/lemur_java/plugin.py b/lemur/plugins/lemur_java/plugin.py index 907f18ae..72aabf61 100644 --- a/lemur/plugins/lemur_java/plugin.py +++ b/lemur/plugins/lemur_java/plugin.py @@ -32,6 +32,26 @@ def run_process(command): raise Exception(stderr) +def split_chain(chain): + """ + Split the chain into individual certificates for import into keystore + + :param chain: + :return: + """ + certs = [] + lines = chain.split('\n') + + cert = [] + for line in lines: + cert.append(line + '\n') + if line == '-----END CERTIFICATE-----': + certs.append("".join(cert)) + cert = [] + + return certs + + class JavaExportPlugin(ExportPlugin): title = 'Java' slug = 'java-export' @@ -41,19 +61,20 @@ class JavaExportPlugin(ExportPlugin): author = 'Kevin Glisson' author_url = 'https://github.com/netflix/lemur' - additional_options = [ + options = [ { 'name': 'type', 'type': 'select', 'required': True, - 'available': ['jks'], + 'available': ['Java Key Store (JKS)'], 'helpMessage': 'Choose the format you wish to export', }, { 'name': 'passphrase', 'type': 'str', 'required': False, - 'helpMessage': 'If no passphrase is generated one will be generated for you.', + 'helpMessage': 'If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8.', + 'validation': '^.{8}$' }, { 'name': 'alias', @@ -63,21 +84,27 @@ class JavaExportPlugin(ExportPlugin): } ] - @staticmethod - def export(body, key, options, **kwargs): + def export(self, body, chain, key, options, **kwargs): """ Generates a Java Keystore or Truststore :param key: - :param kwargs: + :param chain: :param body: :param options: + :param kwargs: """ - if options.get('passphrase'): - passphrase = options['passphrase'] + + if self.get_option('passphrase', options): + passphrase = self.get_option('passphrase', options) else: passphrase = get_psuedo_random_string() + if self.get_option('alias', options): + alias = self.get_option('alias', options) + else: + alias = "blah" + with mktempfile() as cert_tmp: with open(cert_tmp, 'w') as f: f.write(body) @@ -91,7 +118,7 @@ class JavaExportPlugin(ExportPlugin): "openssl", "pkcs12", "-export", - "-name", options.get('alias', 'cert'), + "-name", alias, "-in", cert_tmp, "-inkey", key_tmp, "-out", p12_tmp, @@ -106,23 +133,39 @@ class JavaExportPlugin(ExportPlugin): "-destkeystore", jks_tmp, "-srckeystore", p12_tmp, "-srcstoretype", "PKCS12", - "-alias", options.get('alias', 'cert'), + "-alias", alias, "-srcstorepass", passphrase, "-deststorepass", passphrase ]) - # Import signed cert in to JKS keystore + # Import leaf cert in to JKS keystore run_process([ "keytool", "-importcert", "-file", cert_tmp, "-keystore", jks_tmp, - "-alias", "{0}_cert".format(options.get('alias'), 'cert'), + "-alias", "{0}_cert".format(alias), "-storepass", passphrase, "-noprompt" ]) + # Import the entire chain + for idx, cert in enumerate(split_chain(chain)): + with mktempfile() as c_tmp: + with open(c_tmp, 'w') as f: + f.write(cert) + # Import signed cert in to JKS keystore + run_process([ + "keytool", + "-importcert", + "-file", c_tmp, + "-keystore", jks_tmp, + "-alias", "{0}_cert_{1}".format(alias, idx), + "-storepass", passphrase, + "-noprompt" + ]) + with open(jks_tmp, 'rb') as f: raw = f.read() - return raw + return passphrase, raw diff --git a/lemur/plugins/lemur_java/tests/test_java.py b/lemur/plugins/lemur_java/tests/test_java.py index cf72a289..37aac9a3 100644 --- a/lemur/plugins/lemur_java/tests/test_java.py +++ b/lemur/plugins/lemur_java/tests/test_java.py @@ -59,5 +59,5 @@ def test_export_certificate_to_jks(app): from lemur.plugins.base import plugins p = plugins.get('java-export') options = {'passphrase': 'test1234'} - raw = p.export(EXTERNAL_VALID_STR, PRIVATE_KEY_STR, options) + raw = p.export(EXTERNAL_VALID_STR, "", PRIVATE_KEY_STR, options) assert raw != b"" diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js index 76176a86..f9a63af2 100644 --- a/lemur/static/app/angular/certificates/certificate/certificate.js +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -1,6 +1,39 @@ 'use strict'; angular.module('lemur') + .controller('CertificateExportController', function ($scope, $modalInstance, CertificateApi, CertificateService, PluginService, toaster, editId) { + CertificateApi.get(editId).then(function (certificate) { + $scope.certificate = certificate; + }); + + PluginService.getByType('export').then(function (plugins) { + $scope.plugins = plugins; + }); + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + + $scope.save = function (certificate) { + CertificateService.export(certificate).then( + function () { + toaster.pop({ + type: 'success', + title: certificate.name, + body: 'Successfully exported!' + }); + $modalInstance.close(); + }, + function (response) { + toaster.pop({ + type: 'error', + title: certificate.name, + body: 'Failed to export ' + response.data.message, + timeout: 100000 + }); + }); + }; + }) .controller('CertificateEditController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, NotificationService, toaster, editId) { CertificateApi.get(editId).then(function (certificate) { CertificateService.getNotifications(certificate); diff --git a/lemur/static/app/angular/certificates/services.js b/lemur/static/app/angular/certificates/services.js index 39755866..302d4179 100644 --- a/lemur/static/app/angular/certificates/services.js +++ b/lemur/static/app/angular/certificates/services.js @@ -183,5 +183,9 @@ angular.module('lemur') return certificate.put(); }; + CertificateService.export = function (certificate) { + return certificate.customPOST(certificate.exportOptions, 'export'); + }; + return CertificateService; }); diff --git a/lemur/static/app/angular/certificates/view/view.js b/lemur/static/app/angular/certificates/view/view.js index 392614dd..4a7107c0 100644 --- a/lemur/static/app/angular/certificates/view/view.js +++ b/lemur/static/app/angular/certificates/view/view.js @@ -163,4 +163,18 @@ angular.module('lemur') $scope.certificateTable.reload(); }); }; + + $scope.export = function (certificateId) { + var modalInstance = $modal.open({ + animation: true, + controller: 'CertificateExportController', + templateUrl: '/angular/certificates/certificate/export.tpl.html', + size: 'lg', + resolve: { + editId: function () { + return certificateId; + } + } + }); + }; }); diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html index b8772cf9..d57445fe 100644 --- a/lemur/static/app/angular/certificates/view/view.tpl.html +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -5,12 +5,10 @@
- -
@@ -48,6 +46,9 @@ +
diff --git a/lemur/utils.py b/lemur/utils.py index cb494747..49c57cb3 100644 --- a/lemur/utils.py +++ b/lemur/utils.py @@ -23,7 +23,10 @@ def mktempfile(): try: yield name finally: - os.unlink(name) + try: + os.unlink(name) + except OSError as e: + current_app.logger.debug("No file {0}".format(name)) @contextmanager @@ -32,7 +35,10 @@ def mktemppath(): path = os.path.join(tempfile._get_default_tempdir(), next(tempfile._get_candidate_names())) yield path finally: - os.unlink(path) + try: + os.unlink(path) + except OSError as e: + current_app.logger.debug("No file {0}".format(path)) def get_keys():