From 920d595c1206ae5bc5a6e783052df34b3300c1ae Mon Sep 17 00:00:00 2001 From: kevgliss Date: Wed, 25 Nov 2015 14:54:08 -0800 Subject: [PATCH 1/5] Initial work on #125 --- lemur/certificates/service.py | 18 +++ lemur/certificates/verify.py | 16 +-- lemur/certificates/views.py | 49 ++++++++ lemur/plugins/bases/__init__.py | 1 + lemur/plugins/bases/export.py | 16 +++ lemur/plugins/lemur_java/__init__.py | 5 + lemur/plugins/lemur_java/plugin.py | 128 ++++++++++++++++++++ lemur/plugins/lemur_java/tests/conftest.py | 1 + lemur/plugins/lemur_java/tests/test_java.py | 63 ++++++++++ lemur/tests/conf.py | 2 +- lemur/utils.py | 26 +++- setup.py | 1 + 12 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 lemur/plugins/bases/export.py create mode 100644 lemur/plugins/lemur_java/__init__.py create mode 100644 lemur/plugins/lemur_java/plugin.py create mode 100644 lemur/plugins/lemur_java/tests/conftest.py create mode 100644 lemur/plugins/lemur_java/tests/test_java.py diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 56367521..b1f19519 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -77,6 +77,24 @@ def find_duplicates(cert_body): return Certificate.query.filter_by(body=cert_body).all() +def export(cert_id, export_options): + """ + Exports a certificate to the requested format. This format + may be a binary format. + :param export_options: + :param cert_id: + :return: + """ + cert = get(cert_id) + export_plugin = plugins.get(export_options['slug']) + + data = export_plugin.export(cert.body, cert.key) + if export_options.get('encrypted'): + pass + + return data + + def update(cert_id, owner, description, active, destinations, notifications, replaces): """ Updates a certificate diff --git a/lemur/certificates/verify.py b/lemur/certificates/verify.py index 79afdf50..46402441 100644 --- a/lemur/certificates/verify.py +++ b/lemur/certificates/verify.py @@ -5,7 +5,6 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -import os import requests import subprocess from OpenSSL import crypto @@ -13,20 +12,7 @@ from cryptography import x509 from cryptography.hazmat.backends import default_backend from flask import current_app - -from contextlib import contextmanager -from tempfile import NamedTemporaryFile - - -@contextmanager -def mktempfile(): - with NamedTemporaryFile(delete=False) as f: - name = f.name - - try: - yield name - finally: - os.unlink(name) +from lemur.utils import mktempfile def ocsp_verify(cert_path, issuer_chain_path): diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 08f81853..bc01a861 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -775,6 +775,55 @@ class CertificatesReplacementsList(AuthenticatedResource): return service.get(certificate_id).replaces +class CertficiateExport(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertficiateExport, self).__init__() + + def post(self, certificate_id): + """ + .. http:post:: /certificates/1/export + + Export a certificate + + **Example request**: + + .. sourcecode:: http + + PUT /certificates/1/export HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + args = self.reqparse.parse_args() + + cert = service.get(certificate_id) + role = role_service.get_by_name(cert.owner) + + permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) + + if permission.can(): + data = service.export(certificate_id) + response = make_response(data) + response.headers['content-type'] = 'application/octet-stream' + return response + + return dict(message='You are not authorized to export this certificate'), 403 + + api.add_resource(CertificatesList, '/certificates', endpoint='certificates') api.add_resource(Certificates, '/certificates/', endpoint='certificate') api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') diff --git a/lemur/plugins/bases/__init__.py b/lemur/plugins/bases/__init__.py index a47b5c54..96833e7b 100644 --- a/lemur/plugins/bases/__init__.py +++ b/lemur/plugins/bases/__init__.py @@ -2,3 +2,4 @@ from .destination import DestinationPlugin # noqa from .issuer import IssuerPlugin # noqa from .source import SourcePlugin # noqa from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa +from .export import ExportPlugin # noqa diff --git a/lemur/plugins/bases/export.py b/lemur/plugins/bases/export.py new file mode 100644 index 00000000..70c40054 --- /dev/null +++ b/lemur/plugins/bases/export.py @@ -0,0 +1,16 @@ +""" +.. module: lemur.bases.export + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from lemur.plugins.base import Plugin + + +class ExportPlugin(Plugin): + type = 'export' + + def export(self): + raise NotImplemented diff --git a/lemur/plugins/lemur_java/__init__.py b/lemur/plugins/lemur_java/__init__.py new file mode 100644 index 00000000..8ce5a7f3 --- /dev/null +++ b/lemur/plugins/lemur_java/__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_java/plugin.py b/lemur/plugins/lemur_java/plugin.py new file mode 100644 index 00000000..907f18ae --- /dev/null +++ b/lemur/plugins/lemur_java/plugin.py @@ -0,0 +1,128 @@ +""" +.. module: lemur.plugins.lemur_java.plugin + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +import subprocess + +from flask import current_app + +from lemur.utils import mktempfile, mktemppath +from lemur.plugins.bases import ExportPlugin +from lemur.plugins import lemur_java as java +from lemur.common.utils import get_psuedo_random_string + + +def run_process(command): + """ + Runs a given command with pOpen and wraps some + error handling around it. + :param command: + :return: + """ + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + + if p.returncode != 0: + current_app.logger.debug(" ".join(command)) + current_app.logger.error(stderr) + raise Exception(stderr) + + +class JavaExportPlugin(ExportPlugin): + title = 'Java' + slug = 'java-export' + description = 'Attempts to generate a JKS keystore or truststore' + version = java.VERSION + + author = 'Kevin Glisson' + author_url = 'https://github.com/netflix/lemur' + + additional_options = [ + { + 'name': 'type', + 'type': 'select', + 'required': True, + 'available': ['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.', + }, + { + 'name': 'alias', + 'type': 'str', + 'required': False, + 'helpMessage': 'Enter the alias you wish to use for the keystore.', + } + ] + + @staticmethod + def export(body, key, options, **kwargs): + """ + Generates a Java Keystore or Truststore + + :param key: + :param kwargs: + :param body: + :param options: + """ + if options.get('passphrase'): + passphrase = options['passphrase'] + else: + passphrase = get_psuedo_random_string() + + with mktempfile() as cert_tmp: + with open(cert_tmp, 'w') as f: + f.write(body) + with mktempfile() as key_tmp: + with open(key_tmp, 'w') as f: + f.write(key) + + # Create PKCS12 keystore from private key and public certificate + with mktempfile() as p12_tmp: + run_process([ + "openssl", + "pkcs12", + "-export", + "-name", options.get('alias', 'cert'), + "-in", cert_tmp, + "-inkey", key_tmp, + "-out", p12_tmp, + "-password", "pass:{}".format(passphrase) + ]) + + # Convert PKCS12 keystore into a JKS keystore + with mktemppath() as jks_tmp: + run_process([ + "keytool", + "-importkeystore", + "-destkeystore", jks_tmp, + "-srckeystore", p12_tmp, + "-srcstoretype", "PKCS12", + "-alias", options.get('alias', 'cert'), + "-srcstorepass", passphrase, + "-deststorepass", passphrase + ]) + + # Import signed cert in to JKS keystore + run_process([ + "keytool", + "-importcert", + "-file", cert_tmp, + "-keystore", jks_tmp, + "-alias", "{0}_cert".format(options.get('alias'), 'cert'), + "-storepass", passphrase, + "-noprompt" + ]) + + with open(jks_tmp, 'rb') as f: + raw = f.read() + + return raw diff --git a/lemur/plugins/lemur_java/tests/conftest.py b/lemur/plugins/lemur_java/tests/conftest.py new file mode 100644 index 00000000..0e1cd89f --- /dev/null +++ b/lemur/plugins/lemur_java/tests/conftest.py @@ -0,0 +1 @@ +from lemur.tests.conftest import * # noqa diff --git a/lemur/plugins/lemur_java/tests/test_java.py b/lemur/plugins/lemur_java/tests/test_java.py new file mode 100644 index 00000000..cf72a289 --- /dev/null +++ b/lemur/plugins/lemur_java/tests/test_java.py @@ -0,0 +1,63 @@ +PRIVATE_KEY_STR = b""" +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAsXn+QZRATxryRmGXI4fdI+0a2oBwuVh8fC/9bcqX6c5eDmgc +rj6esmc1hpIFxMM3DvkFXX6xISkU6B5fmYDEGZLi7NvcXF3+EoA/SCkP1MFlvqhn +EvNhb0t1fBLs0i/0gfTS/FHBZY1ekHisd/sUetCDZ7F11RxMwws0Oc8bl7j1TpRc +awXFAsh/aWwQOwFeyWU7TtZeAE7sMyWXInBg37tKk1wlv+mN+27WijI091+amkVy +zIV6mA5OHfqbjuqV8uQflN8jE244Qr7shtSk7LpBpWf0M6dC7dXbuUctHFhqcDjy +3IRUl+NisKRoMtq+a0uehfmpFNSUD7F4gdUtSwIDAQABAoIBAGITsZ+aBuPwVzzv +x286MMoeyL1BR4oVzU1v09Rtpf/uLGo3vMnKDzc19A12+rseynl6wi1FyysxIb2Y +s2oID9a2JrOQWLmus66TsuT01CvV6J0xQSzm1MyFXdqANuF84NlEa6hGoeK1+jFK +jr0LQukP+9484oovxnfu5CCiRHRWNZmeuekuYhI1SJf343Tr6jwvyr6KZpnIy0Yt +axuuIZdCfY9ZV2vFG89GwwgwVQrhf14Kv5vBMZrNh1lRGsr0Sqlx5cGkPRAy90lg +HjrRMogrtXr3AR5Pk2qqAYXzZBU2EFhJ3k2njpwOzlSj0r0ZwTmejZ89cco0sW5j ++eQ6aRECgYEA1tkNW75fgwU52Va5VETCzG8II/pZdqNygnoc3z8EutN+1w8f6Tr+ +PdpKSICW0z7Iq4f5k/4wrA5xw1vy5RBMH0ZP29GwHTvCPiTBboR9vWvxQvZn1jb9 +wvKa0RxE18KcF0YIyTnZMubkA17QTFlvCNyZg0iCqeyFYPyqVE+R4AkCgYEA03h1 +XrqECZDDbG9HLUdGbkZNk4VzTcF6dQ3GAPY8M/H7rw5BbvH0RZLOrzl46DDVzKTg +B1VOReAHsxBKFdkqeq1A99CLDow6vHTIEG8DwxkA7/2QPkt8MybwdApUyYnQh5/v +CxwkRt4Mm+EiYfn5iyL8yI+vaQSRToVO/3BND7MCgYAJQSpBJG8qzqPSR9kN1zRo +5/N60ULfSGUbV7U8rJNAlPGmw+EFA+SFt4xxmRBmIxMzyFSo2k8waiLeXmyVD2Go +CzhPaLXkXHmegajPYOelrCulTcXlRVMi/Z5LmaMhhCGDIyInwNUpSybROllQoJ2W +zSHTtODj/usz5U5U+WR4OQKBgHQRosI6t2wUo96peTS18UdnmP7GeZINBuymga5X +eJW+VLkxpuKBNOTW/lCYx+8Rlte7CyebP9oEa9VxtGgniTRKUeVy9lAm0bpMkt7K +QBNebvBKiVhX0DS3Q7U9UmpIFUfLlcXQTW0ERYFtYZTLQpeGvZ5LlyiaFDM34jM7 +7WAXAoGANDPJdQLEuimCOAMx/xoecNWeZIP6ieB0hVBrwLNxsaZlkn1KodUMuvla +VEowbtPRdc9o3VZRh4q9cEakssTvOD70hgUZCFcMarmc37RgRvvD2fsZmDZF6qd3 +QfHplREs9F0sW+eiirczG7up4XL+CA162TtZxW+2GAiQhwhE5jA= +-----END RSA PRIVATE KEY----- +""" + +EXTERNAL_VALID_STR = b""" +-----BEGIN CERTIFICATE----- +MIID2zCCAsOgAwIBAgICA+0wDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlMb3MgR2F0b3MxDTALBgNV +BAMMBHRlc3QxFjAUBgNVBAoMDU5ldGZsaXgsIEluYy4xEzARBgNVBAsMCk9wZXJh +dGlvbnMxIzAhBgkqhkiG9w0BCQEWFGtnbGlzc29uQG5ldGZsaXguY29tMB4XDTE1 +MTEyMzIxNDIxMFoXDTE1MTEyNjIxNDIxMFowcjENMAsGA1UEAwwEdGVzdDEWMBQG +A1UECgwNTmV0ZmxpeCwgSW5jLjETMBEGA1UECwwKT3BlcmF0aW9uczELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALF5/kGUQE8a8kZhlyOH3SPt +GtqAcLlYfHwv/W3Kl+nOXg5oHK4+nrJnNYaSBcTDNw75BV1+sSEpFOgeX5mAxBmS +4uzb3Fxd/hKAP0gpD9TBZb6oZxLzYW9LdXwS7NIv9IH00vxRwWWNXpB4rHf7FHrQ +g2exddUcTMMLNDnPG5e49U6UXGsFxQLIf2lsEDsBXsllO07WXgBO7DMllyJwYN+7 +SpNcJb/pjftu1ooyNPdfmppFcsyFepgOTh36m47qlfLkH5TfIxNuOEK+7IbUpOy6 +QaVn9DOnQu3V27lHLRxYanA48tyEVJfjYrCkaDLavmtLnoX5qRTUlA+xeIHVLUsC +AwEAAaNVMFMwUQYDVR0fBEowSDBGoESgQoZAaHR0cDovL3Rlc3QuY2xvdWRjYS5j +cmwubmV0ZmxpeC5jb20vdGVzdERlY3JpcHRpb25DQVJvb3QvY3JsLnBlbTANBgkq +hkiG9w0BAQsFAAOCAQEAiHREBKg7zhlQ/N7hDIkxgodRSWD7CVbJGSCdkR3Pvr6+ +jHBVNTJUrYqy7sL2pIutoeiSTQEH65/Gbm30mOnNu+lvFKxTxzof6kNYv8cyc8sX +eBuBfSrlTodPFSHXQIpOexZgA0f30LOuXegqzxgXkKg+uMXOez5Zo5pNjTUow0He +oe+V1hfYYvL1rocCmBOkhIGWz7622FxKDawRtZTGVsGsMwMIWyvS3+KQ04K8yHhp +bQOg9zZAoYQuHY1inKBnA0II8eW0hPpJrlZoSqN8Tp0NSBpFiUk3m7KNFP2kITIf +tTneAgyUsgfDxNDifZryZSzg7MH31sTBcYaotSmTXw== +-----END CERTIFICATE----- +""" + + +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) + assert raw != b"" diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index 5ff108ed..e94873ec 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -43,7 +43,7 @@ LOG_FILE = "lemur.log" # modify this if you are not using a local database SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur' - +SQLALCHEMY_TRACK_MODIFICATIONS = False # AWS diff --git a/lemur/utils.py b/lemur/utils.py index ab8f0f63..cb494747 100644 --- a/lemur/utils.py +++ b/lemur/utils.py @@ -5,11 +5,35 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ +import os import six from flask import current_app from cryptography.fernet import Fernet, MultiFernet import sqlalchemy.types as types +from contextlib import contextmanager +import tempfile + + +@contextmanager +def mktempfile(): + with tempfile.NamedTemporaryFile(delete=False) as f: + name = f.name + + try: + yield name + finally: + os.unlink(name) + + +@contextmanager +def mktemppath(): + try: + path = os.path.join(tempfile._get_default_tempdir(), next(tempfile._get_candidate_names())) + yield path + finally: + os.unlink(path) + def get_keys(): """ @@ -26,7 +50,7 @@ def get_keys(): # the fact that there is not a current_app with a config at that point try: keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS') - except: + except Exception: print("no encryption keys") return [] diff --git a/setup.py b/setup.py index 80dcda02..b3df5e5f 100644 --- a/setup.py +++ b/setup.py @@ -154,6 +154,7 @@ setup( 'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin', 'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin', 'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin', + 'java_export = lemur.plugins.lemur_java.plugin:JavaExportPlugin' ], }, classifiers=[ From 8eeed821d34dc9eeb37fcee6e7326f6555094fae Mon Sep 17 00:00:00 2001 From: kevgliss Date: Fri, 27 Nov 2015 13:27:14 -0800 Subject: [PATCH 2/5] Adding UI elements --- lemur/certificates/service.py | 16 ++--- lemur/certificates/views.py | 8 ++- lemur/plugins/base/v1.py | 5 +- lemur/plugins/bases/export.py | 4 ++ lemur/plugins/lemur_java/plugin.py | 69 +++++++++++++++---- lemur/plugins/lemur_java/tests/test_java.py | 2 +- .../certificates/certificate/certificate.js | 33 +++++++++ .../app/angular/certificates/services.js | 4 ++ .../app/angular/certificates/view/view.js | 14 ++++ .../angular/certificates/view/view.tpl.html | 9 +-- lemur/utils.py | 10 ++- 11 files changed, 139 insertions(+), 35 deletions(-) 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(): From ec896461a7f469dc42ea01d61707823e83f5d206 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 30 Nov 2015 09:47:36 -0800 Subject: [PATCH 3/5] Adding final touches to #125 --- bower.json | 3 +- lemur/certificates/views.py | 8 ++--- lemur/plugins/lemur_java/plugin.py | 8 +++-- lemur/static/app/angular/app.js | 3 +- .../certificates/certificate/certificate.js | 29 ++++++++++++++----- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/bower.json b/bower.json index f2c6973d..b4dba71d 100644 --- a/bower.json +++ b/bower.json @@ -32,7 +32,8 @@ "angularjs-toaster": "~0.4.14", "ngletteravatar": "~3.0.1", "angular-ui-router": "~0.2.15", - "angular-clipboard": "~1.1.1" + "angular-clipboard": "~1.1.1", + "angular-file-saver": "~1.0.1" }, "devDependencies": { "angular-mocks": "~1.3", diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 4cba53ae..6a58e93b 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -5,6 +5,7 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ +import base64 from builtins import str from flask import Blueprint, make_response, jsonify @@ -817,10 +818,9 @@ class CertificateExport(AuthenticatedResource): permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) if permission.can(): - passphrase, data = service.export(cert, args['export']['plugin']) - response = make_response(data) - response.headers['content-type'] = 'application/octet-stream' - return response + extension, passphrase, data = service.export(cert, args['export']['plugin']) + # we take a hit in message size when b64 encoding + return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data)) return dict(message='You are not authorized to export this certificate'), 403 diff --git a/lemur/plugins/lemur_java/plugin.py b/lemur/plugins/lemur_java/plugin.py index 72aabf61..6e839dc4 100644 --- a/lemur/plugins/lemur_java/plugin.py +++ b/lemur/plugins/lemur_java/plugin.py @@ -74,7 +74,7 @@ class JavaExportPlugin(ExportPlugin): 'type': 'str', 'required': False, 'helpMessage': 'If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8.', - 'validation': '^.{8}$' + 'validation': '^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$' }, { 'name': 'alias', @@ -105,9 +105,13 @@ class JavaExportPlugin(ExportPlugin): else: alias = "blah" + if not key: + raise Exception("Unable to export, no private key found.") + with mktempfile() as cert_tmp: with open(cert_tmp, 'w') as f: f.write(body) + with mktempfile() as key_tmp: with open(key_tmp, 'w') as f: f.write(key) @@ -168,4 +172,4 @@ class JavaExportPlugin(ExportPlugin): with open(jks_tmp, 'rb') as f: raw = f.read() - return passphrase, raw + return "jks", passphrase, raw diff --git a/lemur/static/app/angular/app.js b/lemur/static/app/angular/app.js index a941814d..5f395a68 100644 --- a/lemur/static/app/angular/app.js +++ b/lemur/static/app/angular/app.js @@ -15,7 +15,8 @@ var lemur = angular 'mgo-angular-wizard', 'satellizer', 'ngLetterAvatar', - 'angular-clipboard' + 'angular-clipboard', + 'ngFileSaver' ]) .config(function ($stateProvider, $urlRouterProvider, $authProvider) { $urlRouterProvider.otherwise('/welcome'); diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js index f9a63af2..8ffec997 100644 --- a/lemur/static/app/angular/certificates/certificate/certificate.js +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('lemur') - .controller('CertificateExportController', function ($scope, $modalInstance, CertificateApi, CertificateService, PluginService, toaster, editId) { + .controller('CertificateExportController', function ($scope, $modalInstance, CertificateApi, CertificateService, PluginService, FileSaver, Blob, toaster, editId) { CertificateApi.get(editId).then(function (certificate) { $scope.certificate = certificate; }); @@ -16,13 +16,26 @@ angular.module('lemur') $scope.save = function (certificate) { CertificateService.export(certificate).then( - function () { - toaster.pop({ - type: 'success', - title: certificate.name, - body: 'Successfully exported!' - }); - $modalInstance.close(); + function (response) { + var byteCharacters = atob(response.data); + var byteArrays = []; + + for (var offset = 0; offset < byteCharacters.length; offset += 512) { + var slice = byteCharacters.slice(offset, offset + 512); + + var byteNumbers = new Array(slice.length); + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + var byteArray = new Uint8Array(byteNumbers); + + byteArrays.push(byteArray); + } + + var blob = new Blob(byteArrays, {type: 'application/octet-stream'}); + saveAs(blob, certificate.name + "." + response.extension); + $scope.passphrase = response.passphrase; }, function (response) { toaster.pop({ From f56c6f2836a90eba63cdbd76f7aa29ae3cd6997a Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 30 Nov 2015 10:10:50 -0800 Subject: [PATCH 4/5] Downgrading req to pass tests. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3df5e5f..4f135bca 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) install_requires = [ 'Flask==0.10.1', - 'Flask-RESTful==0.3.4', + 'Flask-RESTful==0.3.3', 'Flask-SQLAlchemy==2.1', 'Flask-Script==2.0.5', 'Flask-Migrate==1.6.0', From f194e2a1be984da479126cd2b4a9cbe0fcfd5f2b Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 30 Nov 2015 10:24:53 -0800 Subject: [PATCH 5/5] Linting --- .../static/app/angular/certificates/certificate/certificate.js | 2 +- lemur/static/app/angular/certificates/view/view.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js index 8ffec997..4e20aec3 100644 --- a/lemur/static/app/angular/certificates/certificate/certificate.js +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -34,7 +34,7 @@ angular.module('lemur') } var blob = new Blob(byteArrays, {type: 'application/octet-stream'}); - saveAs(blob, certificate.name + "." + response.extension); + FileSaver.saveAs(blob, certificate.name + '.' + response.extension); $scope.passphrase = response.passphrase; }, function (response) { diff --git a/lemur/static/app/angular/certificates/view/view.js b/lemur/static/app/angular/certificates/view/view.js index 4a7107c0..f08f7281 100644 --- a/lemur/static/app/angular/certificates/view/view.js +++ b/lemur/static/app/angular/certificates/view/view.js @@ -165,7 +165,7 @@ angular.module('lemur') }; $scope.export = function (certificateId) { - var modalInstance = $modal.open({ + $modal.open({ animation: true, controller: 'CertificateExportController', templateUrl: '/angular/certificates/certificate/export.tpl.html',