Initial workon the digicert high issuance api. (#531)
This commit is contained in:
parent
734233257c
commit
b46ff4158a
|
@ -50,3 +50,17 @@ def is_weekend(date):
|
||||||
"""
|
"""
|
||||||
if date.weekday() > 5:
|
if date.weekday() > 5:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_conf(app, required_vars):
|
||||||
|
"""
|
||||||
|
Ensures that the given fields are set in the applications conf.
|
||||||
|
|
||||||
|
:param app:
|
||||||
|
:param required_vars: list
|
||||||
|
"""
|
||||||
|
for var in required_vars:
|
||||||
|
if not app.config.get(var):
|
||||||
|
raise Exception("Required variable {var} is not set, ensure that it is set in Lemur's configuration file".format(
|
||||||
|
var=var
|
||||||
|
))
|
||||||
|
|
|
@ -19,6 +19,7 @@ from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from lemur.common.health import mod as health
|
from lemur.common.health import mod as health
|
||||||
|
from lemur.common.utils import validate_conf
|
||||||
from lemur.extensions import db, migrate, principal, smtp_mail, metrics
|
from lemur.extensions import db, migrate, principal, smtp_mail, metrics
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +29,16 @@ DEFAULT_BLUEPRINTS = (
|
||||||
|
|
||||||
API_VERSION = 1
|
API_VERSION = 1
|
||||||
|
|
||||||
|
REQUIRED_VARIABLES = [
|
||||||
|
'LEMUR_SECURITY_TEAM_EMAIL',
|
||||||
|
'LEMUR_DEFAULT_ORGANIZATIONAL_UNIT',
|
||||||
|
'LEMUR_DEFAULT_ORGANIZATION',
|
||||||
|
'LEMUR_DEFAULT_LOCATION',
|
||||||
|
'LEMUR_DEFAULT_COUNTRY',
|
||||||
|
'LEMUR_DEFAULT_STATE',
|
||||||
|
'SQLALCHEMY_DATABASE_URI'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_app(app_name=None, blueprints=None, config=None):
|
def create_app(app_name=None, blueprints=None, config=None):
|
||||||
"""
|
"""
|
||||||
|
@ -104,29 +115,7 @@ def configure_app(app, config=None):
|
||||||
else:
|
else:
|
||||||
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
|
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
|
||||||
|
|
||||||
validate_conf(app)
|
validate_conf(app, REQUIRED_VARIABLES)
|
||||||
|
|
||||||
|
|
||||||
def validate_conf(app):
|
|
||||||
"""
|
|
||||||
There are a few configuration variables that are 'required' by Lemur. Here
|
|
||||||
we validate those required variables are set.
|
|
||||||
"""
|
|
||||||
required_vars = [
|
|
||||||
'LEMUR_SECURITY_TEAM_EMAIL',
|
|
||||||
'LEMUR_DEFAULT_ORGANIZATIONAL_UNIT',
|
|
||||||
'LEMUR_DEFAULT_ORGANIZATION',
|
|
||||||
'LEMUR_DEFAULT_LOCATION',
|
|
||||||
'LEMUR_DEFAULT_COUNTRY',
|
|
||||||
'LEMUR_DEFAULT_STATE',
|
|
||||||
'SQLALCHEMY_DATABASE_URI'
|
|
||||||
]
|
|
||||||
|
|
||||||
for var in required_vars:
|
|
||||||
if not app.config.get(var):
|
|
||||||
raise Exception("Required variable {var} is not set, ensure that it is set in Lemur's configuration file".format(
|
|
||||||
var=var
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def configure_extensions(app):
|
def configure_extensions(app):
|
||||||
|
|
|
@ -27,6 +27,8 @@ from lemur.plugins.bases import IssuerPlugin, SourcePlugin
|
||||||
|
|
||||||
from lemur.plugins import lemur_digicert as digicert
|
from lemur.plugins import lemur_digicert as digicert
|
||||||
|
|
||||||
|
from lemur.common.utils import validate_conf
|
||||||
|
|
||||||
|
|
||||||
def signature_hash(signing_algorithm):
|
def signature_hash(signing_algorithm):
|
||||||
"""Converts Lemur's signing algorithm into a format DigiCert understands.
|
"""Converts Lemur's signing algorithm into a format DigiCert understands.
|
||||||
|
@ -81,7 +83,22 @@ def get_issuance(options):
|
||||||
return validity_years
|
return validity_years
|
||||||
|
|
||||||
|
|
||||||
def process_options(options, csr):
|
def get_additional_names(options):
|
||||||
|
"""
|
||||||
|
Return a list of strings to be added to a SAN certificates.
|
||||||
|
|
||||||
|
:param options:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
names = []
|
||||||
|
# add SANs if present
|
||||||
|
if options.get('extensions', 'sub_alt_names'):
|
||||||
|
for san in options['extensions']['sub_alt_names']['names']:
|
||||||
|
names.append(san['value'])
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def map_fields(options, csr):
|
||||||
"""Set the incoming issuer options to DigiCert fields/options.
|
"""Set the incoming issuer options to DigiCert fields/options.
|
||||||
|
|
||||||
:param options:
|
:param options:
|
||||||
|
@ -102,14 +119,7 @@ def process_options(options, csr):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# add SANs if present
|
data['certificate']['dns_names'] = get_additional_names(options)
|
||||||
if options.get('extensions', 'sub_alt_names'):
|
|
||||||
dns_names = []
|
|
||||||
for san in options['extensions']['sub_alt_names']['names']:
|
|
||||||
dns_names.append(san['value'])
|
|
||||||
|
|
||||||
data['certificate']['dns_names'] = dns_names
|
|
||||||
|
|
||||||
validity_years = get_issuance(options)
|
validity_years = get_issuance(options)
|
||||||
data['custom_expiration_date'] = options['validity_end'].format('YYYY-MM-DD')
|
data['custom_expiration_date'] = options['validity_end'].format('YYYY-MM-DD')
|
||||||
data['validity_years'] = validity_years
|
data['validity_years'] = validity_years
|
||||||
|
@ -117,6 +127,31 @@ def process_options(options, csr):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def map_cis_fields(options, csr):
|
||||||
|
"""
|
||||||
|
MAP issuer options to DigiCert CIS fields/options.
|
||||||
|
|
||||||
|
:param options:
|
||||||
|
:param csr:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"common_name": options['common_name'],
|
||||||
|
"additional_dns_names": get_additional_names(options),
|
||||||
|
"csr": csr,
|
||||||
|
"signature_hash": signature_hash(options.get('signing_algorithm')),
|
||||||
|
"validity": {
|
||||||
|
"valid_to": options['validity_end'].format('YYYY-MM-DD')
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"name": options['organization'],
|
||||||
|
"units": [options['organizational_unit']]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def handle_response(response):
|
def handle_response(response):
|
||||||
"""
|
"""
|
||||||
Handle the DigiCert API response and any errors it might have experienced.
|
Handle the DigiCert API response and any errors it might have experienced.
|
||||||
|
@ -125,41 +160,40 @@ def handle_response(response):
|
||||||
"""
|
"""
|
||||||
metrics.send('digicert_status_code_{0}'.format(response.status_code), 'counter', 1)
|
metrics.send('digicert_status_code_{0}'.format(response.status_code), 'counter', 1)
|
||||||
|
|
||||||
if response.status_code not in [200, 201, 302, 301]:
|
if response.status_code > 399:
|
||||||
raise Exception(response.json()['message'])
|
raise Exception(response.json()['message'])
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
def verify_configuration():
|
|
||||||
"""Verify that needed configuration variables are set before plugin startup."""
|
|
||||||
if not current_app.config.get('DIGICERT_API_KEY'):
|
|
||||||
raise Exception("No Digicert API key found. Ensure that 'DIGICERT_API_KEY' is set in the Lemur conf.")
|
|
||||||
|
|
||||||
if not current_app.config.get('DIGICERT_URL'):
|
|
||||||
raise Exception("No Digicert URL found. Ensure that 'DIGICERT_URL' is set in the Lemur conf.")
|
|
||||||
|
|
||||||
if not current_app.config.get('DIGICERT_ORG_ID'):
|
|
||||||
raise Exception("No Digicert organization ID found. Ensure that 'DIGICERT_ORG_ID' is set in Lemur conf.")
|
|
||||||
|
|
||||||
if not current_app.config.get('DIGICERT_ROOT'):
|
|
||||||
raise Exception("No Digicert root found. Ensure that 'DIGICERT_ROOT' is set in the Lemur conf.")
|
|
||||||
|
|
||||||
if not current_app.config.get('DIGICERT_INTERMEDIATE'):
|
|
||||||
raise Exception("No Digicert intermediate found. Ensure that 'DIGICERT_INTERMEDIATE is set in Lemur conf.")
|
|
||||||
|
|
||||||
|
|
||||||
@retry(stop_max_attempt_number=10, wait_fixed=10000)
|
@retry(stop_max_attempt_number=10, wait_fixed=10000)
|
||||||
def get_certificate_id(session, base_url, order_id):
|
def get_certificate_id(session, base_url, order_id):
|
||||||
"""Retrieve certificate order id from Digicert API."""
|
"""Retrieve certificate order id from Digicert API."""
|
||||||
order_url = "{0}/services/v2/order/certificate/{1}".format(base_url, order_id)
|
order_url = "{0}/services/v2/order/certificate/{1}".format(base_url, order_id)
|
||||||
response_data = handle_response(session.get(order_url))
|
response_data = handle_response(session.get(order_url))
|
||||||
if response_data['status'] != 'issued':
|
if response_data['status'] != 'issued':
|
||||||
|
metrics.send('digicert_retries', 'counter', 1)
|
||||||
raise Exception("Order not in issued state.")
|
raise Exception("Order not in issued state.")
|
||||||
|
|
||||||
return response_data['certificate']['id']
|
return response_data['certificate']['id']
|
||||||
|
|
||||||
|
|
||||||
|
@retry(stop_max_attempt_number=10, wait_fixed=10000)
|
||||||
|
def get_cis_certificate(session, base_url, order_id):
|
||||||
|
"""Retrieve certificate order id from Digicert API."""
|
||||||
|
certificate_url = '{0}/platform/cis/certificate/{1}'.format(base_url, order_id)
|
||||||
|
session.headers.update(
|
||||||
|
{'Accept': 'application/x-pem-file'}
|
||||||
|
)
|
||||||
|
response = session.get(certificate_url)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
metrics.send('digicert_retries', 'counter', 1)
|
||||||
|
raise Exception("Order not in issued state.")
|
||||||
|
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
|
||||||
class DigiCertSourcePlugin(SourcePlugin):
|
class DigiCertSourcePlugin(SourcePlugin):
|
||||||
"""Wrap the Digicert Certifcate API."""
|
"""Wrap the Digicert Certifcate API."""
|
||||||
title = 'DigiCert'
|
title = 'DigiCert'
|
||||||
|
@ -172,12 +206,19 @@ class DigiCertSourcePlugin(SourcePlugin):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize source with appropriate details."""
|
"""Initialize source with appropriate details."""
|
||||||
verify_configuration()
|
required_vars = [
|
||||||
|
'DIGICERT_API_KEY',
|
||||||
|
'DIGICERT_URL',
|
||||||
|
'DIGICERT_ORG_ID',
|
||||||
|
'DIGICERT_ROOT',
|
||||||
|
'DIGICERT_INTERMEDIATE'
|
||||||
|
]
|
||||||
|
validate_conf(current_app, required_vars)
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.session.headers.update(
|
self.session.headers.update(
|
||||||
{
|
{
|
||||||
'X-DC-DEVKEY': current_app.config.get('DIGICERT_API_KEY'),
|
'X-DC-DEVKEY': current_app.config['DIGICERT_API_KEY'],
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -190,7 +231,6 @@ class DigiCertSourcePlugin(SourcePlugin):
|
||||||
|
|
||||||
class DigiCertIssuerPlugin(IssuerPlugin):
|
class DigiCertIssuerPlugin(IssuerPlugin):
|
||||||
"""Wrap the Digicert Issuer API."""
|
"""Wrap the Digicert Issuer API."""
|
||||||
|
|
||||||
title = 'DigiCert'
|
title = 'DigiCert'
|
||||||
slug = 'digicert-issuer'
|
slug = 'digicert-issuer'
|
||||||
description = "Enables the creation of certificates by"
|
description = "Enables the creation of certificates by"
|
||||||
|
@ -202,12 +242,20 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the issuer with the appropriate details."""
|
"""Initialize the issuer with the appropriate details."""
|
||||||
verify_configuration()
|
required_vars = [
|
||||||
|
'DIGICERT_API_KEY',
|
||||||
|
'DIGICERT_URL',
|
||||||
|
'DIGICERT_ORG_ID',
|
||||||
|
'DIGICERT_ROOT',
|
||||||
|
'DIGICERT_INTERMEDIATE'
|
||||||
|
]
|
||||||
|
|
||||||
|
validate_conf(current_app, required_vars)
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.session.headers.update(
|
self.session.headers.update(
|
||||||
{
|
{
|
||||||
'X-DC-DEVKEY': current_app.config.get('DIGICERT_API_KEY'),
|
'X-DC-DEVKEY': current_app.config['DIGICERT_API_KEY'],
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -225,7 +273,7 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
||||||
|
|
||||||
# make certificate request
|
# make certificate request
|
||||||
determinator_url = "{0}/services/v2/order/certificate/ssl".format(base_url)
|
determinator_url = "{0}/services/v2/order/certificate/ssl".format(base_url)
|
||||||
data = process_options(issuer_options, csr)
|
data = map_fields(issuer_options, csr)
|
||||||
response = self.session.post(determinator_url, data=json.dumps(data))
|
response = self.session.post(determinator_url, data=json.dumps(data))
|
||||||
order_id = response.json()['id']
|
order_id = response.json()['id']
|
||||||
|
|
||||||
|
@ -249,3 +297,67 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
||||||
"""
|
"""
|
||||||
role = {'username': '', 'password': '', 'name': 'digicert'}
|
role = {'username': '', 'password': '', 'name': 'digicert'}
|
||||||
return current_app.config.get('DIGICERT_ROOT'), "", [role]
|
return current_app.config.get('DIGICERT_ROOT'), "", [role]
|
||||||
|
|
||||||
|
|
||||||
|
class DigiCertCISIssuerPlugin(IssuerPlugin):
|
||||||
|
"""Wrap the Digicert Certificate Issuing API."""
|
||||||
|
title = 'DigiCert CIS'
|
||||||
|
slug = 'digicert-cis-issuer'
|
||||||
|
description = "Enables the creation of certificates by the DigiCert CIS REST API."
|
||||||
|
version = digicert.VERSION
|
||||||
|
|
||||||
|
author = 'Kevin Glisson'
|
||||||
|
author_url = 'https://github.com/netflix/lemur.git'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the issuer with the appropriate details."""
|
||||||
|
required_vars = [
|
||||||
|
'DIGICERT_CIS_API_KEY',
|
||||||
|
'DIGICERT_CIS_URL',
|
||||||
|
'DIGICERT_CIS_ORG_ID',
|
||||||
|
'DIGICERT_CIS_ROOT',
|
||||||
|
'DIGICERT_CIS_INTERMEDIATE',
|
||||||
|
'DIGICERT_CIS_PROFILE_NAME'
|
||||||
|
]
|
||||||
|
|
||||||
|
validate_conf(current_app, required_vars)
|
||||||
|
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update(
|
||||||
|
{
|
||||||
|
'X-DC-DEVKEY': current_app.config['DIGICERT_CIS_API_KEY'],
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
super(DigiCertCISIssuerPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def create_certificate(self, csr, issuer_options):
|
||||||
|
"""Create a DigiCert certificate."""
|
||||||
|
base_url = current_app.config.get('DIGICERT_CIS_URL')
|
||||||
|
|
||||||
|
# make certificate request
|
||||||
|
create_url = '{0}/platform/cis/certificate'
|
||||||
|
|
||||||
|
data = map_cis_fields(issuer_options, csr)
|
||||||
|
response = self.session.post(create_url, data=json.dumps(data))
|
||||||
|
order_id = response.json()['id']
|
||||||
|
|
||||||
|
# retrieve certificate
|
||||||
|
certificate_pem = get_cis_certificate(self.session, base_url, order_id)
|
||||||
|
end_entity, intermediate, root = pem.parse(certificate_pem)
|
||||||
|
return str(end_entity), str(intermediate)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_authority(options):
|
||||||
|
"""Create an authority.
|
||||||
|
|
||||||
|
Creates an authority, this authority is then used by Lemur to
|
||||||
|
allow a user to specify which Certificate Authority they want
|
||||||
|
to sign their certificate.
|
||||||
|
|
||||||
|
:param options:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
role = {'username': '', 'password': '', 'name': 'digicert'}
|
||||||
|
return current_app.config.get('DIGICERT_CIS_ROOT'), "", [role]
|
||||||
|
|
|
@ -5,8 +5,8 @@ from freezegun import freeze_time
|
||||||
from lemur.tests.vectors import CSR_STR
|
from lemur.tests.vectors import CSR_STR
|
||||||
|
|
||||||
|
|
||||||
def test_process_options(app):
|
def test_map_fields(app):
|
||||||
from lemur.plugins.lemur_digicert.plugin import process_options
|
from lemur.plugins.lemur_digicert.plugin import map_fields
|
||||||
|
|
||||||
names = ['one.example.com', 'two.example.com', 'three.example.com']
|
names = ['one.example.com', 'two.example.com', 'three.example.com']
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ def test_process_options(app):
|
||||||
'validity_start': arrow.get(2016, 10, 30)
|
'validity_start': arrow.get(2016, 10, 30)
|
||||||
}
|
}
|
||||||
|
|
||||||
data = process_options(options, CSR_STR)
|
data = map_fields(options, CSR_STR)
|
||||||
|
|
||||||
assert data == {
|
assert data == {
|
||||||
'certificate': {
|
'certificate': {
|
||||||
|
@ -38,6 +38,40 @@ def test_process_options(app):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_cis_fields(app):
|
||||||
|
from lemur.plugins.lemur_digicert.plugin import map_cis_fields
|
||||||
|
|
||||||
|
names = ['one.example.com', 'two.example.com', 'three.example.com']
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'common_name': 'example.com',
|
||||||
|
'owner': 'bob@example.com',
|
||||||
|
'description': 'test certificate',
|
||||||
|
'extensions': {
|
||||||
|
'sub_alt_names': {
|
||||||
|
'names': [{'name_type': 'DNSName', 'value': x} for x in names]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'organization': 'Example, Inc.',
|
||||||
|
'organizational_unit': 'Example Org',
|
||||||
|
'validity_end': arrow.get(2017, 5, 7),
|
||||||
|
'validity_start': arrow.get(2016, 10, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = map_cis_fields(options, CSR_STR)
|
||||||
|
|
||||||
|
assert data == {
|
||||||
|
'common_name': 'example.com',
|
||||||
|
'csr': CSR_STR,
|
||||||
|
'additional_dns_names': names,
|
||||||
|
'signature_hash': 'sha256',
|
||||||
|
'organization': {'name': 'Example, Inc.', 'units': ['Example Org']},
|
||||||
|
'validity': {
|
||||||
|
'valid_to': arrow.get(2017, 5, 7).format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_issuance():
|
def test_issuance():
|
||||||
from lemur.plugins.lemur_digicert.plugin import get_issuance
|
from lemur.plugins.lemur_digicert.plugin import get_issuance
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue