Merge pull request #3153 from unic/feature/store-acme-account-details

Store ACME account details
This commit is contained in:
Hossein Shafagh 2020-10-12 10:22:16 -07:00 committed by GitHub
commit 896d1af0f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 106 additions and 2 deletions

View File

@ -39,6 +39,22 @@ def update(authority_id, description, owner, active, roles):
return database.update(authority) 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): def mint(**kwargs):
""" """
Creates the authority based on the plugin provided. Creates the authority based on the plugin provided.

View File

@ -32,6 +32,7 @@ from lemur.extensions import metrics, sentry
from lemur.plugins import lemur_acme as acme from lemur.plugins import lemur_acme as acme
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin
from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns
from lemur.authorities import service as authorities_service
from retrying import retry from retrying import retry
@ -240,6 +241,7 @@ class AcmeHandler(object):
existing_regr = options.get("acme_regr", current_app.config.get("ACME_REGR")) existing_regr = options.get("acme_regr", current_app.config.get("ACME_REGR"))
if existing_key and existing_regr: if existing_key and existing_regr:
current_app.logger.debug("Reusing existing ACME account")
# Reuse the same account for each certificate issuance # Reuse the same account for each certificate issuance
key = jose.JWK.json_loads(existing_key) key = jose.JWK.json_loads(existing_key)
regr = messages.RegistrationResource.json_loads(existing_regr) regr = messages.RegistrationResource.json_loads(existing_regr)
@ -253,6 +255,7 @@ class AcmeHandler(object):
# Create an account for each certificate issuance # Create an account for each certificate issuance
key = jose.JWKRSA(key=generate_private_key("RSA2048")) key = jose.JWKRSA(key=generate_private_key("RSA2048"))
current_app.logger.debug("Creating a new ACME account")
current_app.logger.debug( current_app.logger.debug(
"Connecting with directory at {0}".format(directory_url) "Connecting with directory at {0}".format(directory_url)
) )
@ -262,6 +265,27 @@ class AcmeHandler(object):
registration = client.new_account_and_tos( registration = client.new_account_and_tos(
messages.NewRegistration.from_data(email=email) 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)) current_app.logger.debug("Connected: {0}".format(registration.uri))
return client, registration return client, registration
@ -447,6 +471,13 @@ class ACMEIssuerPlugin(IssuerPlugin):
"validation": "/^-----BEGIN CERTIFICATE-----/", "validation": "/^-----BEGIN CERTIFICATE-----/",
"helpMessage": "Certificate to use", "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): def __init__(self, *args, **kwargs):

View File

@ -1,8 +1,10 @@
import unittest import unittest
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
import josepy as jose
from cryptography.x509 import DNSName from cryptography.x509 import DNSName
from lemur.plugins.lemur_acme import plugin from lemur.plugins.lemur_acme import plugin
from lemur.common.utils import generate_private_key
from mock import MagicMock from mock import MagicMock
@ -165,11 +167,65 @@ class TestAcme(unittest.TestCase):
with self.assertRaises(Exception): with self.assertRaises(Exception):
self.acme.setup_acme_client(mock_authority) 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.BackwardsCompatibleClientV2")
@patch("lemur.plugins.lemur_acme.plugin.current_app") @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 = 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_client = Mock()
mock_registration = Mock() mock_registration = Mock()
mock_registration.uri = "http://test.com" mock_registration.uri = "http://test.com"
@ -178,6 +234,7 @@ class TestAcme(unittest.TestCase):
mock_acme.return_value = mock_client mock_acme.return_value = mock_client
mock_current_app.config = {} mock_current_app.config = {}
result_client, result_registration = self.acme.setup_acme_client(mock_authority) result_client, result_registration = self.acme.setup_acme_client(mock_authority)
mock_authorities_service.update_options.assert_not_called()
assert result_client assert result_client
assert result_registration assert result_registration