Merge pull request #6 from Netflix/master

Entrust plugin : Test
This commit is contained in:
sirferl 2020-09-24 14:56:50 +02:00 committed by GitHub
commit 85da1f1c75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 478 additions and 64 deletions

View File

@ -328,6 +328,54 @@ Lemur supports sending certification expiration notifications through SES and SM
LEMUR_SECURITY_TEAM_EMAIL_INTERVALS = [15, 2] LEMUR_SECURITY_TEAM_EMAIL_INTERVALS = [15, 2]
Celery Options
---------------
To make use of automated tasks within lemur (e.g. syncing source/destinations, or reissuing ACME certificates), you
need to configure celery. See :ref:`Periodic Tasks <PeriodicTasks>` for more in depth documentation.
.. data:: CELERY_RESULT_BACKEND
:noindex:
The url to your redis backend (needs to be in the format `redis://<host>:<port>/<database>`)
.. data:: CELERY_BROKER_URL
:noindex:
The url to your redis broker (needs to be in the format `redis://<host>:<port>/<database>`)
.. data:: CELERY_IMPORTS
:noindex:
The module that celery needs to import, in our case thats `lemur.common.celery`
.. data:: CELERY_TIMEZONE
:noindex:
The timezone for celery to work with
.. data:: CELERYBEAT_SCHEDULE
:noindex:
This defines the schedule, with which the celery beat makes the worker run the specified tasks.
Since the celery module, relies on the RedisHandler, the following options also need to be set.
.. data:: REDIS_HOST
:noindex:
Hostname of your redis instance
.. data:: REDIS_PORT
:noindex:
Port on which redis is running (default: 6379)
.. data:: REDIS_DB
:noindex:
Which redis database to be used, by default redis offers databases 0-15 (default: 0)
Authentication Options Authentication Options
---------------------- ----------------------
Lemur currently supports Basic Authentication, LDAP Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily. Lemur currently supports Basic Authentication, LDAP Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
@ -1123,6 +1171,23 @@ The following configuration properties are required to use the PowerDNS ACME Plu
File/Dir path to CA Bundle: Verifies the TLS certificate was issued by a Certificate Authority in the provided CA bundle. File/Dir path to CA Bundle: Verifies the TLS certificate was issued by a Certificate Authority in the provided CA bundle.
ACME Plugin
~~~~~~~~~~~~
The following configration properties are optional for the ACME plugin to use. They allow reusing an existing ACME
account. See :ref:`Using a pre-existing ACME account <AcmeAccountReuse>` for more details.
.. data:: ACME_PRIVATE_KEY
:noindex:
This is the private key, the account was registered with (in JWK format)
.. data:: ACME_REGR
:noindex:
This is the registration for the ACME account, the most important part is the uri attribute (in JSON)
.. _CommandLineInterface: .. _CommandLineInterface:
Command Line Interface Command Line Interface

View File

