Merge branch 'master' into get_by_attributes
This commit is contained in:
@ -41,6 +41,7 @@ class LdapPrincipal():
|
||||
self.ldap_default_role = current_app.config.get("LEMUR_DEFAULT_ROLE", None)
|
||||
self.ldap_required_group = current_app.config.get("LDAP_REQUIRED_GROUP", None)
|
||||
self.ldap_groups_to_roles = current_app.config.get("LDAP_GROUPS_TO_ROLES", None)
|
||||
self.ldap_is_active_directory = current_app.config.get("LDAP_IS_ACTIVE_DIRECTORY", False)
|
||||
self.ldap_attrs = ['memberOf']
|
||||
self.ldap_client = None
|
||||
self.ldap_groups = None
|
||||
@ -168,11 +169,28 @@ class LdapPrincipal():
|
||||
except ldap.LDAPError as e:
|
||||
raise Exception("ldap error: {0}".format(e))
|
||||
|
||||
lgroups = self.ldap_client.search_s(self.ldap_base_dn,
|
||||
ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs)[0][1]['memberOf']
|
||||
# lgroups is a list of utf-8 encoded strings
|
||||
# convert to a single string of groups to allow matching
|
||||
self.ldap_groups = b''.join(lgroups).decode('ascii')
|
||||
if self.ldap_is_active_directory:
|
||||
# Lookup user DN, needed to search for group membership
|
||||
userdn = self.ldap_client.search_s(self.ldap_base_dn,
|
||||
ldap.SCOPE_SUBTREE, ldap_filter,
|
||||
['distinguishedName'])[0][1]['distinguishedName'][0]
|
||||
userdn = userdn.decode('utf-8')
|
||||
# Search all groups that have the userDN as a member
|
||||
groupfilter = '(&(objectclass=group)(member:1.2.840.113556.1.4.1941:={0}))'.format(userdn)
|
||||
lgroups = self.ldap_client.search_s(self.ldap_base_dn, ldap.SCOPE_SUBTREE, groupfilter, ['cn'])
|
||||
|
||||
# Create a list of group CN's from the result
|
||||
self.ldap_groups = []
|
||||
for group in lgroups:
|
||||
(dn, values) = group
|
||||
self.ldap_groups.append(values['cn'][0].decode('ascii'))
|
||||
else:
|
||||
lgroups = self.ldap_client.search_s(self.ldap_base_dn,
|
||||
ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs)[0][1]['memberOf']
|
||||
# lgroups is a list of utf-8 encoded strings
|
||||
# convert to a single string of groups to allow matching
|
||||
self.ldap_groups = b''.join(lgroups).decode('ascii')
|
||||
|
||||
self.ldap_client.unbind()
|
||||
|
||||
def _ldap_validate_conf(self):
|
||||
|
@ -15,6 +15,7 @@ from lemur import database
|
||||
from lemur.common.utils import truthiness
|
||||
from lemur.extensions import metrics
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.certificates.service import upload
|
||||
@ -178,6 +179,13 @@ def render(args):
|
||||
terms = filt.split(';')
|
||||
if 'active' in filt:
|
||||
query = query.filter(Authority.active == truthiness(terms[1]))
|
||||
elif 'cn' in filt:
|
||||
term = '%{0}%'.format(terms[1])
|
||||
sub_query = database.session_query(Certificate.root_authority_id) \
|
||||
.filter(Certificate.cn.ilike(term)) \
|
||||
.subquery()
|
||||
|
||||
query = query.filter(Authority.id.in_(sub_query))
|
||||
else:
|
||||
query = database.filter(query, Authority, terms)
|
||||
|
||||
|
@ -265,30 +265,31 @@ def query(fqdns, issuer, owner, expired):
|
||||
table = []
|
||||
|
||||
q = database.session_query(Certificate)
|
||||
if issuer:
|
||||
sub_query = database.session_query(Authority.id) \
|
||||
.filter(Authority.name.ilike('%{0}%'.format(issuer))) \
|
||||
.subquery()
|
||||
|
||||
sub_query = database.session_query(Authority.id) \
|
||||
.filter(Authority.name.ilike('%{0}%'.format(issuer))) \
|
||||
.subquery()
|
||||
|
||||
q = q.filter(
|
||||
or_(
|
||||
Certificate.issuer.ilike('%{0}%'.format(issuer)),
|
||||
Certificate.authority_id.in_(sub_query)
|
||||
q = q.filter(
|
||||
or_(
|
||||
Certificate.issuer.ilike('%{0}%'.format(issuer)),
|
||||
Certificate.authority_id.in_(sub_query)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
q = q.filter(Certificate.owner.ilike('%{0}%'.format(owner)))
|
||||
if owner:
|
||||
q = q.filter(Certificate.owner.ilike('%{0}%'.format(owner)))
|
||||
|
||||
if not expired:
|
||||
q = q.filter(Certificate.expired == False) # noqa
|
||||
|
||||
for f in fqdns.split(','):
|
||||
q = q.filter(
|
||||
or_(
|
||||
Certificate.cn.ilike('%{0}%'.format(f)),
|
||||
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(f)))
|
||||
if fqdns:
|
||||
for f in fqdns.split(','):
|
||||
q = q.filter(
|
||||
or_(
|
||||
Certificate.cn.ilike('%{0}%'.format(f)),
|
||||
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(f)))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for c in q.all():
|
||||
table.append([c.id, c.name, c.owner, c.issuer])
|
||||
@ -363,10 +364,7 @@ def check_revoked():
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
|
||||
if status is None:
|
||||
cert.status = 'unknown'
|
||||
else:
|
||||
cert.status = 'valid' if status else 'revoked'
|
||||
cert.status = 'valid' if status else 'revoked'
|
||||
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
|
@ -19,7 +19,7 @@ from sqlalchemy.sql.expression import case, extract
|
||||
from sqlalchemy_utils.types.arrow import ArrowType
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from lemur.common import defaults, utils
|
||||
from lemur.common import defaults, utils, validators
|
||||
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||
from lemur.database import db
|
||||
from lemur.domains.models import Domain
|
||||
@ -77,6 +77,14 @@ def get_or_increase_name(name, serial):
|
||||
|
||||
class Certificate(db.Model):
|
||||
__tablename__ = 'certificates'
|
||||
__table_args__ = (
|
||||
Index('ix_certificates_cn', "cn",
|
||||
postgresql_ops={"cn": "gin_trgm_ops"},
|
||||
postgresql_using='gin'),
|
||||
Index('ix_certificates_name', "name",
|
||||
postgresql_ops={"name": "gin_trgm_ops"},
|
||||
postgresql_using='gin'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
ix = Index('ix_certificates_id_desc', id.desc(), postgresql_using='btree', unique=True)
|
||||
external_id = Column(String(128))
|
||||
@ -130,7 +138,6 @@ class Certificate(db.Model):
|
||||
logs = relationship('Log', backref='certificate')
|
||||
endpoints = relationship('Endpoint', backref='certificate')
|
||||
rotation_policy = relationship("RotationPolicy")
|
||||
|
||||
sensitive_fields = ('private_key',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -179,6 +186,18 @@ class Certificate(db.Model):
|
||||
for domain in defaults.domains(cert):
|
||||
self.domains.append(Domain(name=domain))
|
||||
|
||||
# Check integrity before saving anything into the database.
|
||||
# For user-facing API calls, validation should also be done in schema validators.
|
||||
self.check_integrity()
|
||||
|
||||
def check_integrity(self):
|
||||
"""
|
||||
Integrity checks: Does the cert have a matching private key?
|
||||
"""
|
||||
if self.private_key:
|
||||
validators.verify_private_key_match(utils.parse_private_key(self.private_key), self.parsed_cert,
|
||||
error_class=AssertionError)
|
||||
|
||||
@cached_property
|
||||
def parsed_cert(self):
|
||||
assert self.body, "Certificate body not set"
|
||||
@ -208,6 +227,10 @@ class Certificate(db.Model):
|
||||
def location(self):
|
||||
return defaults.location(self.parsed_cert)
|
||||
|
||||
@property
|
||||
def distinguished_name(self):
|
||||
return self.parsed_cert.subject.rfc4514_string()
|
||||
|
||||
@property
|
||||
def key_type(self):
|
||||
if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
|
||||
@ -359,7 +382,7 @@ def update_destinations(target, value, initiator):
|
||||
destination_plugin = plugins.get(value.plugin_name)
|
||||
status = FAILURE_METRIC_STATUS
|
||||
try:
|
||||
if target.private_key:
|
||||
if target.private_key or not destination_plugin.requires_key:
|
||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
except Exception as e:
|
||||
|
@ -10,7 +10,7 @@ from marshmallow import fields, validate, validates_schema, post_load, pre_load
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.common import validators, missing
|
||||
from lemur.common import missing, utils, validators
|
||||
from lemur.common.fields import ArrowDateTime, Hex
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.constants import CERTIFICATE_KEY_TYPES
|
||||
@ -206,6 +206,7 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||
|
||||
cn = fields.String()
|
||||
common_name = fields.String(attribute='cn')
|
||||
distinguished_name = fields.String()
|
||||
|
||||
not_after = fields.DateTime()
|
||||
validity_end = ArrowDateTime(attribute='not_after')
|
||||
@ -242,8 +243,8 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
|
||||
authority = fields.Nested(AssociatedAuthoritySchema, required=False)
|
||||
notify = fields.Boolean(missing=True)
|
||||
external_id = fields.String(missing=None, allow_none=True)
|
||||
private_key = fields.String(validate=validators.private_key)
|
||||
body = fields.String(required=True, validate=validators.public_certificate)
|
||||
private_key = fields.String()
|
||||
body = fields.String(required=True)
|
||||
chain = fields.String(validate=validators.public_certificate, missing=None,
|
||||
allow_none=True) # TODO this could be multiple certificates
|
||||
|
||||
@ -258,6 +259,26 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
|
||||
if not data.get('private_key'):
|
||||
raise ValidationError('Destinations require private key.')
|
||||
|
||||
@validates_schema
|
||||
def validate_cert_private_key(self, data):
|
||||
cert = None
|
||||
key = None
|
||||
if data.get('body'):
|
||||
try:
|
||||
cert = utils.parse_certificate(data['body'])
|
||||
except ValueError:
|
||||
raise ValidationError("Public certificate presented is not valid.", field_names=['body'])
|
||||
|
||||
if data.get('private_key'):
|
||||
try:
|
||||
key = utils.parse_private_key(data['private_key'])
|
||||
except ValueError:
|
||||
raise ValidationError("Private key presented is not valid.", field_names=['private_key'])
|
||||
|
||||
if cert and key:
|
||||
# Throws ValidationError
|
||||
validators.verify_private_key_match(key, cert)
|
||||
|
||||
|
||||
class CertificateExportInputSchema(LemurInputSchema):
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
@ -237,11 +237,6 @@ def upload(**kwargs):
|
||||
else:
|
||||
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.authority = kwargs.get('authority')
|
||||
cert = database.create(cert)
|
||||
@ -291,6 +286,14 @@ def create(**kwargs):
|
||||
certificate_issued.send(certificate=cert, authority=cert.authority)
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
|
||||
if isinstance(cert, PendingCertificate):
|
||||
# We need to refresh the pending certificate to avoid "Instance is not bound to a Session; "
|
||||
# "attribute refresh operation cannot proceed"
|
||||
pending_cert = database.session_query(PendingCertificate).get(cert.id)
|
||||
from lemur.common.celery import fetch_acme_cert
|
||||
if not current_app.config.get("ACME_DISABLE_AUTORESOLVE", False):
|
||||
fetch_acme_cert.apply_async((pending_cert.id,), countdown=5)
|
||||
|
||||
return cert
|
||||
|
||||
|
||||
@ -314,7 +317,7 @@ def render(args):
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
term = '%{0}%'.format(terms[1])
|
||||
term = '{0}%'.format(terms[1])
|
||||
# Exact matches for quotes. Only applies to name, issuer, and cn
|
||||
if terms[1].startswith('"') and terms[1].endswith('"'):
|
||||
term = terms[1][1:-1]
|
||||
@ -378,7 +381,8 @@ def render(args):
|
||||
now = arrow.now().format('YYYY-MM-DD')
|
||||
query = query.filter(Certificate.not_after <= to).filter(Certificate.not_after >= now)
|
||||
|
||||
return database.sort_and_page(query, Certificate, args)
|
||||
result = database.sort_and_page(query, Certificate, args)
|
||||
return result
|
||||
|
||||
|
||||
def create_csr(**csr_config):
|
||||
@ -439,10 +443,7 @@ def create_csr(**csr_config):
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
|
||||
if isinstance(private_key, bytes):
|
||||
private_key = private_key.decode('utf-8')
|
||||
).decode('utf-8')
|
||||
|
||||
csr = request.public_bytes(
|
||||
encoding=serialization.Encoding.PEM
|
||||
@ -554,6 +555,9 @@ def reissue_certificate(certificate, replace=None, user=None):
|
||||
"""
|
||||
primitives = get_certificate_primitives(certificate)
|
||||
|
||||
if primitives.get("csr"):
|
||||
# We do not want to re-use the CSR when creating a certificate because this defeats the purpose of rotation.
|
||||
del primitives["csr"]
|
||||
if not user:
|
||||
primitives['creator'] = certificate.user
|
||||
|
||||
|
@ -19,14 +19,17 @@ from lemur.factory import create_app
|
||||
from lemur.notifications.messaging import send_pending_failure_notification
|
||||
from lemur.pending_certificates import service as pending_certificate_service
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.sources.cli import clean, validate_sources
|
||||
from lemur.sources.cli import clean, sync, validate_sources
|
||||
|
||||
flask_app = create_app()
|
||||
if current_app:
|
||||
flask_app = current_app
|
||||
else:
|
||||
flask_app = create_app()
|
||||
|
||||
|
||||
def make_celery(app):
|
||||
celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],
|
||||
broker=app.config['CELERY_BROKER_URL'])
|
||||
celery = Celery(app.import_name, backend=app.config.get('CELERY_RESULT_BACKEND'),
|
||||
broker=app.config.get('CELERY_BROKER_URL'))
|
||||
celery.conf.update(app.config)
|
||||
TaskBase = celery.Task
|
||||
|
||||
@ -53,8 +56,10 @@ def fetch_acme_cert(id):
|
||||
id: an id of a PendingCertificate
|
||||
"""
|
||||
log_data = {
|
||||
"function": "{}.{}".format(__name__, sys._getframe().f_code.co_name)
|
||||
"function": "{}.{}".format(__name__, sys._getframe().f_code.co_name),
|
||||
"message": "Resolving pending certificate {}".format(id)
|
||||
}
|
||||
current_app.logger.debug(log_data)
|
||||
pending_certs = pending_certificate_service.get_pending_certs([id])
|
||||
new = 0
|
||||
failed = 0
|
||||
@ -138,12 +143,22 @@ def fetch_all_pending_acme_certs():
|
||||
"""Instantiate celery workers to resolve all pending Acme certificates"""
|
||||
pending_certs = pending_certificate_service.get_unresolved_pending_certs()
|
||||
|
||||
log_data = {
|
||||
"function": "{}.{}".format(__name__, sys._getframe().f_code.co_name),
|
||||
"message": "Starting job."
|
||||
}
|
||||
|
||||
current_app.logger.debug(log_data)
|
||||
|
||||
# We only care about certs using the acme-issuer plugin
|
||||
for cert in pending_certs:
|
||||
cert_authority = get_authority(cert.authority_id)
|
||||
if cert_authority.plugin_name == 'acme-issuer':
|
||||
if cert.last_updated == cert.date_created or datetime.now(
|
||||
timezone.utc) - cert.last_updated > timedelta(minutes=5):
|
||||
if datetime.now(timezone.utc) - cert.last_updated > timedelta(minutes=5):
|
||||
log_data["message"] = "Triggering job for cert {}".format(cert.name)
|
||||
log_data["cert_name"] = cert.name
|
||||
log_data["cert_id"] = cert.id
|
||||
current_app.logger.debug(log_data)
|
||||
fetch_acme_cert.delay(cert.id)
|
||||
|
||||
|
||||
@ -188,3 +203,26 @@ def clean_source(source):
|
||||
"""
|
||||
current_app.logger.debug("Cleaning source {}".format(source))
|
||||
clean([source], True)
|
||||
|
||||
|
||||
@celery.task()
|
||||
def sync_all_sources():
|
||||
"""
|
||||
This function will sync certificates from all sources. This function triggers one celery task per source.
|
||||
"""
|
||||
sources = validate_sources("all")
|
||||
for source in sources:
|
||||
current_app.logger.debug("Creating celery task to sync source {}".format(source.label))
|
||||
sync_source.delay(source.label)
|
||||
|
||||
|
||||
@celery.task()
|
||||
def sync_source(source):
|
||||
"""
|
||||
This celery task will sync the specified source.
|
||||
|
||||
:param source:
|
||||
:return:
|
||||
"""
|
||||
current_app.logger.debug("Syncing source {}".format(source))
|
||||
sync([source])
|
||||
|
@ -7,18 +7,21 @@ from lemur.extensions import sentry
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
|
||||
def text_to_slug(value):
|
||||
"""Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters."""
|
||||
def text_to_slug(value, joiner='-'):
|
||||
"""
|
||||
Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters.
|
||||
A series of non-alphanumeric characters is replaced with the joiner character.
|
||||
"""
|
||||
|
||||
# Strip all character accents: decompose Unicode characters and then drop combining chars.
|
||||
value = ''.join(c for c in unicodedata.normalize('NFKD', value) if not unicodedata.combining(c))
|
||||
|
||||
# Replace all remaining non-alphanumeric characters with '-'. Multiple characters get collapsed into a single dash.
|
||||
# Except, keep 'xn--' used in IDNA domain names as is.
|
||||
value = re.sub(r'[^A-Za-z0-9.]+(?<!xn--)', '-', value)
|
||||
# Replace all remaining non-alphanumeric characters with joiner string. Multiple characters get collapsed into a
|
||||
# single joiner. Except, keep 'xn--' used in IDNA domain names as is.
|
||||
value = re.sub(r'[^A-Za-z0-9.]+(?<!xn--)', joiner, value)
|
||||
|
||||
# '-' in the beginning or end of string looks ugly.
|
||||
return value.strip('-')
|
||||
return value.strip(joiner)
|
||||
|
||||
|
||||
def certificate_name(common_name, issuer, not_before, not_after, san):
|
||||
@ -224,25 +227,20 @@ def bitstrength(cert):
|
||||
|
||||
def issuer(cert):
|
||||
"""
|
||||
Gets a sane issuer name from a given certificate.
|
||||
Gets a sane issuer slug from a given certificate, stripping non-alphanumeric characters.
|
||||
|
||||
:param cert:
|
||||
:return: Issuer
|
||||
:return: Issuer slug
|
||||
"""
|
||||
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
try:
|
||||
# Try organization name or fall back to CN
|
||||
issuer = (cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) or
|
||||
cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME))
|
||||
issuer = str(issuer[0].value)
|
||||
for c in delchars:
|
||||
issuer = issuer.replace(c, "")
|
||||
return issuer
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
current_app.logger.error("Unable to get issuer! {0}".format(e))
|
||||
# Try Common Name or fall back to Organization name
|
||||
attrs = (cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) or
|
||||
cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME))
|
||||
if not attrs:
|
||||
current_app.logger.error("Unable to get issuer! Cert serial {:x}".format(cert.serial_number))
|
||||
return "Unknown"
|
||||
|
||||
return text_to_slug(attrs[0].value, '')
|
||||
|
||||
|
||||
def not_before(cert):
|
||||
"""
|
||||
|
@ -350,6 +350,7 @@ class SubjectAlternativeNameExtension(Field):
|
||||
value = value.dotted_string
|
||||
else:
|
||||
current_app.logger.warning('Unknown SubAltName type: {name}'.format(name=name))
|
||||
continue
|
||||
|
||||
general_names.append({'nameType': name_type, 'value': value})
|
||||
|
||||
|
@ -16,6 +16,7 @@ def convert_validity_years(data):
|
||||
data['validity_start'] = now.isoformat()
|
||||
|
||||
end = now.replace(years=+int(data['validity_years']))
|
||||
|
||||
if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True):
|
||||
if is_weekend(end):
|
||||
end = end.replace(days=-2)
|
||||
|
@ -12,7 +12,9 @@ import string
|
||||
import sqlalchemy
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from flask_restful.reqparse import RequestParser
|
||||
from sqlalchemy import and_, func
|
||||
|
||||
@ -46,10 +48,22 @@ def parse_certificate(body):
|
||||
:param body:
|
||||
:return:
|
||||
"""
|
||||
if isinstance(body, str):
|
||||
body = body.encode('utf-8')
|
||||
assert isinstance(body, str)
|
||||
|
||||
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):
|
||||
"""
|
||||
Parses a PEM-format private key (RSA, DSA, ECDSA or any other supported algorithm).
|
||||
|
||||
Raises ValueError for an invalid string. Raises AssertionError when passed value is not str-type.
|
||||
|
||||
:param private_key: String containing PEM private key
|
||||
"""
|
||||
assert isinstance(private_key, str)
|
||||
|
||||
return load_pem_private_key(private_key.encode('utf8'), password=None, backend=default_backend())
|
||||
|
||||
|
||||
def parse_csr(csr):
|
||||
@ -59,10 +73,9 @@ def parse_csr(csr):
|
||||
:param csr:
|
||||
:return:
|
||||
"""
|
||||
if isinstance(csr, str):
|
||||
csr = csr.encode('utf-8')
|
||||
assert isinstance(csr, str)
|
||||
|
||||
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):
|
||||
@ -211,3 +224,13 @@ def truthiness(s):
|
||||
"""If input string resembles something truthy then return True, else False."""
|
||||
|
||||
return s.lower() in ('true', 'yes', 'on', 't', '1')
|
||||
|
||||
|
||||
def find_matching_certificates_by_hash(cert, matching_certs):
|
||||
"""Given a Cryptography-formatted certificate cert, and Lemur-formatted certificates (matching_certs),
|
||||
determine if any of the certificate hashes match and return the matches."""
|
||||
matching = []
|
||||
for c in matching_certs:
|
||||
if parse_certificate(c.body).fingerprint(hashes.SHA256()) == cert.fingerprint(hashes.SHA256()):
|
||||
matching.append(c)
|
||||
return matching
|
||||
|
@ -2,14 +2,12 @@ import re
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.x509 import NameOID
|
||||
from flask import current_app
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
from lemur.common.utils import parse_certificate, is_weekend
|
||||
from lemur.domains import service as domain_service
|
||||
|
||||
|
||||
def public_certificate(body):
|
||||
@ -26,22 +24,6 @@ def public_certificate(body):
|
||||
raise ValidationError('Public certificate presented is not valid.')
|
||||
|
||||
|
||||
def private_key(key):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param key:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
if isinstance(key, bytes):
|
||||
serialization.load_pem_private_key(key, None, backend=default_backend())
|
||||
else:
|
||||
serialization.load_pem_private_key(key.encode('utf-8'), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('Private key presented is not valid.')
|
||||
|
||||
|
||||
def common_name(value):
|
||||
"""If the common name could be a domain name, apply domain validation rules."""
|
||||
# Common name could be a domain name, or a human-readable name of the subject (often used in CA names or client
|
||||
@ -66,6 +48,9 @@ def sensitive_domain(domain):
|
||||
raise ValidationError('Domain {0} does not match whitelisted domain patterns. '
|
||||
'Contact an administrator to issue the certificate.'.format(domain))
|
||||
|
||||
# Avoid circular import.
|
||||
from lemur.domains import service as domain_service
|
||||
|
||||
if any(d.sensitive for d in domain_service.get_by_name(domain)):
|
||||
raise ValidationError('Domain {0} has been marked as sensitive. '
|
||||
'Contact an administrator to issue the certificate.'.format(domain))
|
||||
@ -141,3 +126,15 @@ def dates(data):
|
||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def verify_private_key_match(key, cert, error_class=ValidationError):
|
||||
"""
|
||||
Checks that the supplied private key matches the certificate.
|
||||
|
||||
:param cert: Parsed certificate
|
||||
:param key: Parsed private key
|
||||
:param error_class: Exception class to raise on error
|
||||
"""
|
||||
if key.public_key().public_numbers() != cert.public_key().public_numbers():
|
||||
raise error_class("Private key does not match certificate.")
|
||||
|
@ -10,12 +10,12 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from inflection import underscore
|
||||
from sqlalchemy import exc, func
|
||||
from sqlalchemy import exc, func, distinct
|
||||
from sqlalchemy.orm import make_transient, lazyload
|
||||
from sqlalchemy.sql import and_, or_
|
||||
from sqlalchemy.orm import make_transient
|
||||
|
||||
from lemur.extensions import db
|
||||
from lemur.exceptions import AttrNotFound, DuplicateError
|
||||
from lemur.extensions import db
|
||||
|
||||
|
||||
def filter_none(kwargs):
|
||||
@ -273,7 +273,31 @@ def get_count(q):
|
||||
:param q:
|
||||
:return:
|
||||
"""
|
||||
count_q = q.statement.with_only_columns([func.count()]).order_by(None)
|
||||
disable_group_by = False
|
||||
if len(q._entities) > 1:
|
||||
# currently support only one entity
|
||||
raise Exception('only one entity is supported for get_count, got: %s' % q)
|
||||
entity = q._entities[0]
|
||||
if hasattr(entity, 'column'):
|
||||
# _ColumnEntity has column attr - on case: query(Model.column)...
|
||||
col = entity.column
|
||||
if q._group_by and q._distinct:
|
||||
# which query can have both?
|
||||
raise NotImplementedError
|
||||
if q._group_by or q._distinct:
|
||||
col = distinct(col)
|
||||
if q._group_by:
|
||||
# need to disable group_by and enable distinct - we can do this because we have only 1 entity
|
||||
disable_group_by = True
|
||||
count_func = func.count(col)
|
||||
else:
|
||||
# _MapperEntity doesn't have column attr - on case: query(Model)...
|
||||
count_func = func.count()
|
||||
if q._group_by and not disable_group_by:
|
||||
count_func = count_func.over(None)
|
||||
count_q = q.options(lazyload('*')).statement.with_only_columns([count_func]).order_by(None)
|
||||
if disable_group_by:
|
||||
count_q = count_q.group_by(None)
|
||||
count = q.session.execute(count_q).scalar()
|
||||
return count
|
||||
|
||||
|
@ -23,7 +23,8 @@ class DnsProvider(db.Model):
|
||||
status = Column(String(length=128), nullable=True)
|
||||
options = Column(JSON, nullable=True)
|
||||
domains = Column(JSON, nullable=True)
|
||||
certificates = relationship("Certificate", backref='dns_provider', foreign_keys='Certificate.dns_provider_id')
|
||||
certificates = relationship("Certificate", backref='dns_provider', foreign_keys='Certificate.dns_provider_id',
|
||||
lazy='dynamic')
|
||||
|
||||
def __init__(self, name, description, provider_type, credentials):
|
||||
self.name = name
|
||||
|
@ -7,13 +7,18 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Index
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
|
||||
class Domain(db.Model):
|
||||
__tablename__ = 'domains'
|
||||
__table_args__ = (
|
||||
Index('ix_domains_name_gin', "name",
|
||||
postgresql_ops={"name": "gin_trgm_ops"},
|
||||
postgresql_using='gin'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(256), index=True)
|
||||
sensitive = Column(Boolean, default=False)
|
||||
|
@ -47,7 +47,7 @@ from lemur.logs.models import Log # noqa
|
||||
from lemur.endpoints.models import Endpoint # noqa
|
||||
from lemur.policies.models import RotationPolicy # noqa
|
||||
from lemur.pending_certificates.models import PendingCertificate # noqa
|
||||
|
||||
from lemur.dns_providers.models import DnsProvider # noqa
|
||||
|
||||
manager = Manager(create_app)
|
||||
manager.add_option('-c', '--config', dest='config')
|
||||
@ -273,10 +273,11 @@ class CreateUser(Command):
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
Option('-e', '--email', dest='email', required=True),
|
||||
Option('-a', '--active', dest='active', default=True),
|
||||
Option('-r', '--roles', dest='roles', action='append', default=[])
|
||||
Option('-r', '--roles', dest='roles', action='append', default=[]),
|
||||
Option('-p', '--password', dest='password', default=None)
|
||||
)
|
||||
|
||||
def run(self, username, email, active, roles):
|
||||
def run(self, username, email, active, roles, password):
|
||||
role_objs = []
|
||||
for r in roles:
|
||||
role_obj = role_service.get_by_name(r)
|
||||
@ -286,14 +287,16 @@ class CreateUser(Command):
|
||||
sys.stderr.write("[!] Cannot find role {0}\n".format(r))
|
||||
sys.exit(1)
|
||||
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
if not password:
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
password = password1
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
user_service.create(username, password1, email, active, None, role_objs)
|
||||
user_service.create(username, password, email, active, None, role_objs)
|
||||
sys.stdout.write("[+] Created new user: {0}\n".format(username))
|
||||
|
||||
|
||||
|
@ -21,6 +21,14 @@ COLUMNS = ["notification_id", "certificate_id"]
|
||||
|
||||
|
||||
def upgrade():
|
||||
connection = op.get_bind()
|
||||
# Delete duplicate entries
|
||||
connection.execute("""\
|
||||
DELETE FROM certificate_notification_associations WHERE ctid NOT IN (
|
||||
-- Select the first tuple ID for each (notification_id, certificate_id) combination and keep that
|
||||
SELECT min(ctid) FROM certificate_notification_associations GROUP BY notification_id, certificate_id
|
||||
)
|
||||
""")
|
||||
op.create_unique_constraint(CONSTRAINT_NAME, TABLE, COLUMNS)
|
||||
|
||||
|
||||
|
31
lemur/migrations/versions/ee827d1e1974_.py
Normal file
31
lemur/migrations/versions/ee827d1e1974_.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Add pg_trgm indexes on certain attributes used for CN / Name filtering in ILIKE queries.
|
||||
|
||||
Revision ID: ee827d1e1974
|
||||
Revises: 7ead443ba911
|
||||
Create Date: 2018-11-05 09:49:40.226368
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ee827d1e1974'
|
||||
down_revision = '7ead443ba911'
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.exc import ProgrammingError
|
||||
|
||||
def upgrade():
|
||||
connection = op.get_bind()
|
||||
connection.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
|
||||
|
||||
op.create_index('ix_certificates_cn', 'certificates', ['cn'], unique=False, postgresql_ops={'cn': 'gin_trgm_ops'},
|
||||
postgresql_using='gin')
|
||||
op.create_index('ix_certificates_name', 'certificates', ['name'], unique=False,
|
||||
postgresql_ops={'name': 'gin_trgm_ops'}, postgresql_using='gin')
|
||||
op.create_index('ix_domains_name_gin', 'domains', ['name'], unique=False, postgresql_ops={'name': 'gin_trgm_ops'},
|
||||
postgresql_using='gin')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('ix_domains_name', table_name='domains')
|
||||
op.drop_index('ix_certificates_name', table_name='certificates')
|
||||
op.drop_index('ix_certificates_cn', table_name='certificates')
|
@ -8,24 +8,21 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
|
||||
import arrow
|
||||
from datetime import timedelta
|
||||
from flask import current_app
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
from lemur import database
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.certificates.schemas import certificate_notification_output_schema
|
||||
from lemur.common.utils import windowed_query
|
||||
from lemur.constants import FAILURE_METRIC_STATUS, SUCCESS_METRIC_STATUS
|
||||
from lemur.extensions import metrics, sentry
|
||||
from lemur.common.utils import windowed_query
|
||||
|
||||
from lemur.certificates.schemas import certificate_notification_output_schema
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.pending_certificates.schemas import pending_certificate_output_schema
|
||||
|
||||
from lemur.plugins import plugins
|
||||
from lemur.plugins.utils import get_plugin_option
|
||||
|
||||
@ -74,10 +71,11 @@ def get_eligible_certificates(exclude=None):
|
||||
notification_groups = []
|
||||
|
||||
for certificate in items:
|
||||
notification = needs_notification(certificate)
|
||||
notifications = needs_notification(certificate)
|
||||
|
||||
if notification:
|
||||
notification_groups.append((notification, certificate))
|
||||
if notifications:
|
||||
for notification in notifications:
|
||||
notification_groups.append((notification, certificate))
|
||||
|
||||
# group by notification
|
||||
for notification, items in groupby(notification_groups, lambda x: x[0].label):
|
||||
@ -133,11 +131,21 @@ def send_expiration_notifications(exclude):
|
||||
notification_data.append(cert_data)
|
||||
security_data.append(cert_data)
|
||||
|
||||
notification_recipient = get_plugin_option('recipients', notification.options)
|
||||
if notification_recipient:
|
||||
notification_recipient = notification_recipient.split(",")
|
||||
|
||||
if send_notification('expiration', notification_data, [owner], notification):
|
||||
success += 1
|
||||
else:
|
||||
failure += 1
|
||||
|
||||
if notification_recipient and owner != notification_recipient and security_email != notification_recipient:
|
||||
if send_notification('expiration', notification_data, notification_recipient, notification):
|
||||
success += 1
|
||||
else:
|
||||
failure += 1
|
||||
|
||||
if send_notification('expiration', security_data, security_email, notification):
|
||||
success += 1
|
||||
else:
|
||||
@ -228,6 +236,8 @@ def needs_notification(certificate):
|
||||
now = arrow.utcnow()
|
||||
days = (certificate.not_after - now).days
|
||||
|
||||
notifications = []
|
||||
|
||||
for notification in certificate.notifications:
|
||||
if not notification.active or not notification.options:
|
||||
return
|
||||
@ -248,4 +258,5 @@ def needs_notification(certificate):
|
||||
raise Exception("Invalid base unit for expiration interval: {0}".format(unit))
|
||||
|
||||
if days == interval:
|
||||
return notification
|
||||
notifications.append(notification)
|
||||
return notifications
|
||||
|
@ -10,7 +10,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy_utils import JSONType
|
||||
from sqlalchemy_utils.types.arrow import ArrowType
|
||||
|
||||
from lemur.certificates.models import get_or_increase_name
|
||||
from lemur.certificates.models import get_sequence
|
||||
from lemur.common import defaults, utils
|
||||
from lemur.database import db
|
||||
from lemur.models import pending_cert_source_associations, \
|
||||
@ -19,6 +19,28 @@ from lemur.models import pending_cert_source_associations, \
|
||||
from lemur.utils import Vault
|
||||
|
||||
|
||||
def get_or_increase_name(name, serial):
|
||||
certificates = PendingCertificate.query.filter(PendingCertificate.name.ilike('{0}%'.format(name))).all()
|
||||
|
||||
if not certificates:
|
||||
return name
|
||||
|
||||
serial_name = '{0}-{1}'.format(name, hex(int(serial))[2:].upper())
|
||||
certificates = PendingCertificate.query.filter(PendingCertificate.name.ilike('{0}%'.format(serial_name))).all()
|
||||
|
||||
if not certificates:
|
||||
return serial_name
|
||||
|
||||
ends = [0]
|
||||
root, end = get_sequence(serial_name)
|
||||
for cert in certificates:
|
||||
root, end = get_sequence(cert.name)
|
||||
if end:
|
||||
ends.append(end)
|
||||
|
||||
return '{0}-{1}'.format(root, max(ends) + 1)
|
||||
|
||||
|
||||
class PendingCertificate(db.Model):
|
||||
__tablename__ = 'pending_certs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
@ -5,7 +5,7 @@ import dns.exception
|
||||
import dns.name
|
||||
import dns.query
|
||||
import dns.resolver
|
||||
from dyn.tm.errors import DynectCreateError
|
||||
from dyn.tm.errors import DynectCreateError, DynectGetError
|
||||
from dyn.tm.session import DynectSession
|
||||
from dyn.tm.zones import Node, Zone, get_all_zones
|
||||
from flask import current_app
|
||||
@ -119,7 +119,11 @@ def delete_txt_record(change_id, account_number, domain, token):
|
||||
zone = Zone(zone_name)
|
||||
node = Node(zone_name, fqdn)
|
||||
|
||||
all_txt_records = node.get_all_records_by_type('TXT')
|
||||
try:
|
||||
all_txt_records = node.get_all_records_by_type('TXT')
|
||||
except DynectGetError:
|
||||
# No Text Records remain or host is not in the zone anymore because all records have been deleted.
|
||||
return
|
||||
for txt_record in all_txt_records:
|
||||
if txt_record.txtdata == ("{}".format(token)):
|
||||
current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn))
|
||||
|
@ -44,7 +44,11 @@ class AuthorizationRecord(object):
|
||||
class AcmeHandler(object):
|
||||
def __init__(self):
|
||||
self.dns_providers_for_domain = {}
|
||||
self.all_dns_providers = dns_provider_service.get_all_dns_providers()
|
||||
try:
|
||||
self.all_dns_providers = dns_provider_service.get_all_dns_providers()
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to fetch DNS Providers: {}".format(e))
|
||||
self.all_dns_providers = []
|
||||
|
||||
def find_dns_challenge(self, authorizations):
|
||||
dns_challenges = []
|
||||
@ -211,12 +215,18 @@ class AcmeHandler(object):
|
||||
:return: dns_providers: List of DNS providers that have the correct zone.
|
||||
"""
|
||||
self.dns_providers_for_domain[domain] = []
|
||||
match_length = 0
|
||||
for dns_provider in self.all_dns_providers:
|
||||
if not dns_provider.domains:
|
||||
continue
|
||||
for name in dns_provider.domains:
|
||||
if domain.endswith("." + name):
|
||||
self.dns_providers_for_domain[domain].append(dns_provider)
|
||||
if len(name) > match_length:
|
||||
self.dns_providers_for_domain[domain] = [dns_provider]
|
||||
match_length = len(name)
|
||||
elif len(name) == match_length:
|
||||
self.dns_providers_for_domain[domain].append(dns_provider)
|
||||
|
||||
return self.dns_providers_for_domain
|
||||
|
||||
def finalize_authorizations(self, acme_client, authorizations):
|
||||
@ -329,9 +339,10 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
|
||||
self.acme = AcmeHandler()
|
||||
|
||||
def get_dns_provider(self, type):
|
||||
self.acme = AcmeHandler()
|
||||
|
||||
provider_types = {
|
||||
'cloudflare': cloudflare,
|
||||
'dyn': dyn,
|
||||
@ -343,12 +354,14 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||
return provider
|
||||
|
||||
def get_all_zones(self, dns_provider):
|
||||
self.acme = AcmeHandler()
|
||||
dns_provider_options = json.loads(dns_provider.credentials)
|
||||
account_number = dns_provider_options.get("account_id")
|
||||
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
|
||||
return dns_provider_plugin.get_zones(account_number=account_number)
|
||||
|
||||
def get_ordered_certificate(self, pending_cert):
|
||||
self.acme = AcmeHandler()
|
||||
acme_client, registration = self.acme.setup_acme_client(pending_cert.authority)
|
||||
order_info = authorization_service.get(pending_cert.external_id)
|
||||
if pending_cert.dns_provider_id:
|
||||
@ -384,6 +397,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||
return cert
|
||||
|
||||
def get_ordered_certificates(self, pending_certs):
|
||||
self.acme = AcmeHandler()
|
||||
pending = []
|
||||
certs = []
|
||||
for pending_cert in pending_certs:
|
||||
@ -466,6 +480,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||
:param issuer_options:
|
||||
:return: :raise Exception:
|
||||
"""
|
||||
self.acme = AcmeHandler()
|
||||
authority = issuer_options.get('authority')
|
||||
create_immediately = issuer_options.get('create_immediately', False)
|
||||
acme_client, registration = self.acme.setup_acme_client(authority)
|
||||
|
@ -95,7 +95,7 @@ def get_all_elbs_v2(**kwargs):
|
||||
|
||||
|
||||
@sts_client('elbv2')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
|
||||
"""
|
||||
Get a listener ARN from an endpoint.
|
||||
@ -113,7 +113,7 @@ def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
|
||||
|
||||
|
||||
@sts_client('elb')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def get_elbs(**kwargs):
|
||||
"""
|
||||
Fetches one page elb objects for a given account and region.
|
||||
@ -123,7 +123,7 @@ def get_elbs(**kwargs):
|
||||
|
||||
|
||||
@sts_client('elbv2')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def get_elbs_v2(**kwargs):
|
||||
"""
|
||||
Fetches one page of elb objects for a given account and region.
|
||||
@ -136,7 +136,7 @@ def get_elbs_v2(**kwargs):
|
||||
|
||||
|
||||
@sts_client('elbv2')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def describe_listeners_v2(**kwargs):
|
||||
"""
|
||||
Fetches one page of listener objects for a given elb arn.
|
||||
@ -149,7 +149,7 @@ def describe_listeners_v2(**kwargs):
|
||||
|
||||
|
||||
@sts_client('elb')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
||||
"""
|
||||
Fetching all policies currently associated with an ELB.
|
||||
@ -161,7 +161,7 @@ def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
||||
|
||||
|
||||
@sts_client('elbv2')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def describe_ssl_policies_v2(policy_names, **kwargs):
|
||||
"""
|
||||
Fetching all policies currently associated with an ELB.
|
||||
@ -173,7 +173,7 @@ def describe_ssl_policies_v2(policy_names, **kwargs):
|
||||
|
||||
|
||||
@sts_client('elb')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def describe_load_balancer_types(policies, **kwargs):
|
||||
"""
|
||||
Describe the policies with policy details.
|
||||
@ -185,7 +185,7 @@ def describe_load_balancer_types(policies, **kwargs):
|
||||
|
||||
|
||||
@sts_client('elb')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def attach_certificate(name, port, certificate_id, **kwargs):
|
||||
"""
|
||||
Attaches a certificate to a listener, throws exception
|
||||
@ -205,7 +205,7 @@ def attach_certificate(name, port, certificate_id, **kwargs):
|
||||
|
||||
|
||||
@sts_client('elbv2')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def attach_certificate_v2(listener_arn, port, certificates, **kwargs):
|
||||
"""
|
||||
Attaches a certificate to a listener, throws exception
|
||||
|
@ -52,7 +52,7 @@ def create_arn_from_cert(account_number, region, certificate_name):
|
||||
|
||||
|
||||
@sts_client('iam')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
|
||||
"""
|
||||
Upload a certificate to AWS
|
||||
@ -64,6 +64,7 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
|
||||
:param path:
|
||||
:return:
|
||||
"""
|
||||
assert isinstance(private_key, str)
|
||||
client = kwargs.pop('client')
|
||||
|
||||
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('/')
|
||||
|
||||
try:
|
||||
if isinstance(private_key, bytes):
|
||||
private_key = private_key.decode("utf-8")
|
||||
if cert_chain:
|
||||
return client.upload_server_certificate(
|
||||
Path=path,
|
||||
@ -95,7 +94,7 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
|
||||
|
||||
|
||||
@sts_client('iam')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def delete_cert(cert_name, **kwargs):
|
||||
"""
|
||||
Delete a certificate from AWS
|
||||
@ -112,7 +111,7 @@ def delete_cert(cert_name, **kwargs):
|
||||
|
||||
|
||||
@sts_client('iam')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def get_certificate(name, **kwargs):
|
||||
"""
|
||||
Retrieves an SSL certificate.
|
||||
@ -126,7 +125,7 @@ def get_certificate(name, **kwargs):
|
||||
|
||||
|
||||
@sts_client('iam')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||
def get_certificates(**kwargs):
|
||||
"""
|
||||
Fetches one page of certificate objects for a given account.
|
||||
|
@ -35,8 +35,8 @@
|
||||
from flask import current_app
|
||||
|
||||
from lemur.plugins import lemur_aws as aws
|
||||
from lemur.plugins.lemur_aws import iam, s3, elb, ec2
|
||||
from lemur.plugins.bases import DestinationPlugin, ExportDestinationPlugin, SourcePlugin
|
||||
from lemur.plugins.lemur_aws import iam, s3, elb, ec2
|
||||
|
||||
|
||||
def get_region_from_dns(dns):
|
||||
@ -163,7 +163,7 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
'name': 'accountNumber',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^[0-9]{12,12}$/',
|
||||
'validation': '[0-9]{12}',
|
||||
'helpMessage': 'Must be a valid AWS account number!',
|
||||
},
|
||||
{
|
||||
@ -279,14 +279,14 @@ class S3DestinationPlugin(ExportDestinationPlugin):
|
||||
'name': 'bucket',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^$|\s+/',
|
||||
'validation': '[0-9a-z.-]{3,63}',
|
||||
'helpMessage': 'Must be a valid S3 bucket name!',
|
||||
},
|
||||
{
|
||||
'name': 'accountNumber',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^[0-9]{12,12}$/',
|
||||
'validation': '[0-9]{12}',
|
||||
'helpMessage': 'A valid AWS account number with permission to access S3',
|
||||
},
|
||||
{
|
||||
@ -308,7 +308,6 @@ class S3DestinationPlugin(ExportDestinationPlugin):
|
||||
'name': 'prefix',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '/^$|\s+/',
|
||||
'helpMessage': 'Must be a valid S3 object prefix!',
|
||||
}
|
||||
]
|
||||
|
@ -9,14 +9,22 @@ from functools import wraps
|
||||
|
||||
import boto3
|
||||
|
||||
from botocore.config import Config
|
||||
from flask import current_app
|
||||
|
||||
|
||||
config = Config(
|
||||
retries=dict(
|
||||
max_attempts=20
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def sts_client(service, service_type='client'):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
sts = boto3.client('sts')
|
||||
sts = boto3.client('sts', config=config)
|
||||
arn = 'arn:aws:iam::{0}:role/{1}'.format(
|
||||
kwargs.pop('account_number'),
|
||||
current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')
|
||||
@ -31,7 +39,8 @@ def sts_client(service, service_type='client'):
|
||||
region_name=kwargs.pop('region', 'us-east-1'),
|
||||
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||
aws_session_token=role['Credentials']['SessionToken']
|
||||
aws_session_token=role['Credentials']['SessionToken'],
|
||||
config=config
|
||||
)
|
||||
kwargs['client'] = client
|
||||
elif service_type == 'resource':
|
||||
@ -40,7 +49,8 @@ def sts_client(service, service_type='client'):
|
||||
region_name=kwargs.pop('region', 'us-east-1'),
|
||||
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||
aws_session_token=role['Credentials']['SessionToken']
|
||||
aws_session_token=role['Credentials']['SessionToken'],
|
||||
config=config
|
||||
)
|
||||
kwargs['resource'] = resource
|
||||
return f(*args, **kwargs)
|
||||
|
@ -4,7 +4,7 @@ from moto import mock_sts, mock_elb
|
||||
|
||||
@mock_sts()
|
||||
@mock_elb()
|
||||
def test_get_all_elbs(app):
|
||||
def test_get_all_elbs(app, aws_credentials):
|
||||
from lemur.plugins.lemur_aws.elb import get_all_elbs
|
||||
client = boto3.client('elb', region_name='us-east-1')
|
||||
|
||||
|
@ -10,6 +10,9 @@
|
||||
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
from flask import current_app
|
||||
|
||||
@ -48,6 +51,21 @@ class CfsslIssuerPlugin(IssuerPlugin):
|
||||
data = {'certificate_request': csr}
|
||||
data = json.dumps(data)
|
||||
|
||||
try:
|
||||
hex_key = current_app.config.get('CFSSL_KEY')
|
||||
key = bytes.fromhex(hex_key)
|
||||
except (ValueError, NameError):
|
||||
# unable to find CFSSL_KEY in config, continue using normal sign method
|
||||
pass
|
||||
else:
|
||||
data = data.encode()
|
||||
|
||||
token = base64.b64encode(hmac.new(key, data, digestmod=hashlib.sha256).digest())
|
||||
data = base64.b64encode(data)
|
||||
|
||||
data = json.dumps({'token': token.decode('utf-8'), 'request': data.decode('utf-8')})
|
||||
|
||||
url = "{0}{1}".format(current_app.config.get('CFSSL_URL'), '/api/v1/cfssl/authsign')
|
||||
response = self.session.post(url, data=data.encode(encoding='utf_8', errors='strict'))
|
||||
if response.status_code > 399:
|
||||
metrics.send('cfssl_create_certificate_failure', 'counter', 1)
|
||||
|
@ -14,6 +14,7 @@ from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
|
||||
from lemur.common.utils import parse_private_key
|
||||
from lemur.plugins.bases import IssuerPlugin
|
||||
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"):
|
||||
# Issue certificate signed by an existing lemur_certificates authority
|
||||
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
|
||||
authority_key_identifier_public = options['authority'].authority_certificate.public_key
|
||||
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:
|
||||
# Issue certificate that is self-signed (new lemur_certificates root authority)
|
||||
issuer_subject = csr.subject
|
||||
issuer_private_key = private_key
|
||||
chain_cert_pem = ""
|
||||
authority_key_identifier_public = csr.public_key()
|
||||
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
|
||||
pass
|
||||
|
||||
private_key = serialization.load_pem_private_key(
|
||||
bytes(str(issuer_private_key).encode('utf-8')),
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
private_key = parse_private_key(private_key)
|
||||
|
||||
cert = builder.sign(private_key, hashes.SHA256(), default_backend())
|
||||
cert_pem = cert.public_bytes(
|
||||
|
@ -38,14 +38,9 @@ def create_csr(cert, chain, csr_tmp, key):
|
||||
:param csr_tmp:
|
||||
:param key:
|
||||
"""
|
||||
if isinstance(cert, bytes):
|
||||
cert = cert.decode('utf-8')
|
||||
|
||||
if isinstance(chain, bytes):
|
||||
chain = chain.decode('utf-8')
|
||||
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode('utf-8')
|
||||
assert isinstance(cert, str)
|
||||
assert isinstance(chain, str)
|
||||
assert isinstance(key, str)
|
||||
|
||||
with mktempfile() as key_tmp:
|
||||
with open(key_tmp, 'w') as f:
|
||||
|
@ -59,11 +59,8 @@ def split_chain(chain):
|
||||
|
||||
|
||||
def create_truststore(cert, chain, jks_tmp, alias, passphrase):
|
||||
if isinstance(cert, bytes):
|
||||
cert = cert.decode('utf-8')
|
||||
|
||||
if isinstance(chain, bytes):
|
||||
chain = chain.decode('utf-8')
|
||||
assert isinstance(cert, str)
|
||||
assert isinstance(chain, str)
|
||||
|
||||
with mktempfile() as cert_tmp:
|
||||
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):
|
||||
if isinstance(cert, bytes):
|
||||
cert = cert.decode('utf-8')
|
||||
|
||||
if isinstance(chain, bytes):
|
||||
chain = chain.decode('utf-8')
|
||||
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode('utf-8')
|
||||
assert isinstance(cert, str)
|
||||
assert isinstance(chain, str)
|
||||
assert isinstance(key, str)
|
||||
|
||||
# Create PKCS12 keystore from private key and public certificate
|
||||
with mktempfile() as cert_tmp:
|
||||
|
@ -11,31 +11,37 @@
|
||||
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
import urllib
|
||||
import requests
|
||||
import itertools
|
||||
import os
|
||||
|
||||
from lemur.certificates.models import Certificate
|
||||
import requests
|
||||
from flask import current_app
|
||||
|
||||
from lemur.common.defaults import common_name
|
||||
from lemur.common.utils import parse_certificate
|
||||
from lemur.plugins.bases import DestinationPlugin
|
||||
|
||||
DEFAULT_API_VERSION = 'v1'
|
||||
|
||||
|
||||
def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data):
|
||||
|
||||
# _resolve_uri(k8s_base_uri, namespace, kind, name, api_ver=DEFAULT_API_VERSION)
|
||||
url = _resolve_uri(k8s_base_uri, namespace, kind)
|
||||
current_app.logger.debug("K8S POST request URL: %s", url)
|
||||
|
||||
create_resp = k8s_api.post(url, json=data)
|
||||
current_app.logger.debug("K8S POST response: %s", create_resp)
|
||||
|
||||
if 200 <= create_resp.status_code <= 299:
|
||||
return None
|
||||
|
||||
elif create_resp.json()['reason'] != 'AlreadyExists':
|
||||
elif create_resp.json().get('reason', '') != 'AlreadyExists':
|
||||
return create_resp.content
|
||||
|
||||
update_resp = k8s_api.put(_resolve_uri(k8s_base_uri, namespace, kind, name), json=data)
|
||||
url = _resolve_uri(k8s_base_uri, namespace, kind, name)
|
||||
current_app.logger.debug("K8S PUT request URL: %s", url)
|
||||
|
||||
update_resp = k8s_api.put(url, json=data)
|
||||
current_app.logger.debug("K8S PUT response: %s", update_resp)
|
||||
|
||||
if not 200 <= update_resp.status_code <= 299:
|
||||
return update_resp.content
|
||||
@ -43,11 +49,12 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data):
|
||||
return
|
||||
|
||||
|
||||
def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION,):
|
||||
def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION):
|
||||
api_group = 'api'
|
||||
if '/' in api_ver:
|
||||
api_group = 'apis'
|
||||
return '{base}/{api_group}/{api_ver}/namespaces'.format(base=k8s_base_uri, api_group=api_group, api_ver=api_ver) + ('/' + namespace if namespace else '')
|
||||
return '{base}/{api_group}/{api_ver}/namespaces'.format(base=k8s_base_uri, api_group=api_group, api_ver=api_ver) + (
|
||||
'/' + namespace if namespace else '')
|
||||
|
||||
|
||||
def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_VERSION):
|
||||
@ -61,6 +68,41 @@ def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_V
|
||||
]))
|
||||
|
||||
|
||||
# Performs Base64 encoding of string to string using the base64.b64encode() function
|
||||
# which encodes bytes to bytes.
|
||||
def base64encode(string):
|
||||
return base64.b64encode(string.encode()).decode()
|
||||
|
||||
|
||||
def build_secret(secret_format, secret_name, body, private_key, cert_chain):
|
||||
secret = {
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Secret',
|
||||
'type': 'Opaque',
|
||||
'metadata': {
|
||||
'name': secret_name,
|
||||
}
|
||||
}
|
||||
if secret_format == 'Full':
|
||||
secret['data'] = {
|
||||
'combined.pem': base64encode('%s\n%s' % (body, private_key)),
|
||||
'ca.crt': base64encode(cert_chain),
|
||||
'service.key': base64encode(private_key),
|
||||
'service.crt': base64encode(body),
|
||||
}
|
||||
if secret_format == 'TLS':
|
||||
secret['type'] = 'kubernetes.io/tls'
|
||||
secret['data'] = {
|
||||
'tls.crt': base64encode(cert_chain),
|
||||
'tls.key': base64encode(private_key)
|
||||
}
|
||||
if secret_format == 'Certificate':
|
||||
secret['data'] = {
|
||||
'tls.crt': base64encode(cert_chain),
|
||||
}
|
||||
return secret
|
||||
|
||||
|
||||
class KubernetesDestinationPlugin(DestinationPlugin):
|
||||
title = 'Kubernetes'
|
||||
slug = 'kubernetes-destination'
|
||||
@ -70,35 +112,81 @@ class KubernetesDestinationPlugin(DestinationPlugin):
|
||||
author_url = 'https://github.com/mik373/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'secretNameFormat',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
# Validation is difficult. This regex is used by kubectl to validate secret names:
|
||||
# [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
|
||||
# Allowing the insertion of "{common_name}" (or any other such placeholder}
|
||||
# at any point in the string proved very challenging and had a tendency to
|
||||
# cause my browser to hang. The specified expression will allow any valid string
|
||||
# but will also accept many invalid strings.
|
||||
'validation': '(?:[a-z0-9.-]|\\{common_name\\})+',
|
||||
'helpMessage': 'Must be a valid secret name, possibly including "{common_name}"',
|
||||
'default': '{common_name}'
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesURL',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '@(https?|http)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s]*)?$@iS',
|
||||
'required': False,
|
||||
'validation': 'https?://[a-zA-Z0-9.-]+(?::[0-9]+)?',
|
||||
'helpMessage': 'Must be a valid Kubernetes server URL!',
|
||||
'default': 'https://kubernetes.default'
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesAuthToken',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^$|\s+/',
|
||||
'required': False,
|
||||
'validation': '[0-9a-zA-Z-_.]+',
|
||||
'helpMessage': 'Must be a valid Kubernetes server Token!',
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesServerCertificate',
|
||||
'name': 'kubernetesAuthTokenFile',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^$|\s+/',
|
||||
'required': False,
|
||||
'validation': '(/[^/]+)+',
|
||||
'helpMessage': 'Must be a valid file path!',
|
||||
'default': '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesServerCertificate',
|
||||
'type': 'textarea',
|
||||
'required': False,
|
||||
'validation': '-----BEGIN CERTIFICATE-----[a-zA-Z0-9/+\\s\\r\\n]+-----END CERTIFICATE-----',
|
||||
'helpMessage': 'Must be a valid Kubernetes server Certificate!',
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesServerCertificateFile',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '(/[^/]+)+',
|
||||
'helpMessage': 'Must be a valid file path!',
|
||||
'default': '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesNamespace',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^$|\s+/',
|
||||
'required': False,
|
||||
'validation': '[a-z0-9]([-a-z0-9]*[a-z0-9])?',
|
||||
'helpMessage': 'Must be a valid Kubernetes Namespace!',
|
||||
},
|
||||
|
||||
{
|
||||
'name': 'kubernetesNamespaceFile',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '(/[^/]+)+',
|
||||
'helpMessage': 'Must be a valid file path!',
|
||||
'default': '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
|
||||
},
|
||||
{
|
||||
'name': 'secretFormat',
|
||||
'type': 'select',
|
||||
'required': True,
|
||||
'available': ['Full', 'TLS', 'Certificate'],
|
||||
'helpMessage': 'The type of Secret to create.',
|
||||
'default': 'Full'
|
||||
}
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -106,56 +194,91 @@ class KubernetesDestinationPlugin(DestinationPlugin):
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
|
||||
k8_bearer = self.get_option('kubernetesAuthToken', options)
|
||||
k8_cert = self.get_option('kubernetesServerCertificate', options)
|
||||
k8_namespace = self.get_option('kubernetesNamespace', options)
|
||||
k8_base_uri = self.get_option('kubernetesURL', options)
|
||||
try:
|
||||
k8_base_uri = self.get_option('kubernetesURL', options)
|
||||
secret_format = self.get_option('secretFormat', options)
|
||||
k8s_api = K8sSession(
|
||||
self.k8s_bearer(options),
|
||||
self.k8s_cert(options)
|
||||
)
|
||||
cn = common_name(parse_certificate(body))
|
||||
secret_name_format = self.get_option('secretNameFormat', options)
|
||||
secret_name = secret_name_format.format(common_name=cn)
|
||||
secret = build_secret(secret_format, secret_name, body, private_key, cert_chain)
|
||||
err = ensure_resource(
|
||||
k8s_api,
|
||||
k8s_base_uri=k8_base_uri,
|
||||
namespace=self.k8s_namespace(options),
|
||||
kind="secret",
|
||||
name=secret_name,
|
||||
data=secret
|
||||
)
|
||||
|
||||
k8s_api = K8sSession(k8_bearer, k8_cert)
|
||||
|
||||
cert = Certificate(body=body)
|
||||
|
||||
# in the future once runtime properties can be passed-in - use passed-in secret name
|
||||
secret_name = 'certs-' + urllib.quote_plus(cert.name)
|
||||
|
||||
err = ensure_resource(k8s_api, k8s_base_uri=k8_base_uri, namespace=k8_namespace, kind="secret", name=secret_name, data={
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Secret',
|
||||
'metadata': {
|
||||
'name': secret_name,
|
||||
},
|
||||
'data': {
|
||||
'combined.pem': base64.b64encode(body + private_key),
|
||||
'ca.crt': base64.b64encode(cert_chain),
|
||||
'service.key': base64.b64encode(private_key),
|
||||
'service.crt': base64.b64encode(body),
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.exception("Exception in upload: {}".format(e), exc_info=True)
|
||||
raise
|
||||
|
||||
if err is not None:
|
||||
current_app.logger.error("Error deploying resource: %s", err)
|
||||
raise Exception("Error uploading secret: " + err)
|
||||
|
||||
def k8s_bearer(self, options):
|
||||
bearer = self.get_option('kubernetesAuthToken', options)
|
||||
if not bearer:
|
||||
bearer_file = self.get_option('kubernetesAuthTokenFile', options)
|
||||
with open(bearer_file, "r") as file:
|
||||
bearer = file.readline()
|
||||
if bearer:
|
||||
current_app.logger.debug("Using token read from %s", bearer_file)
|
||||
else:
|
||||
raise Exception("Unable to locate token in options or from %s", bearer_file)
|
||||
else:
|
||||
current_app.logger.debug("Using token from options")
|
||||
return bearer
|
||||
|
||||
def k8s_cert(self, options):
|
||||
cert_file = self.get_option('kubernetesServerCertificateFile', options)
|
||||
cert = self.get_option('kubernetesServerCertificate', options)
|
||||
if cert:
|
||||
cert_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'k8.cert')
|
||||
with open(cert_file, "w") as text_file:
|
||||
text_file.write(cert)
|
||||
current_app.logger.debug("Using certificate from options")
|
||||
else:
|
||||
current_app.logger.debug("Using certificate from %s", cert_file)
|
||||
return cert_file
|
||||
|
||||
def k8s_namespace(self, options):
|
||||
namespace = self.get_option('kubernetesNamespace', options)
|
||||
if not namespace:
|
||||
namespace_file = self.get_option('kubernetesNamespaceFile', options)
|
||||
with open(namespace_file, "r") as file:
|
||||
namespace = file.readline()
|
||||
if namespace:
|
||||
current_app.logger.debug("Using namespace %s from %s", namespace, namespace_file)
|
||||
else:
|
||||
raise Exception("Unable to locate namespace in options or from %s", namespace_file)
|
||||
else:
|
||||
current_app.logger.debug("Using namespace %s from options", namespace)
|
||||
return namespace
|
||||
|
||||
|
||||
class K8sSession(requests.Session):
|
||||
|
||||
def __init__(self, bearer, cert):
|
||||
def __init__(self, bearer, cert_file):
|
||||
super(K8sSession, self).__init__()
|
||||
|
||||
self.headers.update({
|
||||
'Authorization': 'Bearer %s' % bearer
|
||||
})
|
||||
|
||||
k8_ca = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'k8.cert')
|
||||
self.verify = cert_file
|
||||
|
||||
with open(k8_ca, "w") as text_file:
|
||||
text_file.write(cert)
|
||||
|
||||
self.verify = k8_ca
|
||||
|
||||
def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=30, allow_redirects=True, proxies=None,
|
||||
hooks=None, stream=None, verify=None, cert=None, json=None):
|
||||
def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None,
|
||||
timeout=30, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None,
|
||||
json=None):
|
||||
"""
|
||||
This method overrides the default timeout to be 10s.
|
||||
"""
|
||||
return super(K8sSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream,
|
||||
verify, cert, json)
|
||||
return super(K8sSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout,
|
||||
allow_redirects, proxies, hooks, stream, verify, cert, json)
|
||||
|
@ -44,14 +44,9 @@ def create_pkcs12(cert, chain, p12_tmp, key, alias, passphrase):
|
||||
:param alias:
|
||||
:param passphrase:
|
||||
"""
|
||||
if isinstance(cert, bytes):
|
||||
cert = cert.decode('utf-8')
|
||||
|
||||
if isinstance(chain, bytes):
|
||||
chain = chain.decode('utf-8')
|
||||
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode('utf-8')
|
||||
assert isinstance(cert, str)
|
||||
assert isinstance(chain, str)
|
||||
assert isinstance(key, str)
|
||||
|
||||
with mktempfile() as key_tmp:
|
||||
with open(key_tmp, 'w') as f:
|
||||
|
@ -111,10 +111,19 @@ def process_options(options):
|
||||
|
||||
data['subject_alt_names'] = ",".join(get_additional_names(options))
|
||||
|
||||
if options.get('validity_end') > arrow.utcnow().replace(years=2):
|
||||
raise Exception("Verisign issued certificates cannot exceed two years in validity")
|
||||
|
||||
if options.get('validity_end'):
|
||||
period = get_default_issuance(options)
|
||||
data['specificEndDate'] = options['validity_end'].format("MM/DD/YYYY")
|
||||
data['validityPeriod'] = period
|
||||
# VeriSign (Symantec) only accepts strictly smaller than 2 year end date
|
||||
if options.get('validity_end') < arrow.utcnow().replace(years=2).replace(days=-1):
|
||||
period = get_default_issuance(options)
|
||||
data['specificEndDate'] = options['validity_end'].format("MM/DD/YYYY")
|
||||
data['validityPeriod'] = period
|
||||
else:
|
||||
# allowing Symantec website setting the end date, given the validity period
|
||||
data['validityPeriod'] = str(get_default_issuance(options))
|
||||
options.pop('validity_end', None)
|
||||
|
||||
elif options.get('validity_years'):
|
||||
if options['validity_years'] in [1, 2]:
|
||||
|
@ -93,6 +93,7 @@ def sync(source_strings):
|
||||
)
|
||||
|
||||
sentry.captureException()
|
||||
metrics.send('source_sync_fail', 'counter', 1, metric_tags={'source': source.label, 'status': status})
|
||||
|
||||
metrics.send('source_sync', 'counter', 1, metric_tags={'source': source.label, 'status': status})
|
||||
|
||||
|
@ -17,7 +17,7 @@ from lemur.endpoints import service as endpoint_service
|
||||
from lemur.destinations import service as destination_service
|
||||
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
from lemur.common.utils import parse_certificate
|
||||
from lemur.common.utils import find_matching_certificates_by_hash, parse_certificate
|
||||
from lemur.common.defaults import serial
|
||||
|
||||
from lemur.plugins.base import plugins
|
||||
@ -131,7 +131,8 @@ def sync_certificates(source, user):
|
||||
|
||||
if not exists:
|
||||
cert = parse_certificate(certificate['body'])
|
||||
exists = certificate_service.get_by_serial(serial(cert))
|
||||
matching_serials = certificate_service.get_by_serial(serial(cert))
|
||||
exists = find_matching_certificates_by_hash(cert, matching_serials)
|
||||
|
||||
if not certificate.get('owner'):
|
||||
certificate['owner'] = user.email
|
||||
|
@ -83,6 +83,8 @@
|
||||
</div>
|
||||
<!-- Certificate fields -->
|
||||
<div class="list-group-item">
|
||||
<dt>Distinguished Name</dt>
|
||||
<dd>{{ certificate.distinguishedName }}</dd>
|
||||
<dt>Certificate Authority</dt>
|
||||
<dd>{{ certificate.authority ? certificate.authority.name : "Imported" }} <span class="text-muted">({{ certificate.issuer }})</span></dd>
|
||||
<dt>Serial</dt>
|
||||
|
@ -47,7 +47,9 @@
|
||||
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i for i in item.available"
|
||||
ng-model="item.value"></select>
|
||||
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
|
||||
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value"/>
|
||||
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value" ng-pattern="item.validation"/>
|
||||
<textarea name="sub" ng-if="item.type == 'textarea'" class="form-control"
|
||||
ng-model="item.value" ng-pattern="item.validation"></textarea>
|
||||
<div ng-if="item.type == 'export-plugin'">
|
||||
<form name="exportForm" class="form-horizontal" role="form" novalidate>
|
||||
<select class="form-control" ng-model="item.value"
|
||||
@ -69,6 +71,8 @@
|
||||
ng-model="item.value">
|
||||
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control"
|
||||
ng-model="item.value" ng-pattern="item.validation"/>
|
||||
<textarea name="sub" ng-if="item.type == 'textarea'" class="form-control"
|
||||
ng-model="item.value" ng-pattern="item.validation"></textarea>
|
||||
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine"
|
||||
class="help-block">{{ item.helpMessage }}</p>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
# This is just Python which means you can inherit and tweak settings
|
||||
|
||||
import os
|
||||
|
||||
_basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
THREADS_PER_PAGE = 8
|
||||
@ -78,14 +78,12 @@ DIGICERT_API_KEY = 'api-key'
|
||||
DIGICERT_ORG_ID = 111111
|
||||
DIGICERT_ROOT = "ROOT"
|
||||
|
||||
|
||||
VERISIGN_URL = 'http://example.com'
|
||||
VERISIGN_PEM_PATH = '~/'
|
||||
VERISIGN_FIRST_NAME = 'Jim'
|
||||
VERISIGN_LAST_NAME = 'Bob'
|
||||
VERSIGN_EMAIL = 'jim@example.com'
|
||||
|
||||
|
||||
ACME_AWS_ACCOUNT_NUMBER = '11111111111'
|
||||
|
||||
ACME_PRIVATE_KEY = '''
|
||||
@ -180,6 +178,7 @@ ACME_URL = 'https://acme-v01.api.letsencrypt.org'
|
||||
ACME_EMAIL = 'jim@example.com'
|
||||
ACME_TEL = '4088675309'
|
||||
ACME_DIRECTORY_URL = 'https://acme-v01.api.letsencrypt.org'
|
||||
ACME_DISABLE_AUTORESOLVE = True
|
||||
|
||||
LDAP_AUTH = True
|
||||
LDAP_BIND_URI = 'ldap://localhost'
|
||||
|
@ -3,19 +3,18 @@ import os
|
||||
import datetime
|
||||
import pytest
|
||||
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_principal import identity_changed, Identity
|
||||
|
||||
from lemur import create_app
|
||||
from lemur.common.utils import parse_private_key
|
||||
from lemur.database import db as _db
|
||||
from lemur.auth.service import create_token
|
||||
from lemur.tests.vectors import SAN_CERT_KEY
|
||||
from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY
|
||||
|
||||
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
||||
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
|
||||
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory
|
||||
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, CryptoAuthorityFactory
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
@ -91,6 +90,13 @@ def authority(session):
|
||||
return a
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def crypto_authority(session):
|
||||
a = CryptoAuthorityFactory()
|
||||
session.commit()
|
||||
return a
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_authority(session):
|
||||
a = AsyncAuthorityFactory()
|
||||
@ -228,7 +234,12 @@ def logged_in_admin(session, app):
|
||||
|
||||
@pytest.fixture
|
||||
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
|
||||
def issuer_private_key():
|
||||
return parse_private_key(INTERMEDIATE_KEY)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -240,3 +251,11 @@ def cert_builder(private_key):
|
||||
.public_key(private_key.public_key())
|
||||
.not_valid_before(datetime.datetime(2017, 12, 22))
|
||||
.not_valid_after(datetime.datetime(2040, 1, 1)))
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def aws_credentials():
|
||||
os.environ['AWS_ACCESS_KEY_ID'] = 'testing'
|
||||
os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing'
|
||||
os.environ['AWS_SECURITY_TOKEN'] = 'testing'
|
||||
os.environ['AWS_SESSION_TOKEN'] = 'testing'
|
||||
|
@ -168,6 +168,11 @@ class AsyncAuthorityFactory(AuthorityFactory):
|
||||
authority_certificate = SubFactory(CertificateFactory)
|
||||
|
||||
|
||||
class CryptoAuthorityFactory(AuthorityFactory):
|
||||
"""Authority factory based on 'cryptography' plugin."""
|
||||
plugin = {'slug': 'cryptography-issuer'}
|
||||
|
||||
|
||||
class DestinationFactory(BaseFactory):
|
||||
"""Destination factory."""
|
||||
plugin_name = 'test-destination'
|
||||
|
@ -18,7 +18,7 @@ from lemur.domains.models import Domain
|
||||
|
||||
|
||||
from lemur.tests.vectors import VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN, CSR_STR, \
|
||||
INTERMEDIATE_CERT_STR, SAN_CERT_STR, SAN_CERT_KEY
|
||||
INTERMEDIATE_CERT_STR, SAN_CERT_STR, SAN_CERT_KEY, ROOTCA_KEY, ROOTCA_CERT_STR
|
||||
|
||||
|
||||
def test_get_or_increase_name(session, certificate):
|
||||
@ -448,6 +448,85 @@ def test_certificate_sensitive_name(client, authority, session, logged_in_user):
|
||||
assert errors['common_name'][0].startswith("Domain sensitive.example.com has been marked as sensitive")
|
||||
|
||||
|
||||
def test_certificate_upload_schema_ok(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'name': 'Jane',
|
||||
'owner': 'pwner@example.com',
|
||||
'body': SAN_CERT_STR,
|
||||
'privateKey': SAN_CERT_KEY,
|
||||
'chain': INTERMEDIATE_CERT_STR,
|
||||
'external_id': '1234',
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert not errors
|
||||
|
||||
|
||||
def test_certificate_upload_schema_minimal(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'owner': 'pwner@example.com',
|
||||
'body': SAN_CERT_STR,
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert not errors
|
||||
|
||||
|
||||
def test_certificate_upload_schema_long_chain(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'owner': 'pwner@example.com',
|
||||
'body': SAN_CERT_STR,
|
||||
'chain': INTERMEDIATE_CERT_STR + '\n' + ROOTCA_CERT_STR
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert not errors
|
||||
|
||||
|
||||
def test_certificate_upload_schema_invalid_body(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'owner': 'pwner@example.com',
|
||||
'body': 'Hereby I certify that this is a valid body',
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert errors == {'body': ['Public certificate presented is not valid.']}
|
||||
|
||||
|
||||
def test_certificate_upload_schema_invalid_pkey(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'owner': 'pwner@example.com',
|
||||
'body': SAN_CERT_STR,
|
||||
'privateKey': 'Look at me Im a private key!!111',
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert errors == {'private_key': ['Private key presented is not valid.']}
|
||||
|
||||
|
||||
def test_certificate_upload_schema_invalid_chain(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'body': SAN_CERT_STR,
|
||||
'chain': 'CHAINSAW',
|
||||
'owner': 'pwner@example.com',
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert errors == {'chain': ['Public certificate presented is not valid.']}
|
||||
|
||||
|
||||
def test_certificate_upload_schema_wrong_pkey(client):
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
data = {
|
||||
'body': SAN_CERT_STR,
|
||||
'privateKey': ROOTCA_KEY,
|
||||
'chain': INTERMEDIATE_CERT_STR,
|
||||
'owner': 'pwner@example.com',
|
||||
}
|
||||
data, errors = CertificateUploadInputSchema().load(data)
|
||||
assert errors == {'_schema': ['Private key does not match certificate.']}
|
||||
|
||||
|
||||
def test_create_basic_csr(client):
|
||||
csr_config = dict(
|
||||
common_name='example.com',
|
||||
@ -545,8 +624,11 @@ def test_create_certificate(issuer_plugin, authority, user):
|
||||
assert cert.name == 'ACustomName1'
|
||||
|
||||
|
||||
def test_reissue_certificate(issuer_plugin, authority, certificate):
|
||||
def test_reissue_certificate(issuer_plugin, crypto_authority, certificate, logged_in_user):
|
||||
from lemur.certificates.service import reissue_certificate
|
||||
|
||||
# test-authority would return a mismatching private key, so use 'cryptography-issuer' plugin instead.
|
||||
certificate.authority = crypto_authority
|
||||
new_cert = reissue_certificate(certificate)
|
||||
assert new_cert
|
||||
|
||||
@ -570,7 +652,7 @@ def test_import(user):
|
||||
assert str(cert.not_after) == '2047-12-31T22:00:00+00:00'
|
||||
assert str(cert.not_before) == '2017-12-31T22:00:00+00:00'
|
||||
assert cert.issuer == 'LemurTrustUnittestsClass1CA2018'
|
||||
assert cert.name == 'SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231-AFF2DB4F8D2D4D8E80FA382AE27C2333-2'
|
||||
assert cert.name.startswith('SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231')
|
||||
|
||||
cert = import_certificate(body=SAN_CERT_STR, chain=INTERMEDIATE_CERT_STR, private_key=SAN_CERT_KEY, owner='joe@example.com', name='ACustomName2', creator=user['user'])
|
||||
assert cert.name == 'ACustomName2'
|
||||
@ -620,6 +702,12 @@ def test_certificate_get_body(client):
|
||||
response_body = client.get(api.url_for(Certificates, certificate_id=1), headers=VALID_USER_HEADER_TOKEN).json
|
||||
assert response_body['serial'] == '211983098819107449768450703123665283596'
|
||||
assert response_body['serialHex'] == '9F7A75B39DAE4C3F9524C68B06DA6A0C'
|
||||
assert response_body['distinguishedName'] == ('CN=LemurTrust Unittests Class 1 CA 2018,'
|
||||
'O=LemurTrust Enterprises Ltd,'
|
||||
'OU=Unittesting Operations Center,'
|
||||
'C=EE,'
|
||||
'ST=N/A,'
|
||||
'L=Earth')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token,status", [
|
||||
|
@ -1,3 +1,7 @@
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
|
||||
from .vectors import SAN_CERT, WILDCARD_CERT, INTERMEDIATE_CERT
|
||||
|
||||
|
||||
@ -41,12 +45,14 @@ def test_cert_issuer(client):
|
||||
def test_text_to_slug(client):
|
||||
from lemur.common.defaults import text_to_slug
|
||||
assert text_to_slug('test - string') == 'test-string'
|
||||
assert text_to_slug('test - string', '') == 'teststring'
|
||||
# Accented characters are decomposed
|
||||
assert text_to_slug('föö bär') == 'foo-bar'
|
||||
# Melt away the Unicode Snowman
|
||||
assert text_to_slug('\u2603') == ''
|
||||
assert text_to_slug('\u2603test\u2603') == 'test'
|
||||
assert text_to_slug('snow\u2603man') == 'snow-man'
|
||||
assert text_to_slug('snow\u2603man', '') == 'snowman'
|
||||
# IDNA-encoded domain names should be kept as-is
|
||||
assert text_to_slug('xn--i1b6eqas.xn--xmpl-loa9b3671b.com') == 'xn--i1b6eqas.xn--xmpl-loa9b3671b.com'
|
||||
|
||||
@ -75,3 +81,29 @@ def test_create_name(client):
|
||||
datetime(2015, 5, 12, 0, 0, 0),
|
||||
False
|
||||
) == 'xn--mnchen-3ya.de-VertrauenswurdigAutoritat-20150507-20150512'
|
||||
|
||||
|
||||
def test_issuer(client, cert_builder, issuer_private_key):
|
||||
from lemur.common.defaults import issuer
|
||||
|
||||
assert issuer(INTERMEDIATE_CERT) == 'LemurTrustUnittestsRootCA2018'
|
||||
|
||||
# We need to override builder's issuer name
|
||||
cert_builder._issuer_name = None
|
||||
# Unicode issuer name
|
||||
cert = (cert_builder
|
||||
.issuer_name(x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, 'Vertrauenswürdig Autorität')]))
|
||||
.sign(issuer_private_key, hashes.SHA256(), default_backend()))
|
||||
assert issuer(cert) == 'VertrauenswurdigAutoritat'
|
||||
|
||||
# Fallback to 'Organization' field when issuer CN is missing
|
||||
cert = (cert_builder
|
||||
.issuer_name(x509.Name([x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, 'No Such Organization')]))
|
||||
.sign(issuer_private_key, hashes.SHA256(), default_backend()))
|
||||
assert issuer(cert) == 'NoSuchOrganization'
|
||||
|
||||
# Missing issuer name
|
||||
cert = (cert_builder
|
||||
.issuer_name(x509.Name([]))
|
||||
.sign(issuer_private_key, hashes.SHA256(), default_backend()))
|
||||
assert issuer(cert) == 'Unknown'
|
||||
|
@ -2,11 +2,10 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from lemur.pending_certificates.views import * # noqa
|
||||
from .vectors import CSR_STR, INTERMEDIATE_CERT_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, \
|
||||
VALID_USER_HEADER_TOKEN, WILDCARD_CERT_STR
|
||||
|
||||
from lemur.pending_certificates.views import * # noqa
|
||||
|
||||
|
||||
def test_increment_attempt(pending_certificate):
|
||||
from lemur.pending_certificates.service import increment_attempt
|
||||
@ -17,7 +16,8 @@ def test_increment_attempt(pending_certificate):
|
||||
|
||||
def test_create_pending_certificate(async_issuer_plugin, async_authority, user):
|
||||
from lemur.certificates.service import create
|
||||
pending_cert = create(authority=async_authority, csr=CSR_STR, owner='joe@example.com', creator=user['user'], common_name='ACommonName')
|
||||
pending_cert = create(authority=async_authority, csr=CSR_STR, owner='joe@example.com', creator=user['user'],
|
||||
common_name='ACommonName')
|
||||
assert pending_cert.external_id == '12345'
|
||||
|
||||
|
||||
|
@ -1,16 +1,28 @@
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from .vectors import SAN_CERT_KEY
|
||||
|
||||
import pytest
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.common.utils import parse_private_key
|
||||
from lemur.common.validators import verify_private_key_match
|
||||
from lemur.tests.vectors import INTERMEDIATE_CERT, SAN_CERT, SAN_CERT_KEY
|
||||
|
||||
|
||||
def test_private_key(session):
|
||||
from lemur.common.validators import private_key
|
||||
parse_private_key(SAN_CERT_KEY)
|
||||
|
||||
private_key(SAN_CERT_KEY)
|
||||
with pytest.raises(ValueError):
|
||||
parse_private_key('invalid_private_key')
|
||||
|
||||
|
||||
def test_validate_private_key(session):
|
||||
key = parse_private_key(SAN_CERT_KEY)
|
||||
|
||||
verify_private_key_match(key, SAN_CERT)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
private_key('invalid_private_key')
|
||||
# Wrong key for certificate
|
||||
verify_private_key_match(key, INTERMEDIATE_CERT)
|
||||
|
||||
|
||||
def test_sub_alt_type(session):
|
||||
|
Reference in New Issue
Block a user