diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index c70c6fc5..0913e629 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -39,6 +39,22 @@ 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. diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 16d61a0f..8bc1485f 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 @@ -240,6 +241,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 +255,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) ) @@ -262,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 @@ -447,6 +471,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": False, + } ] def __init__(self, *args, **kwargs): diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 8320a2de..ab246563 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,11 +167,65 @@ 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(self, mock_current_app, mock_acme): + 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.options = '[{"name": "mock_name", "value": "mock_value"}]' + 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") + @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.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, mock_authorities_service): + mock_authority = Mock() + 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" @@ -178,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