Ejemplo n.º 1
0
def main():
    argument_spec = get_default_argspec()
    argument_spec.update(
        dict(
            url=dict(type='str'),
            method=dict(type='str',
                        choices=['get', 'post', 'directory-only'],
                        default='get'),
            content=dict(type='str'),
            fail_on_acme_error=dict(type='bool', default=True),
        ))
    module = AnsibleModule(
        argument_spec=argument_spec,
        mutually_exclusive=(['account_key_src', 'account_key_content'], ),
        required_if=(
            ['method', 'get', ['url']],
            ['method', 'post', ['url', 'content']],
            [
                'method', 'get', ['account_key_src', 'account_key_content'],
                True
            ],
            [
                'method', 'post', ['account_key_src', 'account_key_content'],
                True
            ],
        ),
    )
    backend = create_backend(module, False)

    result = dict()
    changed = False
    try:
        # Get hold of ACMEClient and ACMEAccount objects (includes directory)
        client = ACMEClient(module, backend)
        method = module.params['method']
        result['directory'] = client.directory.directory
        # Do we have to do more requests?
        if method != 'directory-only':
            url = module.params['url']
            fail_on_acme_error = module.params['fail_on_acme_error']
            # Do request
            if method == 'get':
                data, info = client.get_request(url,
                                                parse_json_result=False,
                                                fail_on_error=False)
            elif method == 'post':
                changed = True  # only POSTs can change
                data, info = client.send_signed_request(
                    url,
                    to_bytes(module.params['content']),
                    parse_json_result=False,
                    encode_payload=False,
                    fail_on_error=False)
            # Update results
            result.update(dict(
                headers=info,
                output_text=to_native(data),
            ))
            # See if we can parse the result as JSON
            try:
                result['output_json'] = module.from_json(to_text(data))
            except Exception as dummy:
                pass
            # Fail if error was returned
            if fail_on_acme_error and info['status'] >= 400:
                raise ACMEProtocolException(info=info, content_json=result)
        # Done!
        module.exit_json(changed=changed, **result)
    except ModuleFailException as e:
        e.do_fail(module, **result)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)
Ejemplo n.º 4
0
def main():
    argument_spec = get_default_argspec()
    argument_spec.update(
        dict(
            modify_account=dict(type='bool', default=True),
            account_email=dict(type='str'),
            agreement=dict(type='str'),
            terms_agreed=dict(type='bool', default=False),
            challenge=dict(type='str',
                           default='http-01',
                           choices=['http-01', 'dns-01', 'tls-alpn-01']),
            csr=dict(type='path', aliases=['src']),
            csr_content=dict(type='str'),
            data=dict(type='dict'),
            dest=dict(type='path', aliases=['cert']),
            fullchain_dest=dict(type='path', aliases=['fullchain']),
            chain_dest=dict(type='path', aliases=['chain']),
            remaining_days=dict(type='int', default=10),
            deactivate_authzs=dict(type='bool', default=False),
            force=dict(type='bool', default=False),
            retrieve_all_alternates=dict(type='bool', default=False),
            select_chain=dict(type='list',
                              elements='dict',
                              options=dict(
                                  test_certificates=dict(
                                      type='str',
                                      default='all',
                                      choices=['first', 'last', 'all']),
                                  issuer=dict(type='dict'),
                                  subject=dict(type='dict'),
                                  subject_key_identifier=dict(type='str'),
                                  authority_key_identifier=dict(type='str'),
                              )),
        ))
    module = AnsibleModule(
        argument_spec=argument_spec,
        required_one_of=(
            ['account_key_src', 'account_key_content'],
            ['dest', 'fullchain_dest'],
            ['csr', 'csr_content'],
        ),
        mutually_exclusive=(
            ['account_key_src', 'account_key_content'],
            ['csr', 'csr_content'],
        ),
        supports_check_mode=True,
    )
    backend = create_backend(module, False)

    try:
        if module.params.get('dest'):
            cert_days = backend.get_cert_days(module.params['dest'])
        else:
            cert_days = backend.get_cert_days(module.params['fullchain_dest'])

        if module.params[
                'force'] or cert_days < module.params['remaining_days']:
            # If checkmode is active, base the changed state solely on the status
            # of the certificate file as all other actions (accessing an account, checking
            # the authorization status...) would lead to potential changes of the current
            # state
            if module.check_mode:
                module.exit_json(changed=True,
                                 authorizations={},
                                 challenge_data={},
                                 cert_days=cert_days)
            else:
                client = ACMECertificateClient(module, backend)
                client.cert_days = cert_days
                other = dict()
                is_first_step = client.is_first_step()
                if is_first_step:
                    # First run: start challenges / start new order
                    client.start_challenges()
                else:
                    # Second run: finish challenges, and get certificate
                    try:
                        client.finish_challenges()
                        client.get_certificate()
                        if client.all_chains is not None:
                            other['all_chains'] = client.all_chains
                    finally:
                        if module.params['deactivate_authzs']:
                            client.deactivate_authzs()
                data, data_dns = client.get_challenges_data(
                    first_step=is_first_step)
                auths = dict()
                for k, v in client.authorizations.items():
                    # Remove "type:" from key
                    auths[split_identifier(k)[1]] = v.to_json()
                module.exit_json(changed=client.changed,
                                 authorizations=auths,
                                 finalize_uri=client.order.finalize_uri
                                 if client.order else None,
                                 order_uri=client.order_uri,
                                 account_uri=client.client.account_uri,
                                 challenge_data=data,
                                 challenge_data_dns=data_dns,
                                 cert_days=client.cert_days,
                                 **other)
        else:
            module.exit_json(changed=False, cert_days=cert_days)
    except ModuleFailException as e:
        e.do_fail(module)
Ejemplo n.º 5
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)