Пример #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:
                ca_handler_module = importlib.import_module('acme.ca_handler')
        else:
            if 'CAhandler' in config_dic:
                ca_handler_module = importlib.import_module('acme.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

        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 loading 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 loading parsing 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 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
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.dbstore = DBstore(self.debug, self.logger)
        self.message = Message(self.debug, self.server_name, self.logger)
        self.path_dic = {'cert_path': '/acme/cert/'}
        self.tnauthlist_support = False

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

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

    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 CAhandler(self.debug, self.logger) as ca_handler:
                (error, certificate, certificate_raw) = ca_handler.enroll(csr)
                if certificate:
                    result = self.store_cert(certificate_name, certificate,
                                             certificate_raw)
                else:
                    result = None
                    # store error message for later analysis
                    self.store_cert_error(certificate_name, error)
        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 (result, error, detail)

    def info(self, certificate_name):
        """ get certificate from database """
        self.logger.debug('Certificate.info({0})'.format(certificate_name))
        return self.dbstore.certificate_lookup('name', certificate_name)

    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']), '')

        response_dic = {}
        # fetch certificate dictionary from DB
        certificate_dic = self.info(certificate_name)

        if 'cert' in certificate_dic:
            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'] = 403
            response_dic['data'] = 'NotFound'
        self.logger.debug(
            'Certificate.new_get({0}) ended'.format(response_dic))
        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'])
            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)

        self.logger.debug(
            'Certificate.new_post() ended with: {0}'.format(response_dic))
        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 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 store_cert(self, certificate_name, certificate, raw):
        """ 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
        }
        cert_id = self.dbstore.certificate_add(data_dic)
        self.logger.debug('Certificate.store_cert({0}) ended'.format(cert_id))
        return cert_id

    def store_cert_error(self, certificate_name, error):
        """ get key for a specific account id """
        self.logger.debug(
            'Certificate.store_error({0})'.format(certificate_name))
        data_dic = {'error': error, 'name': certificate_name}
        cert_id = self.dbstore.certificate_add(data_dic)
        self.logger.debug('Certificate.store_error({0}) ended'.format(cert_id))
        return cert_id

    def store_csr(self, order_name, csr):
        """ get key for a specific account id """
        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}
        self.dbstore.certificate_add(data_dic)
        self.logger.debug('Certificate.store_csr() ended')
        return certificate_name

    def revocation_request_validate(self, account_name, payload):
        """ chec CSR """
        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 revocation_reason_check(self, reason):
        """ check reason """
        self.logger.debug(
            'Certificate.check_revocation_reason({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 = None
        if reason in allowed_reasons:
            result = allowed_reasons[reason]
        self.logger.debug(
            'Certificate.store_csr() ended with {0}'.format(result))
        return result

    def account_check(self, account_name, certificate):
        """ check account """
        self.logger.debug('Certificate.issuer_check()')
        return self.dbstore.certificate_account_check(
            account_name, b64_url_recode(self.logger, certificate))

    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
        identifier_dic = self.dbstore.order_lookup('name', order_name,
                                                   ['identifiers'])

        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:
                # reload identifiers (case senetive)
                try:
                    identifiers = json.loads(identifier_dic['identifiers'])
                except BaseException:
                    identifiers = []
                # get list of certextensions in base64 format
                tnauthlist = cert_tnauthlist_get(self.logger, certificate)

                for identifier in identifiers:
                    # get the tnauthlist identifier
                    if identifier['type'].lower() == 'tnauthlist':
                        # check if tnauthlist extension is in extension list
                        if identifier['value'] in tnauthlist:
                            identifier_status.append(True)
                        else:
                            identifier_status.append(False)
            else:
                # get sans
                san_list = cert_san_get(self.logger, certificate)
                identifier_status = self.identifer_status_list(
                    identifiers, san_list)

        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 load_config(self):
        """" load config from file """
        self.logger.debug('Certificate.load_config()')
        config_dic = load_config()
        if 'Order' in config_dic:
            self.tnauthlist_support = config_dic.getboolean(
                'Order', 'tnauthlist_support', fallback=False)
        self.logger.debug('Certificate.load_config() ended.')

    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
        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 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() endet with:{0}'.format(certificate_dic))

        # empty list of statuses
        identifier_status = []

        if 'order' in certificate_dic:
            # get identifiers for order
            identifier_dic = self.dbstore.order_lookup(
                'name', certificate_dic['order'], ['identifiers'])

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

                tnauthlist_identifer_in = self.tnauth_identifier_check(
                    identifiers)

                if self.tnauthlist_support and tnauthlist_identifer_in:
                    # reload identifiers (case senetive)
                    try:
                        identifiers = json.loads(identifier_dic['identifiers'])
                    except BaseException:
                        identifiers = []

                    # get list of certextensions in base64 format
                    tnauthlist = csr_tnauthlist_get(self.logger, csr)

                    for identifier in identifiers:
                        # get the tnauthlist identifier
                        if identifier['type'].lower() == 'tnauthlist':
                            # check if tnauthlist extension is in extension list
                            if identifier['value'] in tnauthlist:
                                identifier_status.append(True)
                            else:
                                identifier_status.append(False)
                else:
                    # get sans
                    san_list = csr_san_get(self.logger, csr)
                    identifier_status = self.identifer_status_list(
                        identifiers, san_list)

        else:
            result = 'error'

        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 (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)

        self.logger.debug(
            'Certificate.identifer_status_list() ended with {0}'.format(
                identifier_status))
        return identifier_status
