From e64e2a41d512f9b19f8ffb2ecdf649841a16569b Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 23 Sep 2020 16:36:59 +0200 Subject: [PATCH 01/14] Add update_options to authorities service --- lemur/authorities/service.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index c70c6fc5..c734f408 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -39,6 +39,21 @@ def update(authority_id, description, owner, active, roles): return database.update(authority) +def update_options(authority_id, options): + """ + Update an authority with new options. + + :param authority_id: + :param options: the new options to be saved into the authority + :return: + """ + + authority = get(authority_id) + + authority.options = options + + return database.update(authority) + def mint(**kwargs): """ Creates the authority based on the plugin provided. From 898b5da6613294403da6683f20c45abe3f4bd7f3 Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 23 Sep 2020 16:38:38 +0200 Subject: [PATCH 02/14] Add store_account option to acme plugin --- lemur/plugins/lemur_acme/plugin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 16d61a0f..ec4a5b84 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -240,6 +240,7 @@ class AcmeHandler(object): existing_regr = options.get("acme_regr", current_app.config.get("ACME_REGR")) if existing_key and existing_regr: + current_app.logger.debug("Reusing existing ACME account") # Reuse the same account for each certificate issuance key = jose.JWK.json_loads(existing_key) regr = messages.RegistrationResource.json_loads(existing_regr) @@ -253,6 +254,7 @@ class AcmeHandler(object): # Create an account for each certificate issuance key = jose.JWKRSA(key=generate_private_key("RSA2048")) + current_app.logger.debug("Creating a new ACME account") current_app.logger.debug( "Connecting with directory at {0}".format(directory_url) ) @@ -447,6 +449,13 @@ class ACMEIssuerPlugin(IssuerPlugin): "validation": "/^-----BEGIN CERTIFICATE-----/", "helpMessage": "Certificate to use", }, + { + "name": "store_account", + "type": "bool", + "required": False, + "helpMessage": "Disable to create a new account for each ACME request", + "default": True, + } ] def __init__(self, *args, **kwargs): From eed628dbab1d423cd08ff99027957b06368039ae Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 23 Sep 2020 16:38:57 +0200 Subject: [PATCH 03/14] Implement storage of acme account --- lemur/plugins/lemur_acme/plugin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index ec4a5b84..b77b1765 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -32,6 +32,7 @@ from lemur.extensions import metrics, sentry from lemur.plugins import lemur_acme as acme from lemur.plugins.bases import IssuerPlugin from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns +from lemur.authorities import service as authorities_service from retrying import retry @@ -264,6 +265,27 @@ class AcmeHandler(object): registration = client.new_account_and_tos( messages.NewRegistration.from_data(email=email) ) + + # if store_account is checked, add the private_key and registration resources to the options + if options['store_account']: + new_options = json.loads(authority.options) + # the key returned by fields_to_partial_json is missing the key type, so we add it manually + key_dict = key.fields_to_partial_json() + key_dict["kty"] = "RSA" + acme_private_key = { + "name": "acme_private_key", + "value": json.dumps(key_dict) + } + new_options.append(acme_private_key) + + acme_regr = { + "name": "acme_regr", + "value": json.dumps({"body": {}, "uri": registration.uri}) + } + new_options.append(acme_regr) + + authorities_service.update_options(authority.id, options=json.dumps(new_options)) + current_app.logger.debug("Connected: {0}".format(registration.uri)) return client, registration From 7e6fb740b3bf843a65730c60f813e759c37fc217 Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 23 Sep 2020 16:57:48 +0200 Subject: [PATCH 04/14] Fix flake8/linting errors --- lemur/authorities/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index c734f408..0913e629 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -54,6 +54,7 @@ def update_options(authority_id, options): return database.update(authority) + def mint(**kwargs): """ Creates the authority based on the plugin provided. From e0708410d0657da42f6acb08ddd3e9a3ecd05c3b Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 23 Sep 2020 17:12:49 +0200 Subject: [PATCH 05/14] Add store_account value to options in test_setup_acme_client_success --- lemur/plugins/lemur_acme/tests/test_acme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 8320a2de..0356175c 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -169,7 +169,8 @@ class TestAcme(unittest.TestCase): @patch("lemur.plugins.lemur_acme.plugin.current_app") def test_setup_acme_client_success(self, mock_current_app, mock_acme): mock_authority = Mock() - mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}]' + mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \ + '{"name": "store_account", "value": false}] ' mock_client = Mock() mock_registration = Mock() mock_registration.uri = "http://test.com" From bf66de0bfd1a3cd8506a54e5d01304a388cbeb3b Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 30 Sep 2020 12:59:52 +0200 Subject: [PATCH 06/14] Add Test for saving the accound details --- lemur/plugins/lemur_acme/tests/test_acme.py | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 0356175c..b8d97489 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -165,12 +165,42 @@ class TestAcme(unittest.TestCase): with self.assertRaises(Exception): self.acme.setup_acme_client(mock_authority) + @patch("lemur.plugins.lemur_acme.plugin.jose.JWKRSA.fields_to_partial_json") + @patch("lemur.plugins.lemur_acme.plugin.authorities_service") + @patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2") + @patch("lemur.plugins.lemur_acme.plugin.current_app") + def test_setup_acme_client_success_store_new_account(self, mock_current_app, mock_acme, mock_authorities_service, + mock_key_generation): + mock_authority = Mock() + mock_authority.id = 2 + mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \ + '{"name": "store_account", "value": true}]' + mock_client = Mock() + mock_registration = Mock() + mock_registration.uri = "http://test.com" + mock_client.register = mock_registration + mock_client.agree_to_tos = Mock(return_value=True) + mock_client.new_account_and_tos.return_value = mock_registration + mock_acme.return_value = mock_client + mock_current_app.config = {} + + mock_key_generation.return_value = {"n": "PwIOkViO"} + + mock_authorities_service.update_options = Mock(return_value=True) + + self.acme.setup_acme_client(mock_authority) + + mock_authorities_service.update_options.assert_called_with(2, options='[{"name": "mock_name", "value": "mock_value"}, ' + '{"name": "store_account", "value": true}, ' + '{"name": "acme_private_key", "value": "{\\"n\\": \\"PwIOkViO\\", \\"kty\\": \\"RSA\\"}"}, ' + '{"name": "acme_regr", "value": "{\\"body\\": {}, \\"uri\\": \\"http://test.com\\"}"}]') + @patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2") @patch("lemur.plugins.lemur_acme.plugin.current_app") def test_setup_acme_client_success(self, mock_current_app, mock_acme): mock_authority = Mock() mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \ - '{"name": "store_account", "value": false}] ' + '{"name": "store_account", "value": false}]' mock_client = Mock() mock_registration = Mock() mock_registration.uri = "http://test.com" From 9abd3e97e7f4d90276a7ba78ca3c66792afdbb21 Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 30 Sep 2020 13:21:34 +0200 Subject: [PATCH 07/14] Add test loading acme account from authority --- lemur/plugins/lemur_acme/tests/test_acme.py | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index b8d97489..8cd4783f 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -1,8 +1,10 @@ import unittest from unittest.mock import patch, Mock +import josepy as jose from cryptography.x509 import DNSName from lemur.plugins.lemur_acme import plugin +from lemur.common.utils import generate_private_key from mock import MagicMock @@ -165,6 +167,28 @@ class TestAcme(unittest.TestCase): with self.assertRaises(Exception): self.acme.setup_acme_client(mock_authority) + @patch("lemur.plugins.lemur_acme.plugin.jose.JWK.json_loads") + @patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2") + @patch("lemur.plugins.lemur_acme.plugin.current_app") + def test_setup_acme_client_success_load_account_from_authority(self, mock_current_app, mock_acme, mock_key_json_load): + mock_authority = Mock() + mock_authority.id = 2 + mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \ + '{"name": "store_account", "value": true},' \ + '{"name": "acme_private_key", "value": "{\\"n\\": \\"PwIOkViO\\", \\"kty\\": \\"RSA\\"}"}, ' \ + '{"name": "acme_regr", "value": "{\\"body\\": {}, \\"uri\\": \\"http://test.com\\"}"}]' + mock_client = Mock() + mock_acme.return_value = mock_client + mock_current_app.config = {} + + mock_key_json_load.return_value = jose.JWKRSA(key=generate_private_key("RSA2048")) + + result_client, result_registration = self.acme.setup_acme_client(mock_authority) + + mock_acme.new_account_and_tos.assert_not_called() + assert result_client + assert not result_registration + @patch("lemur.plugins.lemur_acme.plugin.jose.JWKRSA.fields_to_partial_json") @patch("lemur.plugins.lemur_acme.plugin.authorities_service") @patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2") From 835339694081258f9e815a5b99d134a33ad55cba Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 30 Sep 2020 13:42:16 +0200 Subject: [PATCH 08/14] Improve tests --- lemur/plugins/lemur_acme/tests/test_acme.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 8cd4783f..ab246563 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -219,9 +219,10 @@ class TestAcme(unittest.TestCase): '{"name": "acme_private_key", "value": "{\\"n\\": \\"PwIOkViO\\", \\"kty\\": \\"RSA\\"}"}, ' '{"name": "acme_regr", "value": "{\\"body\\": {}, \\"uri\\": \\"http://test.com\\"}"}]') + @patch("lemur.plugins.lemur_acme.plugin.authorities_service") @patch("lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2") @patch("lemur.plugins.lemur_acme.plugin.current_app") - def test_setup_acme_client_success(self, mock_current_app, mock_acme): + def test_setup_acme_client_success(self, mock_current_app, mock_acme, mock_authorities_service): mock_authority = Mock() mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}, ' \ '{"name": "store_account", "value": false}]' @@ -233,6 +234,7 @@ class TestAcme(unittest.TestCase): mock_acme.return_value = mock_client mock_current_app.config = {} result_client, result_registration = self.acme.setup_acme_client(mock_authority) + mock_authorities_service.update_options.assert_not_called() assert result_client assert result_registration From 57534d86cd03e3d8585ffe52d4d5dd58a5b007fe Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 30 Sep 2020 17:46:14 +0200 Subject: [PATCH 09/14] Disable account saving by default --- lemur/plugins/lemur_acme/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index b77b1765..8bc1485f 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -476,7 +476,7 @@ class ACMEIssuerPlugin(IssuerPlugin): "type": "bool", "required": False, "helpMessage": "Disable to create a new account for each ACME request", - "default": True, + "default": False, } ] From 4f696abb5d6b14e80ab4dea966323525aa80850c Mon Sep 17 00:00:00 2001 From: Hossein Shafagh Date: Wed, 7 Oct 2020 20:02:27 -0700 Subject: [PATCH 10/14] adding util method to convert PKCS7 to pem --- lemur/common/utils.py | 19 +++++++++- lemur/tests/test_utils.py | 14 ++++++++ lemur/tests/vectors.py | 75 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 283d1eec..19b256e8 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -9,6 +9,7 @@ import random import re import string +import pem import sqlalchemy from cryptography import x509 @@ -16,7 +17,7 @@ from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding -from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding, pkcs7 from flask_restful.reqparse import RequestParser from sqlalchemy import and_, func @@ -357,3 +358,19 @@ def find_matching_certificates_by_hash(cert, matching_certs): ): matching.append(c) return matching + + +def convert_pkcs7_bytes_to_pem(certs_pkcs7): + """ + Given a list of certificates in pkcs7 encoding (bytes), covert them into a list of PEM encoded files + :raises ValueError or ValidationError + :param certs_pkcs7: + :return: list of certs in PEM format + """ + + certificates = pkcs7.load_pem_pkcs7_certificates(certs_pkcs7) + certificates_pem = [] + for cert in certificates: + certificates_pem.append(pem.parse(cert.public_bytes(encoding=Encoding.PEM))[0]) + + return certificates_pem diff --git a/lemur/tests/test_utils.py b/lemur/tests/test_utils.py index 162e53b0..f4be023b 100644 --- a/lemur/tests/test_utils.py +++ b/lemur/tests/test_utils.py @@ -10,6 +10,7 @@ from lemur.tests.vectors import ( ECDSA_SECP384r1_CERT, ECDSA_SECP384r1_CERT_STR, DSA_CERT, + CERT_CHAIN_PKCS7_PEM ) @@ -114,3 +115,16 @@ 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") + + +def test_convert_pkcs7_bytes_to_pem(): + from lemur.common.utils import convert_pkcs7_bytes_to_pem + from lemur.common.utils import parse_certificate + cert_chain = convert_pkcs7_bytes_to_pem(CERT_CHAIN_PKCS7_PEM) + assert(len(cert_chain) == 3) + + leaf = cert_chain[1] + root = cert_chain[2] + + assert(parse_certificate("\n".join(str(root).splitlines())) == ROOTCA_CERT) + assert (parse_certificate("\n".join(str(leaf).splitlines())) == INTERMEDIATE_CERT) diff --git a/lemur/tests/vectors.py b/lemur/tests/vectors.py index 0768cdac..7a78818c 100644 --- a/lemur/tests/vectors.py +++ b/lemur/tests/vectors.py @@ -512,3 +512,78 @@ BglghkgBZQMEAwIDMAAwLQIVANubSNMSLt8plN9ZV3cp4pe3lMYCAhQPLLE7rTgm -----END CERTIFICATE----- """ DSA_CERT = parse_certificate(DSA_CERT_STR) + + +CERT_CHAIN_PKCS7_STR = """ +-----BEGIN PKCS7----- +MIIMfwYJKoZIhvcNAQcCoIIMcDCCDGwCAQExADALBgkqhkiG9w0BBwGgggxSMIIE +FjCCAv6gAwIBAgIQbIbX/Ap0Roqzf5HeN5akmzANBgkqhkiG9w0BAQsFADCBpDEq +MCgGA1UEAwwhTGVtdXJUcnVzdCBVbml0dGVzdHMgUm9vdCBDQSAyMDE4MSMwIQYD +VQQKDBpMZW11clRydXN0IEVudGVycHJpc2VzIEx0ZDEmMCQGA1UECwwdVW5pdHRl +c3RpbmcgT3BlcmF0aW9ucyBDZW50ZXIxCzAJBgNVBAYTAkVFMQwwCgYDVQQIDANO +L0ExDjAMBgNVBAcMBUVhcnRoMB4XDTE3MTIzMTIyMDAwMFoXDTQ3MTIzMTIyMDAw +MFowgaQxKjAoBgNVBAMMIUxlbXVyVHJ1c3QgVW5pdHRlc3RzIFJvb3QgQ0EgMjAx +ODEjMCEGA1UECgwaTGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsM +HVVuaXR0ZXN0aW5nIE9wZXJhdGlvbnMgQ2VudGVyMQswCQYDVQQGEwJFRTEMMAoG +A1UECAwDTi9BMQ4wDAYDVQQHDAVFYXJ0aDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL8laXtLXyM64t5dz2B9q+4VvOsChefBi2PlGudqxDuRN3l0Kmcf +un6x2Gng24pTlGdtmiTEWA0a2F8HRLv4YBWhuYleVeBPtf1fF1/SuYgkJOWT7S5q +k/od/tUOLHS0Y067st3FydnFQTKpAuYveEkxleFrMS8hX8cuEgbER+8ybiXKn4Gs +yM/om6lsTyBoaLp5yTAoQb4jAWDbiz1xcjPSkvH2lm7rLGtKoylCYwxRsMh2nZcR +r1OXVhYHXwpYHVB/jVAjy7PAWQ316hi6mpPYbBV+yfn2GUfGuytqyoXLEsrM3iEE +AkU0mJjQmYsCDM3r7ONHTM+UFEk47HCZJccCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFL12SFeOTTDdGKsHKozeByG +HY6nMA0GCSqGSIb3DQEBCwUAA4IBAQAJfe0/uAHobkxth38dqrSFmTo+D5/TMlRt +3hdgjlah6sD2+/DObCyut/XhQWCgTNWyRi4xTKgLh5KSoeJ9EMkADGEgDkU2vjBg +5FmGZsxg6bqjxehK+2HvASJoTH8r41xmTioav7a2i3wNhaNSntw2QRTQBQEDOIzH +RpPDQ2quErjA8nSifE2xmAAr3g+FuookTTJuv37s2cS59zRYsg+WC3+TtPpRssvo +bJ6Xe2D4cCVjUmsqtFEztMgdqgmlcWyGdUKeXdi7CMoeTb4uO+9qRQq46wYWn7K1 +z+W0Kp5yhnnPAoOioAP4vjASDx3z3RnLaZvMmcO7YdCIwhE5oGV0MIIEGjCCAwKg +AwIBAgIRAJ96dbOdrkw/lSTGiwbaagwwDQYJKoZIhvcNAQELBQAwgaQxKjAoBgNV +BAMMIUxlbXVyVHJ1c3QgVW5pdHRlc3RzIFJvb3QgQ0EgMjAxODEjMCEGA1UECgwa +TGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsMHVVuaXR0ZXN0aW5n +IE9wZXJhdGlvbnMgQ2VudGVyMQswCQYDVQQGEwJFRTEMMAoGA1UECAwDTi9BMQ4w +DAYDVQQHDAVFYXJ0aDAeFw0xNzEyMzEyMjAwMDBaFw00NzEyMzEyMjAwMDBaMIGn +MS0wKwYDVQQDDCRMZW11clRydXN0IFVuaXR0ZXN0cyBDbGFzcyAxIENBIDIwMTgx +IzAhBgNVBAoMGkxlbXVyVHJ1c3QgRW50ZXJwcmlzZXMgTHRkMSYwJAYDVQQLDB1V +bml0dGVzdGluZyBPcGVyYXRpb25zIENlbnRlcjELMAkGA1UEBhMCRUUxDDAKBgNV +BAgMA04vQTEOMAwGA1UEBwwFRWFydGgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDR+qNdfNsLhGvgw3IgCQNakL2B9dpQtkVnvAXhdRZqJETm/tHLkGvO +NWTXAwGdoiKv6+0j3I5InUsW+wzUPewcfj+PLNu4mFMq8jH/gPhTElKiAztPRdm8 +QKchvrqiaU6uEbia8ClM6uPpIi8StxE1aJRYL03p0WeMJjJPrsl6eSSdpR4qL69G +Td1n5je9OuWAcn5utXXnt/jO4vNeFRjlGp/0n3JmTDd9w4vtAyY9UrdGgo37eBmi +6mXt5J9i//NenhaiOVU81RqxZM2Jt1kkg2WSjcqcIQfBEWp9StG46VmHLaL+9/v2 +XAV3tL1VilJGj6PoFMb4gY5MXthfGSiXAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQstpQr0iMBVfv0lODIsMgT9+9o +ezANBgkqhkiG9w0BAQsFAAOCAQEASYQbv1Qwb5zES6Gb5LEhrAcH81ZB2uIpKd3K +i6AS4fLJVymMGkUs0RZjt39Ep4qX1zf0hn82Yh9YwRalrkgu+tzKrp0JgegNe6+g +yFRrJC0SIGA4zc3M02m/n4tdaouU2lp6jhmWruL3g25ZkgbQ8LO2zjpSMtblR2eu +vR2+bI7TepklyG71qx5y6/N8x5PT+hnTlleiZeE/ji9D96MZlpWB4kBihekWmxup +tED22z/tpQtac+hPBNgt8z1uFVEYN2rKEcCE7V6Qk7icS+M4Vb7M3D8kLyWDubs9 +Yy3l0EWjOXQXxEhTaKEm4gSuY/j+Y35bBVkA2Fcyuq7msiTgrzCCBBYwggL+oAMC +AQICEGyG1/wKdEaKs3+R3jeWpJswDQYJKoZIhvcNAQELBQAwgaQxKjAoBgNVBAMM +IUxlbXVyVHJ1c3QgVW5pdHRlc3RzIFJvb3QgQ0EgMjAxODEjMCEGA1UECgwaTGVt +dXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsMHVVuaXR0ZXN0aW5nIE9w +ZXJhdGlvbnMgQ2VudGVyMQswCQYDVQQGEwJFRTEMMAoGA1UECAwDTi9BMQ4wDAYD +VQQHDAVFYXJ0aDAeFw0xNzEyMzEyMjAwMDBaFw00NzEyMzEyMjAwMDBaMIGkMSow +KAYDVQQDDCFMZW11clRydXN0IFVuaXR0ZXN0cyBSb290IENBIDIwMTgxIzAhBgNV +BAoMGkxlbXVyVHJ1c3QgRW50ZXJwcmlzZXMgTHRkMSYwJAYDVQQLDB1Vbml0dGVz +dGluZyBPcGVyYXRpb25zIENlbnRlcjELMAkGA1UEBhMCRUUxDDAKBgNVBAgMA04v +QTEOMAwGA1UEBwwFRWFydGgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC/JWl7S18jOuLeXc9gfavuFbzrAoXnwYtj5RrnasQ7kTd5dCpnH7p+sdhp4NuK +U5RnbZokxFgNGthfB0S7+GAVobmJXlXgT7X9Xxdf0rmIJCTlk+0uapP6Hf7VDix0 +tGNOu7LdxcnZxUEyqQLmL3hJMZXhazEvIV/HLhIGxEfvMm4lyp+BrMjP6JupbE8g +aGi6eckwKEG+IwFg24s9cXIz0pLx9pZu6yxrSqMpQmMMUbDIdp2XEa9Tl1YWB18K +WB1Qf41QI8uzwFkN9eoYupqT2GwVfsn59hlHxrsrasqFyxLKzN4hBAJFNJiY0JmL +AgzN6+zjR0zPlBRJOOxwmSXHAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRS9dkhXjk0w3RirByqM3gchh2OpzANBgkq +hkiG9w0BAQsFAAOCAQEACX3tP7gB6G5MbYd/Haq0hZk6Pg+f0zJUbd4XYI5WoerA +9vvwzmwsrrf14UFgoEzVskYuMUyoC4eSkqHifRDJAAxhIA5FNr4wYORZhmbMYOm6 +o8XoSvth7wEiaEx/K+NcZk4qGr+2tot8DYWjUp7cNkEU0AUBAziMx0aTw0NqrhK4 +wPJ0onxNsZgAK94PhbqKJE0ybr9+7NnEufc0WLIPlgt/k7T6UbLL6Gyel3tg+HAl +Y1JrKrRRM7TIHaoJpXFshnVCnl3YuwjKHk2+LjvvakUKuOsGFp+ytc/ltCqecoZ5 +zwKDoqAD+L4wEg8d890Zy2mbzJnDu2HQiMIROaBldKEAMQA= +-----END PKCS7----- +""" + +CERT_CHAIN_PKCS7_PEM = CERT_CHAIN_PKCS7_STR.encode('utf-8') From 1a270cd315f6b298870cc0adb29e1bca613de49f Mon Sep 17 00:00:00 2001 From: Hossein Shafagh Date: Wed, 7 Oct 2020 20:06:20 -0700 Subject: [PATCH 11/14] switching from static DigiCert ICAs to dynamic ones to support: https://knowledge.digicert.com/alerts/DigiCert-ICA-Update.html --- lemur/plugins/lemur_digicert/plugin.py | 28 +++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index 3948acbb..fd94de57 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -21,7 +21,7 @@ import requests import sys from cryptography import x509 from flask import current_app, g -from lemur.common.utils import validate_conf +from lemur.common.utils import validate_conf, convert_pkcs7_bytes_to_pem from lemur.extensions import metrics from lemur.plugins import lemur_digicert as digicert from lemur.plugins.bases import IssuerPlugin, SourcePlugin @@ -235,15 +235,18 @@ def get_certificate_id(session, base_url, order_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"}) + """Retrieve certificate order id from Digicert API, including the chain""" + certificate_url = "{0}/platform/cis/certificate/{1}/download".format(base_url, order_id) + session.headers.update({"Accept": "application/x-pkcs7-certificates"}) response = session.get(certificate_url) if response.status_code == 404: raise Exception("Order not in issued state.") - return response.content + cert_chain_pem = convert_pkcs7_bytes_to_pem(response.content) + if len(cert_chain_pem) < 3: + raise Exception("Missing the certificate chain") + return cert_chain_pem class DigiCertSourcePlugin(SourcePlugin): @@ -552,22 +555,15 @@ class DigiCertCISIssuerPlugin(IssuerPlugin): data = handle_cis_response(response) # retrieve certificate - certificate_pem = get_cis_certificate(self.session, base_url, data["id"]) + certificate_chain_pem = get_cis_certificate(self.session, base_url, data["id"]) self.session.headers.pop("Accept") - end_entity = pem.parse(certificate_pem)[0] + end_entity = certificate_chain_pem[0] + intermediate = certificate_chain_pem[1] - if "ECC" in issuer_options["key_type"]: - return ( - "\n".join(str(end_entity).splitlines()), - current_app.config.get("DIGICERT_ECC_CIS_INTERMEDIATES", {}).get(issuer_options['authority'].name), - data["id"], - ) - - # By default return RSA return ( "\n".join(str(end_entity).splitlines()), - current_app.config.get("DIGICERT_CIS_INTERMEDIATES", {}).get(issuer_options['authority'].name), + "\n".join(str(intermediate).splitlines()), data["id"], ) From 42e9b8b62749adbabc6ec3ea7dd4395d7406107a Mon Sep 17 00:00:00 2001 From: Hossein Shafagh Date: Fri, 9 Oct 2020 15:40:25 -0700 Subject: [PATCH 12/14] removing the intermediary from being optional --- lemur/plugins/lemur_digicert/plugin.py | 2 -- lemur/tests/conf.py | 1 - 2 files changed, 3 deletions(-) diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index fd94de57..cf01c9d1 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -450,7 +450,6 @@ class DigiCertCISSourcePlugin(SourcePlugin): "DIGICERT_CIS_API_KEY", "DIGICERT_CIS_URL", "DIGICERT_CIS_ROOTS", - "DIGICERT_CIS_INTERMEDIATES", "DIGICERT_CIS_PROFILE_NAMES", ] validate_conf(current_app, required_vars) @@ -525,7 +524,6 @@ class DigiCertCISIssuerPlugin(IssuerPlugin): "DIGICERT_CIS_API_KEY", "DIGICERT_CIS_URL", "DIGICERT_CIS_ROOTS", - "DIGICERT_CIS_INTERMEDIATES", "DIGICERT_CIS_PROFILE_NAMES", ] diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index f1019d04..fc6bda98 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -99,7 +99,6 @@ DIGICERT_CIS_URL = "mock://www.digicert.com" DIGICERT_CIS_PROFILE_NAMES = {"sha2-rsa-ecc-root": "ssl_plus"} DIGICERT_CIS_API_KEY = "api-key" DIGICERT_CIS_ROOTS = {"root": "ROOT"} -DIGICERT_CIS_INTERMEDIATES = {"inter": "INTERMEDIATE_CA_CERT"} VERISIGN_URL = "http://example.com" VERISIGN_PEM_PATH = "~/" From d52e0d4e09c742e14fc2dbc3d874f2132029e166 Mon Sep 17 00:00:00 2001 From: sayali Date: Fri, 9 Oct 2020 16:55:19 -0700 Subject: [PATCH 13/14] Certificate edit: update role and notification with owner change --- lemur/certificates/schemas.py | 32 ++++++++++++++++++++++++++++++-- lemur/certificates/service.py | 23 ++++++++++++++--------- lemur/certificates/views.py | 4 ++++ lemur/roles/service.py | 8 ++++++++ lemur/tests/test_certificates.py | 6 ++++++ 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 5da342e5..72ffb063 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -23,6 +23,7 @@ from lemur.domains.schemas import DomainNestedOutputSchema from lemur.notifications import service as notification_service from lemur.notifications.schemas import NotificationNestedOutputSchema from lemur.policies.schemas import RotationPolicyNestedOutputSchema +from lemur.roles import service as roles_service from lemur.roles.schemas import RoleNestedOutputSchema from lemur.schemas import ( AssociatedAuthoritySchema, @@ -184,13 +185,33 @@ class CertificateEditInputSchema(CertificateSchema): data["replaces"] = data[ "replacements" ] # TODO remove when field is deprecated + + if data.get("owner"): + # Check if role already exists. This avoids adding duplicate role. + if data.get("roles") and any(r.get("name") == data["owner"] for r in data["roles"]): + return data + + # Add required role + owner_role = roles_service.get_or_create( + data["owner"], + description="Auto generated role based on owner: {0}".format(data["owner"]) + ) + + # Put role info in correct format using RoleNestedOutputSchema + owner_role_dict = RoleNestedOutputSchema().dump(owner_role).data + if data.get("roles"): + data["roles"].append(owner_role_dict) + else: + data["roles"] = [owner_role_dict] + return data @post_load def enforce_notifications(self, data): """ - Ensures that when an owner changes, default notifications are added for the new owner. - Old owner notifications are retained unless explicitly removed. + Add default notification for current owner if none exist. + This ensures that the default notifications are added in the even of owner change. + Old owner notifications are retained unless explicitly removed later in the code path. :param data: :return: """ @@ -198,11 +219,18 @@ class CertificateEditInputSchema(CertificateSchema): notification_name = "DEFAULT_{0}".format( data["owner"].split("@")[0].upper() ) + + # Even if one default role exists, return + # This allows a User to remove unwanted default notification for current owner + if any(n.label.startswith(notification_name) for n in data["notifications"]): + return data + data[ "notifications" ] += notification_service.create_default_expiration_notifications( notification_name, [data["owner"]] ) + return data diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index b676cffb..812ec101 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -256,6 +256,14 @@ def update(cert_id, **kwargs): return database.update(cert) +def cleanup_owner_roles_notification(owner_name, kwargs): + kwargs["roles"] = [r for r in kwargs["roles"] if r.name != owner_name] + notification_prefix = "DEFAULT_{0}".format( + owner_name.split("@")[0].upper() + ) + kwargs["notifications"] = [n for n in kwargs["notifications"] if not n.label.startswith(notification_prefix)] + + def update_notify(cert, notify_flag): """ Toggle notification value which is a boolean @@ -268,16 +276,13 @@ def update_notify(cert, notify_flag): def create_certificate_roles(**kwargs): - # create an role for the owner and assign it - owner_role = role_service.get_by_name(kwargs["owner"]) - - if not owner_role: - owner_role = role_service.create( - kwargs["owner"], - description="Auto generated role based on owner: {0}".format( - kwargs["owner"] - ), + # create a role for the owner and assign it + owner_role = role_service.get_or_create( + kwargs["owner"], + description="Auto generated role based on owner: {0}".format( + kwargs["owner"] ) + ) # ensure that the authority's owner is also associated with the certificate if kwargs.get("authority"): diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 0eaba4e5..18746636 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -884,6 +884,10 @@ class Certificates(AuthenticatedResource): 400, ) + # if owner is changed, remove all notifications and roles associated with old owner + if cert.owner != data["owner"]: + service.cleanup_owner_roles_notification(cert.owner, data) + cert = service.update(certificate_id, **data) log_service.create(g.current_user, "update_cert", certificate=cert) return cert diff --git a/lemur/roles/service.py b/lemur/roles/service.py index 51597d6e..fa4c9c97 100644 --- a/lemur/roles/service.py +++ b/lemur/roles/service.py @@ -128,3 +128,11 @@ def render(args): query = database.filter(query, Role, terms) return database.sort_and_page(query, Role, args) + + +def get_or_create(role_name, description): + role = get_by_name(role_name) + if not role: + role = create(name=role_name, description=description) + + return role diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 8403461b..4edc485f 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -180,7 +180,10 @@ def test_certificate_edit_schema(session): input_data = {"owner": "bob@example.com"} data, errors = CertificateEditInputSchema().load(input_data) + + assert not errors assert len(data["notifications"]) == 3 + assert data["roles"][0].name == input_data["owner"] def test_authority_key_identifier_schema(): @@ -970,6 +973,9 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin): headers=VALID_ADMIN_HEADER_TOKEN, ) assert resp.status_code == 200 + assert len(certificate.notifications) == 3 + assert certificate.roles[0].name == "bob@example.com" + assert certificate.notify @pytest.mark.parametrize( From fb4df8865bc703f934bdc5b7db4eb202db585cde Mon Sep 17 00:00:00 2001 From: sayali Date: Fri, 9 Oct 2020 17:57:35 -0700 Subject: [PATCH 14/14] Formatting changes and typo --- lemur/certificates/schemas.py | 4 ++-- lemur/certificates/service.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 72ffb063..688d6ba4 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -194,7 +194,7 @@ class CertificateEditInputSchema(CertificateSchema): # Add required role owner_role = roles_service.get_or_create( data["owner"], - description="Auto generated role based on owner: {0}".format(data["owner"]) + description=f"Auto generated role based on owner: {data['owner']}" ) # Put role info in correct format using RoleNestedOutputSchema @@ -210,7 +210,7 @@ class CertificateEditInputSchema(CertificateSchema): def enforce_notifications(self, data): """ Add default notification for current owner if none exist. - This ensures that the default notifications are added in the even of owner change. + This ensures that the default notifications are added in the event of owner change. Old owner notifications are retained unless explicitly removed later in the code path. :param data: :return: diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 812ec101..6d1bd2ac 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -258,9 +258,7 @@ def update(cert_id, **kwargs): def cleanup_owner_roles_notification(owner_name, kwargs): kwargs["roles"] = [r for r in kwargs["roles"] if r.name != owner_name] - notification_prefix = "DEFAULT_{0}".format( - owner_name.split("@")[0].upper() - ) + notification_prefix = f"DEFAULT_{owner_name.split('@')[0].upper()}" kwargs["notifications"] = [n for n in kwargs["notifications"] if not n.label.startswith(notification_prefix)] @@ -279,9 +277,7 @@ def create_certificate_roles(**kwargs): # create a role for the owner and assign it owner_role = role_service.get_or_create( kwargs["owner"], - description="Auto generated role based on owner: {0}".format( - kwargs["owner"] - ) + description=f"Auto generated role based on owner: {kwargs['owner']}" ) # ensure that the authority's owner is also associated with the certificate