def sign_response(request, response): """ This decorator is used to sign the response. It adds the nonce from the request, if it exist and adds the nonce and the signature to the response. .. note:: This only works for JSON responses. So if we fail to decode the JSON, we just pass on. The usual way to use it is, to wrap the after_request, so that we can also sign errors. @postrequest(sign_response, request=request) def after_request(response): :param request: The Request object :param response: The Response object """ if current_app.config.get("PI_NO_RESPONSE_SIGN"): return response priv_file_name = current_app.config.get("PI_AUDIT_KEY_PRIVATE") try: with open(priv_file_name, 'rb') as priv_file: priv_key = priv_file.read() sign_object = Sign(priv_key, public_key=None) except (IOError, ValueError, TypeError) as e: log.info('Could not load private key from ' 'file {0!s}: {1!r}!'.format(priv_file_name, e)) log.debug(traceback.format_exc()) return response request.all_data = get_all_params(request.values, request.data) # response can be either a Response object or a Tuple (Response, ErrorID) response_value = 200 response_is_tuple = False if type(response).__name__ == "tuple": response_is_tuple = True response_value = response[1] response_object = response[0] else: response_object = response try: content = json.loads(response_object.data) nonce = request.all_data.get("nonce") if nonce: content["nonce"] = nonce content["signature"] = sign_object.sign( json.dumps(content, sort_keys=True)) response_object.data = json.dumps(content) except ValueError: # The response.data is no JSON (but CSV or policy export) # We do no signing in this case. log.info("We only sign JSON response data.") if response_is_tuple: resp = (response_object, response_value) else: resp = response_object return resp
def __init__(self, config=None): super(Audit, self).__init__(config) self.name = "sqlaudit" self.sign_data = not self.config.get("PI_AUDIT_NO_SIGN") self.sign_object = None self.verify_old_sig = self.config.get('PI_CHECK_OLD_SIGNATURES') if self.sign_data: self.read_keys(self.config.get("PI_AUDIT_KEY_PUBLIC"), self.config.get("PI_AUDIT_KEY_PRIVATE")) self.sign_object = Sign(self.private, self.public) # Read column_length from the config file config_column_length = self.config.get("PI_AUDIT_SQL_COLUMN_LENGTH", {}) # fill the missing parts with the default from the models self.custom_column_length = { k: (v if k not in config_column_length else config_column_length[k]) for k, v in column_length.items() } # We can use "sqlaudit" as the key because the SQLAudit connection # string is fixed for a running privacyIDEA instance. # In other words, we will not run into any problems with changing connect strings. self.engine = get_engine(self.name, self._create_engine) # create a configured "Session" class. ``scoped_session`` is not # necessary because we do not share session objects among threads. # We use it anyway as a safety measure. Session = scoped_session(sessionmaker(bind=self.engine)) self.session = Session() # Ensure that the connection gets returned to the pool when the request has # been handled. This may close an already-closed session, but this is not a problem. register_finalizer(self.session.close) self.session._model_changes = {}
def test_07_sign_response(self): builder = EnvironBuilder(method='POST', data={}, headers={}) env = builder.get_environ() env["REMOTE_ADDR"] = "192.168.0.1" req = Request(env) req.values = { "user": "******", "pass": "******", "nonce": "12345678" } res = { "jsonrpc": "2.0", "result": { "status": True, "value": True }, "version": "privacyIDEA test", "id": 1 } resp = Response(json.dumps(res)) from privacyidea.lib.crypto import Sign g.sign_object = Sign("tests/testdata/private.pem", "tests/testdata/public.pem") new_response = sign_response(req, resp) jresult = json.loads(new_response.data) self.assertEqual(jresult.get("nonce"), "12345678") self.assertEqual( jresult.get("signature"), "7220461805369685253863294214862525311437731987121534735993146952136348520396812489782945679627890785973634896605293523175424850299832912878523161817380029213546063467888018205435416020286712762804412024065559270543774578319469096483246637875013247101135063221604113204491121777932147776087110152414627230087278622508771143940031542890514380486863296102037208395371717795767683973979032142677315402422403254992482761563612174177151960004042109847122772813717599078313600692433727690239340230353616318691769042290314664126975201679642739717702497638318611217001361093950139025744740660953017413716736691777322916588328" )
def __init__(self, config=None): self.name = "sqlaudit" self.config = config or {} self.audit_data = {} self.sign_data = not self.config.get("PI_AUDIT_NO_SIGN") self.sign_object = None self.verify_old_sig = self.config.get('PI_CHECK_OLD_SIGNATURES') if self.sign_data: self.read_keys(self.config.get("PI_AUDIT_KEY_PUBLIC"), self.config.get("PI_AUDIT_KEY_PRIVATE")) self.sign_object = Sign(self.private, self.public) # We can use "sqlaudit" as the key because the SQLAudit connection # string is fixed for a running privacyIDEA instance. # In other words, we will not run into any problems with changing connect strings. self.engine = get_engine(self.name, self._create_engine) # create a configured "Session" class. ``scoped_session`` is not # necessary because we do not share session objects among threads. # We use it anyway as a safety measure. Session = scoped_session(sessionmaker(bind=self.engine)) self.session = Session() # Ensure that the connection gets returned to the pool when the request has # been handled. This may close an already-closed session, but this is not a problem. register_finalizer(self.session.close) self.session._model_changes = {}
def test_00_create_sign_object(self): # test with invalid key data with self.assertRaises(Exception): Sign(b'This is not a private key', b'This is not a public key') with self.assertRaises(Exception): priv_key = open(current_app.config.get("PI_AUDIT_KEY_PRIVATE"), 'rb').read() Sign(private_key=priv_key, public_key=b'Still not a public key') # this should work priv_key = open(current_app.config.get("PI_AUDIT_KEY_PRIVATE"), 'rb').read() pub_key = open(current_app.config.get("PI_AUDIT_KEY_PUBLIC"), 'rb').read() so = Sign(priv_key, pub_key) self.assertEqual(so.sig_ver, 'rsa_sha256_pss') # test missing keys so = Sign(public_key=pub_key) res = so.sign('testdata') self.assertEqual(res, '') so = Sign(private_key=priv_key) res = so.verify('testdata', 'testsig') self.assertFalse(res)
def sign_response(request, response): """ This decorator is used to sign the response. It adds the nonce from the request, if it exist and adds the nonce and the signature to the response. .. note:: This only works for JSON responses. So if we fail to decode the JSON, we just pass on. The usual way to use it is, to wrap the after_request, so that we can also sign errors. @postrequest(sign_response, request=request) def after_request(response): :param request: The Request object :param response: The Response object """ if current_app.config.get("PI_NO_RESPONSE_SIGN"): return response priv_file = current_app.config.get("PI_AUDIT_KEY_PRIVATE") pub_file = current_app.config.get("PI_AUDIT_KEY_PUBLIC") sign_object = Sign(priv_file, pub_file) request.all_data = get_all_params(request.values, request.data) # response can be either a Response object or a Tuple (Response, ErrorID) response_value = 200 response_is_tuple = False if type(response).__name__ == "tuple": response_is_tuple = True response_value = response[1] response_object = response[0] else: response_object = response try: content = json.loads(response_object.data) nonce = request.all_data.get("nonce") if nonce: content["nonce"] = nonce content["signature"] = sign_object.sign(json.dumps(content)) response_object.data = json.dumps(content) except ValueError: # The response.data is no JSON (but CSV or policy export) # We do no signing in this case. log.info("We only sign JSON response data.") if response_is_tuple: resp = (response_object, response_value) else: resp = response_object return resp
def test_01_sign_and_verify_data(self): priv_key = open(current_app.config.get("PI_AUDIT_KEY_PRIVATE"), 'rb').read() pub_key = open(current_app.config.get("PI_AUDIT_KEY_PUBLIC"), 'rb').read() so = Sign(priv_key, pub_key) data = 'short text' sig = so.sign(data) self.assertTrue(sig.startswith(so.sig_ver), sig) self.assertTrue(so.verify(data, sig)) data = b'A slightly longer text, this time in binary format.' sig = so.sign(data) self.assertTrue(so.verify(data, sig)) # test with text larger than RSA key size data = b'\x01\x02' * 5000 sig = so.sign(data) self.assertTrue(so.verify(data, sig)) # now test a broken signature data = 'short text' sig = so.sign(data) sig_broken = sig[:-1] + '{:x}'.format((int(sig[-1], 16) + 1) % 16) self.assertFalse(so.verify(data, sig_broken)) # test with non hex string sig_broken = sig[:-1] + 'x' self.assertFalse(so.verify(data, sig_broken)) # now try to verify old signatures # first without enabling old signatures in config short_text_sig = 15197717811878792093921885389298262311612396877333963031070812155820116863657342817645537537961773450510020137791036591085713379948816070430789598146539509027948592633362217308056639775153575635684961642110792013775709164803544619582232081442445758263838942315386909453927493644845757192298617925455779136340217255670113943560463286896994555184188496806420559078552626485909484729552861477888246423469461421103010299470836507229490718177625822972845024556897040292571751452383573549412451282884349017186147757238775308192484937929135306435242403555592741059466194258607967889051881221759976135386624406095324595765010 data = 'short text' self.assertFalse(so.verify(data, short_text_sig)) # now we enable the checking of old signatures short_text_sig = 15197717811878792093921885389298262311612396877333963031070812155820116863657342817645537537961773450510020137791036591085713379948816070430789598146539509027948592633362217308056639775153575635684961642110792013775709164803544619582232081442445758263838942315386909453927493644845757192298617925455779136340217255670113943560463286896994555184188496806420559078552626485909484729552861477888246423469461421103010299470836507229490718177625822972845024556897040292571751452383573549412451282884349017186147757238775308192484937929135306435242403555592741059466194258607967889051881221759976135386624406095324595765010 data = 'short text' self.assertTrue(so.verify(data, short_text_sig, verify_old_sigs=True)) # verify a broken old signature broken_short_text_sig = short_text_sig + 1 self.assertFalse( so.verify(data, broken_short_text_sig, verify_old_sigs=True)) long_data_sig = 991763198885165486007338893972384496025563436289154190056285376683148093829644985815692167116166669178171916463844829424162591848106824431299796818231239278958776853940831433819576852350691126984617641483209392489383319296267416823194661791079316704545017249491961092046751201670544843607206698682190381208022128216306635574292359600514603728560982584561531193227312370683851459162828981766836503134221347324867936277484738573153562229478151744446530191383660477390958159856842222437156763388859923477183453362567547792824054461704970820770533637185477922709297916275611571003099205429044820469679520819043851809079 long_data = b'\x01\x02' * 5000 self.assertTrue( so.verify(long_data, long_data_sig, verify_old_sigs=True))
def read_keys(self, pub, priv): """ Set the private and public key for the audit class. This is achieved by passing the entries. #priv = config.get("privacyideaAudit.key.private") #pub = config.get("privacyideaAudit.key.public") :param pub: Public key, used for verifying the signature :type pub: string with filename :param priv: Private key, used to sign the audit entry :type priv: string with filename :return: None """ self.sign_object = Sign(priv, pub)
def check_signature(subscription): """ This function checks the signature of a subscription. If the signature checking fails, a SignatureError / Exception is raised. :param subscription: The dict of the subscription :return: True """ vendor = subscription.get("by_name").split()[0] enckey = get_app_config_value("PI_ENCFILE", "/etc/privacyidea/enckey") dirname = os.path.dirname(enckey) # In dirname we are searching for <vendor>.pem filename = u"{0!s}/{1!s}.pem".format(dirname, vendor) try: # remove the minutes 00:00:00 subscription["date_from"] = subscription.get("date_from").strftime(SUBSCRIPTION_DATE_FORMAT) subscription["date_till"] = subscription.get("date_till").strftime(SUBSCRIPTION_DATE_FORMAT) sign_string = SIGN_FORMAT.format(**subscription) with open(filename, 'rb') as key_file: sign_obj = Sign(private_key=None, public_key=key_file.read()) signature = subscription.get('signature', '100') r = sign_obj.verify(sign_string, signature, verify_old_sigs=True) subscription["date_from"] = datetime.datetime.strptime( subscription.get("date_from"), SUBSCRIPTION_DATE_FORMAT) subscription["date_till"] = datetime.datetime.strptime( subscription.get("date_till"), SUBSCRIPTION_DATE_FORMAT) except Exception as _e: log.debug(traceback.format_exc()) raise SubscriptionError("Verifying the signature of your subscription " "failed.", application=subscription.get("application")) if not r: raise SubscriptionError("Signature of your subscription does not " "match.", application=subscription.get("application")) return r