@ -49,9 +49,11 @@ The amount of effort you wish to expend ensuring that Lemur has good entropy to
If you wish to generate more entropy for your system we would suggest you take a look at the following resources: If you wish to generate more entropy for your system we would suggest you take a look at the following resources:
- `WES-entropy-client <https://github.com/WhitewoodCrypto/WES-entropy-client>`_ - `WES-entropy-client <https://github.com/Virginian/WES-entropy-client>`_
- `haveged <http://www.issihosts.com/haveged/>`_ - `haveged <http://www.issihosts.com/haveged/>`_
The original *WES-entropy-client* repository by WhitewoodCrypto was removed, the link now points to a fork of it.
For additional information about OpenSSL entropy issues: For additional information about OpenSSL entropy issues:
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_ - `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
@ -313,6 +315,7 @@ It will start a shell from which you can start/stop/restart the service.
You can read all errors that might occur from /tmp/lemur.log. You can read all errors that might occur from /tmp/lemur.log.
.. _PeriodicTasks:
Periodic Tasks Periodic Tasks
============== ==============
@ -386,10 +389,17 @@ To enable celery support, you must also have configuration values that tell Cele
Here are the Celery configuration variables that should be set:: Here are the Celery configuration variables that should be set::
CELERY_RESULT_BACKEND = 'redis://your_redis_url:6379' CELERY_RESULT_BACKEND = 'redis://your_redis_url:6379'
CELERY_BROKER_URL = 'redis://your_redis_url:6379' CELERY_BROKER_URL = 'redis://your_redis_url:6379/0'
CELERY_IMPORTS = ('lemur.common.celery') CELERY_IMPORTS = ('lemur.common.celery')
CELERY_TIMEZONE = 'UTC' CELERY_TIMEZONE = 'UTC'
REDIS_HOST="your_redis_url"
REDIS_PORT=6379
REDIS_DB=0
Out of the box, every Redis instance supports 16 databases. The default database (`REDIS_DB`) is set to 0, however, you can use any of the databases from 0-15. Via `redis.conf` more databases can be supported.
In the `redis://` url, the database number can be added with a slash after the port. (defaults to 0, if omitted)
Do not forget to import crontab module in your configuration file:: Do not forget to import crontab module in your configuration file::
from celery.task.schedules import crontab from celery.task.schedules import crontab
@ -501,3 +511,47 @@ The following must be added to the config file to activate the pinning (the pinn
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE----- -----END CERTIFICATE-----
""" """
.. _AcmeAccountReuse:
LetsEncrypt: Using a pre-existing ACME account
-----------------------------------------------
Let's Encrypt allows reusing an existing ACME account, to create and especially revoke certificates. The current
implementation in the acme plugin, only allows for a single account for all ACME authorities, which might be an issue,
when you try to use Let's Encrypt together with another certificate authority that uses the ACME protocol.
To use an existing account, you need to configure the `ACME_PRIVATE_KEY` and `ACME_REGR` variables in the lemur
configuration.
`ACME_PRIVATE_KEY` needs to be in the JWK format::
{
"kty": "RSA",
"n": "yr1qBwHizA7ME_iV32bY10ILp.....",
"e": "AQAB",
"d": "llBlYhil3I.....",
"p": "-5LW2Lewogo.........",
"q": "zk6dHqHfHksd.........",
"dp": "qfe9fFIu3mu.......",
"dq": "cXFO-loeOyU.......",
"qi": "AfK1sh0_8sLTb..........."
}
Using `python-jwt` converting an existing private key in PEM format is quite easy::
import python_jwt as jwt, jwcrypto.jwk as jwk
priv_key = jwk.JWK.from_pem(b"""-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----""")
print(priv_key.export())
`ACME_REGR` needs to be a valid JSON with a `body` and a `uri` attribute, similar to this::
{"body": {}, "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/<ACCOUNT_NUMBER>"}
The URI can be retrieved from the ACME create account endpoint when creating a new account, using the existing key.

View File

@ -9,10 +9,8 @@ from datetime import timedelta
import arrow import arrow
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from flask import current_app from flask import current_app
from idna.core import InvalidCodepoint from idna.core import InvalidCodepoint
from lemur.common.utils import get_key_type_from_ec_curve
from sqlalchemy import ( from sqlalchemy import (
event, event,
Integer, Integer,
@ -154,6 +152,7 @@ class Certificate(db.Model):
Integer, ForeignKey("authorities.id", ondelete="CASCADE") Integer, ForeignKey("authorities.id", ondelete="CASCADE")
) )
rotation_policy_id = Column(Integer, ForeignKey("rotation_policies.id")) rotation_policy_id = Column(Integer, ForeignKey("rotation_policies.id"))
key_type = Column(String(128))
notifications = relationship( notifications = relationship(
"Notification", "Notification",
@ -297,6 +296,8 @@ class Certificate(db.Model):
def distinguished_name(self): def distinguished_name(self):
return self.parsed_cert.subject.rfc4514_string() return self.parsed_cert.subject.rfc4514_string()
"""
# Commenting this property as key_type is now added as a column. This code can be removed in future.
@property @property
def key_type(self): def key_type(self):
if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey): if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
@ -305,6 +306,7 @@ class Certificate(db.Model):
) )
elif isinstance(self.parsed_cert.public_key(), ec.EllipticCurvePublicKey): elif isinstance(self.parsed_cert.public_key(), ec.EllipticCurvePublicKey):
return get_key_type_from_ec_curve(self.parsed_cert.public_key().curve.name) return get_key_type_from_ec_curve(self.parsed_cert.public_key().curve.name)
"""
@property @property
def validity_remaining(self): def validity_remaining(self):

View File

@ -71,6 +71,23 @@ def parse_private_key(private_key):
) )
def get_key_type_from_certificate(body):
"""
Helper function to determine key type by pasrding given PEM certificate
:param body: PEM string
:return: Key type string
"""
parsed_cert = parse_certificate(body)
if isinstance(parsed_cert.public_key(), rsa.RSAPublicKey):
return "RSA{key_size}".format(
key_size=parsed_cert.public_key().key_size
)
elif isinstance(parsed_cert.public_key(), ec.EllipticCurvePublicKey):
return get_key_type_from_ec_curve(parsed_cert.public_key().curve.name)
def split_pem(data): def split_pem(data):
""" """
Split a string of several PEM payloads to a list of strings. Split a string of several PEM payloads to a list of strings.

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, text, Text from sqlalchemy import Column, Integer, String, text
from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy_utils import ArrowType from sqlalchemy_utils import ArrowType
@ -12,7 +12,7 @@ class DnsProvider(db.Model):
__tablename__ = "dns_providers" __tablename__ = "dns_providers"
id = Column(Integer(), primary_key=True) id = Column(Integer(), primary_key=True)
name = Column(String(length=256), unique=True, nullable=True) name = Column(String(length=256), unique=True, nullable=True)
description = Column(Text(), nullable=True) description = Column(String(length=1024), nullable=True)
provider_type = Column(String(length=256), nullable=True) provider_type = Column(String(length=256), nullable=True)
credentials = Column(Vault, nullable=True) credentials = Column(Vault, nullable=True)
api_endpoint = Column(String(length=256), nullable=True) api_endpoint = Column(String(length=256), nullable=True)

View File

@ -67,7 +67,8 @@ def run_migrations_online():
context.configure( context.configure(
connection=connection, connection=connection,
target_metadata=target_metadata, target_metadata=target_metadata,
**current_app.extensions["migrate"].configure_args **current_app.extensions["migrate"].configure_args,
compare_type=True
) )
try: try:

View File

@ -0,0 +1,26 @@
"""empty message
Revision ID: 434c29e40511
Revises: 8323a5ea723a
Create Date: 2020-09-11 17:24:51.344585
"""
# revision identifiers, used by Alembic.
revision = '434c29e40511'
down_revision = '8323a5ea723a'
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('certificates', sa.Column('key_type', sa.String(length=128), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('certificates', 'key_type')
# ### end Alembic commands ###

View File

@ -0,0 +1,114 @@
"""
This database upgrade updates the key_type information for either
still valid or expired certificates in the last 30 days. For RSA
keys, the algorithm is determined based on the key length. For
the rest of the keys, the certificate body is parsed to determine
the exact key_type information.
Each individual DB change is explicitly committed, and the respective
log is added to a file named db_upgrade.log in the current working
directory. Any error encountered while parsing a certificate will
also be logged along with the certificate ID. If faced with any issue
while running this upgrade, there is no harm in re-running the upgrade.
Each run processes only rows for which key_type information is not yet
determined.
A successful complete run will end up updating the Alembic Version to
the new Revision ID c301c59688d2. Currently, Lemur supports only RSA
and ECC certificates. This could be a long-running job depending upon
the number of DB entries it may process.
Revision ID: c301c59688d2
Revises: 434c29e40511
Create Date: 2020-09-21 14:28:50.757998
"""
# revision identifiers, used by Alembic.
revision = 'c301c59688d2'
down_revision = '434c29e40511'
from alembic import op
from sqlalchemy.sql import text
from lemur.common import utils
import time
import datetime
log_file = open('db_upgrade.log', 'a')
def upgrade():
log_file.write("\n*** Starting new run(%s) ***\n" % datetime.datetime.now())
start_time = time.time()
# Update RSA keys using the key length information
update_key_type_rsa(1024)
update_key_type_rsa(2048)
update_key_type_rsa(4096)
# Process remaining certificates. Though below method does not make any assumptions, most of the remaining ones should be ECC certs.
update_key_type()
log_file.write("--- Total %s seconds ---\n" % (time.time() - start_time))
log_file.close()
def downgrade():
# Change key type column back to null
# Going back 32 days instead of 31 to make sure no certificates are skipped
stmt = text(
"update certificates set key_type=null where not_after > CURRENT_DATE - 32"
)
op.execute(stmt)
"""
Helper methods performing updates for RSA and rest of the keys
"""
def update_key_type_rsa(bits):
log_file.write("Processing certificate with key type RSA %s\n" % bits)
stmt = text(
f"update certificates set key_type='RSA{bits}' where bits={bits} and not_after > CURRENT_DATE - 31 and key_type is null"
)
log_file.write("Query: %s\n" % stmt)
start_time = time.time()
op.execute(stmt)
commit()
log_file.write("--- %s seconds ---\n" % (time.time() - start_time))
def update_key_type():
conn = op.get_bind()
start_time = time.time()
# Loop through all certificates that are valid today or expired in the last 30 days.
for cert_id, body in conn.execute(
text(
"select id, body from certificates where bits < 1024 and not_after > CURRENT_DATE - 31 and key_type is null")
):
try:
cert_key_type = utils.get_key_type_from_certificate(body)
except ValueError as e:
log_file.write("Error in processing certificate - ID: %s Error: %s \n" % (cert_id, str(e)))
else:
log_file.write("Processing certificate - ID: %s key_type: %s\n" % (cert_id, cert_key_type))
stmt = text(
"update certificates set key_type=:key_type where id=:id"
)
stmt = stmt.bindparams(key_type=cert_key_type, id=cert_id)
op.execute(stmt)
commit()
log_file.write("--- %s seconds ---\n" % (time.time() - start_time))
def commit():
stmt = text("commit")
op.execute(stmt)

View File

@ -1,9 +1,12 @@
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
import arrow import arrow
import requests import requests
import json import json
from lemur.plugins import lemur_entrust as ENTRUST import sys
from flask import current_app from flask import current_app
from lemur.plugins import lemur_entrust as entrust
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
from lemur.extensions import metrics from lemur.extensions import metrics
from lemur.common.utils import validate_conf from lemur.common.utils import validate_conf
@ -17,24 +20,24 @@ def log_status_code(r, *args, **kwargs):
:param kwargs: :param kwargs:
:return: :return:
""" """
metrics.send("ENTRUST_status_code_{}".format(r.status_code), "counter", 1) metrics.send(f"entrust_status_code_{r.status_code}", "counter", 1)
def determine_end_date(end_date): def determine_end_date(end_date):
""" """
Determine appropriate end date Determine appropriate end date
:param end_date: :param end_date:
:return: validity_end :return: validity_end as string
""" """
# ENTRUST only allows 13 months of max certificate duration # ENTRUST only allows 13 months of max certificate duration
max_validity_end = arrow.utcnow().shift(years=1, months=+1).format('YYYY-MM-DD') max_validity_end = arrow.utcnow().shift(years=1, months=+1)
if not end_date: if not end_date:
end_date = max_validity_end end_date = max_validity_end
if end_date > max_validity_end: if end_date > max_validity_end:
end_date = max_validity_end end_date = max_validity_end
return end_date return end_date.format('YYYY-MM-DD')
def process_options(options): def process_options(options):
@ -49,7 +52,10 @@ def process_options(options):
# take the value as Cert product-type # take the value as Cert product-type
# else default to "STANDARD_SSL" # else default to "STANDARD_SSL"
authority = options.get("authority").name.upper() authority = options.get("authority").name.upper()
product_type = current_app.config.get("ENTRUST_PRODUCT_{0}".format(authority), "STANDARD_SSL") # STANDARD_SSL (cn=domain, san=www.domain),
# ADVANTAGE_SSL (cn=domain, san=[www.domain, one_more_option]),
# WILDCARD_SSL (unlimited sans, and wildcard)
product_type = current_app.config.get(f"ENTRUST_PRODUCT_{authority}", "STANDARD_SSL")
if options.get("validity_end"): if options.get("validity_end"):
validity_end = determine_end_date(options.get("validity_end")) validity_end = determine_end_date(options.get("validity_end"))
@ -67,6 +73,7 @@ def process_options(options):
"eku": "SERVER_AND_CLIENT_AUTH", "eku": "SERVER_AND_CLIENT_AUTH",
"certType": product_type, "certType": product_type,
"certExpiryDate": validity_end, "certExpiryDate": validity_end,
# "keyType": "RSA", Entrust complaining about this parameter
"tracking": tracking_data "tracking": tracking_data
} }
return data return data
@ -86,23 +93,31 @@ def handle_response(my_response):
404: "Unknown jobId", 404: "Unknown jobId",
429: "Too many requests" 429: "Too many requests"
} }
try: try:
d = json.loads(my_response.content) d = json.loads(my_response.content)
except Exception as e: except ValueError:
# catch an empty jason object here # catch an empty jason object here
d = {'errors': 'No detailled message'} d = {'response': 'No detailed message'}
s = my_response.status_code s = my_response.status_code
if s > 399: if s > 399:
raise Exception("ENTRUST error: {0}\n{1}".format(msg.get(s, s), d['errors'])) raise Exception(f"ENTRUST error: {msg.get(s, s)}\n{d['errors']}")
current_app.logger.info("Response: {0}, {1} ".format(s, d))
log_data = {
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
"message": "Response",
"status": s,
"response": d
}
current_app.logger.info(log_data)
return d return d
class EntrustIssuerPlugin(IssuerPlugin): class EntrustIssuerPlugin(IssuerPlugin):
title = "ENTRUST" title = "Entrust"
slug = "entrust-issuer" slug = "entrust-issuer"
description = "Enables the creation of certificates by ENTRUST" description = "Enables the creation of certificates by ENTRUST"
version = ENTRUST.VERSION version = entrust.VERSION
author = "sirferl" author = "sirferl"
author_url = "https://github.com/sirferl/lemur" author_url = "https://github.com/sirferl/lemur"
@ -119,7 +134,6 @@ class EntrustIssuerPlugin(IssuerPlugin):
"ENTRUST_NAME", "ENTRUST_NAME",
"ENTRUST_EMAIL", "ENTRUST_EMAIL",
"ENTRUST_PHONE", "ENTRUST_PHONE",
"ENTRUST_ISSUING",
] ]
validate_conf(current_app, required_vars) validate_conf(current_app, required_vars)
@ -142,9 +156,12 @@ class EntrustIssuerPlugin(IssuerPlugin):
:param issuer_options: :param issuer_options:
:return: :raise Exception: :return: :raise Exception:
""" """
current_app.logger.info( log_data = {
"Requesting options: {0}".format(issuer_options) "function": f"{__name__}.{sys._getframe().f_code.co_name}",
) "message": "Requesting options",
"options": issuer_options
}
current_app.logger.info(log_data)
url = current_app.config.get("ENTRUST_URL") + "/certificates" url = current_app.config.get("ENTRUST_URL") + "/certificates"
@ -156,36 +173,46 @@ class EntrustIssuerPlugin(IssuerPlugin):
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
raise Exception("Timeout for POST") raise Exception("Timeout for POST")
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
raise Exception("Error for POST {0}".format(e)) raise Exception(f"Error for POST {e}")
response_dict = handle_response(response) response_dict = handle_response(response)
external_id = response_dict['trackingId'] external_id = response_dict['trackingId']
cert = response_dict['endEntityCert'] cert = response_dict['endEntityCert']
chain = response_dict['chainCerts'][1] if len(response_dict['chainCerts']) < 2:
current_app.logger.info( # certificate signed by CA directly, no ICA included ini the chain
"Received Chain: {0}".format(chain) chain = None
) else:
chain = response_dict['chainCerts'][1]
log_data["message"] = "Received Chain"
log_data["options"] = f"chain: {chain}"
current_app.logger.info(log_data)
return cert, chain, external_id return cert, chain, external_id
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, comments):
"""Revoke a Digicert certificate.""" """Revoke an Entrust certificate."""
base_url = current_app.config.get("ENTRUST_URL") base_url = current_app.config.get("ENTRUST_URL")
# make certificate revoke request # make certificate revoke request
revoke_url = "{0}/certificates/{1}/revocations".format( revoke_url = f"{base_url}/certificates/{certificate.external_id}/revocations"
base_url, certificate.external_id if not comments or comments == '':
)
metrics.send("entrust_revoke_certificate", "counter", 1)
if comments == '' or not comments:
comments = "revoked via API" comments = "revoked via API"
data = { data = {
"crlReason": "superseded", "crlReason": "superseded", # enum (keyCompromise, affiliationChanged, superseded, cessationOfOperation)
"revocationComment": comments "revocationComment": comments
} }
response = self.session.post(revoke_url, json=data) response = self.session.post(revoke_url, json=data)
metrics.send("entrust_revoke_certificate", "counter", 1)
return handle_response(response)
data = handle_response(response) def deactivate_certificate(self, certificate):
"""Deactivates an Entrust certificate."""
base_url = current_app.config.get("ENTRUST_URL")
deactivate_url = f"{base_url}/certificates/{certificate.external_id}/deactivations"
response = self.session.post(deactivate_url)
metrics.send("entrust_deactivate_certificate", "counter", 1)
return handle_response(response)
@staticmethod @staticmethod
def create_authority(options): def create_authority(options):
@ -200,7 +227,8 @@ class EntrustIssuerPlugin(IssuerPlugin):
entrust_root = current_app.config.get("ENTRUST_ROOT") entrust_root = current_app.config.get("ENTRUST_ROOT")
entrust_issuing = current_app.config.get("ENTRUST_ISSUING") entrust_issuing = current_app.config.get("ENTRUST_ISSUING")
role = {"username": "", "password": "", "name": "entrust"} role = {"username": "", "password": "", "name": "entrust"}
current_app.logger.info("Creating Auth: {0} {1}".format(options, entrust_issuing)) current_app.logger.info(f"Creating Auth: {options} {entrust_issuing}")
# body, chain, role
return entrust_root, "", [role] return entrust_root, "", [role]
def get_ordered_certificate(self, order_id): def get_ordered_certificate(self, order_id):
@ -211,10 +239,10 @@ class EntrustIssuerPlugin(IssuerPlugin):
class EntrustSourcePlugin(SourcePlugin): class EntrustSourcePlugin(SourcePlugin):
title = "ENTRUST" title = "Entrust"
slug = "entrust-source" slug = "entrust-source"
description = "Enables the collecion of certificates" description = "Enables the collection of certificates"
version = ENTRUST.VERSION version = entrust.VERSION
author = "sirferl" author = "sirferl"
author_url = "https://github.com/sirferl/lemur" author_url = "https://github.com/sirferl/lemur"

