async def test_negative_timestamp(self): signing_key = TEST_PRIVATE_KEY timestamp = int(time.time()) signature = sign_request(signing_key, "GET", "/", timestamp, None) resp = await self.fetch_signed("/", method="GET", address=TEST_ADDRESS, timestamp=timestamp, signature=signature) self.assertResponseCodeEqual(resp, 204) # try replay simply changing the timestamp to negative timestamp = -timestamp resp = await self.fetch_signed("/", method="GET", address=TEST_ADDRESS, timestamp=timestamp, signature=signature) self.assertResponseCodeEqual(resp, 400) # sign with negative timestamp signature = sign_request(signing_key, "GET", "/", timestamp, None) resp = await self.fetch_signed("/", method="GET", address=TEST_ADDRESS, timestamp=timestamp, signature=signature) self.assertResponseCodeEqual(resp, 400)
async def test_transactions_with_known_sender_token_id_but_invalid_signature( self): body = {"from": FAUCET_ADDRESS, "to": TEST_ADDRESS, "value": 10**10} resp = await self.fetch("/tx/skel", method="POST", body=body) self.assertEqual(resp.code, 200) tx = sign_transaction(json_decode(resp.body)['tx'], FAUCET_PRIVATE_KEY) body = {"tx": tx} timestamp = int(time.time()) signature = sign_request(FAUCET_PRIVATE_KEY, "POST", "/v1/tx", timestamp, json_encode(body).encode('utf-8')) resp = await self.fetch_signed("/tx", method="POST", body=body, address=TEST_ADDRESS_2, signature=signature, timestamp=timestamp) self.assertEqual(resp.code, 400, resp.body) self.assertIsNotNone(resp.body) error = json_decode(resp.body) self.assertIn('errors', error) self.assertEqual(len(error['errors']), 1)
async def test_invalid_signature_in_deregistration(self): async with self.pool.acquire() as con: await con.fetchrow("INSERT INTO notification_registrations VALUES ($1, $2)", TEST_ID_ADDRESS, TEST_WALLET_ADDRESS) body = { "addresses": [TEST_ID_ADDRESS, TEST_WALLET_ADDRESS] } timestamp = int(time.time()) signature = sign_request(FAUCET_PRIVATE_KEY, "POST", "/v1/deregister", timestamp, json_encode(body).encode('utf-8')) resp = await self.fetch_signed("/deregister", method="POST", body=body, address=TEST_ID_ADDRESS, signature=signature, timestamp=timestamp) self.assertEqual(resp.code, 400, resp.body) async with self.pool.acquire() as con: rows = await con.fetch("SELECT * FROM notification_registrations WHERE token_id = $1", TEST_ID_ADDRESS) self.assertIsNotNone(rows) self.assertEqual(len(rows), 1)
async def test_urlencoded_data(self): # generate random 2048 byte "file" filedata = os.urandom(2048) content_type, body = encode_multipart_formdata( [], [("file", "test.bin", filedata)]) headers = { "Content-Type": content_type, 'content-length': str(len(body)) } timestamp = int(time.time()) signature = sign_request(TEST_PRIVATE_KEY, "POST", "/", timestamp, body) resp = await self.fetch_signed("/", method="POST", body=body, headers=headers, signature=signature, timestamp=timestamp, address=TEST_ADDRESS) self.assertResponseCodeEqual(resp, 204)
async def test_invalid_signature_in_pn_registration(self): body = { "registration_id": TEST_APN_ID, } timestamp = int(time.time()) signature = sign_request(FAUCET_PRIVATE_KEY, "POST", "/v1/apn/register", timestamp, json_encode(body).encode('utf-8')) resp = await self.fetch_signed("/apn/register", method="POST", body=body, signature=signature, address=TEST_ADDRESS, timestamp=timestamp) self.assertEqual(resp.code, 400, resp.body) async with self.pool.acquire() as con: rows = await con.fetch( "SELECT * FROM push_notification_registrations WHERE token_id = $1", TEST_ADDRESS) self.assertIsNotNone(rows) self.assertEqual(len(rows), 0)
async def test_query_argument_fetch(self): signing_key = TEST_PRIVATE_KEY address = TEST_ADDRESS timestamp = int(time.time()) signature = sign_request(signing_key, "GET", "/", timestamp, None) resp = await self.fetch("/?{}".format( generate_query_args(signature=signature, address=address, timestamp=timestamp))) self.assertResponseCodeEqual(resp, 204)
def __call__(self, r): method = r.method path = "/{}".format(r.url.split('/', 3)[-1]) body = r.body timestamp = int(time.time()) signature = sign_request(self.signing_key, method, path, timestamp, body) r.headers[TOKEN_ID_ADDRESS_HEADER] = self.address r.headers[TOKEN_SIGNATURE_HEADER] = signature r.headers[TOKEN_TIMESTAMP_HEADER] = str(timestamp) return r
async def test_wrong_address(self): timestamp = int(time.time()) body = {"payment_address": TEST_PAYMENT_ADDRESS} signature = sign_request(TEST_PRIVATE_KEY, "POST", "/v1/user", timestamp, body) address = "{}00000".format(TEST_ADDRESS[:-5]) resp = await self.fetch_signed("/user", method="POST", timestamp=timestamp, address=address, signature=signature, body=body) self.assertResponseCodeEqual(resp, 400, resp.body)
async def connect(self): # find out if there's a path prefix added by get_url path = "/{}".format(self.url.split('/', 3)[-1]) address = private_key_to_address(self.signing_key) timestamp = int(time.time()) signature = sign_request(self.signing_key, "GET", path, timestamp, None) request = tornado.httpclient.HTTPRequest(self.url, headers={ TOKEN_ID_ADDRESS_HEADER: address, TOKEN_SIGNATURE_HEADER: signature, TOKEN_TIMESTAMP_HEADER: str(timestamp) }) self.con = await tornado.websocket.websocket_connect(request) return self.con
async def test_invalid_signature(self): body = {"registration_id": "1234567890"} timestamp = int(time.time()) signature = sign_request(FAUCET_PRIVATE_KEY, "POST", "/", timestamp, json_encode(body).encode('utf-8')) resp = await self.fetch_signed("/", method="POST", body=body, address=TEST_ADDRESS, timestamp=timestamp, signature=signature) self.assertEqual(resp.code, 400, resp.body) # make sure query string also fails resp = await self.fetch("/?{}".format( generate_query_args(signature=signature, address=TEST_ADDRESS, timestamp=timestamp))) self.assertEqual(resp.code, 400, resp.body)
def fetch_signed(self, path, *, signing_key=None, signature=None, timestamp=None, address=None, **kwargs): if not isinstance(path, str) or not path.startswith("/"): # for simplicity's sake, don't accept HTTPRequest objects or external urls in the tests raise Exception( "first argument must be path string starting with a / (e.g. /v1/tx)" ) # find out if there's a path prefix added by get_url prefix = "/{}".format(self.get_url(path).split('/', 3)[-1]).split(path)[0] headers = kwargs.setdefault('headers', tornado.httputil.HTTPHeaders()) if 'body' in kwargs: body = kwargs.pop('body') if isinstance(body, dict): headers['Content-Type'] = "application/json" body = tornado.escape.json_encode(body).encode('utf-8') elif isinstance(body, str): # try and find the charset to use to encode this if 'Content-Type' in headers: idx = headers['Content-Type'].find('charset=') if idx >= 0: charset = headers['Content-Type'][idx + 8:] idx = charset.find(';') if idx >= 0: charset = charset[:idx] else: charset = 'utf-8' else: charset = 'utf-8' # encode to a byte string body = body.encode(charset) elif not isinstance(body, bytes): raise Exception("Unable to handle bodys of type '{}'".format( type(body))) else: body = None method = kwargs.setdefault('method', 'GET').upper() if signing_key is None and (address is None or signature is None): raise Exception( "signing_key is required unless address and signature is given" ) if timestamp is None and signature is not None: raise Exception( "timestamp is required if signature is given explicitly") if address is None: address = private_key_to_address(signing_key) if timestamp is None: timestamp = int(time.time()) if signature is None: signature = sign_request(signing_key, method, "{}{}".format(prefix, path), timestamp, body) headers[TOKEN_ID_ADDRESS_HEADER] = address headers[TOKEN_SIGNATURE_HEADER] = signature headers[TOKEN_TIMESTAMP_HEADER] = str(timestamp) # because tornado doesn't like POSTs with body set to None if body is None and method == "POST": body = b"" return self.fetch(path, body=body, **kwargs)