Skip to content

Commit 0a6dc11

Browse files
committed
Add request signing
1 parent 21a2f12 commit 0a6dc11

File tree

4 files changed

+158
-0
lines changed

4 files changed

+158
-0
lines changed

examples/signed_request.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
import sys
3+
import argparse
4+
import messagebird
5+
6+
parser = argparse.ArgumentParser()
7+
parser.add_argument('--signingKey', help='access key for MessageBird API', type=str, required=True)
8+
parser.add_argument('--requestTimestamp', help='the request timestamp', type=str, required=True)
9+
parser.add_argument('--requestBody', help='the request body', type=str, required=True)
10+
parser.add_argument('--signature', help='the signature', type=str, required=True)
11+
args = vars(parser.parse_args())
12+
13+
signed_request = messagebird.SignedRequest(args['signature'], args['timestamp'], args['requestBody'], {})
14+
15+
if signed_request.verify(args['signingKey']):
16+
print("The signed request has been verified.")
17+
else:
18+
print("The signed request cannot be verified.")
19+
20+
if signed_request.isRecent():
21+
print("The signed request is recent.")
22+
else:
23+
print("The signed request is not recent.")

messagebird/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from messagebird.client import Client, ErrorException
2+
from messagebird.signed_request import SignedRequest

messagebird/signed_request.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import hashlib
2+
import hmac
3+
import base64
4+
import time
5+
from collections import OrderedDict
6+
7+
try:
8+
from urllib.parse import urlencode
9+
except ImportError:
10+
from urllib import urlencode
11+
12+
13+
class SignedRequest:
14+
15+
def __init__(self, requestSignature, requestTimestamp, requestBody, requestParameters):
16+
self._requestSignature = requestSignature
17+
self._requestTimestamp = str(requestTimestamp)
18+
self._requestBody = requestBody
19+
self._requestParameters = requestParameters
20+
21+
def verify(self, signing_key):
22+
payload = self._build_payload()
23+
expectedSignature = base64.b64decode(self._requestSignature)
24+
calculatedSignature = hmac.new(signing_key.encode('latin-1'), payload.encode('latin-1'), hashlib.sha256).digest()
25+
return expectedSignature == calculatedSignature
26+
27+
def isRecent(self, offset = 10):
28+
return int(time.time()) - int(self._requestTimestamp) < offset
29+
30+
def _build_payload(self,):
31+
parts = []
32+
parts.append(self._requestTimestamp)
33+
parts.append(urlencode(self._sort_dict(self._requestParameters), True))
34+
checksum_body = hashlib.sha256(self._requestBody.encode('latin-1')).digest()
35+
str_checksum_body = checksum_body.decode('latin-1')
36+
parts.append(str_checksum_body)
37+
38+
return "\n".join(parts)
39+
40+
def _sort_dict(self, dict):
41+
sorted_dict = OrderedDict()
42+
43+
for key in sorted(dict):
44+
sorted_dict[key] = dict[key]
45+
46+
return sorted_dict

tests/test_signed_request.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import unittest
2+
import time
3+
from messagebird.signed_request import SignedRequest
4+
5+
try:
6+
from unittest.mock import Mock
7+
except ImportError:
8+
# mock was added to unittest in Python 3.3, but was an external library
9+
# before.
10+
from mock import Mock
11+
12+
13+
SIGNING_KEY = 'PlLrKaqvZNRR5zAjm42ZT6q1SQxgbbGd'
14+
15+
16+
class TestMessage(unittest.TestCase):
17+
18+
def test_signed_request_withou_body(self):
19+
query = {
20+
'recipient': '31612345678',
21+
'reference': 'FOO',
22+
'statusDatetime': '2019-01-11T09:17:11+00:00',
23+
'id': 'eef0ab57a9e049be946f3821568c2b2e',
24+
'status': 'delivered',
25+
'mccmnc': '20408',
26+
'ported': '1',
27+
}
28+
29+
signature = 'KVBdcVdz2lYMwcBLZCRITgxUfA/WkwSi+T3Wxl2HL6w='
30+
timestamp = 1547198231
31+
body = ''
32+
33+
signed_request = SignedRequest(signature, timestamp, body, query)
34+
self.assertTrue(signed_request.verify(SIGNING_KEY))
35+
36+
def test_signed_request_with_body(self):
37+
query = {
38+
'recipient': '31612345678',
39+
'reference': 'FOO',
40+
'statusDatetime': '2019-01-11T09:17:11+00:00',
41+
'id': 'eef0ab57a9e049be946f3821568c2b2e',
42+
'status': 'delivered',
43+
'mccmnc': '20408',
44+
'ported': '1',
45+
}
46+
47+
signature = '2bl+38H4oHVg03pC3bk2LvCB0IHFgfC4cL5HPQ0LdmI='
48+
timestamp = 1547198231
49+
body = '{"foo":"bar"}'
50+
51+
signed_request = SignedRequest(signature, timestamp, body, query)
52+
self.assertTrue(signed_request.verify(SIGNING_KEY))
53+
54+
def test_incorrectly_signed_request(self):
55+
query = {
56+
'recipient': '31612345678',
57+
'reference': 'BAR',
58+
'statusDatetime': '2019-01-11T09:17:11+00:00',
59+
'id': 'eef0ab57a9e049be946f3821568c2b2e',
60+
'status': 'delivered',
61+
'mccmnc': '20408',
62+
'ported': '1',
63+
}
64+
65+
signature = 'KVBdcVdz2lYMwcBLZCRITgxUfA/WkwSi+T3Wxl2HL6w='
66+
timestamp = 1547198231
67+
body = ''
68+
69+
signed_request = SignedRequest(signature, timestamp, body, query)
70+
self.assertFalse(signed_request.verify(SIGNING_KEY))
71+
72+
def test_recent_signed_request(self):
73+
query = {}
74+
signature = 'KVBdcVdz2lYMwcBLZCRITgxUfA/WkwSi+T3Wxl2HL6w='
75+
timestamp = int(time.time()) - 1
76+
body = ''
77+
78+
signed_request = SignedRequest(signature, timestamp, body, query)
79+
self.assertTrue(signed_request.isRecent())
80+
81+
def test_not_recent_signed_request(self):
82+
query = {}
83+
signature = 'KVBdcVdz2lYMwcBLZCRITgxUfA/WkwSi+T3Wxl2HL6w='
84+
timestamp = int(time.time()) - 100
85+
body = ''
86+
87+
signed_request = SignedRequest(signature, timestamp, body, query)
88+
self.assertFalse(signed_request.isRecent())

0 commit comments

Comments
 (0)