View File

@ -0,0 +1 @@
from lemur.tests.conftest import * # noqa

View File

@ -0,0 +1,54 @@
from unittest.mock import patch, Mock
import arrow
from cryptography import x509
from lemur.plugins.lemur_entrust import plugin
def config_mock(*args):
values = {
"ENTRUST_API_CERT": "-----BEGIN CERTIFICATE-----abc-----END CERTIFICATE-----",
"ENTRUST_API_KEY": False,
"ENTRUST_API_USER": "test",
"ENTRUST_API_PASS": "password",
"ENTRUST_URL": "http",
"ENTRUST_ROOT": None,
"ENTRUST_NAME": "test",
"ENTRUST_EMAIL": "test@lemur.net",
"ENTRUST_PHONE": "0123456",
"ENTRUST_PRODUCT_ENTRUST": "ADVANTAGE_SSL"
}
return values[args[0]]
@patch("lemur.plugins.lemur_entrust.plugin.current_app")
def test_process_options(mock_current_app, authority):
mock_current_app.config.get = Mock(side_effect=config_mock)
plugin.determine_end_date = Mock(return_value=arrow.get(2020, 10, 7).format('YYYY-MM-DD'))
authority.name = "Entrust"
names = [u"one.example.com", u"two.example.com", u"three.example.com"]
options = {
"common_name": "example.com",
"owner": "bob@example.com",
"description": "test certificate",
"extensions": {"sub_alt_names": {"names": [x509.DNSName(x) for x in names]}},
"organization": "Example, Inc.",
"organizational_unit": "Example Org",
"validity_end": arrow.get(2020, 10, 7),
"authority": authority,
}
expected = {
"signingAlg": "SHA-2",
"eku": "SERVER_AND_CLIENT_AUTH",
"certType": "ADVANTAGE_SSL",
"certExpiryDate": arrow.get(2020, 10, 7).format('YYYY-MM-DD'),
"tracking": {
"requesterName": mock_current_app.config.get("ENTRUST_NAME"),
"requesterEmail": mock_current_app.config.get("ENTRUST_EMAIL"),
"requesterPhone": mock_current_app.config.get("ENTRUST_PHONE")
}
}
assert expected == plugin.process_options(options)

