Merge branch 'master' into update-reqs-020719

This commit is contained in:
Hossein Shafagh 2019-02-07 09:57:12 -08:00 committed by GitHub
commit 198826dd66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 257 additions and 70 deletions

View File

@ -221,11 +221,6 @@ def upload(**kwargs):
else: else:
kwargs['roles'] = roles kwargs['roles'] = roles
if kwargs.get('private_key'):
private_key = kwargs['private_key']
if not isinstance(private_key, bytes):
kwargs['private_key'] = private_key.encode('utf-8')
cert = Certificate(**kwargs) cert = Certificate(**kwargs)
cert.authority = kwargs.get('authority') cert.authority = kwargs.get('authority')
cert = database.create(cert) cert = database.create(cert)
@ -432,10 +427,7 @@ def create_csr(**csr_config):
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it
encryption_algorithm=serialization.NoEncryption() encryption_algorithm=serialization.NoEncryption()
) ).decode('utf-8')
if isinstance(private_key, bytes):
private_key = private_key.decode('utf-8')
csr = request.public_bytes( csr = request.public_bytes(
encoding=serialization.Encoding.PEM encoding=serialization.Encoding.PEM

View File

@ -6,6 +6,7 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import base64 import base64
import arrow
from builtins import str from builtins import str
from flask import Blueprint, make_response, jsonify, g from flask import Blueprint, make_response, jsonify, g
@ -660,6 +661,51 @@ class Certificates(AuthenticatedResource):
log_service.create(g.current_user, 'update_cert', certificate=cert) log_service.create(g.current_user, 'update_cert', certificate=cert)
return cert return cert
def delete(self, certificate_id, data=None):
"""
.. http:delete:: /certificates/1
Delete a certificate
**Example request**:
.. sourcecode:: http
DELETE /certificates/1 HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
:reqheader Authorization: OAuth token to authenticate
:statuscode 204: no error
:statuscode 403: unauthenticated
:statusoode 404: certificate not found
"""
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
return dict(message='You are not authorized to delete this certificate'), 403
if arrow.get(cert.not_after) > arrow.utcnow():
return dict(message='Certificate is still valid, only expired certificates can be deleted'), 412
service.update(certificate_id, deleted=True)
log_service.create(g.current_user, 'delete_cert', certificate=cert)
return '', 204
class NotificationCertificatesList(AuthenticatedResource): class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """ """ Defines the 'certificates' endpoint """

View File

@ -48,24 +48,22 @@ def parse_certificate(body):
:param body: :param body:
:return: :return:
""" """
if isinstance(body, str): assert isinstance(body, str)
body = body.encode('utf-8')
return x509.load_pem_x509_certificate(body, default_backend()) return x509.load_pem_x509_certificate(body.encode('utf-8'), default_backend())
def parse_private_key(private_key): def parse_private_key(private_key):
""" """
Parses a PEM-format private key (RSA, DSA, ECDSA or any other supported algorithm). Parses a PEM-format private key (RSA, DSA, ECDSA or any other supported algorithm).
Raises ValueError for an invalid string. Raises ValueError for an invalid string. Raises AssertionError when passed value is not str-type.
:param private_key: String containing PEM private key :param private_key: String containing PEM private key
""" """
if isinstance(private_key, str): assert isinstance(private_key, str)
private_key = private_key.encode('utf8')
return load_pem_private_key(private_key, password=None, backend=default_backend()) return load_pem_private_key(private_key.encode('utf8'), password=None, backend=default_backend())
def parse_csr(csr): def parse_csr(csr):
@ -75,10 +73,9 @@ def parse_csr(csr):
:param csr: :param csr:
:return: :return:
""" """
if isinstance(csr, str): assert isinstance(csr, str)
csr = csr.encode('utf-8')
return x509.load_pem_x509_csr(csr, default_backend()) return x509.load_pem_x509_csr(csr.encode('utf-8'), default_backend())
def get_authority_key(body): def get_authority_key(body):

View File

@ -18,6 +18,6 @@ class Log(db.Model):
__tablename__ = 'logs' __tablename__ = 'logs'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
certificate_id = Column(Integer, ForeignKey('certificates.id')) certificate_id = Column(Integer, ForeignKey('certificates.id'))
log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', name='log_type'), nullable=False) log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', 'delete_cert', name='log_type'), nullable=False)
logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False) logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False)

View File

@ -0,0 +1,22 @@
""" Add delete_cert to log_type enum
Revision ID: 9f79024fe67b
Revises: ee827d1e1974
Create Date: 2019-01-03 15:36:59.181911
"""
# revision identifiers, used by Alembic.
revision = '9f79024fe67b'
down_revision = 'ee827d1e1974'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.sync_enum_values('public', 'log_type', ['create_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'delete_cert', 'key_view', 'revoke_cert', 'update_cert'])
def downgrade():
op.sync_enum_values('public', 'log_type', ['create_cert', 'delete_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'key_view', 'revoke_cert', 'update_cert'])

View File

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

View File

@ -0,0 +1,116 @@
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
import requests
from lemur.plugins import lemur_adcs as ADCS
from certsrv import Certsrv
from OpenSSL import crypto
from flask import current_app
class ADCSIssuerPlugin(IssuerPlugin):
title = 'ADCS'
slug = 'adcs-issuer'
description = 'Enables the creation of certificates by ADCS (Active Directory Certificate Services)'
version = ADCS.VERSION
author = 'sirferl'
author_url = 'https://github.com/sirferl/lemur'
def __init__(self, *args, **kwargs):
"""Initialize the issuer with the appropriate details."""
self.session = requests.Session()
super(ADCSIssuerPlugin, self).__init__(*args, **kwargs)
@staticmethod
def create_authority(options):
"""Create an authority.
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:
"""
adcs_root = current_app.config.get('ADCS_ROOT')
adcs_issuing = current_app.config.get('ADCS_ISSUING')
role = {'username': '', 'password': '', 'name': 'adcs'}
return adcs_root, adcs_issuing, [role]
def create_certificate(self, csr, issuer_options):
adcs_server = current_app.config.get('ADCS_SERVER')
adcs_user = current_app.config.get('ADCS_USER')
adcs_pwd = current_app.config.get('ADCS_PWD')
adcs_auth_method = current_app.config.get('ADCS_AUTH_METHOD')
adcs_template = current_app.config.get('ADCS_TEMPLATE')
ca_server = Certsrv(adcs_server, adcs_user, adcs_pwd, auth_method=adcs_auth_method)
current_app.logger.info("Requesting CSR: {0}".format(csr))
current_app.logger.info("Issuer options: {0}".format(issuer_options))
cert, req_id = ca_server.get_cert(csr, adcs_template, encoding='b64').decode('utf-8').replace('\r\n', '\n')
chain = ca_server.get_ca_cert(encoding='b64').decode('utf-8').replace('\r\n', '\n')
return cert, chain, req_id
def revoke_certificate(self, certificate, comments):
raise NotImplementedError('Not implemented\n', self, certificate, comments)
def get_ordered_certificate(self, order_id):
raise NotImplementedError('Not implemented\n', self, order_id)
def canceled_ordered_certificate(self, pending_cert, **kwargs):
raise NotImplementedError('Not implemented\n', self, pending_cert, **kwargs)
class ADCSSourcePlugin(SourcePlugin):
title = 'ADCS'
slug = 'adcs-source'
description = 'Enables the collecion of certificates'
version = ADCS.VERSION
author = 'sirferl'
author_url = 'https://github.com/sirferl/lemur'
options = [
{
'name': 'dummy',
'type': 'str',
'required': False,
'validation': '/^[0-9]{12,12}$/',
'helpMessage': 'Just to prevent error'
}
]
def get_certificates(self, options, **kwargs):
adcs_server = current_app.config.get('ADCS_SERVER')
adcs_user = current_app.config.get('ADCS_USER')
adcs_pwd = current_app.config.get('ADCS_PWD')
adcs_auth_method = current_app.config.get('ADCS_AUTH_METHOD')
adcs_start = current_app.config.get('ADCS_START')
adcs_stop = current_app.config.get('ADCS_STOP')
ca_server = Certsrv(adcs_server, adcs_user, adcs_pwd, auth_method=adcs_auth_method)
out_certlist = []
for id in range(adcs_start, adcs_stop):
try:
cert = ca_server.get_existing_cert(id, encoding='b64').decode('utf-8').replace('\r\n', '\n')
except Exception as err:
if '{0}'.format(err).find("CERTSRV_E_PROPERTY_EMPTY"):
# this error indicates end of certificate list(?), so we stop
break
else:
# We do nothing in case there is no certificate returned for other reasons
current_app.logger.info("Error with id {0}: {1}".format(id, err))
else:
# we have a certificate
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
# loop through extensions to see if we find "TLS Web Server Authentication"
for e_id in range(0, pubkey.get_extension_count() - 1):
try:
extension = '{0}'.format(pubkey.get_extension(e_id))
except Exception:
extensionn = ''
if extension.find("TLS Web Server Authentication") != -1:
out_certlist.append({
'name': format(pubkey.get_subject().CN),
'body': cert})
break
return out_certlist
def get_endpoints(self, options, **kwargs):
# There are no endpoints in the ADCS
raise NotImplementedError('Not implemented\n', self, options, **kwargs)

View File

@ -64,6 +64,7 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
:param path: :param path:
:return: :return:
""" """
assert isinstance(private_key, str)
client = kwargs.pop('client') client = kwargs.pop('client')
if not path or path == '/': if not path or path == '/':
@ -72,8 +73,6 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
name = name + '-' + path.strip('/') name = name + '-' + path.strip('/')
try: try:
if isinstance(private_key, bytes):
private_key = private_key.decode("utf-8")
if cert_chain: if cert_chain:
return client.upload_server_certificate( return client.upload_server_certificate(
Path=path, Path=path,

View File

@ -14,6 +14,7 @@ from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from lemur.common.utils import parse_private_key
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin
from lemur.plugins import lemur_cryptography as cryptography_issuer from lemur.plugins import lemur_cryptography as cryptography_issuer
@ -40,7 +41,8 @@ def issue_certificate(csr, options, private_key=None):
if options.get("authority"): if options.get("authority"):
# Issue certificate signed by an existing lemur_certificates authority # Issue certificate signed by an existing lemur_certificates authority
issuer_subject = options['authority'].authority_certificate.subject issuer_subject = options['authority'].authority_certificate.subject
issuer_private_key = options['authority'].authority_certificate.private_key assert private_key is None, "Private would be ignored, authority key used instead"
private_key = options['authority'].authority_certificate.private_key
chain_cert_pem = options['authority'].authority_certificate.body chain_cert_pem = options['authority'].authority_certificate.body
authority_key_identifier_public = options['authority'].authority_certificate.public_key authority_key_identifier_public = options['authority'].authority_certificate.public_key
authority_key_identifier_subject = x509.SubjectKeyIdentifier.from_public_key(authority_key_identifier_public) authority_key_identifier_subject = x509.SubjectKeyIdentifier.from_public_key(authority_key_identifier_public)
@ -52,7 +54,6 @@ def issue_certificate(csr, options, private_key=None):
else: else:
# Issue certificate that is self-signed (new lemur_certificates root authority) # Issue certificate that is self-signed (new lemur_certificates root authority)
issuer_subject = csr.subject issuer_subject = csr.subject
issuer_private_key = private_key
chain_cert_pem = "" chain_cert_pem = ""
authority_key_identifier_public = csr.public_key() authority_key_identifier_public = csr.public_key()
authority_key_identifier_subject = None authority_key_identifier_subject = None
@ -112,11 +113,7 @@ def issue_certificate(csr, options, private_key=None):
# FIXME: Not implemented in lemur/schemas.py yet https://github.com/Netflix/lemur/issues/662 # FIXME: Not implemented in lemur/schemas.py yet https://github.com/Netflix/lemur/issues/662
pass pass
private_key = serialization.load_pem_private_key( private_key = parse_private_key(private_key)
bytes(str(issuer_private_key).encode('utf-8')),
password=None,
backend=default_backend()
)
cert = builder.sign(private_key, hashes.SHA256(), default_backend()) cert = builder.sign(private_key, hashes.SHA256(), default_backend())
cert_pem = cert.public_bytes( cert_pem = cert.public_bytes(

View File

@ -38,14 +38,9 @@ def create_csr(cert, chain, csr_tmp, key):
:param csr_tmp: :param csr_tmp:
:param key: :param key:
""" """
if isinstance(cert, bytes): assert isinstance(cert, str)
cert = cert.decode('utf-8') assert isinstance(chain, str)
assert isinstance(key, str)
if isinstance(chain, bytes):
chain = chain.decode('utf-8')
if isinstance(key, bytes):
key = key.decode('utf-8')
with mktempfile() as key_tmp: with mktempfile() as key_tmp:
with open(key_tmp, 'w') as f: with open(key_tmp, 'w') as f:

View File

@ -59,11 +59,8 @@ def split_chain(chain):
def create_truststore(cert, chain, jks_tmp, alias, passphrase): def create_truststore(cert, chain, jks_tmp, alias, passphrase):
if isinstance(cert, bytes): assert isinstance(cert, str)
cert = cert.decode('utf-8') assert isinstance(chain, str)
if isinstance(chain, bytes):
chain = chain.decode('utf-8')
with mktempfile() as cert_tmp: with mktempfile() as cert_tmp:
with open(cert_tmp, 'w') as f: with open(cert_tmp, 'w') as f:
@ -98,14 +95,9 @@ def create_truststore(cert, chain, jks_tmp, alias, passphrase):
def create_keystore(cert, chain, jks_tmp, key, alias, passphrase): def create_keystore(cert, chain, jks_tmp, key, alias, passphrase):
if isinstance(cert, bytes): assert isinstance(cert, str)
cert = cert.decode('utf-8') assert isinstance(chain, str)
assert isinstance(key, str)
if isinstance(chain, bytes):
chain = chain.decode('utf-8')
if isinstance(key, bytes):
key = key.decode('utf-8')
# Create PKCS12 keystore from private key and public certificate # Create PKCS12 keystore from private key and public certificate
with mktempfile() as cert_tmp: with mktempfile() as cert_tmp:

View File

@ -44,14 +44,9 @@ def create_pkcs12(cert, chain, p12_tmp, key, alias, passphrase):
:param alias: :param alias:
:param passphrase: :param passphrase:
""" """
if isinstance(cert, bytes): assert isinstance(cert, str)
cert = cert.decode('utf-8') assert isinstance(chain, str)
assert isinstance(key, str)
if isinstance(chain, bytes):
chain = chain.decode('utf-8')
if isinstance(key, bytes):
key = key.decode('utf-8')
with mktempfile() as key_tmp: with mktempfile() as key_tmp:
with open(key_tmp, 'w') as f: with open(key_tmp, 'w') as f:

View File

@ -3,19 +3,19 @@ import os
import datetime import datetime
import pytest import pytest
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from flask import current_app from flask import current_app
from flask_principal import identity_changed, Identity from flask_principal import identity_changed, Identity
from lemur import create_app from lemur import create_app
from lemur.common.utils import parse_private_key
from lemur.database import db as _db from lemur.database import db as _db
from lemur.auth.service import create_token from lemur.auth.service import create_token
from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \ from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \ CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, CryptoAuthorityFactory RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, InvalidCertificateFactory, \
CryptoAuthorityFactory
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
@ -168,6 +168,15 @@ def pending_certificate(session):
return p return p
@pytest.fixture
def invalid_certificate(session):
u = UserFactory()
a = AsyncAuthorityFactory()
i = InvalidCertificateFactory(user=u, authority=a)
session.commit()
return i
@pytest.fixture @pytest.fixture
def admin_user(session): def admin_user(session):
u = UserFactory() u = UserFactory()
@ -235,12 +244,12 @@ def logged_in_admin(session, app):
@pytest.fixture @pytest.fixture
def private_key(): def private_key():
return load_pem_private_key(SAN_CERT_KEY.encode(), password=None, backend=default_backend()) return parse_private_key(SAN_CERT_KEY)
@pytest.fixture @pytest.fixture
def issuer_private_key(): def issuer_private_key():
return load_pem_private_key(INTERMEDIATE_KEY.encode(), password=None, backend=default_backend()) return parse_private_key(INTERMEDIATE_KEY)
@pytest.fixture @pytest.fixture

View File

@ -20,7 +20,7 @@ from lemur.policies.models import RotationPolicy
from lemur.api_keys.models import ApiKey from lemur.api_keys.models import ApiKey
from .vectors import SAN_CERT_STR, SAN_CERT_KEY, CSR_STR, INTERMEDIATE_CERT_STR, ROOTCA_CERT_STR, INTERMEDIATE_KEY, \ from .vectors import SAN_CERT_STR, SAN_CERT_KEY, CSR_STR, INTERMEDIATE_CERT_STR, ROOTCA_CERT_STR, INTERMEDIATE_KEY, \
WILDCARD_CERT_KEY WILDCARD_CERT_KEY, INVALID_CERT_STR
class BaseFactory(SQLAlchemyModelFactory): class BaseFactory(SQLAlchemyModelFactory):
@ -137,6 +137,11 @@ class CACertificateFactory(CertificateFactory):
private_key = INTERMEDIATE_KEY private_key = INTERMEDIATE_KEY
class InvalidCertificateFactory(CertificateFactory):
body = INVALID_CERT_STR
private_key = ''
class AuthorityFactory(BaseFactory): class AuthorityFactory(BaseFactory):
"""Authority factory.""" """Authority factory."""
name = Sequence(lambda n: 'authority{0}'.format(n)) name = Sequence(lambda n: 'authority{0}'.format(n))

View File

@ -653,15 +653,26 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin):
@pytest.mark.parametrize("token,status", [ @pytest.mark.parametrize("token,status", [
(VALID_USER_HEADER_TOKEN, 405), (VALID_USER_HEADER_TOKEN, 403),
(VALID_ADMIN_HEADER_TOKEN, 405), (VALID_ADMIN_HEADER_TOKEN, 412),
(VALID_ADMIN_API_TOKEN, 405), (VALID_ADMIN_API_TOKEN, 412),
('', 405) ('', 401)
]) ])
def test_certificate_delete(client, token, status): def test_certificate_delete(client, token, status):
assert client.delete(api.url_for(Certificates, certificate_id=1), headers=token).status_code == status assert client.delete(api.url_for(Certificates, certificate_id=1), headers=token).status_code == status
@pytest.mark.parametrize("token,status", [
(VALID_USER_HEADER_TOKEN, 403),
(VALID_ADMIN_HEADER_TOKEN, 204),
(VALID_ADMIN_API_TOKEN, 204),
('', 401)
])
def test_invalid_certificate_delete(client, invalid_certificate, token, status):
assert client.delete(
api.url_for(Certificates, certificate_id=invalid_certificate.id), headers=token).status_code == status
@pytest.mark.parametrize("token,status", [ @pytest.mark.parametrize("token,status", [
(VALID_USER_HEADER_TOKEN, 405), (VALID_USER_HEADER_TOKEN, 405),
(VALID_ADMIN_HEADER_TOKEN, 405), (VALID_ADMIN_HEADER_TOKEN, 405),

View File

@ -17,6 +17,7 @@ babel==2.6.0 # via sphinx
bcrypt==3.1.6 bcrypt==3.1.6
billiard==3.5.0.5 billiard==3.5.0.5
blinker==1.4 blinker==1.4
boto3==1.9.89 boto3==1.9.89
botocore==1.12.89 botocore==1.12.89
celery[redis]==4.2.1 celery[redis]==4.2.1

View File

@ -8,6 +8,7 @@ boto3
botocore botocore
celery[redis] celery[redis]
certifi certifi
certsrv
CloudFlare CloudFlare
cryptography cryptography
dnspython3 dnspython3

View File

@ -19,6 +19,7 @@ boto3==1.9.89
botocore==1.12.89 botocore==1.12.89
celery[redis]==4.2.1 celery[redis]==4.2.1
certifi==2018.11.29 certifi==2018.11.29
certsrv==2.1.0
cffi==1.11.5 # via bcrypt, cryptography, pynacl cffi==1.11.5 # via bcrypt, cryptography, pynacl
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==7.0 # via flask click==7.0 # via flask

View File

@ -154,7 +154,9 @@ setup(
'digicert_cis_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertCISIssuerPlugin', 'digicert_cis_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertCISIssuerPlugin',
'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin', 'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin',
'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin', 'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin',
'sftp_destination = lemur.plugins.lemur_sftp.plugin:SFTPDestinationPlugin' 'sftp_destination = lemur.plugins.lemur_sftp.plugin:SFTPDestinationPlugin',
'adcs_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin',
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin'
], ],
}, },
classifiers=[ classifiers=[