def _new_cert_v1(self):
        '''
        Create a new certificate based on the CSR (ACME v1 protocol).
        Return the certificate object as dict
        https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
        '''
        csr = pem_to_der(self.csr)
        new_cert = {
            "resource": "new-cert",
            "csr": nopad_b64(csr),
        }
        result, info = self.account.send_signed_request(
            self.directory['new-cert'], new_cert)

        chain = []
        if 'link' in info:
            link = info['link']
            parsed_link = re.match(r'<(.+)>;rel="(\w+)"', link)
            if parsed_link and parsed_link.group(2) == "up":
                chain_link = parsed_link.group(1)
                chain_result, chain_info = self.account.get_request(
                    chain_link, parse_json_result=False)
                if chain_info['status'] in [200, 201]:
                    chain = [self._der_to_pem(chain_result)]

        if info['status'] not in [200, 201]:
            raise ModuleFailException(
                "Error new cert: CODE: {0} RESULT: {1}".format(
                    info['status'], result))
        else:
            return {
                'cert': self._der_to_pem(result),
                'uri': info['location'],
                'chain': chain
            }
    def _finalize_cert(self):
        '''
        Create a new certificate based on the csr.
        Return the certificate object as dict
        https://tools.ietf.org/html/rfc8555#section-7.4
        '''
        csr = pem_to_der(self.csr)
        new_cert = {
            "csr": nopad_b64(csr),
        }
        result, info = self.account.send_signed_request(
            self.finalize_uri, new_cert)
        if info['status'] not in [200]:
            raise ModuleFailException(
                "Error new cert: CODE: {0} RESULT: {1}".format(
                    info['status'], result))

        order = info['location']

        status = result['status']
        while status not in ['valid', 'invalid']:
            time.sleep(2)
            result, dummy = self.account.get_request(order)
            status = result['status']

        if status != 'valid':
            raise ModuleFailException(
                "Error new cert: CODE: {0} STATUS: {1} RESULT: {2}".format(
                    info['status'], status, result))

        return result['certificate']
    def _new_cert_v1(self):
        '''
        Create a new certificate based on the CSR (ACME v1 protocol).
        Return the certificate object as dict
        https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
        '''
        csr = pem_to_der(self.csr)
        new_cert = {
            "resource": "new-cert",
            "csr": nopad_b64(csr),
        }
        result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert)

        chain = []

        def f(link, relation):
            if relation == 'up':
                chain_result, chain_info = self.account.get_request(link, parse_json_result=False)
                if chain_info['status'] in [200, 201]:
                    chain.clear()
                    chain.append(self._der_to_pem(chain_result))

        process_links(info, f)

        if info['status'] not in [200, 201]:
            raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
        else:
            return {'cert': self._der_to_pem(result), 'uri': info['location'], 'chain': chain}
Exemple #4
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),
            account_uri=dict(required=False, type='str'),
            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'),
            select_crypto_backend=dict(
                required=False,
                choices=['auto', 'openssl', 'cryptography'],
                default='auto',
                type='str'),
        ),
        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,
    )
    set_crypto_backend(module)

    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 = 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 = 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')
        private_key_content = module.params.get('private_key_content')
        # Revoke certificate
        if private_key or private_key_content:
            # Step 1: load and parse private key
            error, private_key_data = account.parse_key(
                private_key, private_key_content)
            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_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:
            already_revoked = False
            # Standarized error in draft 14 (https://tools.ietf.org/html/draft-ietf-acme-acme-14#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 ModuleFailException(
                'Error revoking certificate: {0} {1}'.format(
                    info['status'], result))
        module.exit_json(changed=True)
    except ModuleFailException as e:
        e.do_fail(module)
Exemple #5
0
def test_pem_to_der(pem, der, tmpdir):
    fn = tmpdir / 'test.pem'
    fn.write(pem)
    assert pem_to_der(str(fn)) == der
Exemple #6
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),
        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,
    )
    handle_standard_module_arguments(module)

    try:
        account = ACMEAccount(module)
        # 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 = 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')
        private_key_content = module.params.get('private_key_content')
        # Revoke certificate
        if private_key or private_key_content:
            # Step 1: load and parse private key
            error, private_key_data = account.parse_key(private_key, private_key_content)
            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_data=private_key_data, jws_header=jws_header)
        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 = account.send_signed_request(endpoint, payload)
        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 ModuleFailException('Error revoking certificate: {0} {1}'.format(info['status'], result))
        module.exit_json(changed=True)
    except ModuleFailException as e:
        e.do_fail(module)