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