예제 #1
0
 def __init__(self, debug=None, srv_name=None, logger=None):
     self.server_name = srv_name
     self.logger = logger
     self.dbstore = DBstore(debug, self.logger)
     self.message = Message(debug, self.server_name, self.logger)
     self.path_dic = {'acct_path': '/acme/acct/'}
     self.inner_header_nonce_allow = False
예제 #2
0
 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
예제 #3
0
 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
예제 #4
0
 def __init__(self, debug=None, srv_name=None, logger=None, expiry=86400):
     self.server_name = srv_name
     self.debug = debug
     self.logger = logger
     self.dbstore = DBstore(debug, self.logger)
     self.message = Message(debug, self.server_name, self.logger)
     self.nonce = Nonce(debug, self.logger)
     self.expiry = expiry
     self.path_dic = {'authz_path': '/acme/authz/'}
예제 #5
0
class Nonce(object):
    """ Nonce handler """
    def __init__(self, debug=None, logger=None):
        self.debug = debug
        self.logger = logger
        self.dbstore = DBstore(self.debug, self.logger)

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

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

    def check(self, protected_decoded):
        """ check nonce """
        self.logger.debug('Nonce.check_nonce()')
        if 'nonce' in protected_decoded:
            (code, message,
             detail) = self.check_and_delete(protected_decoded['nonce'])
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:badNonce'
            detail = 'NONE'
        self.logger.debug('Nonce.check_nonce() ended with:{0}'.format(code))
        return (code, message, detail)

    def check_and_delete(self, nonce):
        """ check if nonce exists and delete it """
        self.logger.debug('Nonce.nonce_check_and_delete({0})'.format(nonce))
        if self.dbstore.nonce_check(nonce):
            self.dbstore.nonce_delete(nonce)
            code = 200
            message = None
            detail = None
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:badNonce'
            detail = nonce
        self.logger.debug(
            'Nonce.check_and_delete() ended with:{0}'.format(code))
        return (code, message, detail)

    def generate_and_add(self):
        """ generate new nonce and store it """
        self.logger.debug('Nonce.nonce_generate_and_add()')
        nonce = self.new()
        self.logger.debug('got nonce: {0}'.format(nonce))
        _id = self.dbstore.nonce_add(nonce)
        self.logger.debug(
            'Nonce.generate_and_add() ended with:{0}'.format(nonce))
        return nonce

    def new(self):
        """ generate a new nonce """
        self.logger.debug('Nonce.nonce_new()')
        return uuid.uuid4().hex
예제 #6
0
 def __init__(self, debug=None, srv_name=None, logger=None):
     self.server_name = srv_name
     self.debug = debug
     self.logger = logger
     self.dbstore = DBstore(debug, self.logger)
     self.message = Message(debug, self.server_name, self.logger)
     self.nonce = Nonce(debug, self.logger)
     self.validity = 86400
     self.expiry_check_disable = False
     self.path_dic = {'authz_path' : '/acme/authz/'}
예제 #7
0
 def __init__(self, debug=None, srv_name=None, logger=None, expiry=3600):
     # self.debug = debug
     self.server_name = srv_name
     self.logger = logger
     self.dbstore = DBstore(debug, self.logger)
     self.message = Message(debug, self.server_name, self.logger)
     self.path_dic = {'chall_path' : '/acme/chall/', 'authz_path' : '/acme/authz/'}
     self.expiry = expiry
     self.challenge_validation_disable = False
     self.tnauthlist_support = False
     self.dns_server_list = None
예제 #8
0
 def __init__(self, debug=None, srv_name=None, logger=None):
     self.server_name = srv_name
     self.logger = logger
     self.dbstore = DBstore(debug, self.logger)
     self.message = Message(debug, self.server_name, self.logger)
     self.path_dic = {'acct_path': '/acme/acct/'}
     self.ecc_only = False
     self.contact_check_disable = False
     self.tos_check_disable = False
     self.inner_header_nonce_allow = False
     self.tos_url = None
예제 #9
0
 def __init__(self, debug=None, srv_name=None, logger=None, expiry=86400):
     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.expiry = expiry
     self.path_dic = {
         'authz_path': '/acme/authz/',
         'order_path': '/acme/order/',
         'cert_path': '/acme/cert/'
     }
     self.tnauthlist_support = False
예제 #10
0
 def __init__(self, debug=None, srv_name=None, logger=None):
     self.debug = debug
     self.logger = logger
     self.nonce = Nonce(self.debug, self.logger)
     self.dbstore = DBstore(self.debug, self.logger)
     self.server_name = srv_name
     self.path_dic = {
         'acct_path': '/acme/acct/',
         'revocation_path': '/acme/revokecert'
     }
     self.disable_dic = {
         'signature_check_disable': False,
         'nonce_check_disable': False
     }
     self._config_load()
예제 #11
0
 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
예제 #12
0
class Signature(object):
    """ Signature handler """
    def __init__(self, debug=None, srv_name=None, logger=None):
        self.debug = debug
        self.logger = logger
        self.dbstore = DBstore(self.debug, self.logger)
        self.server_name = srv_name
        self.revocation_path = '/acme/revokecert'

    def _jwk_load(self, kid):
        """ get key for a specific account id """
        self.logger.debug('Signature._jwk_load({0})'.format(kid))
        try:
            result = self.dbstore.jwk_load(kid)
        except BaseException as err_:
            print(err_)
            self.logger.critical(
                'acme2certifier database error in Signature._hwk_load(): {0}'.
                format(err_))
            result = None
        return result

    def check(self, aname, content, use_emb_key=False, protected=None):
        """ signature check """
        self.logger.debug('Signature.check({0})'.format(aname))
        result = False
        if content:
            error = None
            if aname:
                self.logger.debug('check signature against account key')
                pub_key = self._jwk_load(aname)
                if pub_key:
                    (result, error) = signature_check(self.logger, content,
                                                      pub_key)
                else:
                    error = 'urn:ietf:params:acme:error:accountDoesNotExist'
            elif use_emb_key:
                self.logger.debug(
                    'check signature against key includedn in jwk')
                if 'jwk' in protected:
                    pub_key = protected['jwk']
                    (result, error) = signature_check(self.logger, content,
                                                      pub_key)
                else:
                    error = 'urn:ietf:params:acme:error:accountDoesNotExist'
            else:
                error = 'urn:ietf:params:acme:error:accountDoesNotExist'
        else:
            error = 'urn:ietf:params:acme:error:malformed'

        self.logger.debug('Signature.check() ended with: {0}:{1}'.format(
            result, error))
        return (result, error, None)
예제 #13
0
class Message(object):
    """ Message  handler """
    def __init__(self, debug=None, srv_name=None, logger=None):
        self.debug = debug
        self.logger = logger
        self.nonce = Nonce(self.debug, self.logger)
        self.dbstore = DBstore(self.debug, self.logger)
        self.server_name = srv_name
        self.path_dic = {
            'acct_path': '/acme/acct/',
            'revocation_path': '/acme/revokecert'
        }
        self.disable_dic = {
            'signature_check_disable': False,
            'nonce_check_disable': False
        }
        self._config_load()

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

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

    def _config_load(self):
        """" load config from file """
        self.logger.debug('_config_load()')
        config_dic = load_config()
        if 'Nonce' in config_dic:
            self.disable_dic['nonce_check_disable'] = config_dic.getboolean(
                'Nonce', 'nonce_check_disable', fallback=False)
            self.disable_dic[
                'signature_check_disable'] = config_dic.getboolean(
                    'Nonce', 'signature_check_disable', fallback=False)

    def _name_get(self, content):
        """ get name for account """
        self.logger.debug('Message._name_get()')

        if 'kid' in content:
            self.logger.debug('kid: {0}'.format(content['kid']))
            kid = content['kid'].replace(
                '{0}{1}'.format(self.server_name, self.path_dic['acct_path']),
                '')
            if '/' in kid:
                kid = None
        elif 'jwk' in content and 'url' in content:
            if content['url'] == '{0}{1}'.format(
                    self.server_name, self.path_dic['revocation_path']):
                # this is needed for cases where we get a revocation message signed with account key but account name is missing)
                if 'jwk' in content:
                    try:
                        account_list = self.dbstore.account_lookup(
                            'jwk', json.dumps(content['jwk']))
                    except BaseException as err_:
                        self.logger.critical(
                            'acme2certifier database error in Message._name_get(): {0}'
                            .format(err_))
                        account_list = []
                    if account_list:
                        if 'name' in account_list:
                            kid = account_list['name']
                        else:
                            kid = None
                    else:
                        kid = None
                else:
                    kid = None
            else:
                kid = None
        else:
            kid = None
        self.logger.debug('Message._name_get() returns: {0}'.format(kid))
        return kid

    def check(self, content, use_emb_key=False, skip_nonce_check=False):
        """ validate message """
        self.logger.debug('Message.check()')
        # disable signature check if paramter has been set
        if self.disable_dic['signature_check_disable']:
            self.logger.error(
                '**** SIGNATURE_CHECK_DISABLE!!! Severe security issue ****')
            skip_signature_check = True
        else:
            skip_signature_check = False

        # decode message
        (result, error_detail, protected, payload,
         _signature) = decode_message(self.logger, content)
        account_name = None
        if result:
            # decoding successful - check nonce for anti replay protection
            if skip_nonce_check or self.disable_dic['nonce_check_disable']:
                # nonce check can be skipped by configuration and in case of key-rollover
                if self.disable_dic['nonce_check_disable']:
                    self.logger.error(
                        '**** NONCE CHECK DISABLED!!! Severe security issue ****'
                    )
                else:
                    self.logger.debug(
                        'skip nonce check of inner payload during keyrollover')
                code = 200
                message = None
                detail = None
            else:
                (code, message, detail) = self.nonce.check(protected)

            if code == 200 and not skip_signature_check:
                # nonce check successful - check signature
                account_name = self._name_get(protected)
                signature = Signature(self.debug, self.server_name,
                                      self.logger)
                # we need the decoded protected header to grab a key to verify signature
                (sig_check, error,
                 error_detail) = signature.check(account_name, content,
                                                 use_emb_key, protected)
                if sig_check:
                    code = 200
                    message = None
                    detail = None
                else:
                    code = 403
                    message = error
                    detail = error_detail
        else:
            # message could not get decoded
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = error_detail

        self.logger.debug('Message.check() ended with:{0}'.format(code))
        return (code, message, detail, protected, payload, account_name)

    def prepare_response(self, response_dic, status_dic):
        """ prepare response_dic """
        self.logger.debug('Message.prepare_response()')
        if 'code' not in status_dic:
            status_dic['code'] = 400
            status_dic['message'] = 'urn:ietf:params:acme:error:serverInternal'
            status_dic['detail'] = 'http status code missing'

        if 'message' not in status_dic:
            status_dic['message'] = 'urn:ietf:params:acme:error:serverInternal'

        if 'detail' not in status_dic:
            status_dic['detail'] = None

        # create response
        response_dic['code'] = status_dic['code']

        # create header if not existing
        if 'header' not in response_dic:
            response_dic['header'] = {}

        if status_dic['code'] >= 400:
            if status_dic['detail']:
                # some error occured get details
                error_message = Error(self.debug, self.logger)
                status_dic['detail'] = error_message.enrich_error(
                    status_dic['message'], status_dic['detail'])
                response_dic['data'] = {
                    'status': status_dic['code'],
                    'message': status_dic['message'],
                    'detail': status_dic['detail']
                }
            else:
                response_dic['data'] = {
                    'status': status_dic['code'],
                    'message': status_dic['message']
                }
                # response_dic['data'] = {'status': status_dic['code'], 'message': status_dic['message'], 'detail': None}
        else:
            # add nonce to header
            response_dic['header'][
                'Replay-Nonce'] = self.nonce.generate_and_add()

        return response_dic
