Merge branch 'master' into ADCS-plugin

This commit is contained in:
sirferl 2019-01-28 17:57:46 +01:00 committed by GitHub
commit c77ccdf46e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 267 additions and 88 deletions

View File

@ -15,6 +15,7 @@ from lemur import database
from lemur.common.utils import truthiness from lemur.common.utils import truthiness
from lemur.extensions import metrics from lemur.extensions import metrics
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.certificates.models import Certificate
from lemur.roles import service as role_service from lemur.roles import service as role_service
from lemur.certificates.service import upload from lemur.certificates.service import upload
@ -179,7 +180,12 @@ def render(args):
if 'active' in filt: if 'active' in filt:
query = query.filter(Authority.active == truthiness(terms[1])) query = query.filter(Authority.active == truthiness(terms[1]))
elif 'cn' in filt: elif 'cn' in filt:
query = query.join(Authority.active == truthiness(terms[1])) 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: else:
query = database.filter(query, Authority, terms) query = database.filter(query, Authority, terms)

View File

@ -19,7 +19,7 @@ from sqlalchemy.sql.expression import case, extract
from sqlalchemy_utils.types.arrow import ArrowType from sqlalchemy_utils.types.arrow import ArrowType
from werkzeug.utils import cached_property 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.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
from lemur.database import db from lemur.database import db
from lemur.domains.models import Domain from lemur.domains.models import Domain
@ -186,6 +186,18 @@ class Certificate(db.Model):
for domain in defaults.domains(cert): for domain in defaults.domains(cert):
self.domains.append(Domain(name=domain)) 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 @cached_property
def parsed_cert(self): def parsed_cert(self):
assert self.body, "Certificate body not set" assert self.body, "Certificate body not set"

View File