View File

@ -1,9 +1,18 @@
# This is just Python which means you can inherit and tweak settings # This is just Python which means you can inherit and tweak settings
import os import os
import random
import string
_basedir = os.path.abspath(os.path.dirname(__file__)) _basedir = os.path.abspath(os.path.dirname(__file__))
# generate random secrets for unittest
def get_random_secret(length):
input_ascii = string.ascii_letters + string.digits
return ''.join(random.choice(input_ascii) for i in range(length))
THREADS_PER_PAGE = 8 THREADS_PER_PAGE = 8
# General # General
@ -86,7 +95,6 @@ DIGICERT_CIS_API_KEY = "api-key"
DIGICERT_CIS_ROOTS = {"root": "ROOT"} DIGICERT_CIS_ROOTS = {"root": "ROOT"}
DIGICERT_CIS_INTERMEDIATES = {"inter": "INTERMEDIATE_CA_CERT"} DIGICERT_CIS_INTERMEDIATES = {"inter": "INTERMEDIATE_CA_CERT"}
VERISIGN_URL = "http://example.com" VERISIGN_URL = "http://example.com"
VERISIGN_PEM_PATH = "~/" VERISIGN_PEM_PATH = "~/"
VERISIGN_FIRST_NAME = "Jim" VERISIGN_FIRST_NAME = "Jim"
@ -197,3 +205,41 @@ LDAP_REQUIRED_GROUP = "Lemur Access"
LDAP_DEFAULT_ROLE = "role1" LDAP_DEFAULT_ROLE = "role1"
ALLOW_CERT_DELETION = True ALLOW_CERT_DELETION = True
ENTRUST_API_CERT = "api-cert"
ENTRUST_API_KEY = get_random_secret(32)
ENTRUST_API_USER = "user"
ENTRUST_API_PASS = get_random_secret(32)
ENTRUST_URL = "https://api.entrust.net/enterprise/v2"
ENTRUST_ROOT = """
-----BEGIN CERTIFICATE-----
MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC
VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50
cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs
IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz
dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy
NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu
dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt
dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0
aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj
YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T
RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN
cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW
wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1
U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0
jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP
BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN
BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/
jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ
Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v
1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R
nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH
VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==
-----END CERTIFICATE-----
"""
ENTRUST_NAME = "lemur"
ENTRUST_EMAIL = "lemur@example.com"
ENTRUST_PHONE = "123456"
ENTRUST_ISSUING = ""
ENTRUST_PRODUCT_ENTRUST = "ADVANTAGE_SSL"