예제 #14
0
class Order(object):
    """ class for order handling """
    def __init__(self, debug=None, srv_name=None, logger=None, expiry=86400):
        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.expiry = expiry
        self.path_dic = {
            'authz_path': '/acme/authz/',
            'order_path': '/acme/order/',
            '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 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.expiry

        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

            # add order to db
            oid = self.dbstore.order_add(data_dic)

            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'
                        self.dbstore.authorization_add(auth)
                else:
                    error = 'urn:ietf:params:acme:error:malformed'
        else:
            error = 'urn:ietf:params:acme:error:unsupportedIdentifier'

        # print(auth_dic)
        return (error, order_name, auth_dic, uts_to_date_utc(expires))

    def name_get(self, url):
        """ get ordername """
        self.logger.debug('Order.get_name({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)
        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))
        return self.dbstore.order_lookup('name', order_name)

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

        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 'finalize' in protected['url']:
                    self.logger.debug('finalize request()')
                    if 'csr' in payload:
                        self.logger.debug('CSR found()')
                        # this is a new request
                        (code, certificate_name,
                         detail) = self.process_csr(order_name, payload['csr'])
                        if code == 200:
                            # update order_status / set to valid
                            self.update({
                                'name': order_name,
                                'status': 'valid'
                            })
                        else:
                            self.logger.debug('no CSR found()')
                            code = 400
                            message = 'urn:ietf:params:acme:error:badCSR'
                            detail = 'enrollment failed'
                    else:
                        code = 400
                        message = 'urn:ietf:params:acme:error:badCSR'
                        detail = 'csr is missing in payload'
                else:
                    self.logger.debug('polling request()')
                    code = 200
                    # this is a polling request; lookup certificate
                    cert_dic = self.dbstore.certificate_lookup(
                        'order__name', order_name)
                    if cert_dic:
                        # we found a cert in the database
                        certificate_name = cert_dic['name']
                    else:
                        certificate_name = None
            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)
                response_dic['data']['finalize'] = '{0}{1}{2}/finalize'.format(
                    self.server_name, self.path_dic['order_path'], order_name)
                if certificate_name:
                    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

    def process_csr(self, order_name, csr):
        """ process certificate signing request """
        self.logger.debug('Order.process_csr({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:
                    (_result, error, detail) = certificate.enroll_and_store(
                        certificate_name, csr)
                    if not error:
                        code = 200
                        message = certificate_name
                        detail = None
                    else:
                        code = 500
                        message = error

                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.process_csr() 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))
        return self.dbstore.order_update(data_dic)

    def lookup(self, order_name):
        """ sohw order details based on ordername """
        self.logger.debug('Order.show({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:
                order_dic['identifiers'] = json.loads(tmp_dic['identifiers'])

            authz_list = self.dbstore.authorization_lookup(
                'order__name', order_name, ['name'])
            if authz_list:
                order_dic["authorizations"] = []
                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']))

        return order_dic

    def load_config(self):
        """" load config from file """
        self.logger.debug('Order.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('Order.load_config() ended.')
예제 #15
0
class Authorization(object):
    """ class for order handling """
    def __init__(self, debug=None, srv_name=None, logger=None, expiry=86400):
        self.server_name = srv_name
        self.debug = debug
        self.logger = logger
        self.dbstore = DBstore(debug, self.logger)
        self.message = Message(debug, self.server_name, self.logger)
        self.nonce = Nonce(debug, self.logger)
        self.expiry = expiry
        self.path_dic = {'authz_path': '/acme/authz/'}

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

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

    def authz_info(self, url):
        """ return authzs information """
        self.logger.debug('Authorization.info({0})'.format(url))
        authz_name = url.replace(
            '{0}{1}'.format(self.server_name, self.path_dic['authz_path']), '')
        expires = uts_now() + self.expiry
        token = generate_random_string(self.logger, 32)
        authz_info_dic = {}
        if self.dbstore.authorization_lookup('name', authz_name):

            # update authorization with expiry date and token (just to be sure)
            self.dbstore.authorization_update({
                'name': authz_name,
                'token': token,
                'expires': expires
            })
            authz_info_dic['expires'] = uts_to_date_utc(expires)

            # get authorization information from db to be inserted in message
            tnauth = None
            auth_info = self.dbstore.authorization_lookup(
                'name', authz_name, ['status__name', 'type', 'value'])
            if auth_info:
                authz_info_dic['status'] = auth_info[0]['status__name']
                authz_info_dic['identifier'] = {
                    'type': auth_info[0]['type'],
                    'value': auth_info[0]['value']
                }
                if auth_info[0]['type'] == 'TNAuthList':
                    tnauth = True
            challenge = Challenge(self.debug, self.server_name, self.logger,
                                  expires)
            authz_info_dic['challenges'] = challenge.new_set(
                authz_name, token, tnauth)

        self.logger.debug('Authorization.authz_info() returns: {0}'.format(
            json.dumps(authz_info_dic)))
        return authz_info_dic

    def new_get(self, url):
        """ challenge computation based on get request """
        self.logger.debug('Authorization.new_get()')
        response_dic = {}
        response_dic['code'] = 200
        response_dic['header'] = {}
        response_dic['data'] = self.authz_info(url)
        return response_dic

    def new_post(self, content):
        """ challenge computation based on post request """
        self.logger.debug('Authorization.new_post()')

        response_dic = {}
        # check message
        (code, message, detail, protected, _payload,
         _account_name) = self.message.check(content)
        if code == 200:
            if 'url' in protected:
                auth_info = self.authz_info(protected['url'])
                if auth_info:
                    response_dic['data'] = auth_info
                else:
                    code = 403
                    message = 'urn:ietf:params:acme:error:unauthorized'
                    detail = 'authorizations lookup failed'
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'url is missing in protected'

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

        self.logger.debug('Authorization.new_post() returns: {0}'.format(
            json.dumps(response_dic)))
        return response_dic
예제 #16
0
class Account(object):
    """ ACME server class """
    def __init__(self, debug=None, srv_name=None, logger=None):
        self.server_name = srv_name
        self.logger = logger
        self.dbstore = DBstore(debug, self.logger)
        self.message = Message(debug, self.server_name, self.logger)
        self.path_dic = {'acct_path': '/acme/acct/'}
        self.ecc_only = False
        self.contact_check_disable = False
        self.tos_check_disable = False
        self.inner_header_nonce_allow = False
        self.tos_url = None

    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, content, contact):
        """ prepare db insert and call DBstore helper """
        self.logger.debug('Account.account._add()')
        account_name = generate_random_string(self.logger, 12)

        # check request
        if 'alg' in content and 'jwk' in content:

            if not self.contact_check_disable and not contact:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'incomplete protected payload'
            else:
                # ecc_only check
                if self.ecc_only and not content['alg'].startswith('ES'):
                    code = 403
                    message = 'urn:ietf:params:acme:error:badPublicKey'
                    detail = 'Only ECC keys are supported'
                else:
                    # check jwk
                    data_dic = {
                        'name': account_name,
                        'alg': content['alg'],
                        'jwk': json.dumps(content['jwk']),
                        'contact': json.dumps(contact),
                    }
                    try:
                        (db_name, new) = self.dbstore.account_add(data_dic)
                    except BaseException as err_:
                        self.logger.critical(
                            'Database error in Account._add(): {0}'.format(
                                err_))
                        db_name = None
                        new = False
                    self.logger.debug('god account_name:{0} new:{1}'.format(
                        db_name, new))
                    if new:
                        code = 201
                        message = account_name
                    else:
                        code = 200
                        message = db_name
                    detail = None
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'incomplete protected payload'

        self.logger.debug('Account.account._add() ended with:{0}'.format(code))
        return (code, message, detail)

    def _contact_check(self, content):
        """ check contact information from payload"""
        self.logger.debug('Account._contact_check()')
        code = 200
        message = None
        detail = None
        if 'contact' in content:
            contact_check = validate_email(self.logger, content['contact'])
            if not contact_check:
                # invalidcontact message
                code = 400
                message = 'urn:ietf:params:acme:error:invalidContact'
                detail = ', '.join(content['contact'])
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:invalidContact'
            detail = 'no contacts specified'

        self.logger.debug(
            'Account._contact_check() ended with:{0}'.format(code))
        return (code, message, detail)

    def _contacts_update(self, aname, payload):
        """ update account """
        self.logger.debug('Account.update()')
        (code, message, detail) = self._contact_check(payload)
        if code == 200:
            data_dic = {
                'name': aname,
                'contact': json.dumps(payload['contact'])
            }
            try:
                result = self.dbstore.account_update(data_dic)
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Account._contacts_update(): {0}'
                    .format(err_))
                result = None

            if result:
                code = 200
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:accountDoesNotExist'
                detail = 'update failed'

        return (code, message, detail)

    def _delete(self, aname):
        """ delete account """
        self.logger.debug('Account._delete({0})'.format(aname))
        try:
            result = self.dbstore.account_delete(aname)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Account._delete(): {0}'.
                format(err_))
            result = None

        if result:
            code = 200
            message = None
            detail = None
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:accountDoesNotExist'
            detail = 'deletion failed'

        self.logger.debug('Account._delete() ended with:{0}'.format(code))
        return (code, message, detail)

    def _inner_jws_check(self, outer_protected, inner_protected):
        """ RFC8655 7.3.5 checs of inner JWS """
        self.logger.debug('Account._inner_jws_check()')

        # check for jwk header
        if 'jwk' in inner_protected:
            if 'url' in outer_protected and 'url' in inner_protected:
                # inner and outer JWS must have the same "url" header parameter
                if outer_protected['url'] == inner_protected['url']:
                    if self.inner_header_nonce_allow:
                        code = 200
                        message = None
                        detail = None
                    else:
                        # inner JWS must omit nonce header
                        if 'nonce' not in inner_protected:
                            code = 200
                            message = None
                            detail = None
                        else:
                            code = 400
                            message = 'urn:ietf:params:acme:error:malformed'
                            detail = 'inner jws must omit nonce header'
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'url parameter differ in inner and outer jws'
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'inner or outer jws is missing url header parameter'
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'inner jws is missing jwk'

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

    def _inner_payload_check(self, aname, outer_protected, inner_payload):
        """ RFC8655 7.3.5 checs of inner payload """
        self.logger.debug('Account._inner_payload_check()')

        if 'kid' in outer_protected:
            if 'account' in inner_payload:
                if outer_protected['kid'] == inner_payload['account']:
                    if 'oldkey' in inner_payload:
                        # compare oldkey with database
                        (code, message,
                         detail) = self._key_compare(aname,
                                                     inner_payload['oldkey'])
                    else:
                        code = 400
                        message = 'urn:ietf:params:acme:error:malformed'
                        detail = 'old key is missing'
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'kid and account objects do not match'
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'account object is missing on inner payload'
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'kid is missing in outer header'

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

    def _key_change_validate(self, aname, outer_protected, inner_protected,
                             inner_payload):
        """ validate key_change before exectution """
        self.logger.debug('Account._key_change_validate({0})'.format(aname))
        if 'jwk' in inner_protected:
            # check if we already have the key stored in DB
            key_exists = self._lookup(json.dumps(inner_protected['jwk']),
                                      'jwk')
            if not key_exists:
                (code, message,
                 detail) = self._inner_jws_check(outer_protected,
                                                 inner_protected)

                if code == 200:
                    (code, message, detail) = self._inner_payload_check(
                        aname, outer_protected, inner_payload)
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:badPublicKey'
                detail = 'public key does already exists'
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'inner jws is missing jwk'

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

    def _key_change(self, aname, payload, protected):
        """ key change for a given account """
        self.logger.debug('Account._key_change({0})'.format(aname))

        if 'url' in protected:
            if 'key-change' in protected['url']:
                # check message
                (code, message, detail, inner_protected, inner_payload,
                 _account_name) = self.message.check(json.dumps(payload),
                                                     use_emb_key=True,
                                                     skip_nonce_check=True)
                if code == 200:
                    (code, message, detail) = self._key_change_validate(
                        aname, protected, inner_protected, inner_payload)
                    if code == 200:
                        data_dic = {
                            'name': aname,
                            'jwk': json.dumps(inner_protected['jwk'])
                        }
                        try:
                            result = self.dbstore.account_update(data_dic)
                        except BaseException as err_:
                            self.logger.critical(
                                'acme2certifier database error in Account._key_change(): {0}'
                                .format(err_))
                            result = None
                        if result:
                            code = 200
                            message = None
                            detail = None
                        else:
                            code = 500
                            message = 'urn:ietf:params:acme:error:serverInternal'
                            detail = 'key rollover failed'
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'malformed request. not a key-change'
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'malformed request'

        return (code, message, detail)

    def _key_compare(self, aname, old_key):
        """ compare key with the one stored in database """
        self.logger.debug('Account._key_compare({0})'.format(aname))

        # load current public key from database
        try:
            pub_key = self.dbstore.jwk_load(aname)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Account._key_compare(): {0}'.
                format(err_))
            pub_key = None

        if old_key and pub_key:
            # rewrite alg statement in pubkey statement
            if 'alg' in pub_key and 'alg' in old_key:
                if pub_key['alg'].startswith(
                        'ES') and old_key['alg'] == 'ECDSA':
                    pub_key['alg'] = 'ECDSA'

            if old_key == pub_key:
                code = 200
                message = None
                detail = None
            else:
                code = 401
                message = 'urn:ietf:params:acme:error:unauthorized'
                detail = 'wrong public key'
        else:
            code = 401
            message = 'urn:ietf:params:acme:error:unauthorized'
            detail = 'wrong public key'

        self.logger.debug(
            'Account._key_compare() ended with: {0}'.format(code))
        return (code, message, detail)

    def _config_load(self):
        """" load config from file """
        self.logger.debug('_config_load()')
        config_dic = load_config()
        if 'Account' in config_dic:
            self.inner_header_nonce_allow = config_dic.getboolean(
                'Account', 'inner_header_nonce_allow', fallback=False)
            self.ecc_only = config_dic.getboolean('Account',
                                                  'ecc_only',
                                                  fallback=False)
            self.tos_check_disable = config_dic.getboolean('Account',
                                                           'tos_check_disable',
                                                           fallback=False)
            self.contact_check_disable = config_dic.getboolean(
                'Account', 'contact_check_disable', fallback=False)
        if 'Directory' in config_dic:
            if 'tos_url' in config_dic['Directory']:
                self.tos_url = config_dic['Directory']['tos_url']

    def _lookup(self, value, field='name'):
        """ lookup account """
        self.logger.debug('Account._lookup({0}:{1})'.format(field, value))
        try:
            result = self.dbstore.account_lookup(field, value)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Account._lookup(): {0}'.
                format(err_))
            result = None
        return result

    # pylint: disable=W0212
    def _name_get(self, content):
        """ get id for account depricated"""
        self.logger.debug('Account._name_get()')
        _deprecated = True
        return self.message._name_get(content)

    def _onlyreturnexisting(self, protected, payload):
        """ check onlyreturnexisting """
        self.logger.debug('Account._onlyreturnexisting(}')
        if 'onlyreturnexisting' in payload:
            if payload['onlyreturnexisting']:
                code = None
                message = None
                detail = None

                if 'jwk' in protected:
                    try:
                        result = self.dbstore.account_lookup(
                            'jwk', json.dumps(protected['jwk']))
                    except BaseException as err_:
                        self.logger.critical(
                            'acme2certifier database error in Account._onlyreturnexisting(): {0}'
                            .format(err_))
                        result = None

                    if result:
                        code = 200
                        message = result['name']
                        detail = None
                    else:
                        code = 400
                        message = 'urn:ietf:params:acme:error:accountDoesNotExist'
                        detail = None
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'jwk structure missing'

            else:
                code = 400
                message = 'urn:ietf:params:acme:error:userActionRequired'
                detail = 'onlyReturnExisting must be true'
        else:
            code = 500
            message = 'urn:ietf:params:acme:error:serverInternal'
            detail = 'onlyReturnExisting without payload'

        self.logger.debug(
            'Account.onlyreturnexisting() ended with:{0}'.format(code))
        return (code, message, detail)

    def _tos_check(self, content):
        """ check terms of service """
        self.logger.debug('Account._tos_check()')
        if 'termsofserviceagreed' in content:
            self.logger.debug('tos:{0}'.format(
                content['termsofserviceagreed']))
            if content['termsofserviceagreed']:
                code = 200
                message = None
                detail = None
            else:
                code = 403
                message = 'urn:ietf:params:acme:error:userActionRequired'
                detail = 'tosfalse'
        else:
            self.logger.debug('no tos statement found.')
            code = 403
            message = 'urn:ietf:params:acme:error:userActionRequired'
            detail = 'tosfalse'

        self.logger.debug('Account._tos_check() ended with:{0}'.format(code))
        return (code, message, detail)

    def new(self, content):
        """ generate a new account """
        self.logger.debug('Account.account_new()')

        response_dic = {}
        # check message but skip signature check as this is a new account (True)
        (code, message, detail, protected, payload,
         _account_name) = self.message.check(content, True)
        if code == 200:
            # onlyReturnExisting check
            if 'onlyreturnexisting' in payload:
                (code, message,
                 detail) = self._onlyreturnexisting(protected, payload)
            else:
                # tos check
                if self.tos_url and not self.tos_check_disable:
                    (code, message, detail) = self._tos_check(payload)

                # contact check
                if code == 200 and not self.contact_check_disable:
                    (code, message, detail) = self._contact_check(payload)

                # add account to database
                if code == 200:
                    if 'contact' in payload:
                        contact_list = payload['contact']
                    else:
                        contact_list = []
                    (code, message,
                     detail) = self._add(protected, contact_list)

        if code in (200, 201):
            response_dic['data'] = {}
            if code == 201:
                response_dic['data'] = {
                    'status':
                    'valid',
                    'orders':
                    '{0}{1}{2}/orders'.format(self.server_name,
                                              self.path_dic['acct_path'],
                                              message),
                }
                if 'contact' in payload:
                    response_dic['data']['contact'] = payload['contact']

            response_dic['header'] = {}
            response_dic['header']['Location'] = '{0}{1}{2}'.format(
                self.server_name, self.path_dic['acct_path'], message)
        else:
            if detail == 'tosfalse':
                detail = 'Terms of service must be accepted'

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

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

    def parse(self, content):
        """ parse message """
        self.logger.debug('Account.parse()')

        response_dic = {}
        # check message
        (code, message, detail, protected, payload,
         account_name) = self.message.check(content)
        if code == 200:
            if 'status' in payload:
                # account deactivation
                if payload['status'].lower() == 'deactivated':
                    # account_name = self.message.name_get(protected)
                    (code, message, detail) = self._delete(account_name)
                    if code == 200:
                        response_dic['data'] = payload
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'status attribute without sense'
            elif 'contact' in payload:
                (code, message,
                 detail) = self._contacts_update(account_name, payload)
                if code == 200:
                    account_obj = self._lookup(account_name)
                    response_dic['data'] = {}
                    response_dic['data']['status'] = 'valid'
                    response_dic['data']['key'] = json.loads(
                        account_obj['jwk'])
                    response_dic['data']['contact'] = json.loads(
                        account_obj['contact'])
                    response_dic['data']['createdAt'] = date_to_datestr(
                        account_obj['created_at'])
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:accountDoesNotExist'
                    detail = 'update failed'
            elif 'payload' in payload:
                # this could be a key-change
                (code, message,
                 detail) = self._key_change(account_name, payload, protected)
                if code == 200:
                    response_dic['data'] = {}
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'dont know what to do with this request'
        # prepare/enrich response
        status_dic = {'code': code, 'message': message, 'detail': detail}
        response_dic = self.message.prepare_response(response_dic, status_dic)

        self.logger.debug('Account.account_parse() returns: {0}'.format(
            json.dumps(response_dic)))
        return response_dic