@ -10,7 +10,7 @@ from marshmallow import fields, validate, validates_schema, post_load, pre_load
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.authorities.schemas import AuthorityNestedOutputSchema 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.fields import ArrowDateTime, Hex
from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.constants import CERTIFICATE_KEY_TYPES from lemur.constants import CERTIFICATE_KEY_TYPES
@ -242,8 +242,8 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
authority = fields.Nested(AssociatedAuthoritySchema, required=False) authority = fields.Nested(AssociatedAuthoritySchema, required=False)
notify = fields.Boolean(missing=True) notify = fields.Boolean(missing=True)
external_id = fields.String(missing=None, allow_none=True) external_id = fields.String(missing=None, allow_none=True)
private_key = fields.String(validate=validators.private_key) private_key = fields.String()
body = fields.String(required=True, validate=validators.public_certificate) body = fields.String(required=True)
chain = fields.String(validate=validators.public_certificate, missing=None, chain = fields.String(validate=validators.public_certificate, missing=None,
allow_none=True) # TODO this could be multiple certificates allow_none=True) # TODO this could be multiple certificates
@ -258,6 +258,26 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
if not data.get('private_key'): if not data.get('private_key'):
raise ValidationError('Destinations require 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): class CertificateExportInputSchema(LemurInputSchema):
plugin = fields.Nested(PluginInputSchema) plugin = fields.Nested(PluginInputSchema)

View File

@ -20,7 +20,6 @@ from lemur.common.utils import generate_private_key, truthiness
from lemur.destinations.models import Destination from lemur.destinations.models import Destination
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.extensions import metrics, sentry, signals from lemur.extensions import metrics, sentry, signals
from lemur.models import certificate_associations
from lemur.notifications.models import Notification from lemur.notifications.models import Notification
from lemur.pending_certificates.models import PendingCertificate from lemur.pending_certificates.models import PendingCertificate
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
@ -307,7 +306,7 @@ def render(args):
if filt: if filt:
terms = filt.split(';') terms = filt.split(';')
term = '%{0}%'.format(terms[1]) term = '{0}%'.format(terms[1])
# Exact matches for quotes. Only applies to name, issuer, and cn # Exact matches for quotes. Only applies to name, issuer, and cn
if terms[1].startswith('"') and terms[1].endswith('"'): if terms[1].startswith('"') and terms[1].endswith('"'):
term = terms[1][1:-1] term = terms[1][1:-1]
@ -341,13 +340,13 @@ def render(args):
elif 'id' in terms: elif 'id' in terms:
query = query.filter(Certificate.id == cast(terms[1], Integer)) query = query.filter(Certificate.id == cast(terms[1], Integer))
elif 'name' in terms: elif 'name' in terms:
query = query.outerjoin(certificate_associations).outerjoin(Domain).filter( query = query.filter(
or_( or_(
Certificate.name.ilike(term), Certificate.name.ilike(term),
Domain.name.ilike(term), Certificate.domains.any(Domain.name.ilike(term)),
Certificate.cn.ilike(term), Certificate.cn.ilike(term),
) )
).group_by(Certificate.id) )
else: else:
query = database.filter(query, Certificate, terms) query = database.filter(query, Certificate, terms)

View File

@ -12,7 +12,9 @@ import string
import sqlalchemy import sqlalchemy
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend 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.asymmetric import rsa, ec
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from flask_restful.reqparse import RequestParser from flask_restful.reqparse import RequestParser
from sqlalchemy import and_, func from sqlalchemy import and_, func
@ -52,6 +54,20 @@ def parse_certificate(body):
return x509.load_pem_x509_certificate(body, default_backend()) return x509.load_pem_x509_certificate(body, 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.
:param private_key: String containing PEM private key
"""
if isinstance(private_key, str):
private_key = private_key.encode('utf8')
return load_pem_private_key(private_key, password=None, backend=default_backend())
def parse_csr(csr): def parse_csr(csr):
""" """
Helper function that parses a CSR. Helper function that parses a CSR.
@ -211,3 +227,13 @@ def truthiness(s):
"""If input string resembles something truthy then return True, else False.""" """If input string resembles something truthy then return True, else False."""
return s.lower() in ('true', 'yes', 'on', 't', '1') 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

View File

@ -2,14 +2,12 @@ import re
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import NameOID from cryptography.x509 import NameOID
from flask import current_app from flask import current_app
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.auth.permissions import SensitiveDomainPermission from lemur.auth.permissions import SensitiveDomainPermission
from lemur.common.utils import parse_certificate, is_weekend from lemur.common.utils import parse_certificate, is_weekend
from lemur.domains import service as domain_service
def public_certificate(body): def public_certificate(body):
@ -26,22 +24,6 @@ def public_certificate(body):
raise ValidationError('Public certificate presented is not valid.') 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): def common_name(value):
"""If the common name could be a domain name, apply domain validation rules.""" """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 # 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. ' raise ValidationError('Domain {0} does not match whitelisted domain patterns. '
'Contact an administrator to issue the certificate.'.format(domain)) '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)): if any(d.sensitive for d in domain_service.get_by_name(domain)):
raise ValidationError('Domain {0} has been marked as sensitive. ' raise ValidationError('Domain {0} has been marked as sensitive. '
'Contact an administrator to issue the certificate.'.format(domain)) '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)) raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
return data 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.")

View File

@ -5,7 +5,7 @@ import dns.exception
import dns.name import dns.name
import dns.query import dns.query
import dns.resolver 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.session import DynectSession
from dyn.tm.zones import Node, Zone, get_all_zones from dyn.tm.zones import Node, Zone, get_all_zones
from flask import current_app from flask import current_app
@ -119,7 +119,11 @@ def delete_txt_record(change_id, account_number, domain, token):
zone = Zone(zone_name) zone = Zone(zone_name)
node = Node(zone_name, fqdn) 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: for txt_record in all_txt_records:
if txt_record.txtdata == ("{}".format(token)): if txt_record.txtdata == ("{}".format(token)):
current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn))

View File

@ -95,7 +95,7 @@ def get_all_elbs_v2(**kwargs):
@sts_client('elbv2') @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): def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
""" """
Get a listener ARN from an endpoint. 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') @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): def get_elbs(**kwargs):
""" """
Fetches one page elb objects for a given account and region. Fetches one page elb objects for a given account and region.
@ -123,7 +123,7 @@ def get_elbs(**kwargs):
@sts_client('elbv2') @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): def get_elbs_v2(**kwargs):
""" """
Fetches one page of elb objects for a given account and region. Fetches one page of elb objects for a given account and region.
@ -136,7 +136,7 @@ def get_elbs_v2(**kwargs):
@sts_client('elbv2') @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): def describe_listeners_v2(**kwargs):
""" """
Fetches one page of listener objects for a given elb arn. Fetches one page of listener objects for a given elb arn.
@ -149,7 +149,7 @@ def describe_listeners_v2(**kwargs):
@sts_client('elb') @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): def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
""" """
Fetching all policies currently associated with an ELB. 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') @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): def describe_ssl_policies_v2(policy_names, **kwargs):
""" """
Fetching all policies currently associated with an ELB. Fetching all policies currently associated with an ELB.
@ -173,7 +173,7 @@ def describe_ssl_policies_v2(policy_names, **kwargs):
@sts_client('elb') @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): def describe_load_balancer_types(policies, **kwargs):
""" """
Describe the policies with policy details. Describe the policies with policy details.
@ -185,7 +185,7 @@ def describe_load_balancer_types(policies, **kwargs):
@sts_client('elb') @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): def attach_certificate(name, port, certificate_id, **kwargs):
""" """
Attaches a certificate to a listener, throws exception Attaches a certificate to a listener, throws exception
@ -205,7 +205,7 @@ def attach_certificate(name, port, certificate_id, **kwargs):
@sts_client('elbv2') @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): def attach_certificate_v2(listener_arn, port, certificates, **kwargs):
""" """
Attaches a certificate to a listener, throws exception Attaches a certificate to a listener, throws exception