View File

@ -2,11 +2,13 @@ import pytest
from lemur.tests.vectors import ( from lemur.tests.vectors import (
SAN_CERT, SAN_CERT,
SAN_CERT_STR,
INTERMEDIATE_CERT, INTERMEDIATE_CERT,
ROOTCA_CERT, ROOTCA_CERT,
EC_CERT_EXAMPLE, EC_CERT_EXAMPLE,
ECDSA_PRIME256V1_CERT, ECDSA_PRIME256V1_CERT,
ECDSA_SECP384r1_CERT, ECDSA_SECP384r1_CERT,
ECDSA_SECP384r1_CERT_STR,
DSA_CERT, DSA_CERT,
) )
@ -106,3 +108,9 @@ def test_is_selfsigned(selfsigned_cert):
# unsupported algorithm (DSA) # unsupported algorithm (DSA)
with pytest.raises(Exception): with pytest.raises(Exception):
is_selfsigned(DSA_CERT) is_selfsigned(DSA_CERT)
def test_get_key_type_from_certificate():
from lemur.common.utils import get_key_type_from_certificate
assert (get_key_type_from_certificate(SAN_CERT_STR) == "RSA2048")
assert (get_key_type_from_certificate(ECDSA_SECP384r1_CERT_STR) == "ECCSECP384R1")

