示例#1
0
    def __init__(self, module, backend):
        self.module = module
        self.version = module.params['acme_version']
        self.challenge = module.params['challenge']
        self.csr = module.params['csr']
        self.csr_content = module.params['csr_content']
        self.dest = module.params.get('dest')
        self.fullchain_dest = module.params.get('fullchain_dest')
        self.chain_dest = module.params.get('chain_dest')
        self.client = ACMEClient(module, backend)
        self.account = ACMEAccount(self.client)
        self.directory = self.client.directory
        self.data = module.params['data']
        self.authorizations = None
        self.cert_days = -1
        self.order = None
        self.order_uri = self.data.get('order_uri') if self.data else None
        self.all_chains = None
        self.select_chain_matcher = []

        if self.module.params['select_chain']:
            for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
                self.select_chain_matcher.append(
                    self.client.backend.create_chain_matcher(
                        Criterium(criterium, index=criterium_idx)))

        # Make sure account exists
        modify_account = module.params['modify_account']
        if modify_account or self.version > 1:
            contact = []
            if module.params['account_email']:
                contact.append('mailto:' + module.params['account_email'])
            created, account_data = self.account.setup_account(
                contact,
                agreement=module.params.get('agreement'),
                terms_agreed=module.params.get('terms_agreed'),
                allow_creation=modify_account,
            )
            if account_data is None:
                raise ModuleFailException(msg='Account does not exist or is deactivated.')
            updated = False
            if not created and account_data and modify_account:
                updated, account_data = self.account.update_account(account_data, contact)
            self.changed = created or updated
        else:
            # This happens if modify_account is False and the ACME v1
            # protocol is used. In this case, we do not call setup_account()
            # to avoid accidental creation of an account. This is OK
            # since for ACME v1, the account URI is not needed to send a
            # signed ACME request.
            pass

        if self.csr is not None and not os.path.exists(self.csr):
            raise ModuleFailException("CSR %s not found" % (self.csr))

        # Extract list of identifiers from CSR
        self.identifiers = self.client.backend.get_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
示例#2
0
 def __init__(self, module):
     module.deprecate(
         'Please adjust your custom module/plugin to the ACME module_utils refactor '
         '(https://github.com/ansible-collections/community.crypto/pull/184). The '
         'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
         'your code',
         version='2.0.0',
         collection_name='community.crypto')
     backend = get_compatibility_backend(module)
     self.client = ACMEClient(module, backend)
     self.account = ACMEAccount(self.client)
     self.key = self.client.account_key_file
     self.key_content = self.client.account_key_content
     self.uri = self.client.account_uri
     self.key_data = self.client.account_key_data
     self.jwk = self.client.account_jwk
     self.jws_header = self.client.account_jws_header
     self.directory = self.client.directory
示例#3
0
def main():
    argument_spec = get_default_argspec()
    argument_spec.update(
        dict(retrieve_orders=dict(
            type='str',
            default='ignore',
            choices=['ignore', 'url_list', 'object_list']), ))
    module = AnsibleModule(
        argument_spec=argument_spec,
        required_one_of=(['account_key_src', 'account_key_content'], ),
        mutually_exclusive=(['account_key_src', 'account_key_content'], ),
        supports_check_mode=True,
    )
    if module._name in ('acme_account_facts',
                        'community.crypto.acme_account_facts'):
        module.deprecate(
            "The 'acme_account_facts' module has been renamed to 'acme_account_info'",
            version='2.0.0',
            collection_name='community.crypto')
    backend = create_backend(module, True)

    try:
        client = ACMEClient(module, backend)
        account = ACMEAccount(client)
        # Check whether account exists
        created, account_data = account.setup_account(
            [],
            allow_creation=False,
            remove_account_uri_if_not_exists=True,
        )
        if created:
            raise AssertionError('Unwanted account creation')
        result = {
            'changed': False,
            'exists': client.account_uri is not None,
            'account_uri': client.account_uri,
        }
        if client.account_uri is not None:
            # Make sure promised data is there
            if 'contact' not in account_data:
                account_data['contact'] = []
            account_data['public_account_key'] = client.account_key_data['jwk']
            result['account'] = account_data
            # Retrieve orders list
            if account_data.get(
                    'orders') and module.params['retrieve_orders'] != 'ignore':
                orders = get_orders_list(module, client,
                                         account_data['orders'])
                result['order_uris'] = orders
                if module.params['retrieve_orders'] == 'url_list':
                    module.deprecate(
                        'retrieve_orders=url_list now returns the order URI list as `order_uris`.'
                        ' Right now it also returns this list as `orders` for backwards compatibility,'
                        ' but this will stop in community.crypto 2.0.0',
                        version='2.0.0',
                        collection_name='community.crypto')
                    result['orders'] = orders
                if module.params['retrieve_orders'] == 'object_list':
                    result['orders'] = [
                        get_order(client, order) for order in orders
                    ]
        module.exit_json(**result)
    except ModuleFailException as e:
        e.do_fail(module)
示例#4
0
class ACMECertificateClient(object):
    '''
    ACME client class. Uses an ACME account object and a CSR to
    start and validate ACME challenges and download the respective
    certificates.
    '''
    def __init__(self, module, backend):
        self.module = module
        self.version = module.params['acme_version']
        self.challenge = module.params['challenge']
        self.csr = module.params['csr']
        self.csr_content = module.params['csr_content']
        self.dest = module.params.get('dest')
        self.fullchain_dest = module.params.get('fullchain_dest')
        self.chain_dest = module.params.get('chain_dest')
        self.client = ACMEClient(module, backend)
        self.account = ACMEAccount(self.client)
        self.directory = self.client.directory
        self.data = module.params['data']
        self.authorizations = None
        self.cert_days = -1
        self.order = None
        self.order_uri = self.data.get('order_uri') if self.data else None
        self.all_chains = None
        self.select_chain_matcher = []

        if self.module.params['select_chain']:
            for criterium_idx, criterium in enumerate(
                    self.module.params['select_chain']):
                self.select_chain_matcher.append(
                    self.client.backend.create_chain_matcher(
                        Criterium(criterium, index=criterium_idx)))

        # Make sure account exists
        modify_account = module.params['modify_account']
        if modify_account or self.version > 1:
            contact = []
            if module.params['account_email']:
                contact.append('mailto:' + module.params['account_email'])
            created, account_data = self.account.setup_account(
                contact,
                agreement=module.params.get('agreement'),
                terms_agreed=module.params.get('terms_agreed'),
                allow_creation=modify_account,
            )
            if account_data is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            updated = False
            if not created and account_data and modify_account:
                updated, account_data = self.account.update_account(
                    account_data, contact)
            self.changed = created or updated
        else:
            # This happens if modify_account is False and the ACME v1
            # protocol is used. In this case, we do not call setup_account()
            # to avoid accidental creation of an account. This is OK
            # since for ACME v1, the account URI is not needed to send a
            # signed ACME request.
            pass

        if self.csr is not None and not os.path.exists(self.csr):
            raise ModuleFailException("CSR %s not found" % (self.csr))

        # Extract list of identifiers from CSR
        self.identifiers = self.client.backend.get_csr_identifiers(
            csr_filename=self.csr, csr_content=self.csr_content)

    def is_first_step(self):
        '''
        Return True if this is the first execution of this module, i.e. if a
        sufficient data object from a first run has not been provided.
        '''
        if self.data is None:
            return True
        if self.version == 1:
            # As soon as self.data is a non-empty object, we are in the second stage.
            return not self.data
        else:
            # We are in the second stage if data.order_uri is given (which has been
            # stored in self.order_uri by the constructor).
            return self.order_uri is None

    def start_challenges(self):
        '''
        Create new authorizations for all identifiers of the CSR,
        respectively start a new order for ACME v2.
        '''
        self.authorizations = {}
        if self.version == 1:
            for identifier_type, identifier in self.identifiers:
                if identifier_type != 'dns':
                    raise ModuleFailException(
                        'ACME v1 only supports DNS identifiers!')
            for identifier_type, identifier in self.identifiers:
                authz = Authorization.create(self.client, identifier_type,
                                             identifier)
                self.authorizations[authz.combined_identifier] = authz
        else:
            self.order = Order.create(self.client, self.identifiers)
            self.order_uri = self.order.url
            self.order.load_authorizations(self.client)
            self.authorizations.update(self.order.authorizations)
        self.changed = True

    def get_challenges_data(self, first_step):
        '''
        Get challenge details for the chosen challenge type.
        Return a tuple of generic challenge details, and specialized DNS challenge details.
        '''
        # Get general challenge data
        data = {}
        for type_identifier, authz in self.authorizations.items():
            identifier_type, identifier = split_identifier(type_identifier)
            # Skip valid authentications: their challenges are already valid
            # and do not need to be returned
            if authz.status == 'valid':
                continue
            # We drop the type from the key to preserve backwards compatibility
            data[identifier] = authz.get_challenge_data(self.client)
            if first_step and self.challenge not in data[identifier]:
                raise ModuleFailException(
                    "Found no challenge of type '{0}' for identifier {1}!".
                    format(self.challenge, type_identifier))
        # Get DNS challenge data
        data_dns = {}
        if self.challenge == 'dns-01':
            for identifier, challenges in data.items():
                if self.challenge in challenges:
                    values = data_dns.get(challenges[self.challenge]['record'])
                    if values is None:
                        values = []
                        data_dns[challenges[self.challenge]['record']] = values
                    values.append(challenges[self.challenge]['resource_value'])
        return data, data_dns

    def finish_challenges(self):
        '''
        Verify challenges for all identifiers of the CSR.
        '''
        self.authorizations = {}

        # Step 1: obtain challenge information
        if self.version == 1:
            # For ACME v1, we attempt to create new authzs. Existing ones
            # will be returned instead.
            for identifier_type, identifier in self.identifiers:
                authz = Authorization.create(self.client, identifier_type,
                                             identifier)
                self.authorizations[combine_identifier(identifier_type,
                                                       identifier)] = authz
        else:
            # For ACME v2, we obtain the order object by fetching the
            # order URI, and extract the information from there.
            self.order = Order.from_url(self.client, self.order_uri)
            self.order.load_authorizations(self.client)
            self.authorizations.update(self.order.authorizations)

        # Step 2: validate pending challenges
        for type_identifier, authz in self.authorizations.items():
            if authz.status == 'pending':
                identifier_type, identifier = split_identifier(type_identifier)
                authz.call_validate(self.client, self.challenge)
                self.changed = True

    def download_alternate_chains(self, cert):
        alternate_chains = []
        for alternate in cert.alternates:
            try:
                alt_cert = CertificateChain.download(self.client, alternate)
            except ModuleFailException as e:
                self.module.warn(
                    'Error while downloading alternative certificate {0}: {1}'.
                    format(alternate, e))
                continue
            alternate_chains.append(alt_cert)
        return alternate_chains

    def find_matching_chain(self, chains):
        for criterium_idx, matcher in enumerate(self.select_chain_matcher):
            for chain in chains:
                if matcher.match(chain):
                    self.module.debug(
                        'Found matching chain for criterium {0}'.format(
                            criterium_idx))
                    return chain
        return None

    def get_certificate(self):
        '''
        Request a new certificate and write it to the destination file.
        First verifies whether all authorizations are valid; if not, aborts
        with an error.
        '''
        for identifier_type, identifier in self.identifiers:
            authz = self.authorizations.get(
                combine_identifier(identifier_type, identifier))
            if authz is None:
                raise ModuleFailException(
                    'Found no authorization information for "{identifier}"!'.
                    format(identifier=combine_identifier(
                        identifier_type, identifier)))
            if authz.status != 'valid':
                authz.raise_error(
                    'Status is "{status}" and not "valid"'.format(
                        status=authz.status))

        if self.version == 1:
            cert = retrieve_acme_v1_certificate(
                self.client, pem_to_der(self.csr, self.csr_content))
        else:
            self.order.finalize(self.client,
                                pem_to_der(self.csr, self.csr_content))
            cert = CertificateChain.download(self.client,
                                             self.order.certificate_uri)
            if self.module.params[
                    'retrieve_all_alternates'] or self.select_chain_matcher:
                # Retrieve alternate chains
                alternate_chains = self.download_alternate_chains(cert)

                # Prepare return value for all alternate chains
                if self.module.params['retrieve_all_alternates']:
                    self.all_chains = [cert.to_json()]
                    for alt_chain in alternate_chains:
                        self.all_chains.append(alt_chain.to_json())

                # Try to select alternate chain depending on criteria
                if self.select_chain_matcher:
                    matching_chain = self.find_matching_chain([cert] +
                                                              alternate_chains)
                    if matching_chain:
                        cert = matching_chain
                    else:
                        self.module.debug(
                            'Found no matching alternative chain')

        if cert.cert is not None:
            pem_cert = cert.cert
            chain = cert.chain

            if self.dest and write_file(self.module, self.dest,
                                        pem_cert.encode('utf8')):
                self.cert_days = self.client.backend.get_cert_days(self.dest)
                self.changed = True

            if self.fullchain_dest and write_file(
                    self.module, self.fullchain_dest,
                (pem_cert + "\n".join(chain)).encode('utf8')):
                self.cert_days = self.client.backend.get_cert_days(
                    self.fullchain_dest)
                self.changed = True

            if self.chain_dest and write_file(
                    self.module, self.chain_dest,
                ("\n".join(chain)).encode('utf8')):
                self.changed = True

    def deactivate_authzs(self):
        '''
        Deactivates all valid authz's. Does not raise exceptions.
        https://community.letsencrypt.org/t/authorization-deactivation/19860/2
        https://tools.ietf.org/html/rfc8555#section-7.5.2
        '''
        for authz in self.authorizations.values():
            try:
                authz.deactivate(self.client)
            except Exception:
                # ignore errors
                pass
            if authz.status != 'deactivated':
                self.module.warn(
                    warning='Could not deactivate authz object {0}.'.format(
                        authz.url))
示例#5
0
def main():
    argument_spec = get_default_argspec()
    argument_spec.update(
        dict(
            private_key_src=dict(type='path'),
            private_key_content=dict(type='str', no_log=True),
            private_key_passphrase=dict(type='str', no_log=True),
            certificate=dict(type='path', required=True),
            revoke_reason=dict(type='int'),
        ))
    module = AnsibleModule(
        argument_spec=argument_spec,
        required_one_of=([
            'account_key_src', 'account_key_content', 'private_key_src',
            'private_key_content'
        ], ),
        mutually_exclusive=([
            'account_key_src', 'account_key_content', 'private_key_src',
            'private_key_content'
        ], ),
        supports_check_mode=False,
    )
    backend = create_backend(module, False)

    try:
        client = ACMEClient(module, backend)
        account = ACMEAccount(client)
        # Load certificate
        certificate = pem_to_der(module.params.get('certificate'))
        certificate = nopad_b64(certificate)
        # Construct payload
        payload = {'certificate': certificate}
        if module.params.get('revoke_reason') is not None:
            payload['reason'] = module.params.get('revoke_reason')
        # Determine endpoint
        if module.params.get('acme_version') == 1:
            endpoint = client.directory['revoke-cert']
            payload['resource'] = 'revoke-cert'
        else:
            endpoint = client.directory['revokeCert']
        # Get hold of private key (if available) and make sure it comes from disk
        private_key = module.params.get('private_key_src')
        private_key_content = module.params.get('private_key_content')
        # Revoke certificate
        if private_key or private_key_content:
            passphrase = module.params['private_key_passphrase']
            # Step 1: load and parse private key
            try:
                private_key_data = client.parse_key(private_key,
                                                    private_key_content,
                                                    passphrase=passphrase)
            except KeyParsingError as e:
                raise ModuleFailException(
                    "Error while parsing private key: {msg}".format(msg=e.msg))
            # Step 2: sign revokation request with private key
            jws_header = {
                "alg": private_key_data['alg'],
                "jwk": private_key_data['jwk'],
            }
            result, info = client.send_signed_request(
                endpoint,
                payload,
                key_data=private_key_data,
                jws_header=jws_header,
                fail_on_error=False)
        else:
            # Step 1: get hold of account URI
            created, account_data = account.setup_account(allow_creation=False)
            if created:
                raise AssertionError('Unwanted account creation')
            if account_data is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            # Step 2: sign revokation request with account key
            result, info = client.send_signed_request(endpoint,
                                                      payload,
                                                      fail_on_error=False)
        if info['status'] != 200:
            already_revoked = False
            # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6)
            if result.get(
                    'type') == 'urn:ietf:params:acme:error:alreadyRevoked':
                already_revoked = True
            else:
                # Hack for Boulder errors
                if module.params.get('acme_version') == 1:
                    error_type = 'urn:acme:error:malformed'
                else:
                    error_type = 'urn:ietf:params:acme:error:malformed'
                if result.get('type') == error_type and result.get(
                        'detail') == 'Certificate already revoked':
                    # Fallback: boulder returns this in case the certificate was already revoked.
                    already_revoked = True
            # If we know the certificate was already revoked, we don't fail,
            # but successfully terminate while indicating no change
            if already_revoked:
                module.exit_json(changed=False)
            raise ACMEProtocolException('Failed to revoke certificate',
                                        info=info,
                                        content_json=result)
        module.exit_json(changed=True)
    except ModuleFailException as e:
        e.do_fail(module)
示例#6
0
class ACMELegacyAccount(object):
    '''
    ACME account object. Handles the authorized communication with the
    ACME server. Provides access to account bound information like
    the currently active authorizations and valid certificates
    '''
    def __init__(self, module):
        module.deprecate(
            'Please adjust your custom module/plugin to the ACME module_utils refactor '
            '(https://github.com/ansible-collections/community.crypto/pull/184). The '
            'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
            'your code',
            version='2.0.0',
            collection_name='community.crypto')
        backend = get_compatibility_backend(module)
        self.client = ACMEClient(module, backend)
        self.account = ACMEAccount(self.client)
        self.key = self.client.account_key_file
        self.key_content = self.client.account_key_content
        self.uri = self.client.account_uri
        self.key_data = self.client.account_key_data
        self.jwk = self.client.account_jwk
        self.jws_header = self.client.account_jws_header
        self.directory = self.client.directory

    def get_keyauthorization(self, token):
        '''
        Returns the key authorization for the given token
        https://tools.ietf.org/html/rfc8555#section-8.1
        '''
        return create_key_authorization(self.client, token)

    def parse_key(self, key_file=None, key_content=None):
        '''
        Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
        (error, key_data).
        '''
        try:
            return None, self.client.parse_key(key_file=key_file,
                                               key_content=key_content)
        except KeyParsingError as e:
            return e.msg, {}

    def sign_request(self, protected, payload, key_data, encode_payload=True):
        return self.client.sign_request(protected,
                                        payload,
                                        key_data,
                                        encode_payload=encode_payload)

    def send_signed_request(self,
                            url,
                            payload,
                            key_data=None,
                            jws_header=None,
                            parse_json_result=True,
                            encode_payload=True):
        '''
        Sends a JWS signed HTTP POST request to the ACME server and returns
        the response as dictionary
        https://tools.ietf.org/html/rfc8555#section-6.2

        If payload is None, a POST-as-GET is performed.
        (https://tools.ietf.org/html/rfc8555#section-6.3)
        '''
        return self.client.send_signed_request(
            url,
            payload,
            key_data=key_data,
            jws_header=jws_header,
            parse_json_result=parse_json_result,
            encode_payload=encode_payload,
            fail_on_error=False,
        )

    def get_request(self,
                    uri,
                    parse_json_result=True,
                    headers=None,
                    get_only=False,
                    fail_on_error=True):
        '''
        Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
        to GET if server replies with a status code of 405.
        '''
        return self.client.get_request(
            uri,
            parse_json_result=parse_json_result,
            headers=headers,
            get_only=get_only,
            fail_on_error=fail_on_error,
        )

    def set_account_uri(self, uri):
        '''
        Set account URI. For ACME v2, it needs to be used to sending signed
        requests.
        '''
        self.client.set_account_uri(uri)
        self.uri = self.client.account_uri

    def get_account_data(self):
        '''
        Retrieve account information. Can only be called when the account
        URI is already known (such as after calling setup_account).
        Return None if the account was deactivated, or a dict otherwise.
        '''
        return self.account.get_account_data()

    def setup_account(self,
                      contact=None,
                      agreement=None,
                      terms_agreed=False,
                      allow_creation=True,
                      remove_account_uri_if_not_exists=False,
                      external_account_binding=None):
        '''
        Detect or create an account on the ACME server. For ACME v1,
        as the only way (without knowing an account URI) to test if an
        account exists is to try and create one with the provided account
        key, this method will always result in an account being present
        (except on error situations). For ACME v2, a new account will
        only be created if ``allow_creation`` is set to True.

        For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
        account might be created if it does not yet exist.

        Return a pair ``(created, account_data)``. Here, ``created`` will
        be ``True`` in case the account was created or would be created
        (check mode). ``account_data`` will be the current account data,
        or ``None`` if the account does not exist.

        The account URI will be stored in ``self.uri``; if it is ``None``,
        the account does not exist.

        If specified, ``external_account_binding`` should be a dictionary
        with keys ``kid``, ``alg`` and ``key``
        (https://tools.ietf.org/html/rfc8555#section-7.3.4).

        https://tools.ietf.org/html/rfc8555#section-7.3
        '''
        result = self.account.setup_account(
            contact=contact,
            agreement=agreement,
            terms_agreed=terms_agreed,
            allow_creation=allow_creation,
            remove_account_uri_if_not_exists=remove_account_uri_if_not_exists,
            external_account_binding=external_account_binding,
        )
        self.uri = self.client.account_uri
        return result

    def update_account(self, account_data, contact=None):
        '''
        Update an account on the ACME server. Check mode is fully respected.

        The current account data must be provided as ``account_data``.

        Return a pair ``(updated, account_data)``, where ``updated`` is
        ``True`` in case something changed (contact info updated) or
        would be changed (check mode), and ``account_data`` the updated
        account data.

        https://tools.ietf.org/html/rfc8555#section-7.3.2
        '''
        return self.account.update_account(account_data, contact=contact)
示例#7
0
def main():
    argument_spec = get_default_argspec()
    argument_spec.update(dict(
        terms_agreed=dict(type='bool', default=False),
        state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']),
        allow_creation=dict(type='bool', default=True),
        contact=dict(type='list', elements='str', default=[]),
        new_account_key_src=dict(type='path'),
        new_account_key_content=dict(type='str', no_log=True),
        new_account_key_passphrase=dict(type='str', no_log=True),
        external_account_binding=dict(type='dict', options=dict(
            kid=dict(type='str', required=True),
            alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
            key=dict(type='str', required=True, no_log=True),
        ))
    ))
    module = AnsibleModule(
        argument_spec=argument_spec,
        required_one_of=(
            ['account_key_src', 'account_key_content'],
        ),
        mutually_exclusive=(
            ['account_key_src', 'account_key_content'],
            ['new_account_key_src', 'new_account_key_content'],
        ),
        required_if=(
            # Make sure that for state == changed_key, one of
            # new_account_key_src and new_account_key_content are specified
            ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True],
        ),
        supports_check_mode=True,
    )
    backend = create_backend(module, True)

    if module.params['external_account_binding']:
        # Make sure padding is there
        key = module.params['external_account_binding']['key']
        if len(key) % 4 != 0:
            key = key + ('=' * (4 - (len(key) % 4)))
        # Make sure key is Base64 encoded
        try:
            base64.urlsafe_b64decode(key)
        except Exception as e:
            module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e)
        module.params['external_account_binding']['key'] = key

    try:
        client = ACMEClient(module, backend)
        account = ACMEAccount(client)
        changed = False
        state = module.params.get('state')
        diff_before = {}
        diff_after = {}
        if state == 'absent':
            created, account_data = account.setup_account(allow_creation=False)
            if account_data:
                diff_before = dict(account_data)
                diff_before['public_account_key'] = client.account_key_data['jwk']
            if created:
                raise AssertionError('Unwanted account creation')
            if account_data is not None:
                # Account is not yet deactivated
                if not module.check_mode:
                    # Deactivate it
                    payload = {
                        'status': 'deactivated'
                    }
                    result, info = client.send_signed_request(
                        client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200])
                changed = True
        elif state == 'present':
            allow_creation = module.params.get('allow_creation')
            contact = [str(v) for v in module.params.get('contact')]
            terms_agreed = module.params.get('terms_agreed')
            external_account_binding = module.params.get('external_account_binding')
            created, account_data = account.setup_account(
                contact,
                terms_agreed=terms_agreed,
                allow_creation=allow_creation,
                external_account_binding=external_account_binding,
            )
            if account_data is None:
                raise ModuleFailException(msg='Account does not exist or is deactivated.')
            if created:
                diff_before = {}
            else:
                diff_before = dict(account_data)
                diff_before['public_account_key'] = client.account_key_data['jwk']
            updated = False
            if not created:
                updated, account_data = account.update_account(account_data, contact)
            changed = created or updated
            diff_after = dict(account_data)
            diff_after['public_account_key'] = client.account_key_data['jwk']
        elif state == 'changed_key':
            # Parse new account key
            try:
                new_key_data = client.parse_key(
                    module.params.get('new_account_key_src'),
                    module.params.get('new_account_key_content'),
                    passphrase=module.params.get('new_account_key_passphrase'),
                )
            except KeyParsingError as e:
                raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg))
            # Verify that the account exists and has not been deactivated
            created, account_data = account.setup_account(allow_creation=False)
            if created:
                raise AssertionError('Unwanted account creation')
            if account_data is None:
                raise ModuleFailException(msg='Account does not exist or is deactivated.')
            diff_before = dict(account_data)
            diff_before['public_account_key'] = client.account_key_data['jwk']
            # Now we can start the account key rollover
            if not module.check_mode:
                # Compose inner signed message
                # https://tools.ietf.org/html/rfc8555#section-7.3.5
                url = client.directory['keyChange']
                protected = {
                    "alg": new_key_data['alg'],
                    "jwk": new_key_data['jwk'],
                    "url": url,
                }
                payload = {
                    "account": client.account_uri,
                    "newKey": new_key_data['jwk'],  # specified in draft 12 and older
                    "oldKey": client.account_jwk,  # specified in draft 13 and newer
                }
                data = client.sign_request(protected, payload, new_key_data)
                # Send request and verify result
                result, info = client.send_signed_request(
                    url, data, error_msg='Failed to rollover account key', expected_status_codes=[200])
                if module._diff:
                    client.account_key_data = new_key_data
                    client.account_jws_header['alg'] = new_key_data['alg']
                    diff_after = account.get_account_data()
            elif module._diff:
                # Kind of fake diff_after
                diff_after = dict(diff_before)
            diff_after['public_account_key'] = new_key_data['jwk']
            changed = True
        result = {
            'changed': changed,
            'account_uri': client.account_uri,
        }
        if module._diff:
            result['diff'] = {
                'before': diff_before,
                'after': diff_after,
            }
        module.exit_json(**result)
    except ModuleFailException as e:
        e.do_fail(module)