View File

@ -52,7 +52,7 @@ def create_arn_from_cert(account_number, region, certificate_name):
@sts_client('iam') @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): def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
""" """
Upload a certificate to AWS Upload a certificate to AWS
@ -95,7 +95,7 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
@sts_client('iam') @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): def delete_cert(cert_name, **kwargs):
""" """
Delete a certificate from AWS Delete a certificate from AWS
@ -112,7 +112,7 @@ def delete_cert(cert_name, **kwargs):
@sts_client('iam') @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): def get_certificate(name, **kwargs):
""" """
Retrieves an SSL certificate. Retrieves an SSL certificate.
@ -126,7 +126,7 @@ def get_certificate(name, **kwargs):
@sts_client('iam') @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): def get_certificates(**kwargs):
""" """
Fetches one page of certificate objects for a given account. Fetches one page of certificate objects for a given account.

View File

@ -9,14 +9,22 @@ from functools import wraps
import boto3 import boto3
from botocore.config import Config
from flask import current_app from flask import current_app
config = Config(
retries=dict(
max_attempts=20
)
)
def sts_client(service, service_type='client'): def sts_client(service, service_type='client'):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
sts = boto3.client('sts') sts = boto3.client('sts', config=config)
arn = 'arn:aws:iam::{0}:role/{1}'.format( arn = 'arn:aws:iam::{0}:role/{1}'.format(
kwargs.pop('account_number'), kwargs.pop('account_number'),
current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur') 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'), region_name=kwargs.pop('region', 'us-east-1'),
aws_access_key_id=role['Credentials']['AccessKeyId'], aws_access_key_id=role['Credentials']['AccessKeyId'],
aws_secret_access_key=role['Credentials']['SecretAccessKey'], aws_secret_access_key=role['Credentials']['SecretAccessKey'],
aws_session_token=role['Credentials']['SessionToken'] aws_session_token=role['Credentials']['SessionToken'],
config=config
) )
kwargs['client'] = client kwargs['client'] = client
elif service_type == 'resource': elif service_type == 'resource':
@ -40,7 +49,8 @@ def sts_client(service, service_type='client'):
region_name=kwargs.pop('region', 'us-east-1'), region_name=kwargs.pop('region', 'us-east-1'),
aws_access_key_id=role['Credentials']['AccessKeyId'], aws_access_key_id=role['Credentials']['AccessKeyId'],
aws_secret_access_key=role['Credentials']['SecretAccessKey'], aws_secret_access_key=role['Credentials']['SecretAccessKey'],
aws_session_token=role['Credentials']['SessionToken'] aws_session_token=role['Credentials']['SessionToken'],
config=config
) )
kwargs['resource'] = resource kwargs['resource'] = resource
return f(*args, **kwargs) return f(*args, **kwargs)

View File