예제 #17
0
 def __init__(self, debug=None, srv_name=None, logger=None):
     self.debug = debug
     self.logger = logger
     self.dbstore = DBstore(self.debug, self.logger)
     self.server_name = srv_name
     self.revocation_path = '/acme/revokecert'
예제 #18
0
class Nonce(object):
    """ Nonce handler """
    def __init__(self, debug=None, logger=None):
        self.debug = debug
        self.logger = logger
        self.dbstore = DBstore(self.debug, self.logger)

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

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

    def _check_and_delete(self, nonce):
        """ check if nonce exists and delete it """
        self.logger.debug('Nonce.nonce._check_and_delete({0})'.format(nonce))

        try:
            nonce_chk_result = self.dbstore.nonce_check(nonce)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Nonce._check_and_delete(): {0}'
                .format(err_))
            nonce_chk_result = False

        if nonce_chk_result:
            try:
                self.dbstore.nonce_delete(nonce)
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Nonce._check_and_delete(): {0}'
                    .format(err_))
            code = 200
            message = None
            detail = None
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:badNonce'
            detail = nonce
        self.logger.debug(
            'Nonce._check_and_delete() ended with:{0}'.format(code))
        return (code, message, detail)

    def _new(self):
        """ generate a new nonce """
        self.logger.debug('Nonce.nonce__new()')
        return uuid.uuid4().hex

    def check(self, protected_decoded):
        """ check nonce """
        self.logger.debug('Nonce.check_nonce()')
        if 'nonce' in protected_decoded:
            (code, message,
             detail) = self._check_and_delete(protected_decoded['nonce'])
        else:
            code = 400
            message = 'urn:ietf:params:acme:error:badNonce'
            detail = 'NONE'
        self.logger.debug('Nonce.check_nonce() ended with:{0}'.format(code))
        return (code, message, detail)

    def generate_and_add(self):
        """ generate new nonce and store it """
        self.logger.debug('Nonce.nonce_generate_and_add()')
        nonce = self._new()
        self.logger.debug('got nonce: {0}'.format(nonce))
        # self.logger.critical('foo')
        try:
            _id = self.dbstore.nonce_add(nonce)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Nonce.generate_and_add(): {0}'
                .format(err_))
        self.logger.debug(
            'Nonce.generate_and_add() ended with:{0}'.format(nonce))
        return nonce