View File

@ -39,7 +39,7 @@
"gulp-uglify": "^2.0.0", "gulp-uglify": "^2.0.0",
"gulp-useref": "^3.1.2", "gulp-useref": "^3.1.2",
"gulp-util": "^3.0.1", "gulp-util": "^3.0.1",
"http-proxy": "~1.16.2", "http-proxy": ">=1.18.1",
"jshint-stylish": "^2.2.1", "jshint-stylish": "^2.2.1",
"karma": "^4.4.1", "karma": "^4.4.1",
"karma-jasmine": "^1.1.0", "karma-jasmine": "^1.1.0",

View File

@ -4,7 +4,7 @@
# #
# pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in # pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in
# #
acme==1.7.0 # via -r requirements.txt acme==1.8.0 # via -r requirements.txt
alabaster==0.7.12 # via sphinx alabaster==0.7.12 # via sphinx
alembic-autogenerate-enums==0.0.2 # via -r requirements.txt alembic-autogenerate-enums==0.0.2 # via -r requirements.txt
alembic==1.4.2 # via -r requirements.txt, flask-migrate alembic==1.4.2 # via -r requirements.txt, flask-migrate
@ -17,8 +17,8 @@ bcrypt==3.1.7 # via -r requirements.txt, flask-bcrypt, paramiko
beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare
billiard==3.6.3.0 # via -r requirements.txt, celery billiard==3.6.3.0 # via -r requirements.txt, celery
blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven
boto3==1.14.56 # via -r requirements.txt boto3==1.15.2 # via -r requirements.txt
botocore==1.17.56 # via -r requirements.txt, boto3, s3transfer botocore==1.18.2 # via -r requirements.txt, boto3, s3transfer
celery[redis]==4.4.2 # via -r requirements.txt celery[redis]==4.4.2 # via -r requirements.txt
certifi==2020.6.20 # via -r requirements.txt, requests certifi==2020.6.20 # via -r requirements.txt, requests
certsrv==2.1.1 # via -r requirements.txt certsrv==2.1.1 # via -r requirements.txt
@ -29,7 +29,7 @@ cloudflare==2.8.13 # via -r requirements.txt
cryptography==3.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests cryptography==3.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests
dnspython3==1.15.0 # via -r requirements.txt dnspython3==1.15.0 # via -r requirements.txt
dnspython==1.15.0 # via -r requirements.txt, dnspython3 dnspython==1.15.0 # via -r requirements.txt, dnspython3
docutils==0.15.2 # via -r requirements.txt, botocore, sphinx docutils==0.15.2 # via sphinx
dyn==1.8.1 # via -r requirements.txt dyn==1.8.1 # via -r requirements.txt
flask-bcrypt==0.7.1 # via -r requirements.txt flask-bcrypt==0.7.1 # via -r requirements.txt
flask-cors==3.0.9 # via -r requirements.txt flask-cors==3.0.9 # via -r requirements.txt

