class TestACMEHandler(unittest.TestCase): """ test class for ACMEHandler """ acme = None def setUp(self): """ setup unittest """ models_mock = MagicMock() models_mock.acme_srv.db_handler.DBstore.return_value = FakeDBStore modules = {'acme_srv.db_handler': models_mock} patch.dict('sys.modules', modules).start() import logging logging.basicConfig(level=logging.CRITICAL) self.logger = logging.getLogger('test_a2c') from acme_srv.nonce import Nonce self.nonce = Nonce(False, self.logger) def test_001_nonce__new(self): """ test Nonce.new() and check if we get something back """ self.assertIsNotNone(self.nonce._new()) def test_002_nonce_generate_and_add(self): """ test Nonce.nonce_generate_and_add() and check if we get something back """ self.assertIsNotNone(self.nonce.generate_and_add()) def test_003_nonce_check(self): """ test Nonce.nonce_check_and_delete """ self.assertEqual((400, 'urn:ietf:params:acme:error:badNonce', 'NONE'), self.nonce.check({'foo': 'bar'})) def test_004_nonce_check(self): """ test Nonce.nonce_check_and_delete """ self.assertEqual((200, None, None), self.nonce.check({'nonce': 'aaa'})) def test_005_nonce__check_and_delete(self): """ test Nonce.nonce_check_and_delete """ self.assertEqual((200, None, None), self.nonce._check_and_delete('aaa')) def test_006_nonce_generate_and_add(self): """ test Nonce._add() if dbstore.nonce_add raises an exception """ self.nonce.dbstore.nonce_add.side_effect = Exception('exc_nonce_add') with self.assertLogs('test_a2c', level='INFO') as lcm: self.nonce.generate_and_add() self.assertIn( 'CRITICAL:test_a2c:acme2certifier database error in Nonce.generate_and_add(): exc_nonce_add', lcm.output) def test_007_nonce__check_and_delete(self): """ test Nonce._add() if dbstore.nonce_add raises an exception """ self.nonce.dbstore.nonce_check.return_value = True self.nonce.dbstore.nonce_delete.side_effect = Exception( 'exc_nonce_delete') with self.assertLogs('test_a2c', level='INFO') as lcm: self.nonce._check_and_delete('nonce') self.assertIn( 'CRITICAL:test_a2c:acme2certifier database error in Nonce._check_and_delete(): exc_nonce_delete', lcm.output) def test_008_nonce__check_and_delete(self): """ test Nonce._add() if dbstore.nonce_add raises an exception """ self.nonce.dbstore.nonce_check.side_effect = Exception( 'exc_nonce_check') with self.assertLogs('test_a2c', level='INFO') as lcm: self.nonce._check_and_delete('nonce') self.assertIn( 'CRITICAL:test_a2c:acme2certifier database error in Nonce._check_and_delete(): exc_nonce_check', lcm.output) def test_009__enter_(self): """ test enter """ self.nonce.__enter__()
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