예제 #19
0
#!/usr/bin/python
""" database updater """
# pylint: disable=E0401, C0413
import sys
sys.path.insert(0, '..')
sys.path.insert(1, '.')
from acme.helper import logger_setup
from acme.db_handler import DBstore

if __name__ == '__main__':

    DEBUG = True

    # initialize logger
    LOGGER = logger_setup(DEBUG)

    # connect to database and do the upgrade
    DBSTORE = DBstore(DEBUG, LOGGER)
    DBSTORE.db_update()
예제 #20
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
예제 #21
0
class Challenge(object):
    """ Challenge handler """
    def __init__(self, debug=None, srv_name=None, logger=None, expiry=3600):
        # self.debug = debug
        self.server_name = srv_name
        self.logger = logger
        self.dbstore = DBstore(debug, self.logger)
        self.message = Message(debug, self.server_name, self.logger)
        self.path_dic = {
            'chall_path': '/acme/chall/',
            'authz_path': '/acme/authz/'
        }
        self.expiry = expiry
        self.challenge_validation_disable = False
        self.tnauthlist_support = False

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

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

    def check(self, challenge_name, payload):
        """ challene check """
        self.logger.debug('challenge.check({0})'.format(challenge_name))
        challenge_dic = self.dbstore.challenge_lookup('name', challenge_name, [
            'type', 'status__name', 'token', 'authorization__name',
            'authorization__type', 'authorization__value',
            'authorization__token', 'authorization__order__account__name'
        ])
        if 'type' in challenge_dic and 'authorization__value' in challenge_dic and 'token' in challenge_dic and 'authorization__order__account__name' in challenge_dic:
            pub_key = self.dbstore.jwk_load(
                challenge_dic['authorization__order__account__name'])
            if pub_key:
                jwk_thumbprint = jwk_thumbprint_get(self.logger, pub_key)
                if challenge_dic['type'] == 'http-01' and jwk_thumbprint:
                    result = self.validate_http_challenge(
                        challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint)
                elif challenge_dic['type'] == 'dns-01' and jwk_thumbprint:
                    result = self.validate_dns_challenge(
                        challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint)
                elif challenge_dic[
                        'type'] == 'tkauth-01' and jwk_thumbprint and self.tnauthlist_support:
                    result = self.validate_tkauth_challenge(
                        challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint, payload)
                else:
                    self.logger.debug(
                        'unknown challenge type "{0}". Setting check result to False'
                        .format(challenge_dic['type']))
                    result = False
            else:
                result = False
        else:
            result = False
        self.logger.debug('challenge.check() ended with: {0}'.format(result))
        return result

    def get(self, url):
        """ get challenge details based on get request """
        self.logger.debug('challenge.new_get({0})'.format(url))
        challenge_name = self.name_get(url)
        response_dic = {}
        response_dic['code'] = 200
        response_dic['data'] = self.info(challenge_name)
        return response_dic

    def info(self, challenge_name):
        """ get challenge details """
        self.logger.debug('Challenge.info({0})'.format(challenge_name))
        challenge_dic = self.dbstore.challenge_lookup('name', challenge_name)
        return challenge_dic

    def load_config(self):
        """" load config from file """
        self.logger.debug('Challenge.load_config()')
        config_dic = load_config()
        if 'Challenge' in config_dic:
            self.challenge_validation_disable = config_dic.getboolean(
                'Challenge', 'challenge_validation_disable', fallback=False)
        if 'Order' in config_dic:
            self.tnauthlist_support = config_dic.getboolean(
                'Order', 'tnauthlist_support', fallback=False)
        self.logger.debug('Challenge.load_config() ended.')

    def name_get(self, url):
        """ get challenge """
        self.logger.debug('Challenge.get_name({0})'.format(url))
        url_dic = parse_url(self.logger, url)
        challenge_name = url_dic['path'].replace(self.path_dic['chall_path'],
                                                 '')
        if '/' in challenge_name:
            (challenge_name, _sinin) = challenge_name.split('/', 1)
        return challenge_name

    def new(self, authz_name, mtype, token):
        """ new challenge """
        self.logger.debug('Challenge.new({0})'.format(mtype))

        challenge_name = generate_random_string(self.logger, 12)

        data_dic = {
            'name': challenge_name,
            'expires': self.expiry,
            'type': mtype,
            'token': token,
            'authorization': authz_name,
            'status': 2
        }
        chid = self.dbstore.challenge_add(data_dic)

        challenge_dic = {}
        if chid:
            challenge_dic['type'] = mtype
            challenge_dic['url'] = '{0}{1}{2}'.format(
                self.server_name, self.path_dic['chall_path'], challenge_name)
            challenge_dic['token'] = token
            if mtype == 'tkauth-01':
                challenge_dic['tkauth-type'] = 'atc'
        return challenge_dic

    def new_set(self, authz_name, token, tnauth=False):
        """ net challenge set """
        self.logger.debug('Challenge.new_set({0}, {1})'.format(
            authz_name, token))
        challenge_list = []
        if not tnauth:
            challenge_list.append(self.new(authz_name, 'http-01', token))
            challenge_list.append(self.new(authz_name, 'dns-01', token))
        else:
            challenge_list.append(self.new(authz_name, 'tkauth-01', token))
        self.logger.debug(
            'Challenge.new_set returned ({0})'.format(challenge_list))
        return challenge_list

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

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

        if code == 200:
            if 'url' in protected:
                challenge_name = self.name_get(protected['url'])
                if challenge_name:
                    challenge_dic = self.info(challenge_name)

                    # check tnauthlist payload
                    if self.tnauthlist_support:
                        (code, message,
                         detail) = self.validate_tnauthlist_payload(
                             payload, challenge_dic)

                    if code == 200:
                        # update challenge state to 'processing' - i am not so sure about this
                        # self.update({'name' : challenge_name, 'status' : 4})
                        # start validation
                        _validation = self.validate(challenge_name, payload)
                        if challenge_dic:
                            response_dic['data'] = {}
                            challenge_dic['url'] = protected['url']
                            code = 200
                            response_dic['data'] = {}
                            response_dic['data'] = challenge_dic
                            response_dic['header'] = {}
                            response_dic['header'][
                                'Link'] = '<{0}{1}>;rel="up"'.format(
                                    self.server_name,
                                    self.path_dic['authz_path'])
                        else:
                            code = 400
                            message = 'urn:ietf:params:acme:error:malformed'
                            detail = 'invalid challenge: {0}'.format(
                                challenge_name)
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'could not get challenge'
            else:
                code = 400
                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('challenge.parse() returns: {0}'.format(
            json.dumps(response_dic)))
        return response_dic

    def update(self, data_dic):
        """ update challenge """
        self.logger.debug('Challenge.update({0})'.format(data_dic))
        self.dbstore.challenge_update(data_dic)

    def update_authz(self, challenge_name):
        """ update authorizsation based on challenge_name """
        self.logger.debug('Challenge.update_authz({0})'.format(challenge_name))

        # lookup autorization based on challenge_name
        authz_name = self.dbstore.challenge_lookup(
            'name', challenge_name, ['authorization__name'])['authorization']
        self.dbstore.authorization_update({
            'name': authz_name,
            'status': 'valid'
        })
        # print(authz_name)

    def validate(self, challenge_name, payload):
        """ validate challenge"""
        self.logger.debug('Challenge.validate({0}: {1})'.format(
            challenge_name, payload))
        if self.challenge_validation_disable:
            self.logger.debug(
                'CHALLENGE VALIDATION DISABLED. SETTING challenge status to valid'
            )
            challenge_check = True
        else:
            challenge_check = self.check(challenge_name, payload)

        if challenge_check:
            self.update({'name': challenge_name, 'status': 'valid'})
            # authorization update to ready state
            self.update_authz(challenge_name)

        if payload:
            if 'keyAuthorization' in payload:
                # update challenge to ready state
                data_dic = {
                    'name': challenge_name,
                    'keyauthorization': payload['keyAuthorization']
                }
                self.update(data_dic)

        self.logger.debug(
            'Challenge.validate() ended with:{0}'.format(challenge_check))

    def validate_dns_challenge(self, fqdn, token, jwk_thumbprint):
        """ validate dns challenge """
        self.logger.debug('Challenge.validate_dns_challenge()')

        # rewrite fqdn
        fqdn = '_acme-challenge.{0}'.format(fqdn)

        # compute sha256 hash
        _hash = b64_url_encode(
            self.logger,
            sha256_hash(self.logger, '{0}.{1}'.format(token, jwk_thumbprint)))
        # query dns
        txt = txt_get(self.logger, fqdn)

        # compare computed hash with result from DNS query
        if _hash == txt:
            result = True
        else:
            result = False
        self.logger.debug(
            'Challenge.validate_dns_challenge() ended with: {0}'.format(
                result))
        return result

    def validate_http_challenge(self, fqdn, token, jwk_thumbprint):
        """ validate http challenge """
        self.logger.debug('Challenge.validate_http_challenge()')

        req = url_get(
            self.logger,
            'http://{0}/.well-known/acme-challenge/{1}'.format(fqdn, token))
        if req:
            if req.splitlines()[0] == '{0}.{1}'.format(token, jwk_thumbprint):
                result = True
            else:
                result = False
        else:
            result = False
        self.logger.debug(
            'Challenge.validate_http_challenge() ended with: {0}'.format(
                result))
        return result

    def validate_tkauth_challenge(self, tnauthlist, _token, _jwk_thumbprint,
                                  payload):
        """ validate tkauth challenge """
        self.logger.debug(
            'Challenge.validate_tkauth_challenge({0}:{1})'.format(
                tnauthlist, payload))

        result = True
        self.logger.debug(
            'Challenge.validate_tkauth_challenge() ended with: {0}'.format(
                result))
        return result

    def validate_tnauthlist_payload(self, payload, challenge_dic):
        """ check payload in cae tnauthlist option has been set """
        self.logger.debug(
            'Challenge.validate_tnauthlist_payload({0}:{1})'.format(
                payload, challenge_dic))

        code = 400
        message = None
        detail = None

        if 'type' in challenge_dic:
            if challenge_dic['type'] == 'tkauth-01':
                self.logger.debug('tkauth identifier found')
                # check if we havegot an atc claim in the challenge request
                if 'atc' in payload:
                    # check if we got a SPC token in the challenge request
                    if not bool(payload['atc']):
                        code = 400
                        message = 'urn:ietf:params:acme:error:malformed'
                        detail = 'SPC token is missing'
                    else:
                        code = 200
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'atc claim is missing'
            else:
                code = 200
        else:
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'invalid challenge: {0}'.format(challenge_dic)

        self.logger.debug(
            'Challenge.validate_tnauthlist_payload() ended with:{0}'.format(
                code))
        return (code, message, detail)
