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