Skip to content

Commit 2248f20

Browse files
authored
Signing method (#60)
* Added signature method implementation. * Fixed issue for non-string values.
1 parent 55bb562 commit 2248f20

File tree

4 files changed

+81
-9
lines changed

4 files changed

+81
-9
lines changed

docs/quickstart.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ In order to check signatures for incoming webhook requests, you'll also
6262
need to specify the ``signature_secret`` argument (or the
6363
``NEXMO_SIGNATURE_SECRET`` environment variable).
6464

65+
If the argument ``signature_method`` is omitted, it will default to the md5 hash
66+
algorithm. Otherwise, it will use the selected method as in md5, sha1, sha256 or
67+
sha512 with hmac.
68+
6569
SMS API
6670
-------
6771

@@ -256,6 +260,16 @@ Validate webhook signatures
256260
else:
257261
# invalid signature
258262
263+
264+
or by using signature method via POST:
265+
266+
client = nexmo.Client(signature_secret='secret', signature_method='sha256')
267+
268+
if client.check_signature(request.body.decode()):
269+
# valid signature
270+
else:
271+
# invalid signature
272+
259273
Docs:
260274
`https://docs.nexmo.com/messaging/signing-messages <https://docs.nexmo.com/messaging/signing-messages?utm_source=DEV_REL&utm_medium=github&utm_campaign=python-client-library>`__
261275

nexmo/__init__.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ def __init__(self, **kwargs):
4141
self.api_secret = kwargs.get('secret', None) or os.environ.get('NEXMO_API_SECRET', None)
4242

4343
self.signature_secret = kwargs.get('signature_secret', None) or os.environ.get('NEXMO_SIGNATURE_SECRET', None)
44+
self.signature_method = kwargs.get('signature_method', None) or os.environ.get('NEXMO_SIGNATURE_METHOD', None)
45+
46+
if self.signature_method == 'md5':
47+
self.signature_method = hashlib.md5
48+
elif self.signature_method == 'sha1':
49+
self.signature_method = hashlib.sha1
50+
elif self.signature_method == 'sha256':
51+
self.signature_method = hashlib.sha256
52+
elif self.signature_method == 'sha512':
53+
self.signature_method = hashlib.sha512
4454

4555
self.application_id = kwargs.get('application_id', None)
4656

@@ -245,25 +255,32 @@ def send_dtmf(self, uuid, params=None, **kwargs):
245255
def check_signature(self, params):
246256
params = dict(params)
247257

248-
signature = params.pop('sig', '')
258+
signature = params.pop('sig', '').lower()
249259

250260
return hmac.compare_digest(signature, self.signature(params))
251261

252262
def signature(self, params):
253-
md5 = hashlib.md5()
263+
if self.signature_method:
264+
hasher = hmac.new(self.signature_secret.encode(), digestmod=self.signature_method)
265+
else:
266+
hasher = hashlib.md5()
254267

255268
# Add timestamp if not already present
256269
if not params.get("timestamp"):
257270
params["timestamp"] = int(time.time())
258271

259272
for key in sorted(params):
260-
# Replace & and = with _ in parameter values to avoid
261-
# parameter injection.
262-
safe_key = key.replace("&", "_").replace("=", "_")
263-
safe_value = str(params[key]).replace("&", "_").replace("=", "_")
264-
md5.update(u'&{0}={1}'.format(safe_key, safe_value).encode('utf-8'))
265-
md5.update(self.signature_secret.encode('utf-8'))
266-
return md5.hexdigest()
273+
value = params[key]
274+
275+
if isinstance(value, str):
276+
value = value.replace('&', '_').replace('=', '_')
277+
278+
hasher.update('&{0}={1}'.format(key, value).encode('utf-8'))
279+
280+
if self.signature_method is None:
281+
hasher.update(self.signature_secret.encode())
282+
283+
return hasher.hexdigest()
267284

268285
def get(self, host, request_uri, params=None):
269286
uri = 'https://' + host + request_uri

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(self):
1414
import nexmo
1515
self.api_key = 'nexmo-api-key'
1616
self.api_secret = 'nexmo-api-secret'
17+
self.signature_secret = 'secret'
1718
self.application_id = 'nexmo-application-id'
1819
self.private_key = read_file('data/private_key.txt')
1920
self.public_key = read_file('data/public_key.txt')

tests/test_nexmo.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,46 @@ def test_signature_adds_timestamp(dummy_data):
118118
assert params['timestamp'] is not None
119119

120120

121+
def test_signature_md5(dummy_data):
122+
params = {'a': '1', 'b': '2', 'timestamp': '1461605396'}
123+
client = nexmo.Client(
124+
key=dummy_data.api_key,
125+
secret=dummy_data.api_secret,
126+
signature_secret=dummy_data.signature_secret,
127+
signature_method='md5')
128+
assert client.signature(params) == 'c15c21ced558c93a226c305f58f902f2'
129+
130+
131+
def test_signature_sha1(dummy_data):
132+
params = {'a': '1', 'b': '2', 'timestamp': '1461605396'}
133+
client = nexmo.Client(
134+
key=dummy_data.api_key,
135+
secret=dummy_data.api_secret,
136+
signature_secret=dummy_data.signature_secret,
137+
signature_method='sha1')
138+
assert client.signature(params) == '3e19a4e6880fdc2c1426bfd0587c98b9532f0210'
139+
140+
141+
def test_signature_sha256(dummy_data):
142+
params = {'a': '1', 'b': '2', 'timestamp': '1461605396'}
143+
client = nexmo.Client(
144+
key=dummy_data.api_key,
145+
secret=dummy_data.api_secret,
146+
signature_secret=dummy_data.signature_secret,
147+
signature_method='sha256')
148+
assert client.signature(params) == 'a321e824b9b816be7c3f28859a31749a098713d39f613c80d455bbaffae1cd24'
149+
150+
151+
def test_signature_sha512(dummy_data):
152+
params = {'a': '1', 'b': '2', 'timestamp': '1461605396'}
153+
client = nexmo.Client(
154+
key=dummy_data.api_key,
155+
secret=dummy_data.api_secret,
156+
signature_secret=dummy_data.signature_secret,
157+
signature_method='sha512')
158+
assert client.signature(params) == '812a18f76680fa0fe1b8bd9ee1625466ceb1bd96242e4d050d2cfd9a7b40166c63ed26ec9702168781b6edcf1633db8ff95af9341701004eec3fcf9550572ee8'
159+
160+
121161
def test_client_doesnt_require_api_key():
122162
client = nexmo.Client(application_id='myid', private_key='abc\nde')
123163
assert client is not None

0 commit comments

Comments
 (0)