@ -17,7 +17,7 @@ from lemur.endpoints import service as endpoint_service
from lemur.destinations import service as destination_service from lemur.destinations import service as destination_service
from lemur.certificates.schemas import CertificateUploadInputSchema 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.common.defaults import serial
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
@ -126,7 +126,8 @@ def sync_certificates(source, user):
if not exists: if not exists:
cert = parse_certificate(certificate['body']) 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'): if not certificate.get('owner'):
certificate['owner'] = user.email certificate['owner'] = user.email

View File

@ -15,7 +15,7 @@ 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 RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, CryptoAuthorityFactory
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
@ -91,6 +91,13 @@ def authority(session):
return a return a
@pytest.fixture
def crypto_authority(session):
a = CryptoAuthorityFactory()
session.commit()
return a
@pytest.fixture @pytest.fixture
def async_authority(session): def async_authority(session):
a = AsyncAuthorityFactory() a = AsyncAuthorityFactory()

View File

@ -168,6 +168,11 @@ class AsyncAuthorityFactory(AuthorityFactory):
authority_certificate = SubFactory(CertificateFactory) authority_certificate = SubFactory(CertificateFactory)
class CryptoAuthorityFactory(AuthorityFactory):
"""Authority factory based on 'cryptography' plugin."""
plugin = {'slug': 'cryptography-issuer'}
class DestinationFactory(BaseFactory): class DestinationFactory(BaseFactory):
"""Destination factory.""" """Destination factory."""
plugin_name = 'test-destination' plugin_name = 'test-destination'

View File

@ -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, \ 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): def test_get_or_increase_name(session, certificate):
@ -365,6 +365,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") 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): def test_create_basic_csr(client):
csr_config = dict( csr_config = dict(
common_name='example.com', common_name='example.com',
@ -462,8 +541,11 @@ def test_create_certificate(issuer_plugin, authority, user):
assert cert.name == 'ACustomName1' 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 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) new_cert = reissue_certificate(certificate)
assert new_cert assert new_cert
@ -487,7 +569,7 @@ def test_import(user):
assert str(cert.not_after) == '2047-12-31T22:00:00+00:00' 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 str(cert.not_before) == '2017-12-31T22:00:00+00:00'
assert cert.issuer == 'LemurTrustUnittestsClass1CA2018' 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']) 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' assert cert.name == 'ACustomName2'

View File

@ -1,16 +1,28 @@
import pytest
from datetime import datetime from datetime import datetime
from .vectors import SAN_CERT_KEY
import pytest
from marshmallow.exceptions import ValidationError 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): 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): 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): def test_sub_alt_type(session):

View File

@ -5,8 +5,7 @@
# pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in # pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in
# #
aspy.yaml==1.1.1 # via pre-commit aspy.yaml==1.1.1 # via pre-commit
bleach==3.0.2 # via readme-renderer bleach==3.1.0 # via readme-renderer
cached-property==1.5.1 # via pre-commit
certifi==2018.11.29 # via requests certifi==2018.11.29 # via requests
cfgv==1.4.0 # via pre-commit cfgv==1.4.0 # via pre-commit
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
@ -15,12 +14,11 @@ flake8==3.5.0
identify==1.1.8 # via pre-commit identify==1.1.8 # via pre-commit
idna==2.8 # via requests idna==2.8 # via requests
importlib-metadata==0.8 # via pre-commit importlib-metadata==0.8 # via pre-commit
importlib-resources==1.0.2 # via pre-commit
invoke==1.2.0 invoke==1.2.0
mccabe==0.6.1 # via flake8 mccabe==0.6.1 # via flake8
nodeenv==1.3.3 nodeenv==1.3.3
pkginfo==1.5.0 # via twine pkginfo==1.5.0.1 # via twine
pre-commit==1.13.0 pre-commit==1.14.2
pycodestyle==2.3.1 # via flake8 pycodestyle==2.3.1 # via flake8
pyflakes==1.6.0 # via flake8 pyflakes==1.6.0 # via flake8
pygments==2.3.1 # via readme-renderer pygments==2.3.1 # via readme-renderer
@ -30,7 +28,7 @@ requests-toolbelt==0.8.0 # via twine
requests==2.21.0 # via requests-toolbelt, twine requests==2.21.0 # via requests-toolbelt, twine
six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer
toml==0.10.0 # via pre-commit toml==0.10.0 # via pre-commit
tqdm==4.29.0 # via twine tqdm==4.29.1 # via twine
twine==1.12.1 twine==1.12.1
urllib3==1.24.1 # via requests urllib3==1.24.1 # via requests
virtualenv==16.2.0 # via pre-commit virtualenv==16.2.0 # via pre-commit

