Support LetsEncrypt accounts

This commit is contained in:
Curtis Castrapel 2018-07-30 15:25:02 -07:00
parent 46cd1a21f7
commit 7463d47057
6 changed files with 75 additions and 22 deletions

View File

@ -108,12 +108,13 @@ def fetch_all_acme():
error_log["message"] = "Deleting pending certificate" error_log["message"] = "Deleting pending certificate"
send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify) send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify)
pending_certificate_service.delete_by_id(pending_cert.id) 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) 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["message"] = "Complete"
log_data["new"] = new log_data["new"] = new
log_data["failed"] = failed log_data["failed"] = failed

View File

@ -140,14 +140,27 @@ def setup_acme_client(authority):
tel = options.get('telephone', current_app.config.get('ACME_TEL')) tel = options.get('telephone', current_app.config.get('ACME_TEL'))
directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL')) 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) net = ClientNetwork(key, account=None)
client = BackwardsCompatibleClientV2(net, key, directory_url) client = BackwardsCompatibleClientV2(net, key, directory_url)
registration = client.new_account_and_tos(messages.NewRegistration.from_data(email=email)) registration = client.new_account_and_tos(messages.NewRegistration.from_data(email=email))
current_app.logger.debug("Connected: {0}".format(registration.uri)) current_app.logger.debug("Connected: {0}".format(registration.uri))
return client, registration return client, registration
@ -196,6 +209,35 @@ def finalize_authorizations(acme_client, account_number, dns_provider, authoriza
return authorizations 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): class ACMEIssuerPlugin(IssuerPlugin):
title = 'Acme' title = 'Acme'
slug = 'acme-issuer' slug = 'acme-issuer'
@ -333,12 +375,21 @@ class ACMEIssuerPlugin(IssuerPlugin):
"cert": cert, "cert": cert,
"pending_cert": entry["pending_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) current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True)
certs.append({ certs.append({
"cert": False, "cert": False,
"pending_cert": entry["pending_cert"], "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 return certs
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):

View File

@ -134,6 +134,7 @@ class TestAcme(unittest.TestCase):
mock_client.register = mock_registration mock_client.register = mock_registration
mock_client.agree_to_tos = Mock(return_value=True) mock_client.agree_to_tos = Mock(return_value=True)
mock_acme.return_value = mock_client mock_acme.return_value = mock_client
mock_current_app.config = {}
result_client, result_registration = plugin.setup_acme_client(mock_authority) result_client, result_registration = plugin.setup_acme_client(mock_authority)
assert result_client assert result_client
assert result_registration assert result_registration

View File

@ -15,8 +15,8 @@ asyncpool==1.0
babel==2.6.0 # via sphinx babel==2.6.0 # via sphinx
bcrypt==3.1.4 bcrypt==3.1.4
blinker==1.4 blinker==1.4
boto3==1.7.62 boto3==1.7.65
botocore==1.10.62 botocore==1.10.65
certifi==2018.4.16 certifi==2018.4.16
cffi==1.11.5 cffi==1.11.5
chardet==3.0.4 chardet==3.0.4
@ -55,11 +55,11 @@ mock==2.0.0
ndg-httpsclient==0.5.1 ndg-httpsclient==0.5.1
packaging==17.1 # via sphinx packaging==17.1 # via sphinx
paramiko==2.4.1 paramiko==2.4.1
pbr==4.1.1 pbr==4.2.0
pem==18.1.0 pem==18.1.0
psycopg2==2.7.5 psycopg2==2.7.5
pyasn1-modules==0.2.2 pyasn1-modules==0.2.2
pyasn1==0.4.3 pyasn1==0.4.4
pycparser==2.18 pycparser==2.18
pygments==2.2.0 # via sphinx pygments==2.2.0 # via sphinx
pyjwt==1.6.4 pyjwt==1.6.4

View File

@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography
atomicwrites==1.1.5 # via pytest atomicwrites==1.1.5 # via pytest
attrs==18.1.0 # via pytest attrs==18.1.0 # via pytest
aws-xray-sdk==0.95 # via moto 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 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 certifi==2018.4.16 # via requests
cffi==1.11.5 # via cryptography cffi==1.11.5 # via cryptography
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
@ -37,14 +37,14 @@ more-itertools==4.2.0 # via pytest
moto==1.3.3 moto==1.3.3
nose==1.3.7 nose==1.3.7
pbr==4.2.0 # via mock pbr==4.2.0 # via mock
pluggy==0.6.0 # via pytest pluggy==0.7.1 # via pytest
py==1.5.4 # via pytest py==1.5.4 # via pytest
pyaml==17.12.1 # via moto pyaml==17.12.1 # via moto
pycparser==2.18 # via cffi pycparser==2.18 # via cffi
pyflakes==2.0.0 pyflakes==2.0.0
pytest-flask==0.10.0 pytest-flask==0.10.0
pytest-mock==1.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 python-dateutil==2.6.1 # via botocore, faker, freezegun, moto
pytz==2018.5 # via moto pytz==2018.5 # via moto
pyyaml==3.13 # via pyaml pyyaml==3.13 # via pyaml

View File

@ -13,8 +13,8 @@ asn1crypto==0.24.0 # via cryptography
asyncpool==1.0 asyncpool==1.0
bcrypt==3.1.4 # via flask-bcrypt, paramiko bcrypt==3.1.4 # via flask-bcrypt, paramiko
blinker==1.4 # via flask-mail, flask-principal, raven blinker==1.4 # via flask-mail, flask-principal, raven
boto3==1.7.65 boto3==1.7.66
botocore==1.10.65 # via boto3, s3transfer botocore==1.10.66 # via boto3, s3transfer
certifi==2018.4.16 certifi==2018.4.16
cffi==1.11.5 # via bcrypt, cryptography, pynacl cffi==1.11.5 # via bcrypt, cryptography, pynacl
chardet==3.0.4 # via requests chardet==3.0.4 # via requests