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

Commit b27cd2c

Browse files
committed
Rename to httpsig
Updated to latest "HTTP Signature" spec. Updated interface a bit to be easier to use. Updated parsing of many aspects of the header to be more sane and standard.
1 parent f92a4aa commit b27cd2c

File tree

12 files changed

+84
-66
lines changed

12 files changed

+84
-66
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ celeryd.log
1919
celeryd.pid
2020
joy_rsa
2121
joy_rsa.pub
22-
22+
.noseids

CHANGES.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
Changes
22
-------
33

4-
0.2.0 ()
4+
0.2.0-AK (2014-Jun-23)
5+
~~~~~~~~~~~~~~~~~~~~~~
6+
* Removed HTTP version from request-line, per spec (breaks backwards compatability).
7+
* Removed auto-generation of missing Date header (ensures client compatability).
8+
9+
0.2.0 (unreleased)
510
~~~~~~~~
611

712
* Update to newer spec (incompatible with prior version).
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False)
175175
return {"version": dirname[len(parentdir_prefix):], "full": ""}
176176

177177
tag_prefix = "v"
178-
parentdir_prefix = "http_signature-"
179-
versionfile_source = "http_signature/_version.py"
178+
parentdir_prefix = "httpsig-"
179+
versionfile_source = "httpsig/_version.py"
180180

181181
def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
182182
variables = { "refnames": git_refnames, "full": git_full }
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,20 @@ class HTTPSignatureAuth(AuthBase):
1414
algorithm is one of the six specified algorithms
1515
headers is a list of http headers to be included in the signing string, defaulting to "Date" alone.
1616
'''
17-
def __init__(self, key_id='', secret='', algorithm='rsa-sha256',
17+
def __init__(self, key_id='', secret='', algorithm='hmac-sha256',
1818
headers=None, allow_agent=False):
1919
headers = headers or []
2020
self.header_signer = HeaderSigner(key_id=key_id, secret=secret,
2121
algorithm=algorithm, headers=headers)
2222
self.uses_host = 'host' in [h.lower() for h in headers]
2323

2424
def __call__(self, r):
25-
headers = self.header_signer.sign_headers(
25+
headers = self.header_signer.sign(
2626
r.headers,
2727
# 'Host' header unavailable in request object at this point
2828
# if 'host' header is needed, extract it from the url
2929
host=urlparse(r.url).netloc if self.uses_host else None,
3030
method=r.method,
31-
path=r.path_url,
32-
http_version='1.1')
31+
path=r.path_url)
3332
r.headers.update(headers)
3433
return r
Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,23 @@ def _get_key(self, secret):
4848
rsa_key = RSA.importKey(k, pw)
4949
return PKCS1_v1_5.new(rsa_key)
5050

51-
def sign_rsa(self, sign_string):
51+
def _sign_rsa(self, sign_string):
5252
h = self._hash.new()
5353
h.update(sign_string)
5454
return self._rsa.sign(h)
5555

56-
def sign_hmac(self, sign_string):
56+
def _sign_hmac(self, sign_string):
5757
hmac = self._hash.copy()
5858
hmac.update(sign_string)
5959
return hmac.digest()
6060

6161

62-
def sign(self, sign_string):
62+
def _sign(self, sign_string):
6363
data = None
6464
if self._rsa:
65-
data = self.sign_rsa(sign_string)
65+
data = self._sign_rsa(sign_string)
6666
elif self._hash:
67-
data = self.sign_hmac(sign_string)
67+
data = self._sign_hmac(sign_string)
6868
if not data:
6969
raise SystemError('No valid encryption: try allow_agent=False ?')
7070
return base64.b64encode(data)
@@ -113,12 +113,15 @@ class HeaderSigner(Signer):
113113
algorithm is one of the six specified algorithms
114114
headers is a list of http headers to be included in the signing string, defaulting to "Date" alone.
115115
'''
116-
def __init__(self, key_id='', secret='~/.ssh/id_rsa',
117-
algorithm='rsa-sha256', headers=None):
116+
def __init__(self, key_id='', secret='~/.ssh/id_rsa', algorithm='rsa-sha256', headers=None):
117+
118+
#PyCrypto wants strings, not unicode. We're not so demanding as an API.
119+
key_id = str(key_id)
120+
secret = str(secret)
121+
118122
super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm)
119123
self.headers = headers
120-
self.signature_template = self.build_signature_template(
121-
key_id, algorithm, headers)
124+
self.signature_template = self.build_signature_template(key_id, algorithm, headers)
122125

