From 0889076d3ba1ccbb2cbd05888aab0a189ea8bd5d Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Mon, 30 Jul 2018 15:25:02 -0700 Subject: [PATCH] Support LetsEncrypt accounts --- lemur/pending_certificates/cli.py | 11 ++-- lemur/plugins/lemur_acme/plugin.py | 65 ++++++++++++++++++--- lemur/plugins/lemur_acme/tests/test_acme.py | 1 + requirements-docs.txt | 8 +-- requirements-tests.txt | 8 +-- requirements.txt | 4 +- 6 files changed, 75 insertions(+), 22 deletions(-) diff --git a/lemur/pending_certificates/cli.py b/lemur/pending_certificates/cli.py index 6dc01679..0deeaf68 100644 --- a/lemur/pending_certificates/cli.py +++ b/lemur/pending_certificates/cli.py @@ -108,12 +108,13 @@ def fetch_all_acme(): error_log["message"] = "Deleting pending certificate" send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify) pending_certificate_service.delete_by_id(pending_cert.id) + else: + pending_certificate_service.increment_attempt(pending_cert) + pending_certificate_service.update( + cert.get("pending_cert").id, + status=str(cert.get("last_error"))[0:128] + ) current_app.logger.error(error_log) - pending_certificate_service.increment_attempt(pending_cert) - pending_certificate_service.update( - cert.get("pending_cert").id, - status=str(cert.get("last_error"))[0:128] - ) log_data["message"] = "Complete" log_data["new"] = new log_data["failed"] = failed diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 0d3e9c2a..1cfac059 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -140,14 +140,27 @@ def setup_acme_client(authority): tel = options.get('telephone', current_app.config.get('ACME_TEL')) directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL')) - key = jose.JWKRSA(key=generate_private_key('RSA2048')) + existing_key = options.get('acme_private_key', current_app.config.get('ACME_PRIVATE_KEY')) + existing_regr = options.get('acme_regr', current_app.config.get('ACME_REGR')) + print(existing_key) + if existing_key and existing_regr: + # Reuse the same account for each certificate issuance + key = jose.JWK.json_loads(existing_key) + regr = messages.RegistrationResource.json_loads(existing_regr) + current_app.logger.debug("Connecting with directory at {0}".format(directory_url)) + net = ClientNetwork(key, account=regr) + client = BackwardsCompatibleClientV2(net, key, directory_url) + return client, {} + else: + # Create an account for each certificate issuance + key = jose.JWKRSA(key=generate_private_key('RSA2048')) - current_app.logger.debug("Connecting with directory at {0}".format(directory_url)) + current_app.logger.debug("Connecting with directory at {0}".format(directory_url)) - net = ClientNetwork(key, account=None) - client = BackwardsCompatibleClientV2(net, key, directory_url) - registration = client.new_account_and_tos(messages.NewRegistration.from_data(email=email)) - current_app.logger.debug("Connected: {0}".format(registration.uri)) + net = ClientNetwork(key, account=None) + client = BackwardsCompatibleClientV2(net, key, directory_url) + registration = client.new_account_and_tos(messages.NewRegistration.from_data(email=email)) + current_app.logger.debug("Connected: {0}".format(registration.uri)) return client, registration @@ -196,6 +209,35 @@ def finalize_authorizations(acme_client, account_number, dns_provider, authoriza return authorizations +def cleanup_dns_challenges(acme_client, account_number, dns_provider, authorizations, dns_provider_options): + """ + Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called + on an exception + + :param acme_client: + :param account_number: + :param dns_provider: + :param authorizations: + :param dns_provider_options: + :return: + """ + for authz_record in authorizations: + dns_challenges = authz_record.dns_challenge + host_to_validate = maybe_remove_wildcard(authz_record.host) + host_to_validate = maybe_add_extension(host_to_validate, dns_provider_options) + for dns_challenge in dns_challenges: + try: + dns_provider.delete_txt_record( + authz_record.change_id, + account_number, + dns_challenge.validation_domain_name(host_to_validate), + dns_challenge.validation(acme_client.client.net.key) + ) + except Exception: + # If this fails, it's most likely because the record doesn't exist or we're not authorized to modify it. + pass + + class ACMEIssuerPlugin(IssuerPlugin): title = 'Acme' slug = 'acme-issuer' @@ -333,12 +375,21 @@ class ACMEIssuerPlugin(IssuerPlugin): "cert": cert, "pending_cert": entry["pending_cert"], }) - except (PollError, AcmeError, Exception): + except (PollError, AcmeError, Exception) as e: current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True) certs.append({ "cert": False, "pending_cert": entry["pending_cert"], + "last_error": e, }) + # Ensure DNS records get deleted + cleanup_dns_challenges( + entry["acme_client"], + entry["account_number"], + entry["dns_provider_type"], + entry["authorizations"], + entry["dns_provider_options"], + ) return certs def create_certificate(self, csr, issuer_options): diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 2a358a63..69f4e438 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -134,6 +134,7 @@ class TestAcme(unittest.TestCase): mock_client.register = mock_registration mock_client.agree_to_tos = Mock(return_value=True) mock_acme.return_value = mock_client + mock_current_app.config = {} result_client, result_registration = plugin.setup_acme_client(mock_authority) assert result_client assert result_registration diff --git a/requirements-docs.txt b/requirements-docs.txt index 2a688d6f..3244c58e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -15,8 +15,8 @@ asyncpool==1.0 babel==2.6.0 # via sphinx bcrypt==3.1.4 blinker==1.4 -boto3==1.7.62 -botocore==1.10.62 +boto3==1.7.65 +botocore==1.10.65 certifi==2018.4.16 cffi==1.11.5 chardet==3.0.4 @@ -55,11 +55,11 @@ mock==2.0.0 ndg-httpsclient==0.5.1 packaging==17.1 # via sphinx paramiko==2.4.1 -pbr==4.1.1 +pbr==4.2.0 pem==18.1.0 psycopg2==2.7.5 pyasn1-modules==0.2.2 -pyasn1==0.4.3 +pyasn1==0.4.4 pycparser==2.18 pygments==2.2.0 # via sphinx pyjwt==1.6.4 diff --git a/requirements-tests.txt b/requirements-tests.txt index c3405365..a024eb18 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography atomicwrites==1.1.5 # via pytest attrs==18.1.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.7.65 # via moto +boto3==1.7.66 # via moto boto==2.49.0 # via moto -botocore==1.10.65 # via boto3, moto, s3transfer +botocore==1.10.66 # via boto3, moto, s3transfer certifi==2018.4.16 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -37,14 +37,14 @@ more-itertools==4.2.0 # via pytest moto==1.3.3 nose==1.3.7 pbr==4.2.0 # via mock -pluggy==0.6.0 # via pytest +pluggy==0.7.1 # via pytest py==1.5.4 # via pytest pyaml==17.12.1 # via moto pycparser==2.18 # via cffi pyflakes==2.0.0 pytest-flask==0.10.0 pytest-mock==1.10.0 -pytest==3.6.3 +pytest==3.6.4 python-dateutil==2.6.1 # via botocore, faker, freezegun, moto pytz==2018.5 # via moto pyyaml==3.13 # via pyaml diff --git a/requirements.txt b/requirements.txt index 0474e651..a88d2309 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,8 @@ asn1crypto==0.24.0 # via cryptography asyncpool==1.0 bcrypt==3.1.4 # via flask-bcrypt, paramiko blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.7.65 -botocore==1.10.65 # via boto3, s3transfer +boto3==1.7.66 +botocore==1.10.66 # via boto3, s3transfer certifi==2018.4.16 cffi==1.11.5 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests