Beispiel #1
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            terms_agreed=dict(required=False, default=False, type='bool'),
            state=dict(required=True,
                       choices=['absent', 'present', 'changed_key'],
                       type='str'),
            allow_creation=dict(required=False, default=True, type='bool'),
            contact=dict(required=False, type='list', default=[]),
            new_account_key_src=dict(type='path'),
            new_account_key_content=dict(type='str', no_log=True),
        ),
        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,
    )

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')
    if module.params.get('acme_version') < 2:
        module.fail_json(
            msg='The acme_account module requires the ACME v2 protocol!')

    try:
        account = ACMEAccount(module)
        state = module.params.get('state')
        if state == 'absent':
            changed = account.init_account(
                [],
                allow_creation=False,
                update_contact=False,
            )
            if changed:
                raise AssertionError('Unwanted account change')
            if account.uri is not None:
                # Account does exist
                account_data = account.get_account_data()
                if account_data is not None:
                    # Account is not yet deactivated
                    if not module.check_mode:
                        # Deactivate it
                        payload = {'status': 'deactivated'}
                        result, info = account.send_signed_request(
                            account.uri, payload)
                        if info['status'] != 200:
                            raise ModuleFailException(
                                'Error deactivating account: {0} {1}'.format(
                                    info['status'], result))
                    module.exit_json(changed=True, account_uri=account.uri)
            module.exit_json(changed=False, account_uri=account.uri)
        elif state == 'present':
            allow_creation = module.params.get('allow_creation')
            contact = module.params.get('contact')
            terms_agreed = module.params.get('terms_agreed')
            changed = account.init_account(
                contact,
                terms_agreed=terms_agreed,
                allow_creation=allow_creation,
            )
            if account.uri is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            module.exit_json(changed=changed, account_uri=account.uri)
        elif state == 'changed_key':
            # Get hold of new account key
            new_key = module.params.get('new_account_key_src')
            if new_key is None:
                fd, tmpsrc = tempfile.mkstemp()
                module.add_cleanup_file(
                    tmpsrc)  # Ansible will delete the file on exit
                f = os.fdopen(fd, 'wb')
                try:
                    f.write(
                        module.params.get('new_account_key_content').encode(
                            'utf-8'))
                    new_key = tmpsrc
                except Exception as err:
                    try:
                        f.close()
                    except Exception as e:
                        pass
                    raise ModuleFailException(
                        "failed to create temporary content file: %s" %
                        to_native(err),
                        exception=traceback.format_exc())
                f.close()
            # Parse new account key
            error, new_key_data = account.parse_account_key(new_key)
            if error:
                raise ModuleFailException(
                    "error while parsing account key: %s" % error)
            # Verify that the account exists and has not been deactivated
            changed = account.init_account(
                [],
                allow_creation=False,
                update_contact=False,
            )
            if changed:
                raise AssertionError('Unwanted account change')
            if account.uri is None or account.get_account_data() is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            # Now we can start the account key rollover
            if not module.check_mode:
                # Compose inner signed message
                # https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.3.6
                url = account.directory['keyChange']
                protected = {
                    "alg": new_key_data['alg'],
                    "jwk": new_key_data['jwk'],
                    "url": url,
                }
                payload = {
                    "account": account.uri,
                    "newKey": new_key_data['jwk'],  # specified in draft 12
                    "oldKey": account.
                    jwk,  # discussed in https://github.com/ietf-wg-acme/acme/pull/425,
                    # might be required in draft 13
                }
                data = account.sign_request(protected, payload, new_key_data,
                                            new_key)
                # Send request and verify result
                result, info = account.send_signed_request(url, data)
                if info['status'] != 200:
                    raise ModuleFailException(
                        'Error account key rollover: {0} {1}'.format(
                            info['status'], result))
            module.exit_json(changed=True, account_uri=account.uri)
    except ModuleFailException as e:
        e.do_fail(module)
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            private_key_src=dict(type='path'),
            private_key_content=dict(type='str', no_log=True),
            certificate=dict(required=True, type='path'),
            revoke_reason=dict(required=False, type='int'),
        ),
        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,
    )

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')

    try:
        account = ACMEAccount(module)
        # Load certificate
        certificate_lines = []
        try:
            with open(module.params.get('certificate'), "rt") as f:
                header_line_count = 0
                for line in f:
                    if line.startswith('-----'):
                        header_line_count += 1
                        if header_line_count == 2:
                            # If certificate file contains other certs appended
                            # (like intermediate certificates), ignore these.
                            break
                        continue
                    certificate_lines.append(line.strip())
        except Exception as err:
            raise ModuleFailException("cannot load certificate file: %s" %
                                      to_native(err),
                                      exception=traceback.format_exc())
        certificate = nopad_b64(base64.b64decode(''.join(certificate_lines)))
        # 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 = account.directory['revoke-cert']
            payload['resource'] = 'revoke-cert'
        else:
            endpoint = account.directory['revokeCert']
        # Get hold of private key (if available) and make sure it comes from disk
        private_key = module.params.get('private_key_src')
        if module.params.get('private_key_content') is not None:
            fd, tmpsrc = tempfile.mkstemp()
            module.add_cleanup_file(
                tmpsrc)  # Ansible will delete the file on exit
            f = os.fdopen(fd, 'wb')
            try:
                f.write(
                    module.params.get('private_key_content').encode('utf-8'))
                private_key = tmpsrc
            except Exception as err:
                try:
                    f.close()
                except Exception as e:
                    pass
                raise ModuleFailException(
                    "failed to create temporary content file: %s" %
                    to_native(err),
                    exception=traceback.format_exc())
            f.close()
        # Revoke certificate
        if private_key:
            # Step 1: load and parse private key
            error, private_key_data = account.parse_account_key(private_key)
            if error:
                raise ModuleFailException(
                    "error while parsing private key: %s" % error)
            # Step 2: sign revokation request with private key
            jws_header = {
                "alg": private_key_data['alg'],
                "jwk": private_key_data['jwk'],
            }
            result, info = account.send_signed_request(
                endpoint,
                payload,
                key=private_key,
                key_data=private_key_data,
                jws_header=jws_header)
        else:
            # Step 1: get hold of account URI
            changed = account.init_account(
                [],
                allow_creation=False,
                update_contact=False,
            )
            if changed:
                raise AssertionError('Unwanted account change')
            # Step 2: sign revokation request with account key
            result, info = account.send_signed_request(endpoint, payload)
        if info['status'] != 200:
            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.
                module.exit_json(changed=False)
            raise ModuleFailException(
                'Error revoking certificate: {0} {1}'.format(
                    info['status'], result))
        module.exit_json(changed=True)
    except ModuleFailException as e:
        e.do_fail(module)