View File

@ -10,22 +10,21 @@ aws-sam-translator==1.22.0 # via cfn-lint
aws-xray-sdk==2.5.0 # via moto aws-xray-sdk==2.5.0 # via moto
bandit==1.6.2 # via -r requirements-tests.in bandit==1.6.2 # via -r requirements-tests.in
black==20.8b1 # via -r requirements-tests.in black==20.8b1 # via -r requirements-tests.in
boto3==1.14.56 # via aws-sam-translator, moto boto3==1.15.2 # via aws-sam-translator, moto
boto==2.49.0 # via moto boto==2.49.0 # via moto
botocore==1.17.56 # via aws-xray-sdk, boto3, moto, s3transfer botocore==1.18.2 # via aws-xray-sdk, boto3, moto, s3transfer
certifi==2020.6.20 # via requests certifi==2020.6.20 # via requests
cffi==1.14.0 # via cryptography cffi==1.14.0 # via cryptography
cfn-lint==0.29.5 # via moto cfn-lint==0.29.5 # via moto
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==7.1.2 # via black, flask click==7.1.2 # via black, flask
coverage==5.2.1 # via -r requirements-tests.in coverage==5.3 # via -r requirements-tests.in
cryptography==3.1 # via moto, sshpubkeys cryptography==3.1 # via moto, python-jose, sshpubkeys
decorator==4.4.2 # via networkx decorator==4.4.2 # via networkx
docker==4.2.0 # via moto docker==4.2.0 # via moto
docutils==0.15.2 # via botocore ecdsa==0.14.1 # via moto, python-jose, sshpubkeys
ecdsa==0.15 # via python-jose, sshpubkeys
factory-boy==3.0.1 # via -r requirements-tests.in factory-boy==3.0.1 # via -r requirements-tests.in
faker==4.1.2 # via -r requirements-tests.in, factory-boy faker==4.1.3 # via -r requirements-tests.in, factory-boy
fakeredis==1.4.3 # via -r requirements-tests.in fakeredis==1.4.3 # via -r requirements-tests.in
flask==1.1.2 # via pytest-flask flask==1.1.2 # via pytest-flask
freezegun==1.0.0 # via -r requirements-tests.in freezegun==1.0.0 # via -r requirements-tests.in
@ -43,10 +42,10 @@ jsonpatch==1.25 # via cfn-lint
jsonpickle==1.4 # via aws-xray-sdk jsonpickle==1.4 # via aws-xray-sdk
jsonpointer==2.0 # via jsonpatch jsonpointer==2.0 # via jsonpatch
jsonschema==3.2.0 # via aws-sam-translator, cfn-lint jsonschema==3.2.0 # via aws-sam-translator, cfn-lint
markupsafe==1.1.1 # via jinja2 markupsafe==1.1.1 # via jinja2, moto
mock==4.0.2 # via moto mock==4.0.2 # via moto
more-itertools==8.2.0 # via pytest more-itertools==8.2.0 # via moto, pytest
moto==1.3.14 # via -r requirements-tests.in moto==1.3.16 # via -r requirements-tests.in
mypy-extensions==0.4.3 # via black mypy-extensions==0.4.3 # via black
networkx==2.4 # via cfn-lint networkx==2.4 # via cfn-lint
nose==1.3.7 # via -r requirements-tests.in nose==1.3.7 # via -r requirements-tests.in
@ -62,9 +61,9 @@ pyparsing==2.4.7 # via packaging
pyrsistent==0.16.0 # via jsonschema pyrsistent==0.16.0 # via jsonschema
pytest-flask==1.0.0 # via -r requirements-tests.in pytest-flask==1.0.0 # via -r requirements-tests.in
pytest-mock==3.3.1 # via -r requirements-tests.in pytest-mock==3.3.1 # via -r requirements-tests.in
pytest==6.0.1 # via -r requirements-tests.in, pytest-flask, pytest-mock pytest==6.0.2 # via -r requirements-tests.in, pytest-flask, pytest-mock
python-dateutil==2.8.1 # via botocore, faker, freezegun, moto python-dateutil==2.8.1 # via botocore, faker, freezegun, moto
python-jose==3.1.0 # via moto python-jose[cryptography]==3.1.0 # via moto
pytz==2019.3 # via moto pytz==2019.3 # via moto
pyyaml==5.3.1 # via -r requirements-tests.in, bandit, cfn-lint, moto pyyaml==5.3.1 # via -r requirements-tests.in, bandit, cfn-lint, moto
redis==3.5.3 # via fakeredis redis==3.5.3 # via fakeredis
@ -88,7 +87,7 @@ websocket-client==0.57.0 # via docker
werkzeug==1.0.1 # via flask, moto, pytest-flask werkzeug==1.0.1 # via flask, moto, pytest-flask
wrapt==1.12.1 # via aws-xray-sdk wrapt==1.12.1 # via aws-xray-sdk
xmltodict==0.12.0 # via moto xmltodict==0.12.0 # via moto
zipp==3.1.0 # via importlib-metadata zipp==3.1.0 # via importlib-metadata, moto
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# setuptools # setuptools

