From 10bac107ee6e7a565ce8bb60c1c6b37661012ed2 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 17 May 2017 14:24:59 -0700 Subject: [PATCH] Add an account deactivate utility script. (#4254) * Add an account deactivate utility script. This is handy if you created an account with a tool other than Certbot, and want to deactivate the account. * Move deactivate.py to tools. * Add test for ConflictError. * Fix lint error. * Document how to set server. --- acme/acme/client.py | 3 +++ acme/acme/client_test.py | 6 +++++ acme/acme/errors.py | 11 +++++++++ tools/deactivate.py | 49 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 tools/deactivate.py diff --git a/acme/acme/client.py b/acme/acme/client.py index a069876d5..9455159de 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -564,6 +564,9 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes except ValueError: jobj = None + if response.status_code == 409: + raise errors.ConflictError(response.headers.get('Location')) + if not response.ok: if jobj is not None: if response_ct != cls.JSON_ERROR_CONTENT_TYPE: diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 09bb38c00..cd1a90645 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -513,6 +513,12 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual( self.response, self.net._check_response(self.response)) + def test_check_response_conflict(self): + self.response.ok = False + self.response.status_code = 409 + # pylint: disable=protected-access + self.assertRaises(errors.ConflictError, self.net._check_response, self.response) + def test_check_response_jobj(self): self.response.json.return_value = {} for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 7446b60fc..9d991fd75 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -82,3 +82,14 @@ class PollError(ClientError): def __repr__(self): return '{0}(exhausted={1!r}, updated={2!r})'.format( self.__class__.__name__, self.exhausted, self.updated) + +class ConflictError(ClientError): + """Error for when the server returns a 409 (Conflict) HTTP status. + + In the version of ACME implemented by Boulder, this is used to find an + account if you only have the private key, but don't know the account URL. + """ + def __init__(self, location): + self.location = location + super(ConflictError, self).__init__() + diff --git a/tools/deactivate.py b/tools/deactivate.py new file mode 100644 index 000000000..5facc8436 --- /dev/null +++ b/tools/deactivate.py @@ -0,0 +1,49 @@ +""" +Given an ACME account key as input, deactivate the account. + +This can be useful if you created an account with a non-Certbot client and now +want to deactivate it. + +Private key should be in PKCS#8 PEM form. + +To provide the URL for the ACME server you want to use, set it in the $DIRECTORY +environment variable, e.g.: + +DIRECTORY=https://acme-staging.api.letsencrypt.org/directory python \ + deactivate.py private_key.pem +""" +import os +import sys + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization + +from acme import client as acme_client +from acme import errors as acme_errors +from acme import jose +from acme import messages + +DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4000/directory') + +if len(sys.argv) != 2: + print("Usage: python deactivate.py private_key.pem") + sys.exit(1) + +data = open(sys.argv[1], "r").read() +key = jose.JWKRSA(key=serialization.load_pem_private_key( + data, None, default_backend())) + +net = acme_client.ClientNetwork(key, verify_ssl=False, + user_agent="acme account deactivator") + +client = acme_client.Client(DIRECTORY, key=key, net=net) +try: + # We expect this to fail and give us a Conflict response with a Location + # header pointing at the account's URL. + client.register() +except acme_errors.ConflictError as e: + location = e.location +if location is None: + raise "Key was not previously registered (but now is)." +client.deactivate_registration(messages.RegistrationResource(uri=location))