コード例 #1
0
class Certificate(object):
    """ CA  handler """
    def __init__(self, debug=None, srv_name=None, logger=None):
        self.debug = debug
        self.server_name = srv_name
        self.logger = logger
        self.cahandler = None
        self.dbstore = DBstore(self.debug, self.logger)
        self.message = Message(self.debug, self.server_name, self.logger)
        self.path_dic = {'cert_path': '/acme/cert/'}
        self.retry_after = 600
        self.tnauthlist_support = False

    def __enter__(self):
        """ Makes ACMEHandler a Context Manager """
        self._config_load()
        return self

    def __exit__(self, *args):
        """ cose the connection at the end of the context """

    def _account_check(self, account_name, certificate):
        """ check account """
        self.logger.debug('Certificate.issuer_check()')
        try:
            result = self.dbstore.certificate_account_check(
                account_name, b64_url_recode(self.logger, certificate))
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Certificate._account_check(): {0}'
                .format(err_))
            result = None
        return result

    def _authorization_check(self, order_name, certificate):
        """ check if an acount holds authorization for all identifiers = SANs in the certificate """
        self.logger.debug('Certificate._authorization_check()')

        # empty list of statuses
        identifier_status = []

        # get identifiers for order
        try:
            identifier_dic = self.dbstore.order_lookup('name', order_name,
                                                       ['identifiers'])
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Certificate._authorization_check(): {0}'
                .format(err_))
            identifier_dic = {}

        if identifier_dic and 'identifiers' in identifier_dic:
            # load identifiers
            try:
                identifiers = json.loads(identifier_dic['identifiers'].lower())
            except BaseException:
                identifiers = []

            # check if we have a tnauthlist identifier
            tnauthlist_identifer_in = self._tnauth_identifier_check(
                identifiers)
            if self.tnauthlist_support and tnauthlist_identifer_in:
                try:
                    # get list of certextensions in base64 format and identifier status
                    tnauthlist = cert_extensions_get(self.logger, certificate)
                    identifier_status = self._identifer_tnauth_list(
                        identifier_dic, tnauthlist)
                except BaseException as err_:
                    # enough to set identifier_list as empty list
                    identifier_status = []
                    self.logger.warning(
                        'Certificate._authorization_check() error while loading parsing certifcate. Error: {0}'
                        .format(err_))
            else:
                try:
                    # get sans
                    san_list = cert_san_get(self.logger, certificate)
                    identifier_status = self._identifer_status_list(
                        identifiers, san_list)
                except BaseException as err_:
                    # enough to set identifier_list as empty list
                    identifier_status = []
                    self.logger.warning(
                        'Certificate._authorization_check() error while loading parsing certifcate. Error: {0}'
                        .format(err_))

        result = False
        if identifier_status and False not in identifier_status:
            result = True

        self.logger.debug(
            'Certificate._authorization_check() ended with {0}'.format(result))
        return result

    def _config_load(self):
        """" load config from file """
        self.logger.debug('Certificate._config_load()')
        config_dic = load_config()
        if 'Order' in config_dic:
            self.tnauthlist_support = config_dic.getboolean(
                'Order', 'tnauthlist_support', fallback=False)
        if 'CAhandler' in config_dic and 'handler_file' in config_dic[
                'CAhandler']:
            try:
                ca_handler_module = importlib.import_module(
                    ca_handler_get(self.logger,
                                   config_dic['CAhandler']['handler_file']))
            except BaseException as err_:
                self.logger.critical(
                    'Certificate._config_load(): loading CAhandler configured in cfg failed with err: {0}'
                    .format(err_))
                try:
                    ca_handler_module = importlib.import_module(
                        'acme_srv.ca_handler')
                except BaseException as err_:
                    ca_handler_module = None
                    self.logger.critical(
                        'Certificate._config_load(): loading default EABHandler failed with err: {0}'
                        .format(err_))
        else:
            if 'CAhandler' in config_dic:
                ca_handler_module = importlib.import_module(
                    'acme_srv.ca_handler')
            else:
                self.logger.error(
                    'Certificate._config_load(): CAhandler configuration missing in config file'
                )
                ca_handler_module = None

        if ca_handler_module:
            # store handler in variable
            self.cahandler = ca_handler_module.CAhandler

        if 'Directory' in config_dic:
            if 'url_prefix' in config_dic['Directory']:
                self.path_dic = {
                    k: config_dic['Directory']['url_prefix'] + v
                    for k, v in self.path_dic.items()
                }

        self.logger.debug('ca_handler: {0}'.format(ca_handler_module))
        self.logger.debug('Certificate._config_load() ended.')

    def _csr_check(self, certificate_name, csr):
        """ compare csr extensions against order """
        self.logger.debug('Certificate._csr_check()')

        # fetch certificate dictionary from DB
        certificate_dic = self._info(certificate_name)
        self.logger.debug(
            'Certificate._info() ended with:{0}'.format(certificate_dic))

        # empty list of statuses
        identifier_status = []

        if 'order' in certificate_dic:
            # get identifiers for order
            try:
                identifier_dic = self.dbstore.order_lookup(
                    'name', certificate_dic['order'], ['identifiers'])
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Certificate._csr_check(): {0}'
                    .format(err_))
                identifier_dic = {}

            if identifier_dic and 'identifiers' in identifier_dic:
                # load identifiers
                try:
                    identifiers = json.loads(
                        identifier_dic['identifiers'].lower())
                except BaseException:
                    identifiers = []

                # do we need to check for tnauth
                tnauthlist_identifer_in = self._tnauth_identifier_check(
                    identifiers)

                if self.tnauthlist_support and tnauthlist_identifer_in:
                    # get list of certextensions in base64 format
                    try:
                        tnauthlist = csr_extensions_get(self.logger, csr)
                        identifier_status = self._identifer_tnauth_list(
                            identifier_dic, tnauthlist)
                    except BaseException as err_:
                        identifier_status = []
                        self.logger.warning(
                            'Certificate._csr_check() error while parsing csr.\nerror: {0}'
                            .format(err_))
                else:
                    # get sans and compare identifiers against san
                    try:
                        san_list = csr_san_get(self.logger, csr)
                        identifier_status = self._identifer_status_list(
                            identifiers, san_list)
                    except BaseException as err_:
                        identifier_status = []
                        self.logger.warning(
                            'Certificate._csr_check() error while checking csr.\nerror: {0}'
                            .format(err_))

        csr_check_result = False

        if identifier_status and False not in identifier_status:
            csr_check_result = True

        self.logger.debug(
            'Certificate._csr_check() ended with {0}'.format(csr_check_result))
        return csr_check_result

    def _identifer_status_list(self, identifiers, san_list):
        """ compare identifiers and check if each san is in identifer list """
        self.logger.debug('Certificate._identifer_status_list()')

        identifier_status = []
        for san in san_list:
            san_is_in = False
            try:
                (cert_type, cert_value) = san.lower().split(':')
            except BaseException:
                cert_type = None
                cert_value = None

            if cert_type and cert_value:
                for identifier in identifiers:
                    if 'type' in identifier:
                        if (identifier['type'].lower() == cert_type
                                and identifier['value'].lower() == cert_value):
                            san_is_in = True
                            break
            self.logger.debug(
                'SAN check for {0} against identifiers returned {1}'.format(
                    san.lower(), san_is_in))
            identifier_status.append(san_is_in)

        if not identifier_status:
            identifier_status.append(False)

        self.logger.debug(
            'Certificate._identifer_status_list() ended with {0}'.format(
                identifier_status))
        return identifier_status

    def _identifer_tnauth_list(self, identifier_dic, tnauthlist):
        """ compare identifiers and check if each san is in identifer list """
        self.logger.debug('Certificate._identifer_tnauth_list()')

        identifier_status = []
        # reload identifiers (case senetive)
        try:
            identifiers = json.loads(identifier_dic['identifiers'])
        except BaseException:
            identifiers = []

        if tnauthlist and not identifier_dic:
            identifier_status.append(False)
        elif identifiers and tnauthlist:
            for identifier in identifiers:
                # get the tnauthlist identifier
                if 'type' in identifier and identifier['type'].lower(
                ) == 'tnauthlist':
                    # check if tnauthlist extension is in extension list
                    if 'value' in identifier and identifier[
                            'value'] in tnauthlist:
                        identifier_status.append(True)
                    else:
                        identifier_status.append(False)
                else:
                    identifier_status.append(False)
        else:
            identifier_status.append(False)

        self.logger.debug(
            'Certificate._identifer_status_list() ended with {0}'.format(
                identifier_status))
        return identifier_status

    def _info(self,
              certificate_name,
              flist=('name', 'csr', 'cert', 'order__name')):
        """ get certificate from database """
        self.logger.debug('Certificate._info({0})'.format(certificate_name))
        try:
            result = self.dbstore.certificate_lookup('name', certificate_name,
                                                     flist)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Certificate._info(): {0}'.
                format(err_))
            result = None
        return result

    def _invalidation_check(self, cert, timestamp, purge=False):
        """ check if cert must be invalidated """
        if 'name' in cert:
            self.logger.debug('Certificate._invalidation_check({0})'.format(
                cert['name']))
        else:
            self.logger.debug('Certificate._invalidation_check()')

        to_be_cleared = False

        if cert and 'name' in cert:
            if 'cert' in cert and cert['cert'] and 'removed by' in cert[
                    'cert'].lower():
                if not purge:
                    # skip entries which had been cleared before cert[cert] check is needed to cover corner cases
                    to_be_cleared = False
                else:
                    # purge entries
                    to_be_cleared = True

            elif 'expire_uts' in cert:
                # in case cert_expiry in table is 0 try to get it from cert
                if cert['expire_uts'] == 0:
                    if 'cert_raw' in cert and cert['cert_raw']:
                        # get expiration from certificate
                        (issue_uts,
                         expire_uts) = cert_dates_get(self.logger,
                                                      cert['cert_raw'])
                        if 0 < expire_uts < timestamp:
                            # returned date is other than 0 and lower than given timestamp
                            cert['issue_uts'] = issue_uts
                            cert['expire_uts'] = expire_uts
                            to_be_cleared = True
                    else:
                        if 'csr' in cert and cert['csr']:
                            # cover cases for enrollments in flight
                            # we assume that a CSR should turn int a cert within two weeks
                            if 'created_at' in cert:
                                created_at_uts = date_to_uts_utc(
                                    cert['created_at'])
                                if 0 < created_at_uts < timestamp - (14 *
                                                                     86400):
                                    to_be_cleared = True
                            else:
                                # this scneario should never been happen so lets be careful and not clear it
                                to_be_cleared = False
                        else:
                            # no csr and no cert - to be cleared
                            to_be_cleared = True

                else:
                    # expired based on expire_uts from db
                    to_be_cleared = True
            else:
                # this scneario should never been happen so lets be careful and not clear it
                to_be_cleared = False
        else:
            # entries without a cert-name can be to_be_cleared
            to_be_cleared = True

        if 'name' in cert:
            self.logger.debug(
                'Certificate._invalidation_check({0}) ended with {1}'.format(
                    cert['name'], to_be_cleared))
        else:
            self.logger.debug(
                'Certificate._invalidation_check() ended with {0}'.format(
                    to_be_cleared))
        return (to_be_cleared, cert)

    def _revocation_reason_check(self, reason):
        """ check reason """
        self.logger.debug(
            'Certificate._revocation_reason_check({0})'.format(reason))

        # taken from https://tools.ietf.org/html/rfc5280#section-5.3.1
        allowed_reasons = {
            0: 'unspecified',
            1: 'keyCompromise',
            # 2 : 'cACompromise',
            3: 'affiliationChanged',
            4: 'superseded',
            5: 'cessationOfOperation',
            6: 'certificateHold',
            # 8 : 'removeFromCRL',
            # 9 : 'privilegeWithdrawn',
            # 10 : 'aACompromise'
        }

        result = allowed_reasons.get(reason, None)
        self.logger.debug(
            'Certificate._revocation_reason_check() ended with {0}'.format(
                result))
        return result

    def _revocation_request_validate(self, account_name, payload):
        """ check revocaton request for consistency"""
        self.logger.debug(
            'Certificate._revocation_request_validate({0})'.format(
                account_name))

        # set a value to avoid that we are returning none by accident
        code = 400
        error = None
        if 'reason' in payload:
            # check revocatoin reason if we get one
            rev_reason = self._revocation_reason_check(payload['reason'])
            # successful
            if not rev_reason:
                error = 'urn:ietf:params:acme:error:badRevocationReason'
        else:
            # set revocation reason to unspecified
            rev_reason = 'unspecified'

        if rev_reason:
            # check if the account issued the certificate and return the order name
            if 'certificate' in payload:
                order_name = self._account_check(account_name,
                                                 payload['certificate'])
            else:
                order_name = None

            error = rev_reason
            if order_name:
                # check if the account holds the authorization for the identifiers
                auth_chk = self._authorization_check(order_name,
                                                     payload['certificate'])
                if auth_chk:
                    # all good set code to 200
                    code = 200
                else:
                    error = 'urn:ietf:params:acme:error:unauthorized'

        self.logger.debug(
            'Certificate._revocation_request_validate() ended with: {0}, {1}'.
            format(code, error))
        return (code, error)

    def _store_cert(self,
                    certificate_name,
                    certificate,
                    raw,
                    issue_uts=0,
                    expire_uts=0):
        """ get key for a specific account id """
        self.logger.debug(
            'Certificate._store_cert({0})'.format(certificate_name))
        data_dic = {
            'cert': certificate,
            'name': certificate_name,
            'cert_raw': raw,
            'issue_uts': issue_uts,
            'expire_uts': expire_uts
        }
        try:
            cert_id = self.dbstore.certificate_add(data_dic)
        except BaseException as err_:
            cert_id = None
            self.logger.critical(
                'acme2certifier database error in Certificate._store_cert(): {0}'
                .format(err_))
        self.logger.debug('Certificate._store_cert({0}) ended'.format(cert_id))
        return cert_id

    def _store_cert_error(self, certificate_name, error, poll_identifier):
        """ get key for a specific account id """
        self.logger.debug(
            'Certificate._store_cert_error({0})'.format(certificate_name))
        data_dic = {
            'error': error,
            'name': certificate_name,
            'poll_identifier': poll_identifier
        }
        try:
            cert_id = self.dbstore.certificate_add(data_dic)
        except BaseException as err_:
            cert_id = None
            self.logger.critical(
                'acme2certifier database error in Certificate._store_cert(): {0}'
                .format(err_))
        self.logger.debug(
            'Certificate._store_cert_error({0}) ended'.format(cert_id))
        return cert_id

    def _tnauth_identifier_check(self, identifier_dic):
        """ check if we have an tnauthlist_identifier """
        self.logger.debug('Certificate._tnauth_identifier_check()')
        # check if we have a tnauthlist identifier
        tnauthlist_identifer_in = False
        if identifier_dic:
            for identifier in identifier_dic:
                if 'type' in identifier:
                    if identifier['type'].lower() == 'tnauthlist':
                        tnauthlist_identifer_in = True
        self.logger.debug(
            'Certificate._tnauth_identifier_check() ended with: {0}'.format(
                tnauthlist_identifer_in))
        return tnauthlist_identifer_in

    def certlist_search(self,
                        key,
                        value,
                        vlist=('name', 'csr', 'cert', 'order__name')):
        """ get certificate from database """
        self.logger.debug('Certificate.certlist_search({0}: {1})'.format(
            key, value))
        try:
            result = self.dbstore.certificates_search(key, value, vlist)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Certificate.certlist_search(): {0}'
                .format(err_))
            result = None
        return result

    def cleanup(self, timestamp=None, purge=False):
        """ cleanup routine to shrink table-size """
        self.logger.debug('Certificate.cleanup({0},{1})'.format(
            timestamp, purge))

        field_list = [
            'id', 'name', 'expire_uts', 'issue_uts', 'cert', 'cert_raw', 'csr',
            'created_at', 'order__id', 'order__name'
        ]

        # get expired certificates
        try:
            certificate_list = self.dbstore.certificates_search(
                'expire_uts', timestamp, field_list, '<=')
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Certificate.cleanup() search: {0}'
                .format(err_))
            certificate_list = []

        report_list = []
        for cert in certificate_list:
            (to_be_cleared,
             cert) = self._invalidation_check(cert, timestamp, purge)

            if to_be_cleared:
                report_list.append(cert)

        if not purge:
            # we are just modifiying data
            for cert in report_list:
                data_dic = {
                    'name':
                    cert['name'],
                    'expire_uts':
                    cert['expire_uts'],
                    'issue_uts':
                    cert['issue_uts'],
                    'cert':
                    'removed by certificates.cleanup() on {0} '.format(
                        uts_to_date_utc(timestamp)),
                    'cert_raw':
                    cert['cert_raw']
                }
                try:
                    self.dbstore.certificate_add(data_dic)
                except BaseException as err_:
                    self.logger.critical(
                        'acme2certifier database error in Certificate.cleanup() add: {0}'
                        .format(err_))
        else:
            # delete entries from certificates table
            for cert in report_list:
                try:
                    self.dbstore.certificate_delete('id', cert['id'])
                except BaseException as err_:
                    self.logger.critical(
                        'acme2certifier database error in Certificate.cleanup() delete: {0}'
                        .format(err_))
        self.logger.debug('Certificate.cleanup() ended with: {0} certs'.format(
            len(report_list)))
        return (field_list, report_list)

    def dates_update(self):
        """ scan certificates and update issue/expiry date """
        self.logger.debug('Certificate.certificate_dates_update()')

        with Certificate(self.debug, None, self.logger) as certificate:
            cert_list = certificate.certlist_search(
                'issue_uts',
                0,
                vlist=('id', 'name', 'cert', 'cert_raw', 'issue_uts',
                       'expire_uts'))
            self.logger.debug('Got {0} certificates to be updated...'.format(
                len(cert_list)))
            for cert in cert_list:
                if 'issue_uts' in cert and 'expire_uts' in cert:
                    if cert['issue_uts'] == 0 and cert['expire_uts'] == 0:
                        if cert['cert_raw']:
                            (issue_uts, expire_uts) = cert_dates_get(
                                self.logger, cert['cert_raw'])
                            if issue_uts or expire_uts:
                                self._store_cert(cert['name'], cert['cert'],
                                                 cert['cert_raw'], issue_uts,
                                                 expire_uts)
        # return None

    def enroll_and_store(self, certificate_name, csr):
        """ cenroll and store certificater """
        self.logger.debug('Certificate.enroll_and_store({0},{1})'.format(
            certificate_name, csr))

        # check csr against order
        csr_check_result = self._csr_check(certificate_name, csr)
        error = None
        detail = None

        # only continue if self.csr_check returned True
        if csr_check_result:
            with self.cahandler(self.debug, self.logger) as ca_handler:
                (error, certificate, certificate_raw,
                 poll_identifier) = ca_handler.enroll(csr)
                if certificate:
                    (issue_uts,
                     expire_uts) = cert_dates_get(self.logger, certificate_raw)
                    try:
                        result = self._store_cert(certificate_name,
                                                  certificate, certificate_raw,
                                                  issue_uts, expire_uts)
                    except BaseException as err_:
                        result = None
                        self.logger.critical(
                            'acme2certifier database error in Certificate.enroll_and_store(): {0}'
                            .format(err_))
                else:
                    result = None
                    self.logger.error(
                        'acme2certifier enrollment error: {0}'.format(error))
                    # store error message for later analysis
                    try:
                        self._store_cert_error(certificate_name, error,
                                               poll_identifier)
                    except BaseException as err_:
                        result = None
                        self.logger.critical(
                            'acme2certifier database error in Certificate.enroll_and_store(): {0}'
                            .format(err_))

                    # cover polling cases
                    if poll_identifier:
                        detail = poll_identifier
                    else:
                        error = 'urn:ietf:params:acme:error:serverInternal'
        else:
            result = None
            error = 'urn:ietf:params:acme:badCSR'
            detail = 'CSR validation failed'

        self.logger.debug(
            'Certificate.enroll_and_store() ended with: {0}:{1}'.format(
                result, error))
        return (error, detail)

    def new_get(self, url):
        """ get request """
        self.logger.debug('Certificate.new_get({0})'.format(url))
        certificate_name = url.replace(
            '{0}{1}'.format(self.server_name, self.path_dic['cert_path']), '')

        # fetch certificate dictionary from DB
        certificate_dic = self._info(
            certificate_name,
            ['name', 'csr', 'cert', 'order__name', 'order__status_id'])
        response_dic = {}
        if 'order__status_id' in certificate_dic:
            if certificate_dic['order__status_id'] == 5:
                # oder status is valid - download certificate
                if 'cert' in certificate_dic and certificate_dic['cert']:
                    response_dic['code'] = 200
                    # filter certificate and decode it
                    response_dic['data'] = certificate_dic['cert']
                    response_dic['header'] = {}
                    response_dic['header'][
                        'Content-Type'] = 'application/pem-certificate-chain'
                else:
                    response_dic['code'] = 500
                    response_dic[
                        'data'] = 'urn:ietf:params:acme:error:serverInternal'
            elif certificate_dic['order__status_id'] == 4:
                # order status is processing - ratelimiting
                response_dic['header'] = {
                    'Retry-After': '{0}'.format(self.retry_after)
                }
                response_dic['code'] = 403
                response_dic['data'] = 'urn:ietf:params:acme:error:rateLimited'
            else:
                response_dic['code'] = 403
                response_dic[
                    'data'] = 'urn:ietf:params:acme:error:orderNotReady'
        else:
            response_dic['code'] = 500
            response_dic['data'] = 'urn:ietf:params:acme:error:serverInternal'

        self.logger.debug('Certificate.new_get({0}) ended'.format(
            response_dic['code']))

        return response_dic

    def new_post(self, content):
        """ post request """
        self.logger.debug('Certificate.new_post({0})')

        response_dic = {}
        # check message
        (code, message, detail, protected, _payload,
         _account_name) = self.message.check(content)
        if code == 200:
            if 'url' in protected:
                response_dic = self.new_get(protected['url'])
                if response_dic['code'] in (400, 403, 400, 500):
                    code = response_dic['code']
                    message = response_dic['data']
                    detail = None
            else:
                response_dic['code'] = code = 400
                response_dic[
                    'data'] = message = 'urn:ietf:params:acme:error:malformed'
                detail = 'url missing in protected header'

        # prepare/enrich response
        status_dic = {'code': code, 'message': message, 'detail': detail}
        response_dic = self.message.prepare_response(response_dic, status_dic)

        # depending on the response the content of responsedic['data'] can be either string or dict
        # data will get serialzed
        if isinstance(response_dic['data'], dict):
            response_dic['data'] = json.dumps(response_dic['data'])

        # cover cornercase - not sure if we ever run into such situation
        if 'code' in response_dic:
            result = response_dic['code']
        else:
            result = 'no code found'

        self.logger.debug(
            'Certificate.new_post() ended with: {0}'.format(result))
        return response_dic

    def revoke(self, content):
        """ revoke request """
        self.logger.debug('Certificate.revoke()')

        response_dic = {}
        # check message
        (code, message, detail, _protected, payload,
         account_name) = self.message.check(content)

        if code == 200:
            if 'certificate' in payload:
                (code, error) = self._revocation_request_validate(
                    account_name, payload)
                if code == 200:
                    # revocation starts here
                    # revocation reason is stored in error variable
                    rev_date = uts_to_date_utc(uts_now())
                    with self.cahandler(self.debug, self.logger) as ca_handler:
                        (code, message,
                         detail) = ca_handler.revoke(payload['certificate'],
                                                     error, rev_date)
                else:
                    message = error
                    detail = None

            else:
                # message could not get decoded
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'certificate not found'

        # prepare/enrich response
        status_dic = {'code': code, 'message': message, 'detail': detail}
        response_dic = self.message.prepare_response(response_dic, status_dic)

        self.logger.debug(
            'Certificate.revoke() ended with: {0}'.format(response_dic))
        return response_dic

    def poll(self, certificate_name, poll_identifier, csr, order_name):
        """ try to fetch a certificate from CA and store it into database """
        self.logger.debug('Certificate.poll({0}: {1})'.format(
            certificate_name, poll_identifier))

        with self.cahandler(self.debug, self.logger) as ca_handler:
            (error, certificate, certificate_raw, poll_identifier,
             rejected) = ca_handler.poll(certificate_name, poll_identifier,
                                         csr)
            if certificate:
                # get issuing and expiration date
                (issue_uts,
                 expire_uts) = cert_dates_get(self.logger, certificate_raw)
                # update certificate record in database
                _result = self._store_cert(certificate_name, certificate,
                                           certificate_raw, issue_uts,
                                           expire_uts)
                # update order status to 5 (valid)
                try:
                    self.dbstore.order_update({
                        'name': order_name,
                        'status': 'valid'
                    })
                except BaseException as err_:
                    self.logger.critical(
                        'acme2certifier database error in Certificate.poll(): {0}'
                        .format(err_))
            else:
                # store error message for later analysis
                self._store_cert_error(certificate_name, error,
                                       poll_identifier)
                _result = None
                if rejected:
                    try:
                        self.dbstore.order_update({
                            'name': order_name,
                            'status': 'invalid'
                        })
                    except BaseException as err_:
                        self.logger.critical(
                            'acme2certifier database error in Certificate.poll(): {0}'
                            .format(err_))
        self.logger.debug('Certificate.poll({0}: {1})'.format(
            certificate_name, poll_identifier))
        return _result

    def store_csr(self, order_name, csr):
        """ store csr into database """
        self.logger.debug('Certificate.store_csr({0})'.format(order_name))
        certificate_name = generate_random_string(self.logger, 12)
        data_dic = {'order': order_name, 'csr': csr, 'name': certificate_name}
        try:
            self.dbstore.certificate_add(data_dic)
        except BaseException as err_:
            self.logger.critical(
                'Database error in Certificate.store_csr(): {0}'.format(err_))
        self.logger.debug('Certificate.store_csr() ended')
        return certificate_name
