<!-- 
.. title: Adding Account Deactivation to Certbot
.. slug: adding-account-deactivation-to-certbot
.. date: 2016-11-07 00:13:34 UTC
.. tags: 
.. category: 
.. link: 
.. description: 
.. type: text
-->

The recent ACME spec included a way for users to deactivate their accounts.
This post describes adding this feature to certbot. This feature touches almost
every level of certbot, so it gives an instructive look at Certbot's
architecture.

An account in this case means the identity associated some key pair. Accounts
control identifiers (domain names).

All the code describing the ACME protocol is in the `acme/` package.  The
messages between the client and server are defined in
`certbot/acme/acme/messages.py` So we can add the deactivation method here.
From the [ACME spec](
https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-6.2.2) we see:

```
6.2.2.  Account deactivation

   A client may deactivate an account by posting a signed update to the
   server with a status field of "deactivated."  Clients may wish to do
   this when the account key is compromised.

   POST /acme/reg/asdf HTTP/1.1
   Host: example.com
   Content-Type: application/jose+json

   {
     "protected": base64url({
       "alg": "ES256",
       "jwk": {...},
       "nonce": "ntuJWWSic4WVNSqeUmshgg",
       "url": "https://example.com/acme/reg/asdf"
     })
     "payload": base64url({
       "status": "deactivated"
     }),
     "signature": "earzVLd3m5M4xJzR...bVTqn7R08AKOVf3Y"
   }

   The server MUST verify that the request is signed by the account key.
   If the server accepts the deactivation request, it should reply with
   a 200 (OK) status code and the current contents of the registration
   object.

   Once an account is deactivated, the server MUST NOT accept further
   requests authorized by that account's key.  It is up to server policy
   how long to retain data related to that account, whether to revoke
   certificates issued by that account, and whether to send email to
   that account's contacts.  ACME does not provide a way to reactivate a
   deactivated account.
```

## Adding protocol messages in messages.py

So we crack open messages.py to add this message. In the module is the
`UpdateRegistration` class which represents messages to the `reg` endpoint.
And deactivating an account is actually just updating the "status" field to
"deactivated".  So deactivation is just a kind of updating. So all that is needed
is to make `UpdateRegistration` accept a "status" field. We do this by adding
an attribute to the class like:

```python
status = jose.Field('status', omitempty=True)
```

Not so bad right?

## Deactivation with the client in client.py

Now we need to use this thing we added. The acme package has a notion of a
client, which is a thing that speaks the acme protocol. This is done in
`acme/client.py`.  Fortunately we already have code that sends update messages,
so we use this to make a new function:

```python
class Client(object):
    ...

    def deactivate(self, regr):
        """Deactivate registration."""
        update = {'status': 'deactivated'}
        body = messages.UpdateRegistration(**dict(update))
        ...
        # code for checking the response
        ...
        return new_regr

```

### Testing the client

We have to mock out what we response from the CA. So we'd expect the current
contents of the registration to be returned. It's pretty simple
```python
    def test_deactivate_account(self):
        self.response.headers['Location'] = self.regr.uri
        self.response.json.return_value = self.regr.body.to_json()
        self.assertEqual(self.regr, self.client.deactivate(self.regr))
```

## Adding a deactivate command to certbot

We are done in `acme/`. Now we move to `certbot/` where all the non-protocol
specific code lives. We have to add the pieces the user will interact with on
the command line so `cli.py` is the place to start.

`cli.py` is where all the  command line flags are parsed, including
`--update-registration`. Which is similar to deactivation. So we add a thing:

```python

    helpful.add(
        "register", "--deactivate", action="store_true",
        help="Irrevocably deactivate your account. Certificates associated "
             "with your account can still be revoked, but they can not be "
             "renewed.")
```

Now when `certbot register --deactivate` is entered, the python function
`certbot.main.main` gets called which eventually calls `certbot.main.register`.
The `register` function takes a `config` parameter. And because we used the
`--deactivate` flag, this parameter has `config.deactivate == True`. So we can
add the following code.

```python

def register(config, unused_plugins):
    ...
    if config.deactivate:
        if len(accounts) == 0:
            add_msg("Could not find existing account to deactivate.")
        else:
            yesno = zope.component.getUtility(interfaces.IDisplay).yesno
            prompt = ("Are you SURE you would like to irrevocably deactivate "
                      "your account? You will lose access to the domains "
                      "associated with it.")
            wants_deactivate = yesno(prompt, yes_label='Deactivate',
                                     no_label='Abort', cli_flag='--deactivate',
                                     default=False)
            if wants_deactivate:
                acc, acme = _determine_account(config)
                acme_client = client.Client(config, acc, None, None, acme=acme)
                acme_client.acme.deactivate(acc.regr)
                add_msg("Account deactivated.")
            else:
                add_msg("Deactivation aborted.")
        return
```

This song and dance needed to get the `deactivate` method we made earlier isn't
obvious. I basically copied part of this from other places in the code.

Next, this thing we added needs to be tested. So in `certbot/tests/cli_test.py` we add:

```python

    @mock.patch('certbot.main._determine_account')
    @mock.patch('certbot.main.account')
    @mock.patch('certbot.main.client')
    @mock.patch('certbot.main.zope.component.getUtility')
    def test_registration_deactivate(self, mock_utility, mocked_client,
                                     mocked_account, mocked_det):
        mocked_storage = mock.MagicMock()
        mocked_account.AccountFileStorage.return_value = mocked_storage
        mocked_storage.find_all.return_value = ["an account"]
        mocked_det.return_value = (mock.MagicMock(), "foo")
        acme_client = mock.MagicMock()
        mocked_client.Client.return_value = acme_client

        x = self._call_no_clientmock(["register", "--deactivate"])
        self.assertTrue(x[0] is None)
        self.assertTrue(acme_client.acme.deactivate.called)
        m = "Account deactivated."
        self.assertTrue(m in mock_utility().add_message.call_args[0][0])
```

This isn't very pretty either. `certbot/tests/cli_test.py` is a big monolith,
there is currently an [issue](https://github.com/certbot/certbot/issues/2716)
for this.