Пример #3
0
class Trigger(object):
    """ Challenge handler """
    def __init__(self, debug=None, srv_name=None, logger=None):
        self.debug = debug
        self.server_name = srv_name
        self.cahandler = None
        self.logger = logger
        self.dbstore = DBstore(debug, self.logger)
        self.tnauthlist_support = False

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

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

    def _certname_lookup(self, cert_pem):
        """ compared certificate against csr stored in db """
        self.logger.debug('Trigger._certname_lookup()')

        result_list = []
        # extract the public key form certificate
        cert_pubkey = cert_pubkey_get(self.logger, cert_pem)
        with Certificate(self.debug, 'foo', self.logger) as certificate:
            # search certificates in status "processing"
            cert_list = certificate.certlist_search(
                'order__status_id', 4, ('name', 'csr', 'order__name'))

            for cert in cert_list:
                # extract public key from certificate and compare it with pub from cert
                if 'csr' in cert:
                    if cert['csr']:
                        csr_pubkey = csr_pubkey_get(self.logger, cert['csr'])
                        if csr_pubkey == cert_pubkey:
                            result_list.append({
                                'cert_name':
                                cert['name'],
                                'order_name':
                                cert['order__name']
                            })
        self.logger.debug(
            'Trigger._certname_lookup() ended with: {0}'.format(result_list))

        return result_list

    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:
                ca_handler_module = importlib.import_module('acme.ca_handler')
        else:
            if 'CAhandler' in config_dic:
                ca_handler_module = importlib.import_module('acme.ca_handler')
            else:
                self.logger.error(
                    'Trigger._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

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

    def _payload_process(self, payload):
        """ process payload """
        self.logger.debug('Trigger._payload_process()')
        with self.cahandler(self.debug, self.logger) as ca_handler:
            if payload:
                (error, cert_bundle, cert_raw) = ca_handler.trigger(payload)
                if cert_bundle and cert_raw:
                    # returned cert_raw is in dear format, convert to pem to lookup the pubic key
                    cert_pem = convert_byte_to_string(
                        cert_der2pem(b64_decode(self.logger, cert_raw)))

                    # lookup certificate_name by comparing public keys
                    cert_name_list = self._certname_lookup(cert_pem)

                    if cert_name_list:
                        for cert in cert_name_list:
                            data_dic = {
                                'cert': cert_bundle,
                                'name': cert['cert_name'],
                                'cert_raw': cert_raw
                            }
                            try:
                                self.dbstore.certificate_add(data_dic)
                            except BaseException as err_:
                                self.logger.critical(
                                    'acme2certifier database error in trigger._payload_process() add: {0}'
                                    .format(err_))
                            if 'order_name' in cert and cert['order_name']:
                                try:
                                    # update order status to 5 (valid)
                                    self.dbstore.order_update({
                                        'name':
                                        cert['order_name'],
                                        'status':
                                        'valid'
                                    })
                                except BaseException as err_:
                                    self.logger.critical(
                                        'acme2certifier database error in trigger._payload_process() upd: {0}'
                                        .format(err_))
                        code = 200
                        message = 'OK'
                        detail = None
                    else:
                        code = 400
                        message = 'certificate_name lookup failed'
                        detail = None
                else:
                    code = 400
                    message = error
                    detail = None
            else:
                code = 400
                message = 'payload malformed'
                detail = None

        self.logger.debug(
            'Trigger._payload_process() ended with: {0} {1}'.format(
                code, message))
        return (code, message, detail)

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

        # convert to json structure
        try:
            payload = json.loads(convert_byte_to_string(content))
        except BaseException:
            payload = {}

        if 'payload' in payload:
            if payload['payload']:
                (code, message,
                 detail) = self._payload_process(payload['payload'])
            else:
                code = 400
                message = 'malformed'
                detail = 'payload empty'
        else:
            code = 400
            message = 'malformed'
            detail = 'payload missing'
        response_dic = {}
        # check message

        # prepare/enrich response
        response_dic['header'] = {}
        response_dic['code'] = code
        response_dic['data'] = {'status': code, 'message': message}
        if detail:
            response_dic['data']['detail'] = detail

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