View File

@ -7,18 +7,18 @@
acme==0.30.0 acme==0.30.0
alabaster==0.7.12 # via sphinx alabaster==0.7.12 # via sphinx
alembic-autogenerate-enums==0.0.2 alembic-autogenerate-enums==0.0.2
alembic==1.0.5 alembic==1.0.6
amqp==2.3.2 amqp==2.4.0
aniso8601==4.0.1 aniso8601==4.1.0
arrow==0.13.0 arrow==0.13.0
asn1crypto==0.24.0 asn1crypto==0.24.0
asyncpool==1.0 asyncpool==1.0
babel==2.6.0 # via sphinx babel==2.6.0 # via sphinx
bcrypt==3.1.5 bcrypt==3.1.6
billiard==3.5.0.5 billiard==3.5.0.5
blinker==1.4 blinker==1.4
boto3==1.9.75 boto3==1.9.80
botocore==1.12.75 botocore==1.12.80
celery[redis]==4.2.1 celery[redis]==4.2.1
certifi==2018.11.29 certifi==2018.11.29
cffi==1.11.5 cffi==1.11.5
@ -54,7 +54,7 @@ lockfile==0.12.2
mako==1.0.7 mako==1.0.7
markupsafe==1.1.0 markupsafe==1.1.0
marshmallow-sqlalchemy==0.15.0 marshmallow-sqlalchemy==0.15.0
marshmallow==2.17.0 marshmallow==2.18.0
mock==2.0.0 mock==2.0.0
ndg-httpsclient==0.5.1 ndg-httpsclient==0.5.1
packaging==18.0 # via sphinx packaging==18.0 # via sphinx
@ -69,7 +69,7 @@ pygments==2.3.1 # via sphinx
pyjwt==1.7.1 pyjwt==1.7.1
pynacl==1.3.0 pynacl==1.3.0
pyopenssl==18.0.0 pyopenssl==18.0.0
pyparsing==2.3.0 # via packaging pyparsing==2.3.1 # via packaging
pyrfc3339==1.1 pyrfc3339==1.1
python-dateutil==2.7.5 python-dateutil==2.7.5
python-editor==1.0.3 python-editor==1.0.3
@ -87,8 +87,8 @@ sphinx-rtd-theme==0.4.2
sphinx==1.8.3 sphinx==1.8.3
sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-httpdomain==1.7.0
sphinxcontrib-websupport==1.1.0 # via sphinx sphinxcontrib-websupport==1.1.0 # via sphinx
sqlalchemy-utils==0.33.10 sqlalchemy-utils==0.33.11
sqlalchemy==1.2.15 sqlalchemy==1.2.16
tabulate==0.8.2 tabulate==0.8.2
urllib3==1.24.1 urllib3==1.24.1
vine==1.2.0 vine==1.2.0

View File