View File

@ -4,7 +4,7 @@
# #
# pip-compile --no-index --output-file=requirements.txt requirements.in # pip-compile --no-index --output-file=requirements.txt requirements.in
# #
acme==1.7.0 # via -r requirements.in acme==1.8.0 # via -r requirements.in
alembic-autogenerate-enums==0.0.2 # via -r requirements.in alembic-autogenerate-enums==0.0.2 # via -r requirements.in
alembic==1.4.2 # via flask-migrate alembic==1.4.2 # via flask-migrate
amqp==2.5.2 # via kombu amqp==2.5.2 # via kombu
@ -15,8 +15,8 @@ bcrypt==3.1.7 # via flask-bcrypt, paramiko
beautifulsoup4==4.9.1 # via cloudflare beautifulsoup4==4.9.1 # via cloudflare
billiard==3.6.3.0 # via celery billiard==3.6.3.0 # via celery
blinker==1.4 # via flask-mail, flask-principal, raven blinker==1.4 # via flask-mail, flask-principal, raven
boto3==1.14.56 # via -r requirements.in boto3==1.15.2 # via -r requirements.in
botocore==1.17.56 # via -r requirements.in, boto3, s3transfer botocore==1.18.2 # via -r requirements.in, boto3, s3transfer
celery[redis]==4.4.2 # via -r requirements.in celery[redis]==4.4.2 # via -r requirements.in
certifi==2020.6.20 # via -r requirements.in, requests certifi==2020.6.20 # via -r requirements.in, requests
certsrv==2.1.1 # via -r requirements.in certsrv==2.1.1 # via -r requirements.in
@ -27,7 +27,6 @@ cloudflare==2.8.13 # via -r requirements.in
cryptography==3.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests cryptography==3.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests
dnspython3==1.15.0 # via -r requirements.in dnspython3==1.15.0 # via -r requirements.in
dnspython==1.15.0 # via dnspython3 dnspython==1.15.0 # via dnspython3
docutils==0.15.2 # via botocore
dyn==1.8.1 # via -r requirements.in dyn==1.8.1 # via -r requirements.in
flask-bcrypt==0.7.1 # via -r requirements.in flask-bcrypt==0.7.1 # via -r requirements.in
flask-cors==3.0.9 # via -r requirements.in flask-cors==3.0.9 # via -r requirements.in