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 __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 __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 __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 __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 cfg = load_config() if 'Directory' in cfg: if 'url_prefix' in cfg['Directory']: self.revocation_path = cfg['Directory'][ 'url_prefix'] + '/acme/revokecert' else: self.revocation_path = '/acme/revokecert'
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 self.eab_check = False self.eab_handler = None
def __init__(self, _debug=None, logger=None): self.logger = logger self.url = None self.keyfile = None self.key_size = 2048 self.account = None self.email = None self.path_dic = { 'directory_path': '/directory', 'acct_path': '/acme/acct/' } self.dbstore = DBstore(None, self.logger) self.allowed_domainlist = [] self.eab_kid = None self.eab_hmac_key = None
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()
class Acmechallenge(object): """ Acmechallenge handler """ 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) def __enter__(self): """ Makes ACMEHandler a Context Manager """ return self def __exit__(self, *args): """ cose the connection at the end of the context """ def lookup(self, path_info): """ check nonce """ self.logger.debug('Acmechallenge.lookup()') key_authorization = None if path_info: token = path_info.replace('/.well-known/acme-challenge/', '') self.logger.info('Acmechallenge.lookup() token: {0}'.format(token)) challenge_dic = self.dbstore.cahandler_lookup('name', token) if challenge_dic and 'value1' in challenge_dic: key_authorization = challenge_dic['value1'] self.logger.debug( 'Acmechallenge.lookup() ended with: {0}'.format(key_authorization)) return key_authorization
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 __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.hooks = None self.ignore_pre_hook_failure = False self.ignore_post_hook_failure = True self.ignore_success_hook_failure = False 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 self.cert_reusage_timeframe = 0 self.enrollment_timeout = 5
class CAhandler(object): """ EST CA handler """ def __init__(self, _debug=None, logger=None): self.logger = logger self.url = None self.url_dic = {} self.keyfile = None self.key_size = 2048 self.account = None self.email = None self.path_dic = { 'directory_path': '/directory', 'acct_path': '/acme/acct/' } self.dbstore = DBstore(None, self.logger) self.allowed_domainlist = [] self.eab_kid = None self.eab_hmac_key = None def __enter__(self): """ Makes CAhandler a Context Manager """ if not self.url: self._config_load() 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('CAhandler._config_load()') config_dic = load_config() if 'CAhandler' in config_dic: if 'acme_keyfile' in config_dic['CAhandler']: self.keyfile = config_dic['CAhandler']['acme_keyfile'] else: self.logger.error( 'CAhandler._config_load() configuration incomplete: "acme_keyfile" parameter is missing in config file' ) if 'acme_url' in config_dic['CAhandler']: self.url = config_dic['CAhandler']['acme_url'] self.url_dic = parse_url(self.logger, self.url) else: self.logger.error( 'CAhandler._config_load() configuration incomplete: "acme_url" parameter is missing in config file' ) if 'acme_account' in config_dic['CAhandler']: self.account = config_dic['CAhandler']['acme_account'] if 'account_path' in config_dic['CAhandler']: self.path_dic['acct_path'] = config_dic['CAhandler'][ 'account_path'] if 'directory_path' in config_dic['CAhandler']: self.path_dic['directory_path'] = config_dic['CAhandler'][ 'directory_path'] if 'acme_account_keysize' in config_dic['CAhandler']: self.key_size = config_dic['CAhandler']['acme_account_keysize'] if 'acme_account_email' in config_dic['CAhandler']: self.email = config_dic['CAhandler']['acme_account_email'] if 'allowed_domainlist' in config_dic['CAhandler']: try: self.allowed_domainlist = json.loads( config_dic['CAhandler']['allowed_domainlist']) except Exception as err: self.logger.error( 'CAhandler._config_load(): failed to parse allowed_domainlist: {0}' .format(err)) if 'eab_kid' in config_dic['CAhandler']: self.eab_kid = config_dic['CAhandler']['eab_kid'] if 'eab_hmac_key' in config_dic['CAhandler']: self.eab_hmac_key = config_dic['CAhandler']['eab_hmac_key'] self.logger.debug('CAhandler._config_load() ended') else: self.logger.error( 'CAhandler._config_load() configuration incomplete: "CAhandler" section is missing in config file' ) def _challenge_filter(self, authzr, chall_type='http-01'): """ filter authorization for challenge """ self.logger.debug( 'CAhandler._challenge_filter({0})'.format(chall_type)) result = None for challenge in authzr.body.challenges: if challenge.chall.typ == chall_type: result = challenge break if not result: self.logger.error( 'CAhandler._challenge_filter() ended. Could not find challenge of type {0}' .format(chall_type)) return result def _challenge_store(self, challenge_name, challenge_content): """ store challenge into database """ self.logger.debug( 'CAhandler._challenge_store({0})'.format(challenge_name)) if challenge_name and challenge_content: data_dic = {'name': challenge_name, 'value1': challenge_content} # store challenge into db self.dbstore.cahandler_add(data_dic) def _csr_check(self, csr): """ check CSR against definied whitelists """ self.logger.debug('CAhandler._csr_check()') if self.allowed_domainlist: result = False # get sans and build a list _san_list = csr_san_get(self.logger, csr) check_list = [] san_list = [] if _san_list: for san in _san_list: try: # SAN list must be modified/filtered) (_san_type, san_value) = san.lower().split(':') san_list.append(san_value) except Exception: # force check to fail as something went wrong during parsing check_list.append(False) self.logger.debug( 'CAhandler._csr_check(): san_list parsing failed at entry: {0}' .format(san)) # get common name and attach it to san_list cn_ = csr_cn_get(self.logger, csr) if cn_: cn_ = cn_.lower() if cn_ not in san_list: # append cn to san_list self.logger.debug( 'Ahandler._csr_check(): append cn to san_list') san_list.append(cn_) # go over the san list and check each entry for san in san_list: check_list.append( self._list_check(san, self.allowed_domainlist)) if check_list: # cover a cornercase with empty checklist (no san, no cn) if False in check_list: result = False else: result = True else: result = True self.logger.debug( 'CAhandler._csr_check() ended with: {0}'.format(result)) return result def _list_check(self, entry, list_, toggle=False): """ check string against list """ self.logger.debug('CAhandler._list_check({0}:{1})'.format( entry, toggle)) self.logger.debug('check against list: {0}'.format(list_)) # default setting check_result = False if entry: if list_: for regex in list_: if regex.startswith('*.'): regex = regex.replace('*.', '.') regex_compiled = re.compile(regex) if bool(regex_compiled.search(entry)): # parameter is in set flag accordingly and stop loop check_result = True else: # empty list, flip parameter to make the check successful check_result = True if toggle: # toggle result if this is a blacklist check_result = not check_result self.logger.debug( 'CAhandler._list_check() ended with: {0}'.format(check_result)) return check_result def _http_challenge_info(self, authzr, user_key): """ filter challenges and get challenge details """ self.logger.debug('CAhandler._http_challenge_info()') chall_name = None chall_content = None if authzr and user_key: challenge = self._challenge_filter(authzr) chall_content = challenge.chall.validation(user_key) try: (chall_name, _token) = chall_content.split('.', 2) except Exception: self.logger.error( 'CAhandler._http_challenge_info() challenge split failed: {0}' .format(chall_content)) else: if authzr: self.logger.error( 'CAhandler._http_challenge_info() userkey is missing') else: self.logger.error( 'CAhandler._http_challenge_info() authzr is missing') challenge = None self.logger.debug( 'CAhandler._http_challenge_info() ended with {0}'.format( chall_name)) return (chall_name, chall_content, challenge) def _key_generate(self): """ generate key """ self.logger.debug('CAhandler._key_generate({0})'.format(self.key_size)) user_key = josepy.JWKRSA( key=rsa.generate_private_key(public_exponent=65537, key_size=self.key_size, backend=default_backend())) return user_key def _user_key_load(self): """ enroll certificate """ self.logger.debug('CAhandler._user_key_load()') if os.path.exists(self.keyfile): self.logger.debug('CAhandler.enroll() opening user_key') with open(self.keyfile, "r") as keyf: user_key = josepy.JWKRSA.json_loads(keyf.read()) else: self.logger.debug('CAhandler.enroll() generate and register key') user_key = self._key_generate() # dump keyfile to file with open(self.keyfile, "w") as keyf: keyf.write(json.dumps(user_key.to_json())) self.logger.debug('CAhandler._user_key_load() ended') return user_key def _account_lookup(self, acmeclient, reg, directory): """ lookup account """ self.logger.debug('CAhandler._account_lookup()') response = acmeclient._post(directory['newAccount'], reg) regr = acmeclient._regr_from_response(response) regr = acmeclient.query_registration(regr) if regr: self.logger.info( 'CAhandler._account_lookup: found existing account: {0}'. format(regr.uri)) self.account = regr.uri if self.url: # remove url from string self.account = self.account.replace(self.url, '') if 'acct_path' in self.path_dic and self.path_dic['acct_path']: # remove acc_path self.account = self.account.replace(self.path_dic['acct_path'], '') def _account_register(self, acmeclient, user_key, directory): """ register account / check registration """ self.logger.debug('CAhandler._account_register({0})'.format( self.email)) try: # we assume that the account exist and need to query the account id reg = messages.NewRegistration.from_data( key=user_key, email=self.email, terms_of_service_agreed=True, only_return_existing=True) response = acmeclient._post(directory['newAccount'], reg) regr = acmeclient._regr_from_response(response) regr = acmeclient.query_registration(regr) self.logger.debug( 'CAhandler.__account_register(): found existing account: {0}'. format(regr.uri)) except Exception: if self.email: self.logger.debug( 'CAhandler.__account_register(): register new account with email: {0}' .format(self.email)) if self.url and 'host' in self.url_dic and self.url_dic[ 'host'].endswith( 'zerossl.com' ): # lgtm [py/incomplete-url-substring-sanitization] # get zerossl eab credentials self._zerossl_eab_get() if self.eab_kid and self.eab_hmac_key: # we have to do some freaky eab to keep ZeroSSL happy eab = messages.ExternalAccountBinding.from_data( account_public_key=user_key, kid=self.eab_kid, hmac_key=self.eab_hmac_key, directory=directory) reg = messages.NewRegistration.from_data( key=user_key, email=self.email, terms_of_service_agreed=True, external_account_binding=eab) else: # register with email reg = messages.NewRegistration.from_data( key=user_key, email=self.email, terms_of_service_agreed=True) regr = acmeclient.new_account(reg) self.logger.debug( 'CAhandler.__account_register(): new account reqistered: {0}' .format(regr.uri)) else: self.logger.error( 'CAhandler.__account_register(): registration aborted. Email address is missing' ) regr = None if regr: if self.url and 'acct_path' in self.path_dic: self.account = regr.uri.replace(self.url, '').replace( self.path_dic['acct_path'], '') if self.account: self.logger.info( 'acme-account id is {0}. Please add an corresponding acme_account parameter to your acme_srv.cfg to avoid unnecessary lookups' .format(self.account)) return regr def _zerossl_eab_get(self): """ get eab credentials from zerossl """ self.logger.debug('CAhandler._zerossl_eab_get()') zero_eab_email = "http://api.zerossl.com/acme/eab-credentials-email" data = {'email': self.email} response = requests.post(zero_eab_email, data=data) if 'success' in response.json() and response.json( )['success'] and 'eab_kid' in response.json( ) and 'eab_hmac_key' in response.json(): self.eab_kid = response.json()['eab_kid'] self.eab_hmac_key = response.json()['eab_hmac_key'] self.logger.debug( 'CAhandler._zerossl_eab_get() ended successfully') else: self.logger.error( 'CAhandler._zerossl_eab_get() failed: {0}'.format( response.text)) def enroll(self, csr): """ enroll certificate """ # pylint: disable=R0915 self.logger.debug('CAhandler.enroll()') csr_pem = '-----BEGIN CERTIFICATE REQUEST-----\n{0}\n-----END CERTIFICATE REQUEST-----\n'.format( textwrap.fill(str(b64_url_recode(self.logger, csr)), 64)) cert_bundle = None error = None cert_raw = None poll_indentifier = None user_key = None # check CN and SAN against black/whitlist result = self._csr_check(csr) if result: try: user_key = self._user_key_load() net = client.ClientNetwork(user_key) directory = messages.Directory.from_json( net.get('{0}{1}'.format( self.url, self.path_dic['directory_path'])).json()) acmeclient = client.ClientV2(directory, net=net) reg = messages.Registration.from_data( key=user_key, terms_of_service_agreed=True) if self.account: regr = messages.RegistrationResource( uri="{0}{1}{2}".format(self.url, self.path_dic['acct_path'], self.account), body=reg) self.logger.debug( 'CAhandler.enroll(): checking remote registration status' ) regr = acmeclient.query_registration(regr) else: # new account or existing account with missing account id regr = self._account_register(acmeclient, user_key, directory) if regr.body.status == "valid": self.logger.debug( 'CAhandler.enroll() issuing signing order') self.logger.debug('CAhandler.enroll() CSR: ' + str(csr_pem)) order = acmeclient.new_order(csr_pem) # query challenges for authzr in list(order.authorizations): (challenge_name, challenge_content, challenge) = self._http_challenge_info( authzr, user_key) if challenge_name and challenge_content: # store challenge in database to allow challenge validation self._challenge_store(challenge_name, challenge_content) _auth_response = acmeclient.answer_challenge( challenge, challenge.chall.response(user_key) ) # lgtm [py/unused-local-variable] self.logger.debug( 'CAhandler.enroll() polling for certificate') order = acmeclient.poll_and_finalize(order) if order.fullchain_pem: self.logger.debug('CAhandler.enroll() successful') cert_bundle = str(order.fullchain_pem) cert_raw = str( base64.b64encode( crypto.dump_certificate( crypto.FILETYPE_ASN1, crypto.load_certificate( crypto.FILETYPE_PEM, cert_bundle))), 'utf-8') else: # raise Exception("Error getting certificate: " + str(order.error)) self.logger.error( 'CAhandler.enroll: Error getting certificate: {0}'. format(order.error)) error = 'Error getting certificate: {0}'.format( order.error) else: self.logger.error( 'CAhandler.enroll: Bad ACME account: {0}'.format( regr.body.error)) error = 'Bad ACME account: {0}'.format(regr.body.error) # raise Exception("Bad ACME account: " + str(regr.body.error)) except Exception as err: self.logger.error('CAhandler.enroll: error: {0}'.format(err)) error = str(err) finally: del user_key else: error = 'CSR rejected. Either CN or SANs are not allowed by policy' self.logger.error( 'CAhandler.enroll: CSR rejected. Either CN or SANs are not allowed by policy.' ) self.logger.debug('Certificate.enroll() ended') return (error, cert_bundle, cert_raw, poll_indentifier) def poll(self, _cert_name, poll_identifier, _csr): """ poll status of pending CSR and download certificates """ self.logger.debug('CAhandler.poll()') error = "Not implemented" cert_bundle = None cert_raw = None rejected = False self.logger.debug('CAhandler.poll() ended') return (error, cert_bundle, cert_raw, poll_identifier, rejected) def revoke(self, _cert, _rev_reason, _rev_date): """ revoke certificate """ self.logger.debug('CAhandler.revoke()') user_key = None code = 500 message = 'urn:ietf:params:acme:error:serverInternal' detail = None try: certpem = '-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n'.format( textwrap.fill(str(b64_url_recode(self.logger, _cert)), 64)) cert = josepy.ComparableX509( crypto.load_certificate(crypto.FILETYPE_PEM, certpem)) if os.path.exists(self.keyfile): user_key = self._user_key_load() net = client.ClientNetwork(user_key) if user_key: directory = messages.Directory.from_json( net.get('{0}{1}'.format( self.url, self.path_dic['directory_path'])).json()) acmeclient = client.ClientV2(directory, net=net) reg = messages.NewRegistration.from_data( key=user_key, email=self.email, terms_of_service_agreed=True, only_return_existing=True) if not self.account: self._account_lookup(acmeclient, reg, directory) if self.account: regr = messages.RegistrationResource( uri="{0}{1}{2}".format(self.url, self.path_dic['acct_path'], self.account), body=reg) self.logger.debug( 'CAhandler.revoke() checking remote registration status' ) regr = acmeclient.query_registration(regr) if regr.body.status == "valid": self.logger.debug( 'CAhandler.revoke() issuing revocation order') acmeclient.revoke(cert, 1) self.logger.debug('CAhandler.revoke() successfull') code = 200 message = None else: self.logger.error( 'CAhandler.enroll: Bad ACME account: {0}'.format( regr.body.error)) detail = 'Bad ACME account: {0}'.format( regr.body.error) else: self.logger.error( 'CAhandler.revoke(): could not find account key and lookup at acme-endpoint failed.' ) detail = 'account lookup failed' else: self.logger.error( 'CAhandler.revoke(): could not load user_key {0}'.format( self.keyfile)) detail = 'Internal Error' except Exception as err: self.logger.error('CAhandler.enroll: error: {0}'.format(err)) detail = str(err) finally: del user_key self.logger.debug('Certificate.revoke() ended') return (code, message, detail) def trigger(self, _payload): """ process trigger message and return certificate """ self.logger.debug('CAhandler.trigger()') error = "Not implemented" cert_bundle = None cert_raw = None self.logger.debug( 'CAhandler.trigger() ended with error: {0}'.format(error)) return (error, cert_bundle, cert_raw)
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 cfg = load_config() if 'Directory' in cfg: if 'url_prefix' in cfg['Directory']: self.revocation_path = cfg['Directory'][ 'url_prefix'] + '/acme/revokecert' else: 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) def eab_check(self, content, mac_key): """ signature check """ self.logger.debug('Signature.eab_check()') result = False error = None if content and mac_key: (result, error) = signature_check(self.logger, content, mac_key, json_=True) return (result, error)
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 as err_: self.logger.critical( 'Certificate._config_load(): loading CAhandler configured in cfg failed with err: {0}' .format(err_)) try: ca_handler_module = importlib.import_module( 'acme_srv.ca_handler') except BaseException as err_: ca_handler_module = None self.logger.critical( 'Certificate._config_load(): loading default CAhandler failed with err: {0}' .format(err_)) else: if 'CAhandler' in config_dic: ca_handler_module = importlib.import_module( 'acme_srv.ca_handler') else: self.logger.error( '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 Order(object): """ class for order handling """ def __init__(self, debug=None, srv_name=None, logger=None): self.server_name = srv_name self.debug = debug self.logger = logger self.dbstore = DBstore(self.debug, self.logger) self.message = Message(self.debug, self.server_name, self.logger) self.validity = 86400 self.authz_validity = 86400 self.expiry_check_disable = False self.path_dic = { 'authz_path': '/acme/authz/', 'order_path': '/acme/order/', 'cert_path': '/acme/cert/' } self.retry_after = 600 self.tnauthlist_support = False def __enter__(self): """ Makes ACMEHandler a Context Manager """ self._config_load() return self def __exit__(self, *args): """ cose the connection at the end of the context """ def _add(self, payload, aname): """ add order request to database """ self.logger.debug('Order._add({0})'.format(aname)) error = None auth_dic = {} order_name = generate_random_string(self.logger, 12) expires = uts_now() + self.validity if 'identifiers' in payload: data_dic = {'status': 2, 'expires': expires, 'account': aname} data_dic['name'] = order_name data_dic['identifiers'] = json.dumps(payload['identifiers']) #if 'notBefore' in payload: # data_dic['notbefore'] = payload['notBefore'] #if 'notAfter' in payload: # data_dic['notafter'] = payload['notAfter'] # check identifiers error = self._identifiers_check(payload['identifiers']) # change order status if needed if error: data_dic['status'] = 1 try: # add order to db oid = self.dbstore.order_add(data_dic) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._add() order: {0}'. format(err_)) oid = None if not error: if oid: error = None for auth in payload['identifiers']: # generate name auth_name = generate_random_string(self.logger, 12) # store to return to upper func auth_dic[auth_name] = auth.copy() auth['name'] = auth_name auth['order'] = oid auth['status'] = 'pending' auth['expires'] = uts_now() + self.authz_validity try: self.dbstore.authorization_add(auth) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._add() authz: {0}' .format(err_)) else: error = 'urn:ietf:params:acme:error:malformed' else: error = 'urn:ietf:params:acme:error:unsupportedIdentifier' self.logger.debug('Order._add() ended') return (error, order_name, auth_dic, uts_to_date_utc(expires)) def _config_load(self): """" load config from file """ self.logger.debug('Order._config_load()') config_dic = load_config() if 'Order' in config_dic: self.tnauthlist_support = config_dic.getboolean( 'Order', 'tnauthlist_support', fallback=False) self.expiry_check_disable = config_dic.getboolean( 'Order', 'expiry_check_disable', fallback=False) if 'retry_after_timeout' in config_dic['Order']: try: self.retry_after = int( config_dic['Order']['retry_after_timeout']) except BaseException: self.logger.warning( 'Order._config_load(): failed to parse retry_after: {0}' .format(config_dic['Order']['retry_after_timeout'])) if 'validity' in config_dic['Order']: try: self.validity = int(config_dic['Order']['validity']) except BaseException: self.logger.warning( 'Order._config_load(): failed to parse validity: {0}'. format(config_dic['Order']['validity'])) if 'Authorization' in config_dic: if 'validity' in config_dic['Authorization']: try: self.authz_validity = int( config_dic['Authorization']['validity']) except BaseException: self.logger.warning( 'Order._config_load(): failed to parse authz validity: {0}' .format(config_dic['Authorization']['validity'])) if 'Directory' in config_dic: if 'url_prefix' in config_dic['Directory']: self.path_dic = { k: config_dic['Directory']['url_prefix'] + v for k, v in self.path_dic.items() } self.logger.debug('Order._config_load() ended.') def _name_get(self, url): """ get ordername """ self.logger.debug('Order._name_get({0})'.format(url)) url_dic = parse_url(self.logger, url) order_name = url_dic['path'].replace(self.path_dic['order_path'], '') if '/' in order_name: (order_name, _sinin) = order_name.split('/', 1) self.logger.debug('Order._name_get() ended') return order_name def _identifiers_check(self, identifiers_list): """ check validity of identifers in order """ self.logger.debug( 'Order._identifiers_check({0})'.format(identifiers_list)) error = None allowed_identifers = ['dns'] # add tnauthlist to list of supported identfiers if configured to do so if self.tnauthlist_support: allowed_identifers.append('tnauthlist') if identifiers_list and isinstance(identifiers_list, list): for identifier in identifiers_list: if 'type' in identifier: if identifier['type'].lower() not in allowed_identifers: error = 'urn:ietf:params:acme:error:unsupportedIdentifier' break else: error = 'urn:ietf:params:acme:error:malformed' else: error = 'urn:ietf:params:acme:error:malformed' self.logger.debug( 'Order._identifiers_check() done with {0}:'.format(error)) return error def _info(self, order_name): """ list details of an order """ self.logger.debug('Order._info({0})'.format(order_name)) try: result = self.dbstore.order_lookup('name', order_name) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._info(): {0}'.format( err_)) result = None return result def _process(self, order_name, protected, payload): """ process order """ self.logger.debug('Order._process({0})'.format(order_name)) certificate_name = None message = None detail = None if 'url' in protected: if 'finalize' in protected['url']: self.logger.debug('finalize request()') # lookup order-status (must be ready to proceed) order_dic = self._info(order_name) if 'status' in order_dic and order_dic['status'] == 'ready': # update order_status / set to processing self._update({'name': order_name, 'status': 'processing'}) if 'csr' in payload: self.logger.debug('CSR found()') # this is a new request (code, certificate_name, detail) = self._csr_process(order_name, payload['csr']) # change status only if we do not have a poll_identifier (stored in detail variable) if code == 200: if not detail: # update order_status / set to valid self._update({ 'name': order_name, 'status': 'valid' }) else: message = certificate_name detail = 'enrollment failed' else: code = 400 message = 'urn:ietf:params:acme:error:badCSR' detail = 'csr is missing in payload' else: code = 403 message = 'urn:ietf:params:acme:error:orderNotReady' detail = 'Order is not ready' else: self.logger.debug('polling request()') code = 200 # this is a polling request; lookup certificate try: cert_dic = self.dbstore.certificate_lookup( 'order__name', order_name) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._process(): {0}' .format(err_)) cert_dic = {} if cert_dic: # we found a cert in the database # pylint: disable=R1715 if 'name' in cert_dic: certificate_name = cert_dic['name'] else: code = 400 message = 'urn:ietf:params:acme:error:malformed' detail = 'url is missing in protected' self.logger.debug( 'Order._process() ended with order:{0} {1}:{2}:{3}'.format( order_name, code, message, detail)) return (code, message, detail, certificate_name) def _csr_process(self, order_name, csr): """ process certificate signing request """ self.logger.debug('Order._csr_process({0})'.format(order_name)) order_dic = self._info(order_name) if order_dic: # change decoding from b64url to b64 csr = b64_url_recode(self.logger, csr) with Certificate(self.debug, self.server_name, self.logger) as certificate: # certificate = Certificate(self.debug, self.server_name, self.logger) certificate_name = certificate.store_csr(order_name, csr) if certificate_name: (error, detail) = certificate.enroll_and_store( certificate_name, csr) if not error: code = 200 message = certificate_name # detail = None else: code = 400 message = error if message == 'urn:ietf:params:acme:error:serverInternal': code = 500 else: code = 500 message = 'urn:ietf:params:acme:error:serverInternal' detail = 'CSR processing failed' else: code = 400 message = 'urn:ietf:params:acme:error:unauthorized' detail = 'order: {0} not found'.format(order_name) self.logger.debug( 'Order._csr_process() ended with order:{0} {1}:{2}:{3}'.format( order_name, code, message, detail)) return (code, message, detail) def _update(self, data_dic): """ update order based on ordername """ self.logger.debug('Order._update({0})'.format(data_dic)) try: self.dbstore.order_update(data_dic) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._update(): {0}'.format( err_)) def _lookup(self, order_name): """ sohw order details based on ordername """ self.logger.debug('Order._lookup({0})'.format(order_name)) order_dic = {} tmp_dic = self._info(order_name) if tmp_dic: if 'status' in tmp_dic: order_dic['status'] = tmp_dic['status'] if 'expires' in tmp_dic: order_dic['expires'] = uts_to_date_utc(tmp_dic['expires']) if 'notbefore' in tmp_dic: if tmp_dic['notbefore'] != 0: order_dic['notBefore'] = uts_to_date_utc( tmp_dic['notbefore']) if 'notafter' in tmp_dic: if tmp_dic['notafter'] != 0: order_dic['notAfter'] = uts_to_date_utc( tmp_dic['notafter']) if 'identifiers' in tmp_dic: try: order_dic['identifiers'] = json.loads( tmp_dic['identifiers']) except BaseException: self.logger.error( 'Order.lookup(): error while parsing the identifier {0}' .format(tmp_dic['identifiers'])) try: authz_list = self.dbstore.authorization_lookup( 'order__name', order_name, ['name', 'status__name']) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._lookup(): {0}'. format(err_)) authz_list = [] if authz_list: order_dic["authorizations"] = [] # collect status of different authorizations in list validity_list = [] for authz in authz_list: if 'name' in authz: order_dic["authorizations"].append('{0}{1}{2}'.format( self.server_name, self.path_dic['authz_path'], authz['name'])) if 'status__name' in authz: if authz['status__name'] == 'valid': validity_list.append(True) else: validity_list.append(False) # update orders status from pending to ready if validity_list and 'status' in order_dic: if False not in validity_list and order_dic[ 'status'] == 'pending': self._update({'name': order_name, 'status': 'ready'}) self.logger.debug('Order._lookup() ended') return order_dic def invalidate(self, timestamp=None): """ invalidate orders """ self.logger.debug('Order.invalidate({0})'.format(timestamp)) if not timestamp: timestamp = uts_now() self.logger.debug( 'Order.invalidate(): set timestamp to {0}'.format(timestamp)) field_list = [ 'id', 'name', 'expires', 'identifiers', 'created_at', 'status__id', 'status__name', 'account__id', 'account__name', 'account__contact' ] try: order_list = self.dbstore.orders_invalid_search('expires', timestamp, vlist=field_list, operant='<=') except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._invalidate() search: {0}' .format(err_)) order_list = [] output_list = [] for order in order_list: # print(order['id']) # select all orders which are not invalid if 'name' in order and 'status__name' in order and order[ 'status__name'] != 'invalid': # change status and add to output list output_list.append(order) data_dic = {'name': order['name'], 'status': 'invalid'} try: self.dbstore.order_update(data_dic) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Order._invalidate() upd: {0}' .format(err_)) self.logger.debug( 'Order.invalidate() ended: {0} orders identified'.format( len(output_list))) return (field_list, output_list) def new(self, content): """ new oder request """ self.logger.debug('Order.new()') response_dic = {} # check message (code, message, detail, _protected, payload, account_name) = self.message.check(content) if code == 200: (error, order_name, auth_dic, expires) = self._add(payload, account_name) if not error: code = 201 response_dic['header'] = {} response_dic['header']['Location'] = '{0}{1}{2}'.format( self.server_name, self.path_dic['order_path'], order_name) response_dic['data'] = {} response_dic['data']['identifiers'] = [] response_dic['data']['authorizations'] = [] response_dic['data']['status'] = 'pending' response_dic['data']['expires'] = expires response_dic['data']['finalize'] = '{0}{1}{2}/finalize'.format( self.server_name, self.path_dic['order_path'], order_name) for auth_name in auth_dic: response_dic['data']['authorizations'].append( '{0}{1}{2}'.format(self.server_name, self.path_dic['authz_path'], auth_name)) response_dic['data']['identifiers'].append( auth_dic[auth_name]) else: code = 400 message = error detail = 'could not process order' # prepare/enrich response status_dic = {'code': code, 'message': message, 'detail': detail} response_dic = self.message.prepare_response(response_dic, status_dic) self.logger.debug('Order.new() returns: {0}'.format( json.dumps(response_dic))) return response_dic def parse(self, content): """ new oder request """ self.logger.debug('Order.parse()') # invalidate expired orders if not self.expiry_check_disable: self.invalidate() response_dic = {} # check message (code, message, detail, protected, payload, _account_name) = self.message.check(content) if code == 200: if 'url' in protected: order_name = self._name_get(protected['url']) if order_name: order_dic = self._lookup(order_name) if order_dic: (code, message, detail, certificate_name) = self._process( order_name, protected, payload) else: code = 403 message = 'urn:ietf:params:acme:error:orderNotReady' detail = 'order not found' else: code = 400 message = 'urn:ietf:params:acme:error:malformed' detail = 'order name is missing' else: code = 400 message = 'urn:ietf:params:acme:error:malformed' detail = 'url is missing in protected' if code == 200: # create response response_dic['header'] = {} response_dic['header']['Location'] = '{0}{1}{2}'.format( self.server_name, self.path_dic['order_path'], order_name) response_dic['data'] = self._lookup(order_name) if 'status' in response_dic['data'] and response_dic['data'][ 'status'] == 'processing': # set retry header as cert issuane is not completed. response_dic['header']['Retry-After'] = '{0}'.format( self.retry_after) response_dic['data']['finalize'] = '{0}{1}{2}/finalize'.format( self.server_name, self.path_dic['order_path'], order_name) # add the path to certificate if order-status is ready # if certificate_name: if certificate_name and 'status' in response_dic[ 'data'] and response_dic['data']['status'] == 'valid': response_dic['data']['certificate'] = '{0}{1}{2}'.format( self.server_name, self.path_dic['cert_path'], certificate_name) # prepare/enrich response status_dic = {'code': code, 'message': message, 'detail': detail} response_dic = self.message.prepare_response(response_dic, status_dic) self.logger.debug('Order.parse() returns: {0}'.format( json.dumps(response_dic))) return response_dic
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) if 'Directory' in config_dic: if 'url_prefix' in config_dic['Directory']: self.path_dic = { k: config_dic['Directory']['url_prefix'] + v for k, v in self.path_dic.items() } 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) 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 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.info( '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
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.error( '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.error( '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 else: authz_info_dic['status'] = 'pending' 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'])) if 'Directory' in config_dic: if 'url_prefix' in config_dic['Directory']: self.path_dic = { k: config_dic['Directory']['url_prefix'] + v for k, v in self.path_dic.items() } self.logger.debug('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
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 self.proxy_server_list = {} 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() dns_server_list failed with error: {0}' .format(err_)) if 'Order' in config_dic: self.tnauthlist_support = config_dic.getboolean( 'Order', 'tnauthlist_support', fallback=False) if 'Directory' in config_dic: if 'url_prefix' in config_dic['Directory']: self.path_dic = { k: config_dic['Directory']['url_prefix'] + v for k, v in self.path_dic.items() } if 'DEFAULT' in config_dic and 'proxy_server_list' in config_dic[ 'DEFAULT']: try: self.proxy_server_list = json.loads( config_dic['DEFAULT']['proxy_server_list']) except BaseException as err_: self.logger.warning( 'Challenge._config_load() proxy_server_list failed with error: {0}' .format(err_)) 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 base64 encoded '0420' has been taken from acme_srv.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: # check if we need to set a proxy if self.proxy_server_list: proxy_server = proxy_check(self.logger, fqdn, self.proxy_server_list) else: proxy_server = None cert = servercert_get(self.logger, fqdn, 443, proxy_server) 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 to resolve txt record 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_list = 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_list, _hash)) if _hash in txt_list: self.logger.debug('validation successful') result = True else: self.logger.debug('validation not successful') result = False self.logger.debug( 'Challenge._validate_dns_challenge() ended with: {0}'.format( result)) return (result, False) 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: # check if we need to set a proxy if self.proxy_server_list: proxy_server = proxy_check(self.logger, fqdn, self.proxy_server_list) else: proxy_server = None req = url_get(self.logger, 'http://{0}/.well-known/acme-challenge/{1}'.format( fqdn, token), dns_server_list=self.dns_server_list, proxy_server=proxy_server, verify=False) 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: for challenge_type in ['http-01', 'dns-01', 'tls-alpn-01']: challenge_json = self._new(authz_name, challenge_type, token) if challenge_json: challenge_list.append(challenge_json) else: self.logger.error( 'ERROR: Empty challenge returned for {0}'.format( challenge_type)) 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
class Certificate(object): """ CA handler """ def __init__(self, debug=None, srv_name=None, logger=None): self.debug = debug self.server_name = srv_name self.logger = logger self.cahandler = None self.dbstore = DBstore(self.debug, self.logger) self.message = Message(self.debug, self.server_name, self.logger) self.path_dic = {'cert_path': '/acme/cert/'} self.retry_after = 600 self.tnauthlist_support = False def __enter__(self): """ Makes ACMEHandler a Context Manager """ self._config_load() return self def __exit__(self, *args): """ cose the connection at the end of the context """ def _account_check(self, account_name, certificate): """ check account """ self.logger.debug('Certificate.issuer_check()') try: result = self.dbstore.certificate_account_check( account_name, b64_url_recode(self.logger, certificate)) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate._account_check(): {0}' .format(err_)) result = None return result def _authorization_check(self, order_name, certificate): """ check if an acount holds authorization for all identifiers = SANs in the certificate """ self.logger.debug('Certificate._authorization_check()') # empty list of statuses identifier_status = [] # get identifiers for order try: identifier_dic = self.dbstore.order_lookup('name', order_name, ['identifiers']) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate._authorization_check(): {0}' .format(err_)) identifier_dic = {} if identifier_dic and 'identifiers' in identifier_dic: # load identifiers try: identifiers = json.loads(identifier_dic['identifiers'].lower()) except BaseException: identifiers = [] # check if we have a tnauthlist identifier tnauthlist_identifer_in = self._tnauth_identifier_check( identifiers) if self.tnauthlist_support and tnauthlist_identifer_in: try: # get list of certextensions in base64 format and identifier status tnauthlist = cert_extensions_get(self.logger, certificate) identifier_status = self._identifer_tnauth_list( identifier_dic, tnauthlist) except BaseException as err_: # enough to set identifier_list as empty list identifier_status = [] self.logger.warning( 'Certificate._authorization_check() error while loading parsing certifcate. Error: {0}' .format(err_)) else: try: # get sans san_list = cert_san_get(self.logger, certificate) identifier_status = self._identifer_status_list( identifiers, san_list) except BaseException as err_: # enough to set identifier_list as empty list identifier_status = [] self.logger.warning( 'Certificate._authorization_check() error while loading parsing certifcate. Error: {0}' .format(err_)) result = False if identifier_status and False not in identifier_status: result = True self.logger.debug( 'Certificate._authorization_check() ended with {0}'.format(result)) return result def _config_load(self): """" load config from file """ self.logger.debug('Certificate._config_load()') config_dic = load_config() if 'Order' in config_dic: self.tnauthlist_support = config_dic.getboolean( 'Order', 'tnauthlist_support', fallback=False) if 'CAhandler' in config_dic and 'handler_file' in config_dic[ 'CAhandler']: try: ca_handler_module = importlib.import_module( ca_handler_get(self.logger, config_dic['CAhandler']['handler_file'])) except BaseException as err_: self.logger.critical( 'Certificate._config_load(): loading CAhandler configured in cfg failed with err: {0}' .format(err_)) try: ca_handler_module = importlib.import_module( 'acme_srv.ca_handler') except BaseException as err_: ca_handler_module = None self.logger.critical( 'Certificate._config_load(): loading default EABHandler failed with err: {0}' .format(err_)) else: if 'CAhandler' in config_dic: ca_handler_module = importlib.import_module( 'acme_srv.ca_handler') else: self.logger.error( 'Certificate._config_load(): CAhandler configuration missing in config file' ) ca_handler_module = None if ca_handler_module: # store handler in variable self.cahandler = ca_handler_module.CAhandler if 'Directory' in config_dic: if 'url_prefix' in config_dic['Directory']: self.path_dic = { k: config_dic['Directory']['url_prefix'] + v for k, v in self.path_dic.items() } self.logger.debug('ca_handler: {0}'.format(ca_handler_module)) self.logger.debug('Certificate._config_load() ended.') def _csr_check(self, certificate_name, csr): """ compare csr extensions against order """ self.logger.debug('Certificate._csr_check()') # fetch certificate dictionary from DB certificate_dic = self._info(certificate_name) self.logger.debug( 'Certificate._info() ended with:{0}'.format(certificate_dic)) # empty list of statuses identifier_status = [] if 'order' in certificate_dic: # get identifiers for order try: identifier_dic = self.dbstore.order_lookup( 'name', certificate_dic['order'], ['identifiers']) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate._csr_check(): {0}' .format(err_)) identifier_dic = {} if identifier_dic and 'identifiers' in identifier_dic: # load identifiers try: identifiers = json.loads( identifier_dic['identifiers'].lower()) except BaseException: identifiers = [] # do we need to check for tnauth tnauthlist_identifer_in = self._tnauth_identifier_check( identifiers) if self.tnauthlist_support and tnauthlist_identifer_in: # get list of certextensions in base64 format try: tnauthlist = csr_extensions_get(self.logger, csr) identifier_status = self._identifer_tnauth_list( identifier_dic, tnauthlist) except BaseException as err_: identifier_status = [] self.logger.warning( 'Certificate._csr_check() error while parsing csr.\nerror: {0}' .format(err_)) else: # get sans and compare identifiers against san try: san_list = csr_san_get(self.logger, csr) identifier_status = self._identifer_status_list( identifiers, san_list) except BaseException as err_: identifier_status = [] self.logger.warning( 'Certificate._csr_check() error while checking csr.\nerror: {0}' .format(err_)) csr_check_result = False if identifier_status and False not in identifier_status: csr_check_result = True self.logger.debug( 'Certificate._csr_check() ended with {0}'.format(csr_check_result)) return csr_check_result def _identifer_status_list(self, identifiers, san_list): """ compare identifiers and check if each san is in identifer list """ self.logger.debug('Certificate._identifer_status_list()') identifier_status = [] for san in san_list: san_is_in = False try: (cert_type, cert_value) = san.lower().split(':') except BaseException: cert_type = None cert_value = None if cert_type and cert_value: for identifier in identifiers: if 'type' in identifier: if (identifier['type'].lower() == cert_type and identifier['value'].lower() == cert_value): san_is_in = True break self.logger.debug( 'SAN check for {0} against identifiers returned {1}'.format( san.lower(), san_is_in)) identifier_status.append(san_is_in) if not identifier_status: identifier_status.append(False) self.logger.debug( 'Certificate._identifer_status_list() ended with {0}'.format( identifier_status)) return identifier_status def _identifer_tnauth_list(self, identifier_dic, tnauthlist): """ compare identifiers and check if each san is in identifer list """ self.logger.debug('Certificate._identifer_tnauth_list()') identifier_status = [] # reload identifiers (case senetive) try: identifiers = json.loads(identifier_dic['identifiers']) except BaseException: identifiers = [] if tnauthlist and not identifier_dic: identifier_status.append(False) elif identifiers and tnauthlist: for identifier in identifiers: # get the tnauthlist identifier if 'type' in identifier and identifier['type'].lower( ) == 'tnauthlist': # check if tnauthlist extension is in extension list if 'value' in identifier and identifier[ 'value'] in tnauthlist: identifier_status.append(True) else: identifier_status.append(False) else: identifier_status.append(False) else: identifier_status.append(False) self.logger.debug( 'Certificate._identifer_status_list() ended with {0}'.format( identifier_status)) return identifier_status def _info(self, certificate_name, flist=('name', 'csr', 'cert', 'order__name')): """ get certificate from database """ self.logger.debug('Certificate._info({0})'.format(certificate_name)) try: result = self.dbstore.certificate_lookup('name', certificate_name, flist) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate._info(): {0}'. format(err_)) result = None return result def _invalidation_check(self, cert, timestamp, purge=False): """ check if cert must be invalidated """ if 'name' in cert: self.logger.debug('Certificate._invalidation_check({0})'.format( cert['name'])) else: self.logger.debug('Certificate._invalidation_check()') to_be_cleared = False if cert and 'name' in cert: if 'cert' in cert and cert['cert'] and 'removed by' in cert[ 'cert'].lower(): if not purge: # skip entries which had been cleared before cert[cert] check is needed to cover corner cases to_be_cleared = False else: # purge entries to_be_cleared = True elif 'expire_uts' in cert: # in case cert_expiry in table is 0 try to get it from cert if cert['expire_uts'] == 0: if 'cert_raw' in cert and cert['cert_raw']: # get expiration from certificate (issue_uts, expire_uts) = cert_dates_get(self.logger, cert['cert_raw']) if 0 < expire_uts < timestamp: # returned date is other than 0 and lower than given timestamp cert['issue_uts'] = issue_uts cert['expire_uts'] = expire_uts to_be_cleared = True else: if 'csr' in cert and cert['csr']: # cover cases for enrollments in flight # we assume that a CSR should turn int a cert within two weeks if 'created_at' in cert: created_at_uts = date_to_uts_utc( cert['created_at']) if 0 < created_at_uts < timestamp - (14 * 86400): to_be_cleared = True else: # this scneario should never been happen so lets be careful and not clear it to_be_cleared = False else: # no csr and no cert - to be cleared to_be_cleared = True else: # expired based on expire_uts from db to_be_cleared = True else: # this scneario should never been happen so lets be careful and not clear it to_be_cleared = False else: # entries without a cert-name can be to_be_cleared to_be_cleared = True if 'name' in cert: self.logger.debug( 'Certificate._invalidation_check({0}) ended with {1}'.format( cert['name'], to_be_cleared)) else: self.logger.debug( 'Certificate._invalidation_check() ended with {0}'.format( to_be_cleared)) return (to_be_cleared, cert) def _revocation_reason_check(self, reason): """ check reason """ self.logger.debug( 'Certificate._revocation_reason_check({0})'.format(reason)) # taken from https://tools.ietf.org/html/rfc5280#section-5.3.1 allowed_reasons = { 0: 'unspecified', 1: 'keyCompromise', # 2 : 'cACompromise', 3: 'affiliationChanged', 4: 'superseded', 5: 'cessationOfOperation', 6: 'certificateHold', # 8 : 'removeFromCRL', # 9 : 'privilegeWithdrawn', # 10 : 'aACompromise' } result = allowed_reasons.get(reason, None) self.logger.debug( 'Certificate._revocation_reason_check() ended with {0}'.format( result)) return result def _revocation_request_validate(self, account_name, payload): """ check revocaton request for consistency""" self.logger.debug( 'Certificate._revocation_request_validate({0})'.format( account_name)) # set a value to avoid that we are returning none by accident code = 400 error = None if 'reason' in payload: # check revocatoin reason if we get one rev_reason = self._revocation_reason_check(payload['reason']) # successful if not rev_reason: error = 'urn:ietf:params:acme:error:badRevocationReason' else: # set revocation reason to unspecified rev_reason = 'unspecified' if rev_reason: # check if the account issued the certificate and return the order name if 'certificate' in payload: order_name = self._account_check(account_name, payload['certificate']) else: order_name = None error = rev_reason if order_name: # check if the account holds the authorization for the identifiers auth_chk = self._authorization_check(order_name, payload['certificate']) if auth_chk: # all good set code to 200 code = 200 else: error = 'urn:ietf:params:acme:error:unauthorized' self.logger.debug( 'Certificate._revocation_request_validate() ended with: {0}, {1}'. format(code, error)) return (code, error) def _store_cert(self, certificate_name, certificate, raw, issue_uts=0, expire_uts=0): """ get key for a specific account id """ self.logger.debug( 'Certificate._store_cert({0})'.format(certificate_name)) data_dic = { 'cert': certificate, 'name': certificate_name, 'cert_raw': raw, 'issue_uts': issue_uts, 'expire_uts': expire_uts } try: cert_id = self.dbstore.certificate_add(data_dic) except BaseException as err_: cert_id = None self.logger.critical( 'acme2certifier database error in Certificate._store_cert(): {0}' .format(err_)) self.logger.debug('Certificate._store_cert({0}) ended'.format(cert_id)) return cert_id def _store_cert_error(self, certificate_name, error, poll_identifier): """ get key for a specific account id """ self.logger.debug( 'Certificate._store_cert_error({0})'.format(certificate_name)) data_dic = { 'error': error, 'name': certificate_name, 'poll_identifier': poll_identifier } try: cert_id = self.dbstore.certificate_add(data_dic) except BaseException as err_: cert_id = None self.logger.critical( 'acme2certifier database error in Certificate._store_cert(): {0}' .format(err_)) self.logger.debug( 'Certificate._store_cert_error({0}) ended'.format(cert_id)) return cert_id def _tnauth_identifier_check(self, identifier_dic): """ check if we have an tnauthlist_identifier """ self.logger.debug('Certificate._tnauth_identifier_check()') # check if we have a tnauthlist identifier tnauthlist_identifer_in = False if identifier_dic: for identifier in identifier_dic: if 'type' in identifier: if identifier['type'].lower() == 'tnauthlist': tnauthlist_identifer_in = True self.logger.debug( 'Certificate._tnauth_identifier_check() ended with: {0}'.format( tnauthlist_identifer_in)) return tnauthlist_identifer_in def certlist_search(self, key, value, vlist=('name', 'csr', 'cert', 'order__name')): """ get certificate from database """ self.logger.debug('Certificate.certlist_search({0}: {1})'.format( key, value)) try: result = self.dbstore.certificates_search(key, value, vlist) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate.certlist_search(): {0}' .format(err_)) result = None return result def cleanup(self, timestamp=None, purge=False): """ cleanup routine to shrink table-size """ self.logger.debug('Certificate.cleanup({0},{1})'.format( timestamp, purge)) field_list = [ 'id', 'name', 'expire_uts', 'issue_uts', 'cert', 'cert_raw', 'csr', 'created_at', 'order__id', 'order__name' ] # get expired certificates try: certificate_list = self.dbstore.certificates_search( 'expire_uts', timestamp, field_list, '<=') except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate.cleanup() search: {0}' .format(err_)) certificate_list = [] report_list = [] for cert in certificate_list: (to_be_cleared, cert) = self._invalidation_check(cert, timestamp, purge) if to_be_cleared: report_list.append(cert) if not purge: # we are just modifiying data for cert in report_list: data_dic = { 'name': cert['name'], 'expire_uts': cert['expire_uts'], 'issue_uts': cert['issue_uts'], 'cert': 'removed by certificates.cleanup() on {0} '.format( uts_to_date_utc(timestamp)), 'cert_raw': cert['cert_raw'] } try: self.dbstore.certificate_add(data_dic) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate.cleanup() add: {0}' .format(err_)) else: # delete entries from certificates table for cert in report_list: try: self.dbstore.certificate_delete('id', cert['id']) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate.cleanup() delete: {0}' .format(err_)) self.logger.debug('Certificate.cleanup() ended with: {0} certs'.format( len(report_list))) return (field_list, report_list) def dates_update(self): """ scan certificates and update issue/expiry date """ self.logger.debug('Certificate.certificate_dates_update()') with Certificate(self.debug, None, self.logger) as certificate: cert_list = certificate.certlist_search( 'issue_uts', 0, vlist=('id', 'name', 'cert', 'cert_raw', 'issue_uts', 'expire_uts')) self.logger.debug('Got {0} certificates to be updated...'.format( len(cert_list))) for cert in cert_list: if 'issue_uts' in cert and 'expire_uts' in cert: if cert['issue_uts'] == 0 and cert['expire_uts'] == 0: if cert['cert_raw']: (issue_uts, expire_uts) = cert_dates_get( self.logger, cert['cert_raw']) if issue_uts or expire_uts: self._store_cert(cert['name'], cert['cert'], cert['cert_raw'], issue_uts, expire_uts) # return None def enroll_and_store(self, certificate_name, csr): """ cenroll and store certificater """ self.logger.debug('Certificate.enroll_and_store({0},{1})'.format( certificate_name, csr)) # check csr against order csr_check_result = self._csr_check(certificate_name, csr) error = None detail = None # only continue if self.csr_check returned True if csr_check_result: with self.cahandler(self.debug, self.logger) as ca_handler: (error, certificate, certificate_raw, poll_identifier) = ca_handler.enroll(csr) if certificate: (issue_uts, expire_uts) = cert_dates_get(self.logger, certificate_raw) try: result = self._store_cert(certificate_name, certificate, certificate_raw, issue_uts, expire_uts) except BaseException as err_: result = None self.logger.critical( 'acme2certifier database error in Certificate.enroll_and_store(): {0}' .format(err_)) else: result = None self.logger.error( 'acme2certifier enrollment error: {0}'.format(error)) # store error message for later analysis try: self._store_cert_error(certificate_name, error, poll_identifier) except BaseException as err_: result = None self.logger.critical( 'acme2certifier database error in Certificate.enroll_and_store(): {0}' .format(err_)) # cover polling cases if poll_identifier: detail = poll_identifier else: error = 'urn:ietf:params:acme:error:serverInternal' else: result = None error = 'urn:ietf:params:acme:badCSR' detail = 'CSR validation failed' self.logger.debug( 'Certificate.enroll_and_store() ended with: {0}:{1}'.format( result, error)) return (error, detail) def new_get(self, url): """ get request """ self.logger.debug('Certificate.new_get({0})'.format(url)) certificate_name = url.replace( '{0}{1}'.format(self.server_name, self.path_dic['cert_path']), '') # fetch certificate dictionary from DB certificate_dic = self._info( certificate_name, ['name', 'csr', 'cert', 'order__name', 'order__status_id']) response_dic = {} if 'order__status_id' in certificate_dic: if certificate_dic['order__status_id'] == 5: # oder status is valid - download certificate if 'cert' in certificate_dic and certificate_dic['cert']: response_dic['code'] = 200 # filter certificate and decode it response_dic['data'] = certificate_dic['cert'] response_dic['header'] = {} response_dic['header'][ 'Content-Type'] = 'application/pem-certificate-chain' else: response_dic['code'] = 500 response_dic[ 'data'] = 'urn:ietf:params:acme:error:serverInternal' elif certificate_dic['order__status_id'] == 4: # order status is processing - ratelimiting response_dic['header'] = { 'Retry-After': '{0}'.format(self.retry_after) } response_dic['code'] = 403 response_dic['data'] = 'urn:ietf:params:acme:error:rateLimited' else: response_dic['code'] = 403 response_dic[ 'data'] = 'urn:ietf:params:acme:error:orderNotReady' else: response_dic['code'] = 500 response_dic['data'] = 'urn:ietf:params:acme:error:serverInternal' self.logger.debug('Certificate.new_get({0}) ended'.format( response_dic['code'])) return response_dic def new_post(self, content): """ post request """ self.logger.debug('Certificate.new_post({0})') response_dic = {} # check message (code, message, detail, protected, _payload, _account_name) = self.message.check(content) if code == 200: if 'url' in protected: response_dic = self.new_get(protected['url']) if response_dic['code'] in (400, 403, 400, 500): code = response_dic['code'] message = response_dic['data'] detail = None else: response_dic['code'] = code = 400 response_dic[ 'data'] = message = 'urn:ietf:params:acme:error:malformed' detail = 'url missing in protected header' # prepare/enrich response status_dic = {'code': code, 'message': message, 'detail': detail} response_dic = self.message.prepare_response(response_dic, status_dic) # depending on the response the content of responsedic['data'] can be either string or dict # data will get serialzed if isinstance(response_dic['data'], dict): response_dic['data'] = json.dumps(response_dic['data']) # cover cornercase - not sure if we ever run into such situation if 'code' in response_dic: result = response_dic['code'] else: result = 'no code found' self.logger.debug( 'Certificate.new_post() ended with: {0}'.format(result)) return response_dic def revoke(self, content): """ revoke request """ self.logger.debug('Certificate.revoke()') response_dic = {} # check message (code, message, detail, _protected, payload, account_name) = self.message.check(content) if code == 200: if 'certificate' in payload: (code, error) = self._revocation_request_validate( account_name, payload) if code == 200: # revocation starts here # revocation reason is stored in error variable rev_date = uts_to_date_utc(uts_now()) with self.cahandler(self.debug, self.logger) as ca_handler: (code, message, detail) = ca_handler.revoke(payload['certificate'], error, rev_date) else: message = error detail = None else: # message could not get decoded code = 400 message = 'urn:ietf:params:acme:error:malformed' detail = 'certificate not found' # prepare/enrich response status_dic = {'code': code, 'message': message, 'detail': detail} response_dic = self.message.prepare_response(response_dic, status_dic) self.logger.debug( 'Certificate.revoke() ended with: {0}'.format(response_dic)) return response_dic def poll(self, certificate_name, poll_identifier, csr, order_name): """ try to fetch a certificate from CA and store it into database """ self.logger.debug('Certificate.poll({0}: {1})'.format( certificate_name, poll_identifier)) with self.cahandler(self.debug, self.logger) as ca_handler: (error, certificate, certificate_raw, poll_identifier, rejected) = ca_handler.poll(certificate_name, poll_identifier, csr) if certificate: # get issuing and expiration date (issue_uts, expire_uts) = cert_dates_get(self.logger, certificate_raw) # update certificate record in database _result = self._store_cert(certificate_name, certificate, certificate_raw, issue_uts, expire_uts) # update order status to 5 (valid) try: self.dbstore.order_update({ 'name': order_name, 'status': 'valid' }) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate.poll(): {0}' .format(err_)) else: # store error message for later analysis self._store_cert_error(certificate_name, error, poll_identifier) _result = None if rejected: try: self.dbstore.order_update({ 'name': order_name, 'status': 'invalid' }) except BaseException as err_: self.logger.critical( 'acme2certifier database error in Certificate.poll(): {0}' .format(err_)) self.logger.debug('Certificate.poll({0}: {1})'.format( certificate_name, poll_identifier)) return _result def store_csr(self, order_name, csr): """ store csr into database """ self.logger.debug('Certificate.store_csr({0})'.format(order_name)) certificate_name = generate_random_string(self.logger, 12) data_dic = {'order': order_name, 'csr': csr, 'name': certificate_name} try: self.dbstore.certificate_add(data_dic) except BaseException as err_: self.logger.critical( 'Database error in Certificate.store_csr(): {0}'.format(err_)) self.logger.debug('Certificate.store_csr() ended') return certificate_name
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 self.eab_check = False self.eab_handler = 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, payload, 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), } # add eab_kid to data_dic if eab_check is enabled and kid is part of the request if self.eab_check: if payload and 'externalaccountbinding' in payload and payload['externalaccountbinding']: if 'protected' in payload['externalaccountbinding']: eab_kid = self._eab_kid_get(payload['externalaccountbinding']['protected']) self.logger.info('add eab_kid: {0} to data_dic'.format(eab_kid)) if eab_kid: data_dic['eab_kid'] = eab_kid try: (db_name, new) = self.dbstore.account_add(data_dic) except BaseException as err_: self.logger.critical('Account.account._add(): Database error: {0}'.format(err_)) db_name = None new = False self.logger.debug('got 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 _eab_jwk_compare(self, protected, payload): """ compare jwk from outer header with jwk in eab playload """ self.logger.debug('_eab_jwk_compare()') result = False if 'jwk' in protected: # convert outer jwk into string for better comparison if isinstance(protected, dict): jwk_outer = json.dumps(protected['jwk']) # decode inner jwk jwk_inner = b64decode_pad(self.logger, payload) jwk_inner = json.dumps(json.loads(jwk_inner)) if jwk_outer == jwk_inner: result = True self.logger.debug('_eab_jwk_compare() ended with: {0}'.format(result)) return result def _eab_kid_get(self, protected): """ get key identifier for eab validation """ self.logger.debug('_eab_kid_get()') # load protected into json format protected_dic = json.loads(b64decode_pad(self.logger, protected)) # extract kid if isinstance(protected_dic, dict): eab_key_id = protected_dic.get('kid', None) else: eab_key_id = None self.logger.debug('_eab_kid_get() ended with: {0}'.format(eab_key_id)) return eab_key_id def _eab_check(self, protected, payload): """" check for external account binding """ self.logger.debug('_eab_check()') if self.eab_handler and protected and payload and 'externalaccountbinding' in payload and payload['externalaccountbinding']: # compare JWK from protected (outer) header if jwk included in payload of external account binding jwk_compare = self._eab_jwk_compare(protected, payload['externalaccountbinding']['payload']) if jwk_compare and 'protected' in payload['externalaccountbinding']: # get key identifier eab_kid = self._eab_kid_get(payload['externalaccountbinding']['protected']) if eab_kid: # get eab_mac_key with self.eab_handler(self.logger) as eab_handler: eab_mac_key = eab_handler.mac_key_get(eab_kid) else: eab_mac_key = None if eab_mac_key: (result, error) = self._eab_signature_verify(payload['externalaccountbinding'], eab_mac_key) if result: code = 200 message = None detail = None else: code = 403 message = 'urn:ietf:params:acme:error:unauthorized' detail = 'eab signature verification failed' self.logger.error('Account._eab_check() returned error: {0}'.format(error)) else: code = 403 message = 'urn:ietf:params:acme:error:unauthorized' detail = 'eab kid lookup failed' else: code = 403 message = 'urn:ietf:params:acme:error:malformed' detail = 'Malformed request' else: # no external account binding key in payload - error code = 403 message = 'urn:ietf:params:acme:error:externalAccountRequired' detail = 'external account binding required' self.logger.debug('Account._eab_check() ended with:{0}'.format(code)) return (code, message, detail) def _eab_signature_verify(self, content, mac_key): """ verify inner signature """ self.logger.debug('Account._eab_signature_verify()') if content and mac_key: signature = Signature(None, self.server_name, self.logger) jwk_ = json.dumps({'k': mac_key, 'kty': 'oct'}) (sig_check, error) = signature.eab_check(json.dumps(content), jwk_) else: sig_check = False error = None self.logger.debug('Account._eab_signature_verify() ended with: {0}'.format(sig_check)) return (sig_check, error) 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('Account._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 'EABhandler' in config_dic: self.logger.debug('Account._config.load(): loading eab_handler') if 'eab_handler_file' in config_dic['EABhandler']: # mandate eab check regardless if handler could get loaded or not self.eab_check = True try: eab_handler_module = importlib.import_module(ca_handler_get(self.logger, config_dic['EABhandler']['eab_handler_file'])) except BaseException as err_: self.logger.critical('Account._config_load(): loading EABHandler configured in cfg failed with err: {0}'.format(err_)) try: eab_handler_module = importlib.import_module('acme_srv.eab_handler') except BaseException as err_: eab_handler_module = None self.logger.critical('Account._config_load(): loading default EABHandler failed with err: {0}'.format(err_)) if eab_handler_module: # store handler in variable self.eab_handler = eab_handler_module.EABhandler else: self.logger.critical('Account._config_load(): EABHandler configuration is missing in config file') if 'Directory' in config_dic: if 'tos_url' in config_dic['Directory']: self.tos_url = config_dic['Directory']['tos_url'] if 'url_prefix' in config_dic['Directory']: self.path_dic = {k: config_dic['Directory']['url_prefix'] + v for k, v in self.path_dic.items()} self.logger.debug('Account._config_load() ended') 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) # check for external account binding if code == 200 and self.eab_check: (code, message, detail) = self._eab_check(protected, 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, payload, 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) # add exernal account binding if self.eab_check and 'externalaccountbinding' in payload: response_dic['data']['externalaccountbinding'] = payload['externalaccountbinding'] 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
#!/usr/bin/python """ database updater """ # pylint: disable=E0401, C0413 import sys sys.path.insert(0, '..') sys.path.insert(1, '.') from acme_srv.helper import logger_setup # nopep8 from acme_srv.db_handler import DBstore # nopep8 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()
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 Exception 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 Exception 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 Exception: 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) # lgtm [py/clear-text-storage-sensitive-data] 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 Exception: 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 Exception 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
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 Exception as err_: self.logger.critical('acme2certifier database error during nonce_check() in Nonce._check_and_delete(): {0}'.format(err_)) nonce_chk_result = False if nonce_chk_result: try: self.dbstore.nonce_delete(nonce) except Exception as err_: self.logger.critical('acme2certifier database error during nonce_delete() 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) # lgtm [py/unused-local-variable] except Exception 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
def __init__(self, debug=None, logger=None): self.logger = logger self.dbstore = DBstore(debug, self.logger) self.debug = debug
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)