예제 #22
0
class Housekeeping(object):
    """ Housekeeping class """
    def __init__(self, debug=None, logger=None):
        self.logger = logger
        self.dbstore = DBstore(debug, self.logger)
        self.debug = debug

    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 _accountlist_get(self):
        """ get list of certs from database """
        self.logger.debug('Housekeeping._certlist_get()')
        try:
            result = self.dbstore.accountlist_get()
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Housekeeping._accountlist_get(): {0}'
                .format(err_))
            result = None
        return result

    def _certificatelist_get(self):
        """ get list of certs from database """
        self.logger.debug('Housekeeping._certlist_get()')
        try:
            result = self.dbstore.certificatelist_get()
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Housekeeping.certificatelist_get(): {0}'
                .format(err_))
            result = None
        return result

    def _config_load(self):
        """ load config from file """
        self.logger.debug('Housekeeping._config_load()')
        config_dic = load_config()
        if 'Housekeeping' in config_dic:
            pass

    def _convert_data(self, cert_list):
        """ convert data from uts to real date """
        self.logger.debug('Housekeeping._convert_dates()')
        for cert in cert_list:
            expire_list = ('order.expires', 'authorization.expires',
                           'challenge.expires')
            for ele in expire_list:
                if ele in cert and cert[ele]:
                    cert[ele] = uts_to_date_utc(cert[ele], '%Y-%m-%d %H:%M:%S')

            # set uts to 0 if we do not have them in dictionary
            if 'certificate.issue_uts' not in cert or 'certificate.expire_uts' not in cert:
                cert['certificate.issue_uts'] = 0
                cert['certificate.expire_uts'] = 0

            # if uts is zero we try to get the dates from certificate
            if cert['certificate.issue_uts'] == 0 or cert[
                    'certificate.expire_uts'] == 0:
                # cover cases without certificate in dict
                if 'certificate.cert_raw' in cert:
                    (issue_uts,
                     expire_uts) = cert_dates_get(self.logger,
                                                  cert['certificate.cert_raw'])
                    cert['certificate.issue_uts'] = issue_uts
                    cert['certificate.expire_uts'] = expire_uts
                else:
                    cert['certificate.issue_uts'] = 0
                    cert['certificate.expire_uts'] = 0

            if cert['certificate.issue_uts'] > 0 and cert[
                    'certificate.expire_uts'] > 0:
                cert['certificate.issue_date'] = uts_to_date_utc(
                    cert['certificate.issue_uts'], '%Y-%m-%d %H:%M:%S')
                cert['certificate.expire_date'] = uts_to_date_utc(
                    cert['certificate.expire_uts'], '%Y-%m-%d %H:%M:%S')
            else:
                cert['certificate.issue_date'] = ''
                cert['certificate.expire_date'] = ''

        # add serial number
            if 'certificate.cert_raw' in cert:
                try:
                    cert['certificate.serial'] = cert_serial_get(
                        self.logger, cert['certificate.cert_raw'])
                except BaseException:
                    cert['certificate.serial'] = ''

        return cert_list

    def _csv_dump(self, filename, content):
        """ dump content csv file """
        self.logger.debug('Housekeeping._csv_dump()')
        with open(filename, 'w', newline='') as file_:
            writer = csv.writer(file_,
                                delimiter=',',
                                quotechar='"',
                                quoting=csv.QUOTE_NONNUMERIC)
            writer.writerows(content)

    def _json_dump(self, filename, data_):
        """ dump content json file """
        self.logger.debug('Housekeeping._json_dump()')
        jdump = json.dumps(data_, ensure_ascii=False, indent=4, default=str)
        with open(filename, 'w', newline='') as file_:
            file_.write(jdump)

    def _fieldlist_normalize(self, field_list, prefix):
        """ normalize field_list """
        self.logger.debug('Housekeeping._fieldlist_normalize()')
        field_dic = {}
        for field in field_list:
            f_list = field.split('__')
            # items from selected list which do not have a table reference get prefix added
            if len(f_list) == 1:
                new_field = '{0}.{1}'.format(prefix, field)
            elif f_list[-2] == 'status' and len(f_list) >= 3:
                # status fields have one reference more
                new_field = '{0}.{1}.{2}'.format(f_list[-3], f_list[-2],
                                                 f_list[-1])
            else:
                new_field = '{0}.{1}'.format(f_list[-2], f_list[-1])
            field_dic[field] = new_field

        return field_dic

    def _lists_normalize(self, field_list, value_list, prefix):
        """ normalize list """
        self.logger.debug('Housekeeping._list_normalize()')

        field_dic = self._fieldlist_normalize(field_list, prefix)

        new_list = []
        for v_list in value_list:
            # create a temporary dictionary wiht the renamed fields
            tmp_dic = {}
            for field in v_list:
                if field in field_dic:
                    tmp_dic[field_dic[field]] = v_list[field]
            # append dicutionary to list
            new_list.append(tmp_dic)

        # get field_list
        field_list = list(field_dic.values())

        return (field_list, new_list)

    def _to_acc_json(self, account_list):
        """ stack list to json """
        self.logger.debug('Housekeeping._to_acc_json()')

        tmp_json = {}
        error_list = []
        for ele in account_list:

            # we have to ensure that all keys we need to nest are in
            if ele.keys() >= {
                    'account.name', 'order.name', 'authorization.name',
                    'challenge.name'
            }:

                # create account entry in case it does not exist
                if ele['account.name'] not in tmp_json:
                    tmp_json[ele['account.name']] = {}
                    tmp_json[ele['account.name']]['orders_dic'] = {}

                if ele['order.name'] not in tmp_json[
                        ele['account.name']]['orders_dic']:
                    tmp_json[ele['account.name']]['orders_dic'][
                        ele['order.name']] = {}
                    tmp_json[ele['account.name']]['orders_dic'][
                        ele['order.name']]['authorizations_dic'] = {}

                if ele['authorization.name'] not in tmp_json[
                        ele['account.name']]['orders_dic'][
                            ele['order.name']]['authorizations_dic']:
                    tmp_json[ele['account.name']]['orders_dic'][
                        ele['order.name']]['authorizations_dic'][
                            ele['authorization.name']] = {}
                    tmp_json[ele['account.name']]['orders_dic'][
                        ele['order.name']]['authorizations_dic'][
                            ele['authorization.name']]['challenges_dic'] = {}

                if ele['challenge.name'] not in tmp_json[ele['account.name']][
                        'orders_dic'][ele['order.name']]['authorizations_dic'][
                            ele['authorization.name']]['challenges_dic']:
                    tmp_json[ele['account.name']]['orders_dic'][
                        ele['order.name']]['authorizations_dic'][
                            ele['authorization.name']]['challenges_dic'][
                                ele['challenge.name']] = {}

                for value in ele:
                    if value.startswith('account.'):
                        tmp_json[ele['account.name']][value] = ele[value]
                    elif value.startswith('order.'):
                        tmp_json[ele['account.name']]['orders_dic'][
                            ele['order.name']][value] = ele[value]
                    elif value.startswith('authorization.'):
                        tmp_json[ele['account.name']]['orders_dic'][
                            ele['order.name']]['authorizations_dic'][
                                ele['authorization.name']][value] = ele[value]
                    elif value.startswith('challenge'):
                        tmp_json[ele['account.name']]['orders_dic'][
                            ele['order.name']]['authorizations_dic'][
                                ele['authorization.name']]['challenges_dic'][
                                    ele['challenge.name']][value] = ele[value]

            else:
                error_list.append(ele)

        # convert nested dictionaries (challenges, authorizations and orders) into list
        account_list = []
        for account in tmp_json:
            tmp_json[account]['orders'] = []
            for order in tmp_json[account]['orders_dic']:
                tmp_json[account]['orders_dic'][order]['authorizations'] = []
                for authorization in tmp_json[account]['orders_dic'][order][
                        'authorizations_dic']:
                    tmp_json[account]['orders_dic'][order][
                        'authorizations_dic'][authorization][
                            'challenges'] = []
                    # build list from challenges and delete dictionary
                    for _name, challenge in tmp_json[account]['orders_dic'][
                            order]['authorizations_dic'][authorization][
                                'challenges_dic'].items():
                        tmp_json[account]['orders_dic'][order][
                            'authorizations_dic'][authorization][
                                'challenges'].append(challenge)
                    del tmp_json[account]['orders_dic'][order][
                        'authorizations_dic'][authorization]['challenges_dic']
                    # build list from authorizations
                    tmp_json[account]['orders_dic'][order][
                        'authorizations'].append(
                            tmp_json[account]['orders_dic'][order]
                            ['authorizations_dic'][authorization])
                # delete authorization dictionary
                del tmp_json[account]['orders_dic'][order][
                    'authorizations_dic']
                # build list of orders
                tmp_json[account]['orders'].append(
                    tmp_json[account]['orders_dic'][order])
            del tmp_json[account]['orders_dic']

            # add entry to output list
            account_list.append(tmp_json[account])

        # add errors
        if error_list:
            account_list.append({'error_list': error_list})

        return account_list

    def _to_list(self, field_list, cert_list):
        """ convert query to csv format """
        self.logger.debug('Housekeeping._to_list()')
        csv_list = []

        # attach fieldlist as first row
        if field_list:
            csv_list.append(field_list)
        for cert in cert_list:
            tmp_list = []
            # enumarte fields and store them in temporary list
            for field in field_list:
                # in case we are missing a field put empty string in
                if field in cert:
                    try:
                        # we need to deal with some errors from past
                        value = cert[field].replace('\r\n', '\n')
                        value = value.replace('\r', '')
                        value = value.replace('\n', '')
                        tmp_list.append(value)
                    except BaseException:
                        tmp_list.append(cert[field])
                else:
                    tmp_list.append('')

            # append list to output
            csv_list.append(tmp_list)
        self.logger.debug(
            'Housekeeping._to_list() ended with {0} entries'.format(
                len(csv_list)))
        return csv_list

    def accountreport_get(self,
                          report_format='csv',
                          report_name=None,
                          nested=False):
        """ get account report """
        self.logger.debug('Housekeeping.accountreport_get()')
        (field_list, account_list) = self._accountlist_get()

        # normalize lists
        (field_list,
         account_list) = self._lists_normalize(field_list, account_list,
                                               'account')

        # convert dates into human readable format
        account_list = self._convert_data(account_list)

        if report_name:
            if account_list:
                self.logger.debug('output to dump: {0}.{1}'.format(
                    report_name, report_format))
                if report_format == 'csv':
                    self.logger.debug(
                        'Housekeeping.certreport_get() dump in csv-format')
                    csv_list = self._to_list(field_list, account_list)
                    self._csv_dump(
                        '{0}.{1}'.format(report_name, report_format), csv_list)
                elif report_format == 'json':
                    if nested:
                        account_list = self._to_acc_json(account_list)
                    self._json_dump(
                        '{0}.{1}'.format(report_name, report_format),
                        account_list)

        return account_list

    def certreport_get(self, report_format='csv', report_name=None):
        """ get certificate report """
        self.logger.debug('Housekeeping.certreport_get()')

        (field_list, cert_list) = self._certificatelist_get()

        # normalize lists
        (field_list,
         cert_list) = self._lists_normalize(field_list, cert_list,
                                            'certificate')

        # convert dates into human readable format
        cert_list = self._convert_data(cert_list)

        # extend list by additional fields to have the fileds in output
        field_list.insert(2, 'certificate.serial')
        field_list.insert(7, 'certificate.issue_date')
        field_list.insert(8, 'certificate.expire_date')

        if report_name:
            if cert_list:
                self.logger.debug('output to dump: {0}.{1}'.format(
                    report_name, report_format))
                if report_format == 'csv':
                    self.logger.debug(
                        'Housekeeping.certreport_get(): Dump in csv-format')
                    csv_list = self._to_list(field_list, cert_list)
                    self._csv_dump(
                        '{0}.{1}'.format(report_name, report_format), csv_list)
                elif report_format == 'json':
                    self.logger.debug(
                        'Housekeeping.certreport_get(): Dump in json-format')
                    self._json_dump(
                        '{0}.{1}'.format(report_name, report_format),
                        cert_list)
                else:
                    self.logger.info(
                        'Housekeeping.certreport_get(): No dump just return report'
                    )

        return cert_list

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

        with Certificate(self.debug, None, self.logger) as certificate:
            certificate.dates_update()

    def certificates_cleanup(self,
                             uts=None,
                             purge=False,
                             report_format='csv',
                             report_name=None):
        """ database cleanuip certificate-table """
        self.logger.debug('Housekeeping.certificates_cleanup()')
        if not uts:
            uts = uts_now()

        with Certificate(self.debug, None, self.logger) as certificate:
            (field_list, cert_list) = certificate.cleanup(timestamp=uts,
                                                          purge=purge)

            # normalize lists
            # (field_list, cert_list) = self._lists_normalize(field_list, cert_list, 'certificate')

            if report_name:
                if cert_list:
                    # dump report to file
                    if report_format == 'csv':
                        self.logger.debug(
                            'Housekeeping.certificates_cleanup(): Dump in csv-format'
                        )
                        csv_list = self._to_list(field_list, cert_list)
                        self._csv_dump(
                            '{0}.{1}'.format(report_name, report_format),
                            csv_list)
                    elif report_format == 'json':
                        self.logger.debug(
                            'Housekeeping.certificates_cleanup(): Dump in json-format'
                        )
                        self._json_dump(
                            '{0}.{1}'.format(report_name, report_format),
                            cert_list)
                    else:
                        self.logger.debug(
                            'Housekeeping.certificates_cleanup():  No dump just return report'
                        )
                else:
                    self.logger.debug(
                        'Housekeeping.certificates_cleanup(): No certificates to dump'
                    )

        return cert_list

    def authorizations_invalidate(self,
                                  uts=uts_now(),
                                  report_format='csv',
                                  report_name=None):
        """ authorizations cleanup based on expiry date"""
        self.logger.debug(
            'Housekeeping.authorization_invalidate({0})'.format(uts))

        with Authorization(self.debug, None, self.logger) as authorization:
            # get expired orders
            (field_list,
             authorization_list) = authorization.invalidate(timestamp=uts)
            # normalize lists
            (field_list, authorization_list) = self._lists_normalize(
                field_list, authorization_list, 'authorization')
            # convert dates into human readable format
            authorization_list = self._convert_data(authorization_list)

            if report_name:
                if authorization_list:
                    # dump report to file
                    if report_format == 'csv':
                        self.logger.debug(
                            'Housekeeping.authorizations_invalidate(): Dump in csv-format'
                        )
                        csv_list = self._to_list(field_list,
                                                 authorization_list)
                        self._csv_dump(
                            '{0}.{1}'.format(report_name, report_format),
                            csv_list)
                    elif report_format == 'json':
                        self.logger.debug(
                            'Housekeeping.authorizations_invalidate(): Dump in json-format'
                        )
                        self._json_dump(
                            '{0}.{1}'.format(report_name, report_format),
                            authorization_list)
                    else:
                        self.logger.debug(
                            'Housekeeping.authorizations_invalidate():  No dump just return report'
                        )
                else:
                    self.logger.debug(
                        'Housekeeping.authorizations_invalidate(): No authorizations to dump'
                    )

    def dbversion_check(self, version=None):
        """ check database version """
        self.logger.debug('Housekeeping.dbversion_check({0})'.format(version))

        if version:
            try:
                (result, script_name) = self.dbstore.dbversion_get()
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Housekeeping.dbversion_check(): {0}'
                    .format(err_))
                result = None
                script_name = 'handler specific migration'
            if result != version:
                self.logger.critical(
                    'acme2certifier database version mismatch in: version is {0} but should be {1}. Please run the "{2}" script'
                    .format(result, version, script_name))
            else:
                self.logger.debug(
                    'acme2certifier database version: {0} is upto date'.format(
                        version))
        else:
            self.logger.critical(
                'acme2certifier database version could not be verified in Housekeeping.dbversion_check()'
            )

    def orders_invalidate(self,
                          uts=uts_now(),
                          report_format='csv',
                          report_name=None):
        """ orders cleanup based on expiry date"""
        self.logger.debug('Housekeeping.orders_invalidate({0})'.format(uts))

        with Order(self.debug, None, self.logger) as order:
            # get expired orders
            (field_list, order_list) = order.invalidate(timestamp=uts)
            # normalize lists
            (field_list,
             order_list) = self._lists_normalize(field_list, order_list,
                                                 'order')
            # convert dates into human readable format
            order_list = self._convert_data(order_list)

            if report_name:
                if order_list:
                    # dump report to file
                    if report_format == 'csv':
                        self.logger.debug(
                            'Housekeeping.orders_invalidate(): Dump in csv-format'
                        )
                        csv_list = self._to_list(field_list, order_list)
                        self._csv_dump(
                            '{0}.{1}'.format(report_name, report_format),
                            csv_list)
                    elif report_format == 'json':
                        self.logger.debug(
                            'Housekeeping.orders_invalidate(): Dump in json-format'
                        )
                        self._json_dump(
                            '{0}.{1}'.format(report_name, report_format),
                            order_list)
                    else:
                        self.logger.debug(
                            'Housekeeping.orders_invalidate():  No dump just return report'
                        )
                else:
                    self.logger.debug(
                        'Housekeeping.orders_invalidate(): No orders to dump')

        return order_list