コード例 #2
0
ファイル: order.py プロジェクト: netops2devops/acme2certifier
class Order(object):
    """ class for order handling """
    def __init__(self, debug=None, srv_name=None, logger=None):
        self.server_name = srv_name
        self.debug = debug
        self.logger = logger
        self.dbstore = DBstore(self.debug, self.logger)
        self.message = Message(self.debug, self.server_name, self.logger)
        self.validity = 86400
        self.authz_validity = 86400
        self.expiry_check_disable = False
        self.path_dic = {
            'authz_path': '/acme/authz/',
            'order_path': '/acme/order/',
            'cert_path': '/acme/cert/'
        }
        self.retry_after = 600
        self.tnauthlist_support = False

    def __enter__(self):
        """ Makes ACMEHandler a Context Manager """
        self._config_load()
        return self

    def __exit__(self, *args):
        """ cose the connection at the end of the context """

    def _add(self, payload, aname):
        """ add order request to database """
        self.logger.debug('Order._add({0})'.format(aname))
        error = None
        auth_dic = {}
        order_name = generate_random_string(self.logger, 12)
        expires = uts_now() + self.validity

        if 'identifiers' in payload:

            data_dic = {'status': 2, 'expires': expires, 'account': aname}

            data_dic['name'] = order_name
            data_dic['identifiers'] = json.dumps(payload['identifiers'])

            #if 'notBefore' in payload:
            #    data_dic['notbefore'] = payload['notBefore']
            #if 'notAfter' in payload:
            #    data_dic['notafter'] = payload['notAfter']

            # check identifiers
            error = self._identifiers_check(payload['identifiers'])

            # change order status if needed
            if error:
                data_dic['status'] = 1

            try:
                # add order to db
                oid = self.dbstore.order_add(data_dic)
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Order._add() order: {0}'.
                    format(err_))
                oid = None

            if not error:
                if oid:
                    error = None
                    for auth in payload['identifiers']:
                        # generate name
                        auth_name = generate_random_string(self.logger, 12)
                        # store to return to upper func
                        auth_dic[auth_name] = auth.copy()
                        auth['name'] = auth_name
                        auth['order'] = oid
                        auth['status'] = 'pending'
                        auth['expires'] = uts_now() + self.authz_validity
                        try:
                            self.dbstore.authorization_add(auth)
                        except BaseException as err_:
                            self.logger.critical(
                                'acme2certifier database error in Order._add() authz: {0}'
                                .format(err_))
                else:
                    error = 'urn:ietf:params:acme:error:malformed'
        else:
            error = 'urn:ietf:params:acme:error:unsupportedIdentifier'

        self.logger.debug('Order._add() ended')
        return (error, order_name, auth_dic, uts_to_date_utc(expires))

    def _config_load(self):
        """" load config from file """
        self.logger.debug('Order._config_load()')
        config_dic = load_config()
        if 'Order' in config_dic:
            self.tnauthlist_support = config_dic.getboolean(
                'Order', 'tnauthlist_support', fallback=False)
            self.expiry_check_disable = config_dic.getboolean(
                'Order', 'expiry_check_disable', fallback=False)
            if 'retry_after_timeout' in config_dic['Order']:
                try:
                    self.retry_after = int(
                        config_dic['Order']['retry_after_timeout'])
                except BaseException:
                    self.logger.warning(
                        'Order._config_load(): failed to parse retry_after: {0}'
                        .format(config_dic['Order']['retry_after_timeout']))
            if 'validity' in config_dic['Order']:
                try:
                    self.validity = int(config_dic['Order']['validity'])
                except BaseException:
                    self.logger.warning(
                        'Order._config_load(): failed to parse validity: {0}'.
                        format(config_dic['Order']['validity']))
        if 'Authorization' in config_dic:
            if 'validity' in config_dic['Authorization']:
                try:
                    self.authz_validity = int(
                        config_dic['Authorization']['validity'])
                except BaseException:
                    self.logger.warning(
                        'Order._config_load(): failed to parse authz validity: {0}'
                        .format(config_dic['Authorization']['validity']))

        if 'Directory' in config_dic:
            if 'url_prefix' in config_dic['Directory']:
                self.path_dic = {
                    k: config_dic['Directory']['url_prefix'] + v
                    for k, v in self.path_dic.items()
                }

        self.logger.debug('Order._config_load() ended.')

    def _name_get(self, url):
        """ get ordername """
        self.logger.debug('Order._name_get({0})'.format(url))
        url_dic = parse_url(self.logger, url)
        order_name = url_dic['path'].replace(self.path_dic['order_path'], '')
        if '/' in order_name:
            (order_name, _sinin) = order_name.split('/', 1)
        self.logger.debug('Order._name_get() ended')
        return order_name

    def _identifiers_check(self, identifiers_list):
        """ check validity of identifers in order """
        self.logger.debug(
            'Order._identifiers_check({0})'.format(identifiers_list))
        error = None
        allowed_identifers = ['dns']

        # add tnauthlist to list of supported identfiers if configured to do so
        if self.tnauthlist_support:
            allowed_identifers.append('tnauthlist')

        if identifiers_list and isinstance(identifiers_list, list):
            for identifier in identifiers_list:
                if 'type' in identifier:
                    if identifier['type'].lower() not in allowed_identifers:
                        error = 'urn:ietf:params:acme:error:unsupportedIdentifier'
                        break
                else:
                    error = 'urn:ietf:params:acme:error:malformed'
        else:
            error = 'urn:ietf:params:acme:error:malformed'

        self.logger.debug(
            'Order._identifiers_check() done with {0}:'.format(error))
        return error

    def _info(self, order_name):
        """ list details of an order """
        self.logger.debug('Order._info({0})'.format(order_name))
        try:
            result = self.dbstore.order_lookup('name', order_name)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Order._info(): {0}'.format(
                    err_))
            result = None
        return result

    def _process(self, order_name, protected, payload):
        """ process order """
        self.logger.debug('Order._process({0})'.format(order_name))
        certificate_name = None
        message = None
        detail = None

        if 'url' in protected:
            if 'finalize' in protected['url']:
                self.logger.debug('finalize request()')

                # lookup order-status (must be ready to proceed)
                order_dic = self._info(order_name)
                if 'status' in order_dic and order_dic['status'] == 'ready':
                    # update order_status / set to processing
                    self._update({'name': order_name, 'status': 'processing'})
                    if 'csr' in payload:
                        self.logger.debug('CSR found()')
                        # this is a new request
                        (code, certificate_name,
                         detail) = self._csr_process(order_name,
                                                     payload['csr'])
                        # change status only if we do not have a poll_identifier (stored in detail variable)
                        if code == 200:
                            if not detail:
                                # update order_status / set to valid
                                self._update({
                                    'name': order_name,
                                    'status': 'valid'
                                })
                        else:
                            message = certificate_name
                            detail = 'enrollment failed'
                    else:
                        code = 400
                        message = 'urn:ietf:params:acme:error:badCSR'
                        detail = 'csr is missing in payload'
                else:
                    code = 403
                    message = 'urn:ietf:params:acme:error:orderNotReady'
                    detail = 'Order is not ready'
            else:
                self.logger.debug('polling request()')
                code = 200
                # this is a polling request; lookup certificate
                try:
                    cert_dic = self.dbstore.certificate_lookup(
                        'order__name', order_name)
                except BaseException as err_:
                    self.logger.critical(
                        'acme2certifier database error in Order._process(): {0}'
                        .format(err_))
                    cert_dic = {}
                if cert_dic:
                    # we found a cert in the database
                    # pylint: disable=R1715
                    if 'name' in cert_dic:
                        certificate_name = cert_dic['name']
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'url is missing in protected'

        self.logger.debug(
            'Order._process() ended with order:{0} {1}:{2}:{3}'.format(
                order_name, code, message, detail))
        return (code, message, detail, certificate_name)

    def _csr_process(self, order_name, csr):
        """ process certificate signing request """
        self.logger.debug('Order._csr_process({0})'.format(order_name))

        order_dic = self._info(order_name)

        if order_dic:
            # change decoding from b64url to b64
            csr = b64_url_recode(self.logger, csr)

            with Certificate(self.debug, self.server_name,
                             self.logger) as certificate:
                # certificate = Certificate(self.debug, self.server_name, self.logger)
                certificate_name = certificate.store_csr(order_name, csr)
                if certificate_name:
                    (error, detail) = certificate.enroll_and_store(
                        certificate_name, csr)
                    if not error:
                        code = 200
                        message = certificate_name
                        # detail = None
                    else:
                        code = 400
                        message = error
                        if message == 'urn:ietf:params:acme:error:serverInternal':
                            code = 500
                else:
                    code = 500
                    message = 'urn:ietf:params:acme:error:serverInternal'
                    detail = 'CSR processing failed'
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:unauthorized'
            detail = 'order: {0} not found'.format(order_name)

        self.logger.debug(
            'Order._csr_process() ended with order:{0} {1}:{2}:{3}'.format(
                order_name, code, message, detail))
        return (code, message, detail)

    def _update(self, data_dic):
        """ update order based on ordername """
        self.logger.debug('Order._update({0})'.format(data_dic))
        try:
            self.dbstore.order_update(data_dic)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Order._update(): {0}'.format(
                    err_))

    def _lookup(self, order_name):
        """ sohw order details based on ordername """
        self.logger.debug('Order._lookup({0})'.format(order_name))
        order_dic = {}

        tmp_dic = self._info(order_name)
        if tmp_dic:
            if 'status' in tmp_dic:
                order_dic['status'] = tmp_dic['status']
            if 'expires' in tmp_dic:
                order_dic['expires'] = uts_to_date_utc(tmp_dic['expires'])
            if 'notbefore' in tmp_dic:
                if tmp_dic['notbefore'] != 0:
                    order_dic['notBefore'] = uts_to_date_utc(
                        tmp_dic['notbefore'])
            if 'notafter' in tmp_dic:
                if tmp_dic['notafter'] != 0:
                    order_dic['notAfter'] = uts_to_date_utc(
                        tmp_dic['notafter'])
            if 'identifiers' in tmp_dic:
                try:
                    order_dic['identifiers'] = json.loads(
                        tmp_dic['identifiers'])
                except BaseException:
                    self.logger.error(
                        'Order.lookup(): error while parsing the identifier {0}'
                        .format(tmp_dic['identifiers']))
            try:
                authz_list = self.dbstore.authorization_lookup(
                    'order__name', order_name, ['name', 'status__name'])
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Order._lookup(): {0}'.
                    format(err_))
                authz_list = []
            if authz_list:
                order_dic["authorizations"] = []
                # collect status of different authorizations in list
                validity_list = []
                for authz in authz_list:
                    if 'name' in authz:
                        order_dic["authorizations"].append('{0}{1}{2}'.format(
                            self.server_name, self.path_dic['authz_path'],
                            authz['name']))
                    if 'status__name' in authz:
                        if authz['status__name'] == 'valid':
                            validity_list.append(True)
                        else:
                            validity_list.append(False)

                # update orders status from pending to ready
                if validity_list and 'status' in order_dic:
                    if False not in validity_list and order_dic[
                            'status'] == 'pending':
                        self._update({'name': order_name, 'status': 'ready'})

        self.logger.debug('Order._lookup() ended')
        return order_dic

    def invalidate(self, timestamp=None):
        """ invalidate orders """
        self.logger.debug('Order.invalidate({0})'.format(timestamp))
        if not timestamp:
            timestamp = uts_now()
            self.logger.debug(
                'Order.invalidate(): set timestamp to {0}'.format(timestamp))

        field_list = [
            'id', 'name', 'expires', 'identifiers', 'created_at', 'status__id',
            'status__name', 'account__id', 'account__name', 'account__contact'
        ]
        try:
            order_list = self.dbstore.orders_invalid_search('expires',
                                                            timestamp,
                                                            vlist=field_list,
                                                            operant='<=')
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Order._invalidate() search: {0}'
                .format(err_))
            order_list = []
        output_list = []
        for order in order_list:
            # print(order['id'])
            # select all orders which are not invalid
            if 'name' in order and 'status__name' in order and order[
                    'status__name'] != 'invalid':
                # change status and add to output list
                output_list.append(order)
                data_dic = {'name': order['name'], 'status': 'invalid'}
                try:
                    self.dbstore.order_update(data_dic)
                except BaseException as err_:
                    self.logger.critical(
                        'acme2certifier database error in Order._invalidate() upd: {0}'
                        .format(err_))

        self.logger.debug(
            'Order.invalidate() ended: {0} orders identified'.format(
                len(output_list)))
        return (field_list, output_list)

    def new(self, content):
        """ new oder request """
        self.logger.debug('Order.new()')

        response_dic = {}
        # check message
        (code, message, detail, _protected, payload,
         account_name) = self.message.check(content)
        if code == 200:
            (error, order_name, auth_dic,
             expires) = self._add(payload, account_name)
            if not error:
                code = 201
                response_dic['header'] = {}
                response_dic['header']['Location'] = '{0}{1}{2}'.format(
                    self.server_name, self.path_dic['order_path'], order_name)
                response_dic['data'] = {}
                response_dic['data']['identifiers'] = []
                response_dic['data']['authorizations'] = []
                response_dic['data']['status'] = 'pending'
                response_dic['data']['expires'] = expires
                response_dic['data']['finalize'] = '{0}{1}{2}/finalize'.format(
                    self.server_name, self.path_dic['order_path'], order_name)
                for auth_name in auth_dic:
                    response_dic['data']['authorizations'].append(
                        '{0}{1}{2}'.format(self.server_name,
                                           self.path_dic['authz_path'],
                                           auth_name))
                    response_dic['data']['identifiers'].append(
                        auth_dic[auth_name])
            else:
                code = 400
                message = error
                detail = 'could not process order'
        # prepare/enrich response
        status_dic = {'code': code, 'message': message, 'detail': detail}
        response_dic = self.message.prepare_response(response_dic, status_dic)

        self.logger.debug('Order.new() returns: {0}'.format(
            json.dumps(response_dic)))
        return response_dic

    def parse(self, content):
        """ new oder request """
        self.logger.debug('Order.parse()')

        # invalidate expired orders
        if not self.expiry_check_disable:
            self.invalidate()

        response_dic = {}
        # check message
        (code, message, detail, protected, payload,
         _account_name) = self.message.check(content)

        if code == 200:
            if 'url' in protected:
                order_name = self._name_get(protected['url'])
                if order_name:
                    order_dic = self._lookup(order_name)
                    if order_dic:
                        (code, message, detail,
                         certificate_name) = self._process(
                             order_name, protected, payload)
                    else:
                        code = 403
                        message = 'urn:ietf:params:acme:error:orderNotReady'
                        detail = 'order not found'
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'order name is missing'
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'url is missing in protected'

            if code == 200:
                # create response
                response_dic['header'] = {}
                response_dic['header']['Location'] = '{0}{1}{2}'.format(
                    self.server_name, self.path_dic['order_path'], order_name)
                response_dic['data'] = self._lookup(order_name)
                if 'status' in response_dic['data'] and response_dic['data'][
                        'status'] == 'processing':
                    # set retry header as cert issuane is not completed.
                    response_dic['header']['Retry-After'] = '{0}'.format(
                        self.retry_after)
                response_dic['data']['finalize'] = '{0}{1}{2}/finalize'.format(
                    self.server_name, self.path_dic['order_path'], order_name)
                # add the path to certificate if order-status is ready
                # if certificate_name:
                if certificate_name and 'status' in response_dic[
                        'data'] and response_dic['data']['status'] == 'valid':
                    response_dic['data']['certificate'] = '{0}{1}{2}'.format(
                        self.server_name, self.path_dic['cert_path'],
                        certificate_name)

        # prepare/enrich response
        status_dic = {'code': code, 'message': message, 'detail': detail}
        response_dic = self.message.prepare_response(response_dic, status_dic)

        self.logger.debug('Order.parse() returns: {0}'.format(
            json.dumps(response_dic)))
        return response_dic