Skip to content
This repository was archived by the owner on Apr 13, 2024. It is now read-only.

Commit 959ae3d

Browse files
committed
Updated tests with the test data from Draft 8.
1 parent e348056 commit 959ae3d

File tree

4 files changed

+94
-39
lines changed

4 files changed

+94
-39
lines changed

README.rst

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,27 @@ httpsig
77
.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=develop
88
:target: https://travis-ci.org/ahknight/httpsig
99

10-
Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 3`_). This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed.
10+
Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 8`_). This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed.
1111

1212
See the original project_, original Python module_, original spec_, and `current IETF draft`_ for more details on the signing scheme.
1313

1414
.. _project: https://github.com/joyent/node-http-signature
1515
.. _module: https://github.com/zzsnzmn/py-http-signature
1616
.. _spec: https://github.com/joyent/node-http-signature/blob/master/http_signing.md
1717
.. _`current IETF draft`: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
18-
.. _`Draft 3`: http://tools.ietf.org/html/draft-cavage-http-signatures-03
18+
.. _`Draft 8`: http://tools.ietf.org/html/draft-cavage-http-signatures-08
1919

2020
Requirements
2121
------------
2222

23-
* Python 2.7, 3.3, 3.4, 3.5, 3.6
24-
* PyCrypto_
23+
* Python 2.7, 3.3-3.6
24+
* PyCryptodome_
2525

2626
Optional:
2727

2828
* requests_
2929

30-
.. _pycryptodome: https://pypi.python.org/pypi/pycryptodome
30+
.. _PyCryptodome: https://pypi.python.org/pypi/pycryptodome
3131
.. _requests: https://pypi.python.org/pypi/requests
3232

3333
For testing:
@@ -111,6 +111,15 @@ or::
111111

112112
tox
113113

114+
Known Limitations
115+
-----------------
116+
117+
1. Multiple values for the same header are not supported. New headers with the same name will overwrite the previous header. It might be possible to replace the CaseInsensitiveDict with the collection that the email package uses for headers to overcome this limitation.
118+
2. Keyfiles with passwords are not supported. There has been zero vocal demand for this so if you would like it, a PR would be a good way to get it in.
119+
3. Draft 2 added support for the Signature header. As this was principally designed to be an authentication helper, that header is not currently supported. PRs welcome. (It is trivial to move the value after generation, of course.)
120+
4. Draft 2 added support for ecdsa-sha256. This is available in PyCryptodome but has not been added to httpsig. PRs welcome.
121+
122+
114123
License
115124
-------
116125

httpsig/tests/test_signature.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414

1515

