From 40057262e1f2f4d49608077e10f2f4a462fd32f0 Mon Sep 17 00:00:00 2001 From: sirferl Date: Sat, 14 Nov 2020 12:19:16 +0100 Subject: [PATCH 1/8] Azure-Dest: add files --- lemur/plugins/lemur_azure_dest/__init__.py | 4 + lemur/plugins/lemur_azure_dest/plugin.py | 188 ++++++++++++++++++ .../lemur_azure_dest/tests/conftest.py | 1 + 3 files changed, 193 insertions(+) create mode 100644 lemur/plugins/lemur_azure_dest/__init__.py create mode 100755 lemur/plugins/lemur_azure_dest/plugin.py create mode 100644 lemur/plugins/lemur_azure_dest/tests/conftest.py diff --git a/lemur/plugins/lemur_azure_dest/__init__.py b/lemur/plugins/lemur_azure_dest/__init__.py new file mode 100644 index 00000000..f8afd7e3 --- /dev/null +++ b/lemur/plugins/lemur_azure_dest/__init__.py @@ -0,0 +1,4 @@ +try: + VERSION = __import__("pkg_resources").get_distribution(__name__).version +except Exception as e: + VERSION = "unknown" diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py new file mode 100755 index 00000000..12d6e27e --- /dev/null +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -0,0 +1,188 @@ +""" +.. module: lemur.plugins.lemur_azure_dest.plugin + :platform: Unix + :copyright: (c) 2019 + :license: Apache, see LICENCE for more details. + + Plugin for uploading certificates and private key as secret to azure key-vault + that can be pulled down by end point nodes. + +.. moduleauthor:: sirferl +""" +import os +import re +from flask import current_app + +from lemur.common.defaults import common_name, country, state, location, organizational_unit, organization +from lemur.common.utils import parse_certificate +from lemur.plugins.bases import DestinationPlugin +from lemur.plugins.bases import SourcePlugin + +import requests +import json +import base64 + + +def base64encode(string): +# Performs Base64 encoding of string to string using the base64.b64encode() function +# which encodes bytes to bytes. + return base64.b64encode(string.encode()).decode() + + +def handle_response(my_response): + """ + Helper function for parsing responses from the Entrust API. + :param my_response: + :return: :raise Exception: + """ + msg = { + 200: "The request was successful.", + 400: "Keyvault Error" + } + + try: + data = json.loads(my_response.content) + except ValueError: + # catch an empty jason object here + data = {'response': 'No detailed message'} + status_code = my_response.status_code + if status_code > 399: + raise Exception(f"AZURE error: {msg.get(status_code, status_code)}\n{data}") + + log_data = { + "function": f"{__name__}.{sys._getframe().f_code.co_name}", + "message": "Response", + "status": status_code, + "response": data + } + current_app.logger.info(log_data) + if data == {'response': 'No detailed message'}: + # status if no data + return status_code + else: + # return data from the response + return data + + +def get_access_token(tenant, appID, password, self): + """ + Gets the access token with the appid and the password and returns it + + Improvment option: we can try to save it and renew it only when necessary + + :param tenant: Tenant used + :param appID: Application ID from Azure + :param password: password for Application ID + :return: Access token to post to the keyvault + """ + # prepare the call for the access_token + auth_url = f"https://login.microsoftonline.com/{tenant}/oauth2/token" + post_data = { + 'grant_type' : 'client_credentials', + 'client_id' : appID, + 'client_secret' : password, + 'resource' : 'https://vault.azure.net' + } + try: + response = self.session.post(auth_url, data = post_data) + except requests.exceptions.RequestException as e: + current_app.logger.exception(f"AZURE: Error for POST {e}") + + access_token = json.loads(response.content)["access_token"] + return access_token + + +class AzureDestinationPlugin(DestinationPlugin): + """Azure Keyvault Destination plugin for Lemur""" + + title = "Azure" + slug = "azure-keyvault-destination" + description = "Allow the uploading of certificates to Azure key vault" + + author = "Sirferl" + author_url = "https://github.com/sirferl/lemur" + + options = [ + { + "name": "vaultUrl", + "type": "str", + "required": True, + "validation": "^https?://[a-zA-Z0-9.:-]+$", + "helpMessage": "Valid URL to Azure key vault instance", + }, + { + "name": "azureTenant", + "type": "str", + "required": True, + "validation": "^([a-zA-Z0-9/-/?)+$", + "helpMessage": "Tenant for the Azure Key Vault", + }, + { + "name": "appID", + "type": "str", + "required": True, + "validation": "^([a-zA-Z0-9/-/?)+$", + "helpMessage": "AppID for the Azure Key Vault", + }, + { + "name": "azurePassword", + "type": "str", + "required": True, + "validation": "[0-9a-zA-Z.:_-~]+", + "helpMessage": "Tenant password for the Azure Key Vault", + } + ] + + def __init__(self, *args, **kwargs): + self.session = requests.Session() + super(AzureDestinationPlugin, self).__init__(*args, **kwargs) + + + def upload(self, name, body, private_key, cert_chain, options, **kwargs): + """ + Upload certificate and private key + + :param private_key: + :param cert_chain: + :return: + """ + + # we use the common name to identify the certificate + # Azure does not allow "." in the certificate name we replace them with "-" + cert = parse_certificate(body) + certificate_name = common_name(cert).replace(".","-") + + vault_URI = self.get_option("vaultUrl", options) + tenant = self.get_option("azureTenant", options) + app_id = self.get_option("appID", options) + password = self.get_option("azurePassword", options) + + access_token = get_access_token(tenant, app_id, password, self) + + cert_url = f"{vault_URI}/certificates/{certificate_name}/import?api-version=7.1" + post_header = { + "Authorization" : f"Bearer {access_token}" + } + cert_package = f"{body}\n{private_key}" + current_app.logger.debug(f"AZURE: encoded certificate: {cert_package}") + + post_body = { + "value" : cert_package, + "policy" : { + "key_props": { + "exportable" : True, + "kty" : "RSA", + "key_size" : 2048, + "reuse_key" : True + }, + "secret_props": { + "contentType": "application/x-pem-file" + } + } + } + + try: + response = self.session.post(cert_url, headers = post_header, json = post_body) + except requests.exceptions.RequestException as e: + current_app.logger.exception(f"AZURE: Error for POST {e}") + treturn_value = handle_response(response) diff --git a/lemur/plugins/lemur_azure_dest/tests/conftest.py b/lemur/plugins/lemur_azure_dest/tests/conftest.py new file mode 100644 index 00000000..0e1cd89f --- /dev/null +++ b/lemur/plugins/lemur_azure_dest/tests/conftest.py @@ -0,0 +1 @@ +from lemur.tests.conftest import * # noqa From 62230228a7fe379df658464b8162cf276412caec Mon Sep 17 00:00:00 2001 From: sirferl Date: Sat, 14 Nov 2020 12:49:14 +0100 Subject: [PATCH 2/8] Azure-Dest: Working Plugin --- lemur/plugins/lemur_azure_dest/plugin.py | 12 ++++++++++-- setup.py | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py index 12d6e27e..ef4ffd42 100755 --- a/lemur/plugins/lemur_azure_dest/plugin.py +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -14,12 +14,14 @@ import re from flask import current_app from lemur.common.defaults import common_name, country, state, location, organizational_unit, organization -from lemur.common.utils import parse_certificate +from lemur.common.utils import parse_certificate, parse_private_key from lemur.plugins.bases import DestinationPlugin from lemur.plugins.bases import SourcePlugin +from cryptography.hazmat.primitives import serialization import requests import json +import sys import base64 @@ -163,7 +165,13 @@ class AzureDestinationPlugin(DestinationPlugin): post_header = { "Authorization" : f"Bearer {access_token}" } - cert_package = f"{body}\n{private_key}" + key_pkcs8 = parse_private_key(private_key).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + key_pkcs8 = key_pkcs8.decode("utf-8").replace('\\n', '\n') + cert_package = f"{body}\n{key_pkcs8}" current_app.logger.debug(f"AZURE: encoded certificate: {cert_package}") post_body = { diff --git a/setup.py b/setup.py index 59e35b53..b817cc63 100644 --- a/setup.py +++ b/setup.py @@ -157,7 +157,8 @@ setup( 'adcs_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin', 'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin', 'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin', - 'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin' + 'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin', + 'azure_destination = lemur.plugins.lemur_azure_dest.plugin:AzureDestinationPlugin' ], }, classifiers=[ From 48302b6acc8ebe0e696a9ea251abe6c4a5a1d44d Mon Sep 17 00:00:00 2001 From: sirferl Date: Sat, 14 Nov 2020 13:03:27 +0100 Subject: [PATCH 3/8] Azure-Dest: Linted --- lemur/plugins/lemur_azure_dest/plugin.py | 50 +++++++++++------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py index ef4ffd42..ecab3a03 100755 --- a/lemur/plugins/lemur_azure_dest/plugin.py +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -9,14 +9,11 @@ .. moduleauthor:: sirferl """ -import os -import re from flask import current_app -from lemur.common.defaults import common_name, country, state, location, organizational_unit, organization +from lemur.common.defaults import common_name from lemur.common.utils import parse_certificate, parse_private_key from lemur.plugins.bases import DestinationPlugin -from lemur.plugins.bases import SourcePlugin from cryptography.hazmat.primitives import serialization import requests @@ -26,8 +23,8 @@ import base64 def base64encode(string): -# Performs Base64 encoding of string to string using the base64.b64encode() function -# which encodes bytes to bytes. + # Performs Base64 encoding of string to string using the base64.b64encode() function + # which encodes bytes to bytes. return base64.b64encode(string.encode()).decode() @@ -80,13 +77,13 @@ def get_access_token(tenant, appID, password, self): # prepare the call for the access_token auth_url = f"https://login.microsoftonline.com/{tenant}/oauth2/token" post_data = { - 'grant_type' : 'client_credentials', - 'client_id' : appID, - 'client_secret' : password, - 'resource' : 'https://vault.azure.net' + 'grant_type': 'client_credentials', + 'client_id': appID, + 'client_secret': password, + 'resource': 'https://vault.azure.net' } try: - response = self.session.post(auth_url, data = post_data) + response = self.session.post(auth_url, data=post_data) except requests.exceptions.RequestException as e: current_app.logger.exception(f"AZURE: Error for POST {e}") @@ -139,7 +136,6 @@ class AzureDestinationPlugin(DestinationPlugin): self.session = requests.Session() super(AzureDestinationPlugin, self).__init__(*args, **kwargs) - def upload(self, name, body, private_key, cert_chain, options, **kwargs): """ Upload certificate and private key @@ -152,18 +148,18 @@ class AzureDestinationPlugin(DestinationPlugin): # we use the common name to identify the certificate # Azure does not allow "." in the certificate name we replace them with "-" cert = parse_certificate(body) - certificate_name = common_name(cert).replace(".","-") + certificate_name = common_name(cert).replace(".", "-") vault_URI = self.get_option("vaultUrl", options) tenant = self.get_option("azureTenant", options) app_id = self.get_option("appID", options) password = self.get_option("azurePassword", options) - + access_token = get_access_token(tenant, app_id, password, self) cert_url = f"{vault_URI}/certificates/{certificate_name}/import?api-version=7.1" post_header = { - "Authorization" : f"Bearer {access_token}" + "Authorization": f"Bearer {access_token}" } key_pkcs8 = parse_private_key(private_key).private_bytes( encoding=serialization.Encoding.PEM, @@ -171,26 +167,26 @@ class AzureDestinationPlugin(DestinationPlugin): encryption_algorithm=serialization.NoEncryption(), ) key_pkcs8 = key_pkcs8.decode("utf-8").replace('\\n', '\n') - cert_package = f"{body}\n{key_pkcs8}" + cert_package = f"{body}\n{key_pkcs8}" current_app.logger.debug(f"AZURE: encoded certificate: {cert_package}") post_body = { - "value" : cert_package, - "policy" : { + "value": cert_package, + "policy": { "key_props": { - "exportable" : True, - "kty" : "RSA", - "key_size" : 2048, - "reuse_key" : True - }, - "secret_props": { + "exportable": True, + "kty": "RSA", + "key_size": 2048, + "reuse_key": True + }, + "secret_props":{ "contentType": "application/x-pem-file" - } + } } } try: - response = self.session.post(cert_url, headers = post_header, json = post_body) + response = self.session.post(cert_url, headers=post_header, json=post_body) except requests.exceptions.RequestException as e: current_app.logger.exception(f"AZURE: Error for POST {e}") - treturn_value = handle_response(response) + treturn_value = handle_response(response) From 1b5f17d8b83aef32de57fb695c564dfa10e1379d Mon Sep 17 00:00:00 2001 From: sirferl Date: Sun, 15 Nov 2020 10:28:21 +0100 Subject: [PATCH 4/8] Azure-Dest: More Lint, derive keysize from cert, remove debug output --- lemur/plugins/lemur_azure_dest/plugin.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py index ecab3a03..a338d629 100755 --- a/lemur/plugins/lemur_azure_dest/plugin.py +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -11,7 +11,7 @@ """ from flask import current_app -from lemur.common.defaults import common_name +from lemur.common.defaults import common_name, bitstrength from lemur.common.utils import parse_certificate, parse_private_key from lemur.plugins.bases import DestinationPlugin @@ -168,20 +168,19 @@ class AzureDestinationPlugin(DestinationPlugin): ) key_pkcs8 = key_pkcs8.decode("utf-8").replace('\\n', '\n') cert_package = f"{body}\n{key_pkcs8}" - current_app.logger.debug(f"AZURE: encoded certificate: {cert_package}") post_body = { "value": cert_package, "policy": { "key_props": { - "exportable": True, - "kty": "RSA", - "key_size": 2048, - "reuse_key": True - }, - "secret_props":{ - "contentType": "application/x-pem-file" - } + "exportable": True, + "kty": "RSA", + "key_size": bitstrength(cert), + "reuse_key": True + }, + "secret_props": { + "contentType": "application/x-pem-file" + } } } @@ -189,4 +188,4 @@ class AzureDestinationPlugin(DestinationPlugin): response = self.session.post(cert_url, headers=post_header, json=post_body) except requests.exceptions.RequestException as e: current_app.logger.exception(f"AZURE: Error for POST {e}") - treturn_value = handle_response(response) + return_value = handle_response(response) From 0521624ccc3784e357704749de29259984c2fe2a Mon Sep 17 00:00:00 2001 From: sirferl Date: Sun, 15 Nov 2020 10:33:36 +0100 Subject: [PATCH 5/8] Azure-Dest: Lint always finds something --- lemur/plugins/lemur_azure_dest/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py index a338d629..53860942 100755 --- a/lemur/plugins/lemur_azure_dest/plugin.py +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -177,7 +177,7 @@ class AzureDestinationPlugin(DestinationPlugin): "kty": "RSA", "key_size": bitstrength(cert), "reuse_key": True - }, + }, "secret_props": { "contentType": "application/x-pem-file" } From 0f3357ab46a92583cc394ac357f5e850d4f2eae2 Mon Sep 17 00:00:00 2001 From: sirferl Date: Tue, 24 Nov 2020 12:29:25 +0100 Subject: [PATCH 6/8] moved base64encode to common.utils --- lemur/common/utils.py | 5 +++++ lemur/plugins/lemur_azure_dest/plugin.py | 9 +-------- lemur/plugins/lemur_kubernetes/plugin.py | 8 +------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 19b256e8..5d27fa63 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -10,6 +10,7 @@ import random import re import string import pem +import base64 import sqlalchemy from cryptography import x509 @@ -33,6 +34,10 @@ paginated_parser.add_argument("sortBy", type=str, dest="sort_by", location="args paginated_parser.add_argument("filter", type=str, location="args") paginated_parser.add_argument("owner", type=str, location="args") +def base64encode(string): + # Performs Base64 encoding of string to string using the base64.b64encode() function + # which encodes bytes to bytes. + return base64.b64encode(string.encode()).decode() def get_psuedo_random_string(): """ diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py index 53860942..e9521260 100755 --- a/lemur/plugins/lemur_azure_dest/plugin.py +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -12,20 +12,13 @@ from flask import current_app from lemur.common.defaults import common_name, bitstrength -from lemur.common.utils import parse_certificate, parse_private_key +from lemur.common.utils import parse_certificate, parse_private_key, base64encode from lemur.plugins.bases import DestinationPlugin from cryptography.hazmat.primitives import serialization import requests import json import sys -import base64 - - -def base64encode(string): - # Performs Base64 encoding of string to string using the base64.b64encode() function - # which encodes bytes to bytes. - return base64.b64encode(string.encode()).decode() def handle_response(my_response): diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index f7ff00f7..05613227 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -18,7 +18,7 @@ import requests from flask import current_app from lemur.common.defaults import common_name -from lemur.common.utils import parse_certificate +from lemur.common.utils import parse_certificate, base64encode from lemur.plugins.bases import DestinationPlugin DEFAULT_API_VERSION = "v1" @@ -73,12 +73,6 @@ def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_V ) -# Performs Base64 encoding of string to string using the base64.b64encode() function -# which encodes bytes to bytes. -def base64encode(string): - return base64.b64encode(string.encode()).decode() - - def build_secret(secret_format, secret_name, body, private_key, cert_chain): secret = { "apiVersion": "v1", From 56af628c685bfa4296fcf8970aa47ead5d7a9b0c Mon Sep 17 00:00:00 2001 From: sirferl Date: Tue, 24 Nov 2020 12:46:09 +0100 Subject: [PATCH 7/8] moved base64encode to common.utils --- lemur/plugins/lemur_azure_dest/plugin.py | 2 +- lemur/plugins/lemur_kubernetes/plugin.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lemur/plugins/lemur_azure_dest/plugin.py b/lemur/plugins/lemur_azure_dest/plugin.py index e9521260..9282de40 100755 --- a/lemur/plugins/lemur_azure_dest/plugin.py +++ b/lemur/plugins/lemur_azure_dest/plugin.py @@ -12,7 +12,7 @@ from flask import current_app from lemur.common.defaults import common_name, bitstrength -from lemur.common.utils import parse_certificate, parse_private_key, base64encode +from lemur.common.utils import parse_certificate, parse_private_key from lemur.plugins.bases import DestinationPlugin from cryptography.hazmat.primitives import serialization diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index 05613227..79207636 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -10,7 +10,6 @@ .. moduleauthor:: Mikhail Khodorovskiy """ -import base64 import itertools import os From 439e888d9e785d67ff808ad5d846c76889e546c2 Mon Sep 17 00:00:00 2001 From: sirferl Date: Tue, 24 Nov 2020 12:59:42 +0100 Subject: [PATCH 8/8] lint errors --- lemur/common/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 5d27fa63..7c8a14aa 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -34,11 +34,13 @@ paginated_parser.add_argument("sortBy", type=str, dest="sort_by", location="args paginated_parser.add_argument("filter", type=str, location="args") paginated_parser.add_argument("owner", type=str, location="args") + def base64encode(string): # Performs Base64 encoding of string to string using the base64.b64encode() function # which encodes bytes to bytes. return base64.b64encode(string.encode()).decode() + def get_psuedo_random_string(): """ Create a random and strongish challenge.