123126
def build_signature_template(self, key_id, algorithm, headers):
124127
"""
@@ -134,37 +137,40 @@ def build_signature_template(self, key_id, algorithm, headers):
134137
'algorithm': algorithm,
135138
'signature': '%s'}
136139
if headers:
140+
headers = [h.lower() for h in headers]
137141
param_map['headers'] = ' '.join(headers)
138142
kv = map('{0[0]}="{0[1]}"'.format, param_map.items())
139143
kv_string = ','.join(kv)
140144
sig_string = 'Signature {0}'.format(kv_string)
141145
return sig_string
142146

143-
def sign_headers(self, headers, host=None, method=None, path=None,
144-
http_version='1.1'):
147+
def sign(self, headers, host=None, method=None, path=None):
145148
"""
146149
Add Signature Authorization header to case-insensitive header dict.
147150
148151
headers is a case-insensitive dict of mutable headers.
149152
host is a override for the 'host' header (defaults to value in headers).
150-
method is the HTTP method (used for 'request-line').
151-
path is the HTTP path (used for 'request-line').
152-
http_version is the HTTP version (used for 'request-line').
153+
method is the HTTP method (used for '(request-line)').
154+
path is the HTTP path (used for '(request-line)').
153155
"""
154156
headers = CaseInsensitiveDict(headers)
155-
if 'date' not in headers:
156-
now = datetime.now()
157-
stamp = mktime(now.timetuple())
158-
headers['date'] = format_date_time(stamp)
157+
158+
# AK: Possible problem here if the client and server's dates are off
159+
# by even one second, this will fail miserably. This is also not
160+
# in the spec. Should probably be removed.
161+
# if 'date' not in headers:
162+
# now = datetime.now()
163+
# stamp = mktime(now.timetuple())
164+
# headers['date'] = format_date_time(stamp)
165+
159166
required_headers = self.headers or ['date']
160167
signable_list = []
161168
for h in required_headers:
162-
if h == 'request-line':
169+
if h == '(request-line)':
163170
if not method or not path:
164-
raise Exception('method and path arguments required when using "request-line"')
171+
raise Exception('method and path arguments required when using "(request-line)"')
172+
signable_list.append('%s %s' % (method.lower(), path))
165173

166-
signable_list.append('%s %s HTTP/%s' %
167-
(method.upper(), path, http_version))
168174
elif h == 'host':
169175
# 'host' special case due to requests lib restrictions
170176
# 'host' is not available when adding auth so must use a param
@@ -180,8 +186,9 @@ def sign_headers(self, headers, host=None, method=None, path=None,
180186
raise Exception('missing required header "%s"' % (h))
181187

182188
signable_list.append('%s: %s' % (h.lower(), headers[h]))
189+
183190
signable = '\n'.join(signable_list)
184-
signature = self.sign(signable)
191+
signature = self._sign(signable)
185192
headers['Authorization'] = self.signature_template % signature
186-
return headers
187193

194+
return headers
Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from Crypto.PublicKey import RSA
66
from Crypto.Signature import PKCS1_v1_5
77
from base64 import b64decode
8+
from urllib2 import parse_http_list
89

910
from .utils import sig, is_rsa, CaseInsensitiveDict
1011

@@ -28,7 +29,7 @@ def _get_key(self, key_id):
2829
return RSA.importKey(key)
2930

3031

31-
def verify(self, data, signature):
32+
def _verify(self, data, signature):
3233
"""
3334
Checks data against the public key
3435
"""
@@ -47,30 +48,35 @@ class HeaderVerifier(Verifier):
4748
"""
4849
Verifies an HTTP signature from given headers.
4950
"""
50-
def __init__(self, headers, required_headers=None, method=None, path=None,
51-
host=None, http_version='1.1'):
51+
def __init__(self, headers, required_headers=None, method=None, path=None, host=None):
5252

5353
required_headers = required_headers or ['date']
5454
self.auth_dict = self.parse_auth(headers['authorization'])
5555
self.headers = CaseInsensitiveDict(headers)
5656
self.required_headers = [s.lower() for s in required_headers]
57-
self.http_version = http_version
5857
self.method = method
5958
self.path = path
6059
self.host = host
6160
super(HeaderVerifier, self).__init__(key_id=self.auth_dict['keyId'],
6261
hash_algorithm="sha256") # should get hash algorithm from request...
6362

6463
def parse_auth(self, auth):
65-
"""Basic Authorization header parsing."""
64+
"""
65+
Basic Authorization header parsing.
66+
AK: Fails if there is a comma inside a quoted string. Consider urllib2.parse_http_list.
67+
"""
6668
# split 'Signature kvpairs'
6769
s, param_str = auth.split(' ', 1)
70+
6871
# split k1="v1",k2="v2",...
6972
param_list = param_str.split(',')
73+
7074
# convert into [(k1,"v1"), (k2, "v2"), ...]
7175
param_pairs = [p.split('=', 1) for p in param_list]
76+
7277
# convert into {k1:v1, k2:v2, ...}
7378
param_dict = {k: v.strip('"') for k, v in param_pairs}
79+
7480
return param_dict
7581

7682

@@ -87,12 +93,12 @@ def get_signable(self):
8793

8894
signable_list = []
8995
for h in auth_headers:
90-
if h == 'request-line':
96+
if h == '(request-line)':
9197
if not self.method or not self.path:
92-
raise Exception('method and path arguments required when using "request-line"')
98+
raise Exception('method and path arguments required when using "(request-line)"')
9399

94-
signable_list.append('%s %s HTTP/%s' %
95-
(self.method.upper(), self.path, self.http_version))
100+
signable_list.append('%s %s' % (self.method.upper(), self.path))
101+
96102
elif h == 'host':
97103
# 'host' special case due to requests lib restrictions
98104
# 'host' is not available when adding auth so must use a param
@@ -113,8 +119,8 @@ def get_signable(self):
113119
signable = '\n'.join(signable_list)
114120
return signable
115121

116-
def verify_headers(self):
122+
def verify(self):
117123
signing_str = self.get_signable()
118124
# self.auth_dict['keyId']
119125
# self.auth_dict['signature']
120-
return self.verify(signing_str, self.auth_dict['signature'])
126+
return self._verify(signing_str, self.auth_dict['signature'])

tests/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .test_signature import *
2+
from .test_utils import *
3+
from .test_verify import *

tests/test_signature.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import unittest
88

9-
from http_signature.sign import HeaderSigner
9+
from httpsig.sign import HeaderSigner
1010

1111

1212
class TestSign(unittest.TestCase):
@@ -30,7 +30,7 @@ def setUp(self):
3030
def test_date_added(self):
3131
hs = HeaderSigner(key_id='', secret=self.key)
3232
unsigned = {}
33-
signed = hs.sign_headers(unsigned)
33+
signed = hs.sign(unsigned)
3434
self.assertIn('Date', signed)
3535
self.assertIn('Authorization', signed)
3636

@@ -39,7 +39,7 @@ def test_default(self):
3939
unsigned = {
4040
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT'
4141
}
42-
signed = hs.sign_headers(unsigned)
42+
signed = hs.sign(unsigned)
4343
self.assertIn('Date', signed)
4444
self.assertEqual(unsigned['Date'], signed['Date'])
4545
self.assertIn('Authorization', signed)
@@ -53,7 +53,7 @@ def test_default(self):
5353

5454
def test_all(self):
5555
hs = HeaderSigner(key_id='Test', secret=self.key, headers=[
56-
'request-line',
56+
'(request-line)',
5757
'host',
5858
'date',
5959
'content-type',
@@ -67,7 +67,7 @@ def test_all(self):
6767
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
6868
'Content-Length': '18',
6969
}
70-
signed = hs.sign_headers(unsigned, method='POST',
70+
signed = hs.sign(unsigned, method='POST',
7171
path='/foo?param=value&pet=dog')
7272
self.assertIn('Date', signed)
7373
self.assertEqual(unsigned['Date'], signed['Date'])
@@ -78,7 +78,7 @@ def test_all(self):
7878
self.assertIn('signature', params)
7979
self.assertEqual(params['keyId'], 'Test')
8080
self.assertEqual(params['algorithm'], 'rsa-sha256')
81-
self.assertEqual(params['headers'], 'request-line host date content-type content-md5 content-length')
81+
self.assertEqual(params['headers'], '(request-line) host date content-type content-md5 content-length')
8282
self.assertEqual(params['signature'], 'H/AaTDkJvLELy4i1RujnKlS6dm8QWiJvEpn9cKRMi49kKF+mohZ15z1r+mF+XiKS5kOOscyS83olfBtsVhYjPg2Ei3/D9D4Mvb7bFm9IaLJgYTFFuQCghrKQQFPiqJN320emjHxFowpIm1BkstnEU7lktH/XdXVBo8a6Uteiztw=')
8383

8484
if __name__ == '__main__':

0 commit comments

Comments
 (0)