1616
class TestSign(unittest.TestCase):
17-
17+
test_method = 'POST'
18+
test_path = '/foo?param=value&pet=dog'
19+
header_host = 'example.com'
20+
header_date = 'Thu, 05 Jan 2014 21:31:40 GMT'
21+
header_content_type = 'application/json'
22+
header_digest = 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
23+
header_content_length = '18'
24+
1825
def setUp(self):
1926
self.key_path = os.path.join(os.path.dirname(__file__), 'rsa_private.pem')
2027
with open(self.key_path, 'rb') as f:
@@ -23,7 +30,7 @@ def setUp(self):
2330
def test_default(self):
2431
hs = sign.HeaderSigner(key_id='Test', secret=self.key)
2532
unsigned = {
26-
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT'
33+
'Date': self.header_date
2734
}
2835
signed = hs.sign(unsigned)
2936
self.assertIn('Date', signed)
@@ -36,25 +43,51 @@ def test_default(self):
3643
self.assertIn('signature', params)
3744
self.assertEqual(params['keyId'], 'Test')
3845
self.assertEqual(params['algorithm'], 'rsa-sha256')
39-
self.assertEqual(params['signature'], 'ATp0r26dbMIxOopqw0OfABDT7CKMIoENumuruOtarj8n/97Q3htHFYpH8yOSQk3Z5zh8UxUym6FYTb5+A0Nz3NRsXJibnYi7brE/4tx5But9kkFGzG+xpUmimN4c3TMN7OFH//+r8hBf7BT9/GmHDUVZT2JzWGLZES2xDOUuMtA=')
46+
self.assertEqual(params['signature'], 'jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=')
47+
48+
def test_basic(self):
49+
hs = sign.HeaderSigner(key_id='Test', secret=self.key, headers=[
50+
'(request-target)',
51+
'host',
52+
'date',
53+
])
54+
unsigned = {
55+
'Host': self.header_host,
56+
'Date': self.header_date,
57+
}
58+
signed = hs.sign(unsigned, method=self.test_method, path=self.test_path)
59+
60+
self.assertIn('Date', signed)
61+
self.assertEqual(unsigned['Date'], signed['Date'])
62+
self.assertIn('Authorization', signed)
63+
auth = parse_authorization_header(signed['authorization'])
64+
params = auth[1]
65+
self.assertIn('keyId', params)
66+
self.assertIn('algorithm', params)
67+
self.assertIn('signature', params)
68+
self.assertEqual(params['keyId'], 'Test')
69+
self.assertEqual(params['algorithm'], 'rsa-sha256')
70+
self.assertEqual(params['headers'],
71+
'(request-target) host date')
72+
self.assertEqual(params['signature'], 'HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4=')
4073

4174
def test_all(self):
4275
hs = sign.HeaderSigner(key_id='Test', secret=self.key, headers=[
4376
'(request-target)',
4477
'host',
4578
'date',
4679
'content-type',
47-
'content-md5',
80+
'digest',
4881
'content-length'
4982
])
5083
unsigned = {
51-
'Host': 'example.com',
52-
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT',
53-
'Content-Type': 'application/json',
54-
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
55-
'Content-Length': '18',
84+
'Host': self.header_host,
85+
'Date': self.header_date,
86+
'Content-Type': self.header_content_type,
87+
'Digest': self.header_digest,
88+
'Content-Length': self.header_content_length,
5689
}
57-
signed = hs.sign(unsigned, method='POST', path='/foo?param=value&pet=dog')
90+
signed = hs.sign(unsigned, method=self.test_method, path=self.test_path)
5891

5992
self.assertIn('Date', signed)
6093
self.assertEqual(unsigned['Date'], signed['Date'])
@@ -66,5 +99,5 @@ def test_all(self):
6699
self.assertIn('signature', params)
67100
self.assertEqual(params['keyId'], 'Test')
68101
self.assertEqual(params['algorithm'], 'rsa-sha256')
69-
self.assertEqual(params['headers'], '(request-target) host date content-type content-md5 content-length')
70-
self.assertEqual(params['signature'], 'G8/Uh6BBDaqldRi3VfFfklHSFoq8CMt5NUZiepq0q66e+fS3Up3BmXn0NbUnr3L1WgAAZGplifRAJqp2LgeZ5gXNk6UX9zV3hw5BERLWscWXlwX/dvHQES27lGRCvyFv3djHP6Plfd5mhPWRkmjnvqeOOSS0lZJYFYHJz994s6w=')
102+
self.assertEqual(params['headers'], '(request-target) host date content-type digest content-length')
103+
self.assertEqual(params['signature'], 'Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=')

httpsig/tests/test_verify.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ def _parse_auth(self, auth):
2525

2626

2727
class TestVerifyHMACSHA1(BaseTestCase):
28+
test_method = 'POST'
29+
test_path = '/foo?param=value&pet=dog'
30+
header_host = 'example.com'
31+
header_date = 'Thu, 05 Jan 2014 21:31:40 GMT'
32+
header_content_type = 'application/json'
33+
header_digest = 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
34+
header_content_length = '18'
35+
2836
def setUp(self):
2937
secret = b"something special goes here"
3038

@@ -47,7 +55,7 @@ def test_basic_sign(self):
4755

