コード例 #1
0
ファイル: challenges.py プロジェクト: dkwon253/mycode
    def create(cls, client, identifier_type, identifier):
        '''
        Create a new authorization for the given identifier.
        Return the authorization object of the new authorization
        https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
        '''
        new_authz = {
            "identifier": {
                "type": identifier_type,
                "value": identifier,
            },
        }
        if client.version == 1:
            url = client.directory['new-authz']
            new_authz["resource"] = "new-authz"
        else:
            if 'newAuthz' not in client.directory.directory:
                raise ACMEProtocolException(
                    'ACME endpoint does not support pre-authorization')
            url = client.directory['newAuthz']

        result, info = client.send_signed_request(
            url,
            new_authz,
            error_msg='Failed to request challenges',
            expected_status_codes=[200, 201])
        return cls.from_json(client, result, info['location'])
コード例 #2
0
ファイル: challenges.py プロジェクト: dkwon253/mycode
 def raise_error(self, error_msg):
     '''
     Aborts with a specific error for a challenge.
     '''
     error_details = []
     # multiple challenges could have failed at this point, gather error
     # details for all of them before failing
     for challenge in self.challenges:
         if challenge.status == 'invalid':
             msg = 'Challenge {type}'.format(type=challenge.type)
             if 'error' in challenge.data:
                 msg = '{msg}: {problem}'.format(
                     msg=msg,
                     problem=format_error_problem(
                         challenge.data['error'],
                         subproblem_prefix='{0}.'.format(type)),
                 )
             error_details.append(msg)
     raise ACMEProtocolException(
         'Failed to validate challenge for {identifier}: {error}. {details}'
         .format(
             identifier=self.combined_identifier,
             error=error_msg,
             details='; '.join(error_details),
         ),
         identifier=self.combined_identifier,
         authorization=self.data,
     )
コード例 #3
0
    def finalize(self, client, csr_der, wait=True):
        '''
        Create a new certificate based on the csr.
        Return the certificate object as dict
        https://tools.ietf.org/html/rfc8555#section-7.4
        '''
        new_cert = {
            "csr": nopad_b64(csr_der),
        }
        result, info = client.send_signed_request(
            self.finalize_uri,
            new_cert,
            error_msg='Failed to finalizing order',
            expected_status_codes=[200])
        # It is not clear from the RFC whether the finalize call returns the order object or not.
        # Instead of using the result, we call self.refresh(client) below.

        if wait:
            self.wait_for_finalization(client)
        else:
            self.refresh(client)
            if self.status not in ['procesing', 'valid', 'invalid']:
                raise ACMEProtocolException(
                    client.module,
                    'Failed to finalize order; got status "{status}"'.format(
                        status=self.status),
                    info=info,
                    content_json=result)
コード例 #4
0
    def wait_for_finalization(self, client):
        while True:
            self.refresh(client)
            if self.status in ['valid', 'invalid', 'pending', 'ready']:
                break
            time.sleep(2)

        if self.status != 'valid':
            raise ACMEProtocolException(
                'Failed to wait for order to complete; got status "{status}"'.format(status=self.status), content_json=self.data)
コード例 #5
0
def _assert_fetch_url_success(module,
                              response,
                              info,
                              allow_redirect=False,
                              allow_client_error=True,
                              allow_server_error=True):
    if info['status'] < 0:
        raise NetworkException(msg="Failure downloading %s, %s" %
                               (info['url'], info['msg']))

    if (300 <= info['status'] < 400 and not allow_redirect) or \
       (400 <= info['status'] < 500 and not allow_client_error) or \
       (info['status'] >= 500 and not allow_server_error):
        raise ACMEProtocolException(module, info=info, response=response)
コード例 #6
0
def test_acme_protocol_exception(input, from_json, msg, args):
    if from_json is None:
        module = None
    else:
        module = MagicMock()
        module.from_json = from_json
    with pytest.raises(ACMEProtocolException) as exc:
        raise ACMEProtocolException(module, **input)

    print(exc.value.msg)
    print(exc.value.module_fail_args)
    print(msg)
    print(args)
    assert exc.value.msg == msg
    assert exc.value.module_fail_args == args