@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography
atomicwrites==1.2.1 # via pytest atomicwrites==1.2.1 # via pytest
attrs==18.2.0 # via pytest attrs==18.2.0 # via pytest
aws-xray-sdk==0.95 # via moto aws-xray-sdk==0.95 # via moto
boto3==1.9.75 # via moto boto3==1.9.80 # via moto
boto==2.49.0 # via moto boto==2.49.0 # via moto
botocore==1.12.75 # via boto3, moto, s3transfer botocore==1.12.80 # via boto3, moto, s3transfer
certifi==2018.11.29 # via requests certifi==2018.11.29 # via requests
cffi==1.11.5 # via cryptography cffi==1.11.5 # via cryptography
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
@ -18,7 +18,7 @@ click==7.0 # via flask
coverage==4.5.2 coverage==4.5.2
cryptography==2.4.2 # via moto cryptography==2.4.2 # via moto
docker-pycreds==0.4.0 # via docker docker-pycreds==0.4.0 # via docker
docker==3.6.0 # via moto docker==3.7.0 # via moto
docutils==0.14 # via botocore docutils==0.14 # via botocore
ecdsa==0.13 # via python-jose ecdsa==0.13 # via python-jose
factory-boy==2.11.1 factory-boy==2.11.1
@ -38,7 +38,7 @@ more-itertools==5.0.0 # via pytest
moto==1.3.7 moto==1.3.7
nose==1.3.7 nose==1.3.7
pbr==5.1.1 # via mock pbr==5.1.1 # via mock
pluggy==0.8.0 # via pytest pluggy==0.8.1 # via pytest
py==1.7.0 # via pytest py==1.7.0 # via pytest
pyaml==18.11.0 # via moto pyaml==18.11.0 # via moto
pycparser==2.19 # via cffi pycparser==2.19 # via cffi
@ -46,7 +46,7 @@ pycryptodome==3.7.2 # via python-jose
pyflakes==2.0.0 pyflakes==2.0.0
pytest-flask==0.14.0 pytest-flask==0.14.0
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest==4.1.0 pytest==4.1.1
python-dateutil==2.7.5 # via botocore, faker, freezegun, moto python-dateutil==2.7.5 # via botocore, faker, freezegun, moto
python-jose==2.0.2 # via moto python-jose==2.0.2 # via moto
pytz==2018.9 # via moto pytz==2018.9 # via moto
@ -60,5 +60,5 @@ text-unidecode==1.2 # via faker
urllib3==1.24.1 # via botocore, requests urllib3==1.24.1 # via botocore, requests
websocket-client==0.54.0 # via docker websocket-client==0.54.0 # via docker
werkzeug==0.14.1 # via flask, moto, pytest-flask werkzeug==0.14.1 # via flask, moto, pytest-flask
wrapt==1.10.11 # via aws-xray-sdk wrapt==1.11.0 # via aws-xray-sdk
xmltodict==0.11.0 # via moto xmltodict==0.11.0 # via moto

View File

@ -6,17 +6,17 @@
# #
acme==0.30.0 acme==0.30.0
alembic-autogenerate-enums==0.0.2 alembic-autogenerate-enums==0.0.2
alembic==1.0.5 # via flask-migrate alembic==1.0.6 # via flask-migrate
amqp==2.3.2 # via kombu amqp==2.4.0 # via kombu
aniso8601==4.0.1 # via flask-restful aniso8601==4.1.0 # via flask-restful
arrow==0.13.0 arrow==0.13.0
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
asyncpool==1.0 asyncpool==1.0
bcrypt==3.1.5 # via flask-bcrypt, paramiko bcrypt==3.1.6 # via flask-bcrypt, paramiko
billiard==3.5.0.5 # via celery billiard==3.5.0.5 # via celery
blinker==1.4 # via flask-mail, flask-principal, raven blinker==1.4 # via flask-mail, flask-principal, raven
boto3==1.9.75 boto3==1.9.80
botocore==1.12.75 botocore==1.12.80
celery[redis]==4.2.1 celery[redis]==4.2.1
certifi==2018.11.29 certifi==2018.11.29
certsrv==2.1.0 certsrv==2.1.0
@ -52,7 +52,7 @@ lockfile==0.12.2
mako==1.0.7 # via alembic mako==1.0.7 # via alembic
markupsafe==1.1.0 # via jinja2, mako markupsafe==1.1.0 # via jinja2, mako
marshmallow-sqlalchemy==0.15.0 marshmallow-sqlalchemy==0.15.0
marshmallow==2.17.0 marshmallow==2.18.0
mock==2.0.0 # via acme mock==2.0.0 # via acme
ndg-httpsclient==0.5.1 ndg-httpsclient==0.5.1
paramiko==2.4.2 paramiko==2.4.2
@ -78,8 +78,8 @@ requests[security]==2.21.0
retrying==1.3.3 retrying==1.3.3
s3transfer==0.1.13 # via boto3 s3transfer==0.1.13 # via boto3
six==1.12.0 six==1.12.0
sqlalchemy-utils==0.33.10 sqlalchemy-utils==0.33.11
sqlalchemy==1.2.15 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils sqlalchemy==1.2.16 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils
tabulate==0.8.2 tabulate==0.8.2
urllib3==1.24.1 # via botocore, requests urllib3==1.24.1 # via botocore, requests
vine==1.2.0 # via amqp vine==1.2.0 # via amqp