예제 #23
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
예제 #24
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
예제 #25
0
 def __init__(self, debug=None, logger=None):
     self.logger = logger
     self.dbstore = DBstore(debug, self.logger)
     self.debug = debug
예제 #26
0
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']:
                self.retry_after = 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']))

        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:
                order_dic['identifiers'] = json.loads(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
예제 #27
0
class Challenge(object):
    """ Challenge handler """
    def __init__(self, debug=None, srv_name=None, logger=None, expiry=3600):
        # self.debug = debug
        self.server_name = srv_name
        self.logger = logger
        self.dbstore = DBstore(debug, self.logger)
        self.message = Message(debug, self.server_name, self.logger)
        self.path_dic = {
            'chall_path': '/acme/chall/',
            'authz_path': '/acme/authz/'
        }
        self.expiry = expiry
        self.challenge_validation_disable = False
        self.tnauthlist_support = False
        self.dns_server_list = None

    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 _challengelist_search(self,
                              key,
                              value,
                              vlist=('name', 'type', 'status__name', 'token')):
        """ get exsting challegnes for a given authorization """
        self.logger.debug('Challenge._challengelist_search()')

        try:
            challenge_list = self.dbstore.challenges_search(key, value, vlist)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._challengelist_search(): {0}'
                .format(err_))
            challenge_list = []

        challenge_dic = {}
        for challenge in challenge_list:
            if challenge['type'] not in challenge_dic:
                challenge_dic[challenge['type']] = {}

            challenge_dic[challenge['type']]['token'] = challenge['token']
            challenge_dic[challenge['type']]['type'] = challenge['type']
            challenge_dic[challenge['type']]['url'] = challenge['name']
            challenge_dic[challenge['type']]['url'] = '{0}{1}{2}'.format(
                self.server_name, self.path_dic['chall_path'],
                challenge['name'])
            challenge_dic[challenge['type']]['name'] = challenge['name']

        challenge_list = []
        for challenge in challenge_dic:
            challenge_list.append(challenge_dic[challenge])

        self.logger.debug(
            'Challenge._challengelist_search() ended with: {0}'.format(
                challenge_list))
        return challenge_list

    def _check(self, challenge_name, payload):
        """ challenge check """
        self.logger.debug('Challenge._check({0})'.format(challenge_name))
        try:
            challenge_dic = self.dbstore.challenge_lookup(
                'name', challenge_name, [
                    'type', 'status__name', 'token', 'authorization__name',
                    'authorization__type', 'authorization__value',
                    'authorization__token',
                    'authorization__order__account__name'
                ])
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._check() lookup: {0}'
                .format(err_))
            challenge_dic = {}

        if 'type' in challenge_dic and 'authorization__value' in challenge_dic and 'token' in challenge_dic and 'authorization__order__account__name' in challenge_dic:
            try:
                pub_key = self.dbstore.jwk_load(
                    challenge_dic['authorization__order__account__name'])
            except BaseException as err_:
                self.logger.critical(
                    'acme2certifier database error in Challenge._check() jwk: {0}'
                    .format(err_))
                pub_key = None

            if pub_key:
                jwk_thumbprint = jwk_thumbprint_get(self.logger, pub_key)
                if challenge_dic['type'] == 'http-01' and jwk_thumbprint:
                    (result, invalid) = self._validate_http_challenge(
                        challenge_name, challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint)
                elif challenge_dic['type'] == 'dns-01' and jwk_thumbprint:
                    (result, invalid) = self._validate_dns_challenge(
                        challenge_name, challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint)
                elif challenge_dic['type'] == 'tls-alpn-01' and jwk_thumbprint:
                    (result, invalid) = self._validate_alpn_challenge(
                        challenge_name, challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint)
                elif challenge_dic[
                        'type'] == 'tkauth-01' and jwk_thumbprint and self.tnauthlist_support:
                    (result, invalid) = self._validate_tkauth_challenge(
                        challenge_name, challenge_dic['authorization__value'],
                        challenge_dic['token'], jwk_thumbprint, payload)
                else:
                    self.logger.debug(
                        'unknown challenge type "{0}". Setting check result to False'
                        .format(challenge_dic['type']))
                    result = False
                    invalid = True
            else:
                result = False
                invalid = False
        else:
            result = False
            invalid = False
        self.logger.debug('challenge._check() ended with: {0}/{1}'.format(
            result, invalid))
        return (result, invalid)

    def _existing_challenge_validate(self, challenge_list):
        """ validate an existing challenge set """
        self.logger.debug('Challenge._existing_challenge_validate()')
        for challenge in challenge_list:
            _challenge_check = self._validate(challenge, {})

    def _info(self, challenge_name):
        """ get challenge details """
        self.logger.debug('Challenge._info({0})'.format(challenge_name))
        try:
            challenge_dic = self.dbstore.challenge_lookup(
                'name',
                challenge_name,
                vlist=('type', 'token', 'status__name', 'validated'))
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._info(): {0}'.
                format(err_))
            challenge_dic = {}

        if 'status' in challenge_dic and challenge_dic['status'] == 'valid':
            if 'validated' in challenge_dic:
                # convert validated timestamp to RFC3339 format - if it fails remove key from dictionary
                try:
                    challenge_dic['validated'] = uts_to_date_utc(
                        challenge_dic['validated'])
                except BaseException:
                    challenge_dic.pop('validated')
        else:
            if 'validated' in challenge_dic:
                challenge_dic.pop('validated')

        self.logger.debug('Challenge._info({0}) ended'.format(challenge_name))
        return challenge_dic

    def _config_load(self):
        """" load config from file """
        self.logger.debug('Challenge._config_load()')
        config_dic = load_config()
        if 'Challenge' in config_dic:
            self.challenge_validation_disable = config_dic.getboolean(
                'Challenge', 'challenge_validation_disable', fallback=False)
            if 'dns_server_list' in config_dic['Challenge']:
                try:
                    self.dns_server_list = json.loads(
                        config_dic['Challenge']['dns_server_list'])
                except BaseException as err_:
                    self.logger.warning(
                        'Challenge._config_load() failed with error: {0}'.
                        format(err_))

        if 'Order' in config_dic:
            self.tnauthlist_support = config_dic.getboolean(
                'Order', 'tnauthlist_support', fallback=False)
        self.logger.debug('Challenge._config_load() ended.')

    def _name_get(self, url):
        """ get challenge """
        self.logger.debug('Challenge.get_name({0})'.format(url))
        url_dic = parse_url(self.logger, url)
        challenge_name = url_dic['path'].replace(self.path_dic['chall_path'],
                                                 '')
        if '/' in challenge_name:
            (challenge_name, _sinin) = challenge_name.split('/', 1)
        return challenge_name

    def _new(self, authz_name, mtype, token):
        """ new challenge """
        self.logger.debug('Challenge._new({0})'.format(mtype))

        challenge_name = generate_random_string(self.logger, 12)

        data_dic = {
            'name': challenge_name,
            'expires': self.expiry,
            'type': mtype,
            'token': token,
            'authorization': authz_name,
            'status': 2
        }

        try:
            chid = self.dbstore.challenge_add(data_dic)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._new(): {0}'.
                format(err_))
            chid = None

        challenge_dic = {}
        if chid:
            challenge_dic['type'] = mtype
            challenge_dic['url'] = '{0}{1}{2}'.format(
                self.server_name, self.path_dic['chall_path'], challenge_name)
            challenge_dic['token'] = token
            if mtype == 'tkauth-01':
                challenge_dic['tkauth-type'] = 'atc'
        return challenge_dic

    def _update(self, data_dic):
        """ update challenge """
        self.logger.debug('Challenge._update({0})'.format(data_dic))
        try:
            self.dbstore.challenge_update(data_dic)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._update(): {0}'.
                format(err_))
        self.logger.debug('Challenge._update() ended')

    def _update_authz(self, challenge_name, data_dic):
        """ update authorizsation based on challenge_name """
        self.logger.debug(
            'Challenge._update_authz({0})'.format(challenge_name))
        try:
            # lookup autorization based on challenge_name
            authz_name = self.dbstore.challenge_lookup(
                'name', challenge_name,
                ['authorization__name'])['authorization']
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._update_authz() lookup: {0}'
                .format(err_))
            authz_name = None

        if authz_name:
            data_dic['name'] = authz_name
        try:
            # update authorization
            self.dbstore.authorization_update(data_dic)
        except BaseException as err_:
            self.logger.critical(
                'acme2certifier database error in Challenge._update_authz() upd: {0}'
                .format(err_))

        self.logger.debug('Challenge._update_authz() ended')

    def _validate(self, challenge_name, payload):
        """ validate challenge"""
        self.logger.debug('Challenge._validate({0}: {1})'.format(
            challenge_name, payload))
        if self.challenge_validation_disable:
            self.logger.debug(
                'CHALLENGE VALIDATION DISABLED. SETTING challenge status to valid'
            )
            challenge_check = True
            invalid = False
        else:
            (challenge_check, invalid) = self._check(challenge_name, payload)

        if invalid:
            self._update({'name': challenge_name, 'status': 'invalid'})
            # authorization update to valid state
            self._update_authz(challenge_name, {'status': 'invalid'})
        elif challenge_check:
            self._update({
                'name': challenge_name,
                'status': 'valid',
                'validated': uts_now()
            })
            # authorization update to valid state
            self._update_authz(challenge_name, {'status': 'valid'})

        if payload:
            if 'keyAuthorization' in payload:
                # update challenge to ready state
                data_dic = {
                    'name': challenge_name,
                    'keyauthorization': payload['keyAuthorization']
                }
                self._update(data_dic)

        self.logger.debug(
            'Challenge._validate() ended with:{0}'.format(challenge_check))
        return challenge_check

    def _validate_alpn_challenge(self, challenge_name, fqdn, token,
                                 jwk_thumbprint):
        """ validate dns challenge """
        self.logger.debug(
            'Challenge._validate_alpn_challenge({0}:{1}:{2})'.format(
                challenge_name, fqdn, token))

        # resolve name
        (response, invalid) = fqdn_resolve(fqdn, self.dns_server_list)
        self.logger.debug('fqdn_resolve() ended with: {0}/{1}'.format(
            response, invalid))

        # we are expecting a certifiate extension which is the sha256 hexdigest of token in a byte structure
        # which is base 64 encoded '0420' has been taken from acme.sh sources
        sha256_digest = sha256_hash_hex(
            self.logger, '{0}.{1}'.format(token, jwk_thumbprint))
        extension_value = b64_encode(
            self.logger, bytearray.fromhex('0420{0}'.format(sha256_digest)))
        self.logger.debug('computed value: {0}'.format(extension_value))

        if not invalid:
            cert = servercert_get(self.logger, fqdn)
            if cert:
                san_list = cert_san_get(self.logger, cert, recode=False)
                fqdn_in_san = fqdn_in_san_check(self.logger, san_list, fqdn)
                if fqdn_in_san:
                    extension_list = cert_extensions_get(self.logger,
                                                         cert,
                                                         recode=False)
                    if extension_value in extension_list:
                        self.logger.debug('alpn validation successful')
                        result = True
                    else:
                        self.logger.debug('alpn validation not successful')
                        result = False
                else:
                    self.logger.debug('fqdn check against san failed')
                    result = False
            else:
                self.logger.debug('no cert returned...')
                result = False
        else:
            result = False

        self.logger.debug(
            'Challenge._validate_alpn_challenge() ended with: {0}/{1}'.format(
                result, invalid))
        return (result, invalid)

    def _validate_dns_challenge(self, challenge_name, fqdn, token,
                                jwk_thumbprint):
        """ validate dns challenge """
        self.logger.debug(
            'Challenge._validate_dns_challenge({0}:{1}:{2})'.format(
                challenge_name, fqdn, token))

        # handle wildcard domain
        fqdn = self._wcd_manipulate(fqdn)

        # rewrite fqdn
        fqdn = '_acme-challenge.{0}'.format(fqdn)

        # resolve name
        (_response, invalid) = fqdn_resolve(fqdn)

        if not invalid:
            # compute sha256 hash
            _hash = b64_url_encode(
                self.logger,
                sha256_hash(self.logger,
                            '{0}.{1}'.format(token, jwk_thumbprint)))
            # query dns
            txt = txt_get(self.logger, fqdn, self.dns_server_list)

            # compare computed hash with result from DNS query
            self.logger.debug(
                'response_got: {0} response_expected: {1}'.format(txt, _hash))
            if _hash == txt:
                self.logger.debug('validation successful')
                result = True
            else:
                self.logger.debug('validation not successful')
                result = False
        else:
            result = False

        self.logger.debug(
            'Challenge._validate_dns_challenge() ended with: {0}/{1}'.format(
                result, invalid))
        return (result, invalid)

    def _validate_http_challenge(self, challenge_name, fqdn, token,
                                 jwk_thumbprint):
        """ validate http challenge """
        self.logger.debug(
            'Challenge._validate_http_challenge({0}:{1}:{2})'.format(
                challenge_name, fqdn, token))
        # resolve name
        (response, invalid) = fqdn_resolve(fqdn, self.dns_server_list)
        self.logger.debug('fqdn_resolve() ended with: {0}/{1}'.format(
            response, invalid))
        if not invalid:
            req = url_get(
                self.logger,
                'http://{0}/.well-known/acme-challenge/{1}'.format(
                    fqdn, token), self.dns_server_list)
            # make challenge validation unsuccessful
            # req = url_get(self.logger, 'http://{0}/.well-known/acme-challenge/{1}'.format('test.test', 'foo.bar.some.not.existing.ressource'))
            if req:
                response_got = req.splitlines()[0]
                response_expected = '{0}.{1}'.format(token, jwk_thumbprint)
                self.logger.debug(
                    'response_got: {0} response_expected: {1}'.format(
                        response_got, response_expected))
                if response_got == response_expected:
                    self.logger.debug('validation successful')
                    result = True
                else:
                    self.logger.debug('validation not successful')
                    result = False
            else:
                self.logger.debug(
                    'validation not successfull.. no request object')
                result = False
        else:
            result = False

        self.logger.debug(
            'Challenge._validate_http_challenge() ended with: {0}/{1}'.format(
                result, invalid))
        return (result, invalid)

    def _validate_tkauth_challenge(self, challenge_name, tnauthlist, _token,
                                   _jwk_thumbprint, payload):
        """ validate tkauth challenge """
        self.logger.debug(
            'Challenge._validate_tkauth_challenge({0}:{1}:{2})'.format(
                challenge_name, tnauthlist, payload))

        result = True
        invalid = False
        self.logger.debug(
            'Challenge._validate_tkauth_challenge() ended with: {0}/{1}'.
            format(result, invalid))
        return (result, invalid)

    def _validate_tnauthlist_payload(self, payload, challenge_dic):
        """ check payload in cae tnauthlist option has been set """
        self.logger.debug(
            'Challenge._validate_tnauthlist_payload({0}:{1})'.format(
                payload, challenge_dic))

        code = 400
        message = None
        detail = None

        if 'type' in challenge_dic:
            if challenge_dic['type'] == 'tkauth-01':
                self.logger.debug('tkauth identifier found')
                # check if we havegot an atc claim in the challenge request
                if 'atc' in payload:
                    # check if we got a SPC token in the challenge request
                    if not bool(payload['atc']):
                        code = 400
                        message = 'urn:ietf:params:acme:error:malformed'
                        detail = 'SPC token is missing'
                    else:
                        code = 200
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'atc claim is missing'
            else:
                code = 200
        else:
            message = 'urn:ietf:params:acme:error:malformed'
            detail = 'invalid challenge: {0}'.format(challenge_dic)

        self.logger.debug(
            'Challenge._validate_tnauthlist_payload() ended with:{0}'.format(
                code))
        return (code, message, detail)

    def _wcd_manipulate(self, fqdn):
        """ wildcard domain handling """
        self.logger.debug(
            'Challenge._wc_manipulate() for fqdn: {0}'.format(fqdn))
        if fqdn.startswith('*.'):
            fqdn = fqdn[2:]
        self.logger.debug(
            'Challenge._wc_manipulate() ended with: {0}'.format(fqdn))
        return fqdn

    def challengeset_get(self, authz_name, auth_status, token, tnauth):
        """ get the challengeset for an authorization """
        self.logger.debug(
            'Challenge.challengeset_get() for auth: {0}'.format(authz_name))
        # check database if there are exsting challenges for a particular authorization
        challenge_list = self._challengelist_search('authorization__name',
                                                    authz_name)

        if challenge_list:
            self.logger.debug('Challenges found.')
            # trigger challenge validation
            challenge_name_list = []
            for challenge in challenge_list:
                challenge_name_list.append(challenge.pop('name'))
            if auth_status == 'pending':
                self._existing_challenge_validate(challenge_name_list)

        else:
            # new challenges to be created
            self.logger.debug('Challenges not found. Create a new set.')
            challenge_list = self.new_set(authz_name, token, tnauth)

        return challenge_list

    def get(self, url):
        """ get challenge details based on get request """
        self.logger.debug('Challenge.get({0})'.format(url))
        challenge_name = self._name_get(url)
        response_dic = {}
        response_dic['code'] = 200
        response_dic['data'] = self._info(challenge_name)
        return response_dic

    def new_set(self, authz_name, token, tnauth=False):
        """ net challenge set """
        self.logger.debug('Challenge.new_set({0}, {1})'.format(
            authz_name, token))
        challenge_list = []
        if not tnauth:
            challenge_list.append(self._new(authz_name, 'http-01', token))
            challenge_list.append(self._new(authz_name, 'dns-01', token))
            challenge_list.append(self._new(authz_name, 'tls-alpn-01', token))
        else:
            challenge_list.append(self._new(authz_name, 'tkauth-01', token))
        self.logger.debug(
            'Challenge._new_set returned ({0})'.format(challenge_list))
        return challenge_list

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

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

        if code == 200:
            if 'url' in protected:
                challenge_name = self._name_get(protected['url'])
                if challenge_name:
                    challenge_dic = self._info(challenge_name)

                    if challenge_dic:
                        # check tnauthlist payload
                        if self.tnauthlist_support:
                            (code, message,
                             detail) = self._validate_tnauthlist_payload(
                                 payload, challenge_dic)

                        if code == 200:
                            # start validation
                            if 'status' in challenge_dic:
                                if challenge_dic['status'] != 'valid':
                                    _validation = self._validate(
                                        challenge_name, payload)
                                    # query challenge again (bcs. it could get updated by self._validate)
                                    challenge_dic = self._info(challenge_name)
                            else:
                                # rather unlikely that we run in this situation but you never know
                                _validation = self._validate(
                                    challenge_name, payload)
                                # query challenge again (bcs. it could get updated by self._validate)
                                challenge_dic = self._info(challenge_name)

                            response_dic['data'] = {}
                            challenge_dic['url'] = protected['url']
                            code = 200
                            response_dic['data'] = {}
                            response_dic['data'] = challenge_dic
                            response_dic['header'] = {}
                            response_dic['header'][
                                'Link'] = '<{0}{1}>;rel="up"'.format(
                                    self.server_name,
                                    self.path_dic['authz_path'])
                    else:
                        code = 400
                        message = 'urn:ietf:params:acme:error:malformed'
                        detail = 'invalid challenge: {0}'.format(
                            challenge_name)
                else:
                    code = 400
                    message = 'urn:ietf:params:acme:error:malformed'
                    detail = 'could not get challenge'
            else:
                code = 400
                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('challenge.parse() returns: {0}'.format(
            json.dumps(response_dic)))
        return response_dic