コード例 #7
0
ファイル: account.py プロジェクト: dkwon253/mycode
 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.
     '''
     if self.client.account_uri is None:
         raise ModuleFailException("Account URI unknown")
     if self.client.version == 1:
         data = {}
         data['resource'] = 'reg'
         result, info = self.client.send_signed_request(
             self.client.account_uri, data, fail_on_error=False)
     else:
         # try POST-as-GET first (draft-15 or newer)
         data = None
         result, info = self.client.send_signed_request(
             self.client.account_uri, data, fail_on_error=False)
         # check whether that failed with a malformed request error
         if info['status'] >= 400 and result.get(
                 'type') == 'urn:ietf:params:acme:error:malformed':
             # retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
             data = {}
             result, info = self.client.send_signed_request(
                 self.client.account_uri, data, fail_on_error=False)
     if info['status'] in (400, 403) and result.get(
             'type') == 'urn:ietf:params:acme:error:unauthorized':
         # Returned when account is deactivated
         return None
     if info['status'] in (400, 404) and result.get(
             'type') == 'urn:ietf:params:acme:error:accountDoesNotExist':
         # Returned when account does not exist
         return None
     if info['status'] < 200 or info['status'] >= 300:
         raise ACMEProtocolException(self.client.module,
                                     msg='Error retrieving account data',
                                     info=info,
                                     content_json=result)
     return result
コード例 #8
0
ファイル: acme_inspect.py プロジェクト: dkwon253/mycode
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)
コード例 #9
0
ファイル: account.py プロジェクト: dkwon253/mycode
    def _new_reg(self,
                 contact=None,
                 agreement=None,
                 terms_agreed=False,
                 allow_creation=True,
                 external_account_binding=None):
        '''
        Registers a new ACME account. Returns a pair ``(created, data)``.
        Here, ``created`` is ``True`` if the account was created and
        ``False`` if it already existed (e.g. it was not newly created),
        or does not exist. In case the account was created or exists,
        ``data`` contains the account data; otherwise, it is ``None``.

        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
        '''
        contact = contact or []

        if self.client.version == 1:
            new_reg = {'resource': 'new-reg', 'contact': contact}
            if agreement:
                new_reg['agreement'] = agreement
            else:
                new_reg['agreement'] = self.client.directory['meta'][
                    'terms-of-service']
            if external_account_binding is not None:
                raise ModuleFailException(
                    'External account binding is not supported for ACME v1')
            url = self.client.directory['new-reg']
        else:
            if (external_account_binding is not None
                    or self.client.directory['meta'].get(
                        'externalAccountRequired')) and allow_creation:
                # Some ACME servers such as ZeroSSL do not like it when you try to register an existing account
                # and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
                # to see whether the account already exists.

                # Note that we pass contact here: ZeroSSL does not accept regisration calls without contacts, even
                # if onlyReturnExisting is set to true.
                created, data = self._new_reg(contact=contact,
                                              allow_creation=False)
                if data:
                    # An account already exists! Return data
                    return created, data
                # An account does not yet exist. Try to create one next.

            new_reg = {'contact': contact}
            if not allow_creation:
                # https://tools.ietf.org/html/rfc8555#section-7.3.1
                new_reg['onlyReturnExisting'] = True
            if terms_agreed:
                new_reg['termsOfServiceAgreed'] = True
            url = self.client.directory['newAccount']
            if external_account_binding is not None:
                new_reg['externalAccountBinding'] = self.client.sign_request(
                    {
                        'alg': external_account_binding['alg'],
                        'kid': external_account_binding['kid'],
                        'url': url,
                    }, self.client.account_jwk,
                    self.client.backend.create_mac_key(
                        external_account_binding['alg'],
                        external_account_binding['key']))
            elif self.client.directory['meta'].get(
                    'externalAccountRequired') and allow_creation:
                raise ModuleFailException(
                    'To create an account, an external account binding must be specified. '
                    'Use the acme_account module with the external_account_binding option.'
                )

        result, info = self.client.send_signed_request(url,
                                                       new_reg,
                                                       fail_on_error=False)

        if info['status'] in ([200, 201]
                              if self.client.version == 1 else [201]):
            # Account did not exist
            if 'location' in info:
                self.client.set_account_uri(info['location'])
            return True, result
        elif info['status'] == (409 if self.client.version == 1 else 200):
            # Account did exist
            if result.get('status') == 'deactivated':
                # A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and
                # Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should
                # not return a valid account object according to
                # https://tools.ietf.org/html/rfc8555#section-7.3.6:
                #     "Once an account is deactivated, the server MUST NOT accept further
                #      requests authorized by that account's key."
                if not allow_creation:
                    return False, None
                else:
                    raise ModuleFailException("Account is deactivated")
            if 'location' in info:
                self.client.set_account_uri(info['location'])
            return False, result
        elif info['status'] == 400 and result[
                'type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation:
            # Account does not exist (and we didn't try to create it)
            return False, None
        elif info['status'] == 403 and result[
                'type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (
                    result.get('detail') or ''):
            # Account has been deactivated; currently works for Pebble; hasn't been
            # implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971),
            # might need adjustment in error detection.
            if not allow_creation:
                return False, None
            else:
                raise ModuleFailException("Account is deactivated")
        else:
            raise ACMEProtocolException(self.client.module,
                                        msg='Registering ACME account failed',
                                        info=info,
                                        content_json=result)
コード例 #10
0
    def get_request(self,
                    uri,
                    parse_json_result=True,
                    headers=None,
                    get_only=False,
                    fail_on_error=True,
                    error_msg=None,
                    expected_status_codes=None):
        '''
        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.
        '''
        if not get_only and self.version != 1:
            # Try POST-as-GET
            content, info = self.send_signed_request(uri,
                                                     None,
                                                     parse_json_result=False,
                                                     fail_on_error=False)
            if info['status'] == 405:
                # Instead, do unauthenticated GET
                get_only = True
        else:
            # Do unauthenticated GET
            get_only = True

        if get_only:
            # Perform unauthenticated GET
            resp, info = fetch_url(self.module,
                                   uri,
                                   method='GET',
                                   headers=headers)

            _assert_fetch_url_success(self.module, resp, info)

            try:
                content = resp.read()
            except AttributeError:
                content = info.pop('body', None)

        # Process result
        parsed_json_result = False
        if parse_json_result:
            result = {}
            if content:
                if info['content-type'].startswith('application/json'):
                    try:
                        result = self.module.from_json(content.decode('utf8'))
                        parsed_json_result = True
                    except ValueError:
                        raise NetworkException(
                            "Failed to parse the ACME response: {0} {1}".
                            format(uri, content))
                else:
                    result = content
        else:
            result = content

        if fail_on_error and _is_failed(
                info, expected_status_codes=expected_status_codes):
            raise ACMEProtocolException(
                self.module,
                msg=error_msg,
                info=info,
                content=content,
                content_json=result if parsed_json_result else None)
        return result, info
コード例 #11
0
    def send_signed_request(self,
                            url,
                            payload,
                            key_data=None,
                            jws_header=None,
                            parse_json_result=True,
                            encode_payload=True,
                            fail_on_error=True,
                            error_msg=None,
                            expected_status_codes=None):
        '''
        Sends a JWS signed HTTP POST request to the ACME server and returns
        the response as dictionary (if parse_json_result is True) or in raw form
        (if parse_json_result is False).
        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)
        '''
        key_data = key_data or self.account_key_data
        jws_header = jws_header or self.account_jws_header
        failed_tries = 0
        while True:
            protected = copy.deepcopy(jws_header)
            protected["nonce"] = self.directory.get_nonce()
            if self.version != 1:
                protected["url"] = url

            self._log('URL', url)
            self._log('protected', protected)
            self._log('payload', payload)
            data = self.sign_request(protected,
                                     payload,
                                     key_data,
                                     encode_payload=encode_payload)
            if self.version == 1:
                data["header"] = jws_header.copy()
                for k, v in protected.items():
                    dummy = data["header"].pop(k, None)
            self._log('signed request', data)
            data = self.module.jsonify(data)

            headers = {
                'Content-Type': 'application/jose+json',
            }
            resp, info = fetch_url(self.module,
                                   url,
                                   data=data,
                                   headers=headers,
                                   method='POST')
            _assert_fetch_url_success(self.module, resp, info)
            result = {}
            try:
                content = resp.read()
            except AttributeError:
                content = info.pop('body', None)

            if content or not parse_json_result:
                if (parse_json_result
                        and info['content-type'].startswith('application/json')
                    ) or 400 <= info['status'] < 600:
                    try:
                        decoded_result = self.module.from_json(
                            content.decode('utf8'))
                        self._log('parsed result', decoded_result)
                        # In case of badNonce error, try again (up to 5 times)
                        # (https://tools.ietf.org/html/rfc8555#section-6.7)
                        if all((
                                400 <= info['status'] < 600,
                                decoded_result.get('type') ==
                                'urn:ietf:params:acme:error:badNonce',
                                failed_tries <= 5,
                        )):
                            failed_tries += 1
                            continue
                        if parse_json_result:
                            result = decoded_result
                        else:
                            result = content
                    except ValueError:
                        raise NetworkException(
                            "Failed to parse the ACME response: {0} {1}".
                            format(url, content))
                else:
                    result = content

            if fail_on_error and _is_failed(
                    info, expected_status_codes=expected_status_codes):
                raise ACMEProtocolException(
                    self.module,
                    msg=error_msg,
                    info=info,
                    content=content,
                    content_json=result if parse_json_result else None)
            return result, info
コード例 #12
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)