Repository: stellar/django-polaris (python)

Issue:
Wrong callback signature calculation
Looks like signatures for callbacks are calculated incorrectly

Here you get string to compute signature

https://github.com/stellar/django-polaris/blob/a60fe69f656fe9745efad6da9d83b796ef871c63/polaris/utils.py#L278C5-L278C85

and result is something like this:

```
"1697116276.domain.com.{'transaction': {'id': '123123123', 'kind': 'deposit', 'status': 'completed', 'status_eta': None, 'amount_in': '20.0000000', 'amount_out': '20.0000000', 'amount_fee': '0E-7', 'started_at': '2023-10-12T12:17:27.749569Z', 'completed_at': '2023-10-12T12:20:18.536572Z', 'stellar_transaction_id': 'asdfasdfasdf', 'external_transaction_id': 'adsfasdfasdf', 'more_info_url': 'https://anchor.com/sep24/transaction/more_info?id=123123123', 'refunded': False, 'message': 'complete', 'claimable_balance_id': None, 'to': 'GAKFBRS24U3PEN6XDMEX4JEV5NGBARS2ZFF5O4CF3JL464SQSD4MDCPF', 'from': None, 'deposit_memo_type': 'hash', 'deposit_memo': None}}"
```

But in request to anchor you body will be converted to json and will be completely different so anchor won't verify signature

Pull request:
serialize callback body and use it in signature payload
resolves #710 

Polaris' callbacks contain a signatures generated by signing payloads that include a python dict of the transaction. We should be serializing the dict to json and ensuring the serialized string is used for both the signature payload and request body.

Patch:
diff --git a/polaris/utils.py b/polaris/utils.py
index 0a1472a..5fcf160 100644
--- a/polaris/utils.py
+++ b/polaris/utils.py
@@ -301,16 +301,19 @@ def make_on_change_callback(
         raise ValueError("invalid or missing on_change_callback")
     if not timeout:
         timeout = settings.CALLBACK_REQUEST_TIMEOUT
-    callback_body = {"transaction": TransactionSerializer(transaction).data}
+    callback_body = json.dumps({"transaction": TransactionSerializer(transaction).data})
     try:
         signature_header_value = compute_callback_signature(transaction.on_change_callback, callback_body)
     except ValueError:  # 
         logger.error(f"unable to parse host of transaction.on_change_callback for transaction {transaction.id}")
         return None
-    headers = {"Signature": signature_header_value}
+    headers = {
+        "Signature": signature_header_value,
+        "Content-Type": "application/json"
+    }
     return post(
         url=transaction.on_change_callback,
-        json=callback_body,
+        data=callback_body.encode(),
         timeout=timeout,
         headers=headers
     )
@@ -345,17 +348,20 @@ async def make_on_change_callback_async(
     if not timeout:
         timeout = settings.CALLBACK_REQUEST_TIMEOUT
     timeout_obj = aiohttp.ClientTimeout(total=timeout)
-    callback_body = {"transaction": TransactionSerializer(transaction).data}
+    callback_body = json.dumps({"transaction": TransactionSerializer(transaction).data})
     try:
         signature_header_value = compute_callback_signature(transaction.on_change_callback, callback_body)
     except ValueError:  # 
         logger.error(f"unable to parse host of transaction.on_change_callback for transaction {transaction.id}")
         return None
-    headers = {"Signature": signature_header_value}
+    headers = {
+        "Signature": signature_header_value,
+        "Content-Type": "application/json"
+    }
     async with aiohttp.ClientSession(timeout=timeout_obj) as session:
         return await session.post(
             url=transaction.on_change_callback,
-            json=callback_body,
+            data=callback_body.encode(),
             timeout=timeout,
             headers=headers
         )


Test patch:
diff --git a/polaris/tests/test_utils.py b/polaris/tests/test_utils.py
index 92d829d..283e583 100644
--- a/polaris/tests/test_utils.py
+++ b/polaris/tests/test_utils.py
@@ -126,10 +126,11 @@ def test_make_on_change_callback_success(mock_serializer, mock_post):
     mock_serializer.assert_called_once_with(mock_transaction)
     mock_post.assert_called_once_with(
         url=mock_transaction.on_change_callback,
-        json={"transaction": mock_serializer(mock_transaction).data},
+        data=json.dumps({}).encode(),
         timeout=settings.CALLBACK_REQUEST_TIMEOUT,
         headers={
-            "Signature": "test"
+            "Signature": "test",
+            "Content-Type": "application/json"
         }
     )
 
@@ -144,10 +145,11 @@ def test_make_on_change_callback_success_with_timeout(mock_serializer, mock_post
     mock_serializer.assert_called_once_with(mock_transaction)
     mock_post.assert_called_once_with(
         url=mock_transaction.on_change_callback,
-        json={"transaction": mock_serializer(mock_transaction).data},
+        data=json.dumps({}).encode(),
         timeout=5,
         headers={
-            "Signature": "test"
+            "Signature": "test",
+            "Content-Type": "application/json"
         }
     )