예제 #28
0
class Authorization(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(debug, self.logger)
        self.message = Message(debug, self.server_name, self.logger)
        self.nonce = Nonce(debug, self.logger)
        self.validity = 86400
        self.expiry_check_disable = False
        self.path_dic = {'authz_path' : '/acme/authz/'}

    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 _authz_info(self, url):
        """ return authzs information """
        self.logger.debug('Authorization._authz_info({0})'.format(url))
        authz_name = url.replace('{0}{1}'.format(self.server_name, self.path_dic['authz_path']), '')
        expires = uts_now() + self.validity
        token = generate_random_string(self.logger, 32)
        authz_info_dic = {}

        # lookup authorization based on name
        try:
            authz = self.dbstore.authorization_lookup('name', authz_name)
        except BaseException as err_:
            self.logger.critical('acme2certifier database error in Authorization._authz_info(): {0}'.format(err_))
            authz = None

        if authz:
            # update authorization with expiry date and token (just to be sure)
            try:
                self.dbstore.authorization_update({'name' : authz_name, 'token' : token, 'expires' : expires})
            except BaseException as err_:
                self.logger.critical('acme2certifier database error in Authorization._authz_info(): {0}'.format(err_))
            authz_info_dic['expires'] = uts_to_date_utc(expires)

            # get authorization information from db to be inserted in message
            tnauth = None
            try:
                auth_info = self.dbstore.authorization_lookup('name', authz_name, ['status__name', 'type', 'value'])
            except BaseException as err_:
                self.logger.critical('acme2certifier database error in Authorization._authz_info(): {0}'.format(err_))
                auth_info = {}
            if auth_info:
                if 'status__name' in auth_info[0]:
                    authz_info_dic['status'] = auth_info[0]['status__name']
                else:
                    authz_info_dic['status'] = 'pending'

                if 'type' in auth_info[0]  and 'value' in auth_info[0]:
                    authz_info_dic['identifier'] = {'type' : auth_info[0]['type'], 'value' : auth_info[0]['value']}
                    if auth_info[0]['type'] == 'TNAuthList':
                        tnauth = True

            with Challenge(self.debug, self.server_name, self.logger, expires) as challenge:
                # get challenge data (either existing or new ones)
                authz_info_dic['challenges'] = challenge.challengeset_get(authz_name, authz_info_dic['status'], token, tnauth)

        self.logger.debug('Authorization._authz_info() returns: {0}'.format(json.dumps(authz_info_dic)))
        return authz_info_dic

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

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

        field_list = ['id', 'name', 'expires', 'value', 'created_at', 'token', 'status__id', 'status__name', 'order__id', 'order__name']
        try:
            authz_list = self.dbstore.authorizations_expired_search('expires', timestamp, vlist=field_list, operant='<=')
        except BaseException as err_:
            self.logger.critical('acme2certifier database error in Authorization.invalidate(): {0}'.format(err_))
            authz_list = []

        output_list = []
        for authz in authz_list:
            # select all authz which are not invalid
            if 'name' in authz and 'status__name' in authz and authz['status__name'] != 'expired':
                # skip corner cases where authz expiry is set to 0
                if 'expires' not in authz or authz['expires'] > 0:
                    # change status and add to output list
                    output_list.append(authz)
                    data_dic = {'name': authz['name'], 'status': 'expired'}
                    try:
                        self.dbstore.authorization_update(data_dic)
                    except BaseException as err_:
                        self.logger.critical('acme2certifier database error in Authorization.invalidate(): {0}'.format(err_))

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

    def new_get(self, url):
        """ challenge computation based on get request """
        self.logger.debug('Authorization.new_get()')
        response_dic = {}
        response_dic['code'] = 200
        response_dic['header'] = {}
        response_dic['data'] = self._authz_info(url)
        return response_dic

    def new_post(self, content):
        """ challenge computation based on post request """
        self.logger.debug('Authorization.new_post()')

        # invalidate expired authorizations
        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:
                auth_info = self._authz_info(protected['url'])
                if auth_info:
                    response_dic['data'] = auth_info
                else:
                    code = 403
                    message = 'urn:ietf:params:acme:error:unauthorized'
                    detail = 'authorizations lookup failed'
            else:
                code = 400
                message = 'urn:ietf:params:acme:error:malformed'
                detail = 'url is missing in protected'

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

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