4856
def test_default(self):
4957
unsigned = {
50-
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT'
58+
'Date': self.header_date
5159
}
5260

5361
hs = HeaderSigner(key_id="Test", secret=self.sign_secret, algorithm=self.algorithm)
@@ -56,33 +64,33 @@ def test_default(self):
5664
self.assertTrue(hv.verify())
5765

5866
def test_signed_headers(self):
59-
HOST = "example.com"
60-
METHOD = "POST"
61-
PATH = '/foo?param=value&pet=dog'
67+
HOST = self.header_host
68+
METHOD = self.test_method
69+
PATH = self.test_path
6270
hs = HeaderSigner(key_id="Test", secret=self.sign_secret, algorithm=self.algorithm, headers=[
6371
'(request-target)',
6472
'host',
6573
'date',
6674
'content-type',
67-
'content-md5',
75+
'digest',
6876
'content-length'
6977
])
7078
unsigned = {
7179
'Host': HOST,
72-
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT',
73-
'Content-Type': 'application/json',
74-
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
75-
'Content-Length': '18',
80+
'Date': self.header_date,
81+
'Content-Type': self.header_content_type,
82+
'Digest': self.header_digest,
83+
'Content-Length': self.header_content_length,
7684
}
7785
signed = hs.sign(unsigned, method=METHOD, path=PATH)
7886

7987
hv = HeaderVerifier(headers=signed, secret=self.verify_secret, host=HOST, method=METHOD, path=PATH)
8088
self.assertTrue(hv.verify())
8189

8290
def test_incorrect_headers(self):
83-
HOST = "example.com"
84-
METHOD = "POST"
85-
PATH = '/foo?param=value&pet=dog'
91+
HOST = self.header_host
92+
METHOD = self.test_method
93+
PATH = self.test_path
8694
hs = HeaderSigner(secret=self.sign_secret,
8795
key_id="Test",
8896
algorithm=self.algorithm,
@@ -91,14 +99,14 @@ def test_incorrect_headers(self):
9199
'host',
92100
'date',
93101
'content-type',
94-
'content-md5',
102+
'digest',
95103
'content-length'])
96104
unsigned = {
97105
'Host': HOST,
98-
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT',
99-
'Content-Type': 'application/json',
100-
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
101-
'Content-Length': '18',
106+
'Date': self.header_date,
107+
'Content-Type': self.header_content_type,
108+
'Digest': self.header_digest,
109+
'Content-Length': self.header_content_length,
102110
}
103111
signed = hs.sign(unsigned, method=METHOD, path=PATH)
104112

@@ -115,15 +123,15 @@ def test_extra_auth_headers(self):
115123
'host',
116124
'date',
117125
'content-type',
118-
'content-md5',
126+
'digest',
119127
'content-length'
120128
])
121129
unsigned = {
122130
'Host': HOST,
123-
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT',
124-
'Content-Type': 'application/json',
125-
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
126-
'Content-Length': '18',
131+
'Date': self.header_date,
132+
'Content-Type': self.header_content_type,
133+
'Digest': self.header_digest,
134+
'Content-Length': self.header_content_length,
127135
}
128136
signed = hs.sign(unsigned, method=METHOD, path=PATH)
129137
hv = HeaderVerifier(headers=signed, secret=self.verify_secret, method=METHOD, path=PATH, required_headers=['date', '(request-target)'])

httpsig/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ def is_rsa(keyobj):
151151

152152
# based on http://stackoverflow.com/a/2082169/151401
153153
class CaseInsensitiveDict(dict):
154+
""" A case-insensitive dictionary for header storage.
155+
A limitation of this approach is the inability to store
156+
multiple instances of the same header. If that is changed
157+
then we suddenly care about the assembly rules in sec 2.3.
158+
"""
154159
def __init__(self, d=None, **kwargs):
155160
super(CaseInsensitiveDict, self).__init__(**kwargs)
156161
if d:

0 commit comments

Comments
 (0)