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

Commit 3f94e94

Browse files
authored
Merge pull request ahknight#15 from rbignon/master
Ability to supply another signature header like Signature
2 parents 25a2b1e + 4880f6f commit 3f94e94

File tree

5 files changed

+64
-37
lines changed

5 files changed

+64
-37
lines changed

README.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ Known Limitations
116116

117117
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.
118118
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.
119+
3. Draft 2 added support for ecdsa-sha256. This is available in PyCryptodome but has not been added to httpsig. PRs welcome.
121120

122121

123122
License

httpsig/sign.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,18 @@ class HeaderSigner(Signer):
8686
:arg algorithm: one of the six specified algorithms
8787
:arg headers: a list of http headers to be included in the signing
8888
string, defaulting to ['date'].
89+
:arg sign_header: header used to include signature, defaulting to
90+
'authorization'.
8991
"""
90-
def __init__(self, key_id, secret, algorithm=None, headers=None):
92+
def __init__(self, key_id, secret, algorithm=None, headers=None, sign_header='authorization'):
9193
if algorithm is None:
9294
algorithm = DEFAULT_SIGN_ALGORITHM
9395

9496
super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm)
9597
self.headers = headers or ['date']
9698
self.signature_template = build_signature_template(
97-
key_id, algorithm, headers)
99+
key_id, algorithm, headers, sign_header)
100+
self.sign_header = sign_header
98101

99102
def sign(self, headers, host=None, method=None, path=None):
100103
"""
@@ -112,6 +115,6 @@ def sign(self, headers, host=None, method=None, path=None):
112115
required_headers, headers, host, method, path)
113116

114117
signature = self._sign(signable)
115-
headers['authorization'] = self.signature_template % signature
118+
headers[self.sign_header] = self.signature_template % signature
116119

117120
return headers

httpsig/tests/test_verify.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class TestVerifyHMACSHA1(BaseTestCase):
3434
header_content_type = 'application/json'
3535
header_digest = 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
3636
header_content_length = '18'
37+
sign_header = 'authorization'
3738

3839
def setUp(self):
3940
secret = b"something special goes here"
@@ -62,9 +63,11 @@ def test_default(self):
6263
}
6364

6465
hs = HeaderSigner(
65-
key_id="Test", secret=self.sign_secret, algorithm=self.algorithm)
66+
key_id="Test", secret=self.sign_secret, algorithm=self.algorithm,
67+
sign_header=self.sign_header)
6668
signed = hs.sign(unsigned)
67-
hv = HeaderVerifier(headers=signed, secret=self.verify_secret)
69+
hv = HeaderVerifier(
70+
headers=signed, secret=self.verify_secret, sign_header=self.sign_header)
6871
self.assertTrue(hv.verify())
6972

7073
def test_signed_headers(self):
@@ -75,6 +78,7 @@ def test_signed_headers(self):
7578
key_id="Test",
7679
secret=self.sign_secret,
7780
algorithm=self.algorithm,
81+
sign_header=self.sign_header,
7882
headers=[
7983
'(request-target)',
8084
'host',
@@ -94,7 +98,8 @@ def test_signed_headers(self):
9498

9599
hv = HeaderVerifier(
96100
headers=signed, secret=self.verify_secret,
97-
host=HOST, method=METHOD, path=PATH)
101+
host=HOST, method=METHOD, path=PATH,
102+
sign_header=self.sign_header)
98103
self.assertTrue(hv.verify())
99104

100105
def test_incorrect_headers(self):
@@ -104,6 +109,7 @@ def test_incorrect_headers(self):
104109
hs = HeaderSigner(secret=self.sign_secret,
105110
key_id="Test",
106111
algorithm=self.algorithm,
112+
sign_header=self.sign_header,
107113
headers=[
108114
'(request-target)',
109115
'host',
@@ -122,7 +128,8 @@ def test_incorrect_headers(self):
122128

123129
hv = HeaderVerifier(headers=signed, secret=self.verify_secret,
124130
required_headers=["some-other-header"],
125-
host=HOST, method=METHOD, path=PATH)
131+
host=HOST, method=METHOD, path=PATH,
132+
sign_header=self.sign_header)
126133
with self.assertRaises(Exception):
127134
hv.verify()
128135

@@ -133,6 +140,7 @@ def test_extra_auth_headers(self):
133140
hs = HeaderSigner(
134141
key_id="Test",
135142
secret=self.sign_secret,
143+
sign_header=self.sign_header,
136144
algorithm=self.algorithm, headers=[
137145
'(request-target)',
138146
'host',
@@ -154,6 +162,7 @@ def test_extra_auth_headers(self):
154162
secret=self.verify_secret,
155163
method=METHOD,
156164
path=PATH,
165+
sign_header=self.sign_header,
157166
required_headers=['date', '(request-target)'])
158167
self.assertTrue(hv.verify())
159168

@@ -205,3 +214,7 @@ class TestVerifyRSASHA512(TestVerifyRSASHA1):
205214
def setUp(self):
206215
super(TestVerifyRSASHA512, self).setUp()
207216
self.algorithm = "rsa-sha512"
217+
218+
219+
class TestVerifyRSASHA512ChangeHeader(TestVerifyRSASHA1):
220+
sign_header = 'Signature'

httpsig/utils.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ def generate_message(required_headers, headers, host=None, method=None,
8888
return signable
8989

9090

91+
def parse_signature_header(sign_value):
92+
values = {}
93+
if sign_value:
94+
# This is tricky string magic. Let urllib do it.
95+
fields = parse_http_list(sign_value)
96+
97+
for item in fields:
98+
# Only include keypairs.
99+
if '=' in item:
100+
# Split on the first '=' only.
101+
key, value = item.split('=', 1)
102+
if not (len(key) and len(value)):
103+
continue
104+
105+
# Unquote values, if quoted.
106+
if value[0] == '"':
107+
value = value[1:-1]
108+
109+
values[key] = value
110+
return CaseInsensitiveDict(values)
111+
112+
91113
def parse_authorization_header(header):
92114
if not isinstance(header, six.string_types):
93115
header = header.decode("ascii") # HTTP headers cannot be Unicode.
@@ -100,30 +122,13 @@ def parse_authorization_header(header):
100122
# Split up any args into a dictionary.
101123
values = {}
102124
if len(auth) == 2:
103-
auth_value = auth[1]
104-
if auth_value and len(auth_value):
105-
# This is tricky string magic. Let urllib do it.
106-
fields = parse_http_list(auth_value)
107-
108-
for item in fields:
109-
# Only include keypairs.
110-
if '=' in item:
111-
# Split on the first '=' only.
112-
key, value = item.split('=', 1)
113-
if not (len(key) and len(value)):
114-
continue
115-
116-
# Unquote values, if quoted.
117-
if value[0] == '"':
118-
value = value[1:-1]
119-
120-
values[key] = value
125+
values = parse_signature_header(auth[1])
121126

122127
# ("Signature", {"headers": "date", "algorithm": "hmac-sha256", ... })
123-
return (auth[0], CaseInsensitiveDict(values))
128+
return (auth[0], values)
124129

125130

126-
def build_signature_template(key_id, algorithm, headers):
131+
def build_signature_template(key_id, algorithm, headers, sign_header='authorization'):
127132
"""
128133
Build the Signature template for use with the Authorization header.
129134
@@ -142,8 +147,10 @@ def build_signature_template(key_id, algorithm, headers):
142147
param_map['headers'] = ' '.join(headers)
143148
kv = map('{0[0]}="{0[1]}"'.format, param_map.items())
144149
kv_string = ','.join(kv)
145-
sig_string = 'Signature {0}'.format(kv_string)
146-
return sig_string
150+
if sign_header.lower() == 'authorization':
151+
return 'Signature {0}'.format(kv_string)
152+
153+
return kv_string
147154

148155

149156
def lkv(d):

httpsig/verify.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class HeaderVerifier(Verifier):
4747
"""
4848

4949
def __init__(self, headers, secret, required_headers=None, method=None,
50-
path=None, host=None):
50+
path=None, host=None, sign_header='authorization'):
5151
"""
5252
Instantiate a HeaderVerifier object.
5353
@@ -64,16 +64,21 @@ def __init__(self, headers, secret, required_headers=None, method=None,
6464
Required for the '(request-target)' header.
6565
:param host: Optional. The value to use for the Host
6666
header, if not supplied in :param:headers.
67+
:param sign_header: Optional. The header where the signature is.
68+
Default is 'authorization'.
6769
"""
6870
required_headers = required_headers or ['date']
71+
self.headers = CaseInsensitiveDict(headers)
6972

70-
auth = parse_authorization_header(headers['authorization'])
71-
if len(auth) == 2:
72-
self.auth_dict = auth[1]
73+
if sign_header.lower() == 'authorization':
74+
auth = parse_authorization_header(self.headers['authorization'])
75+
if len(auth) == 2:
76+
self.auth_dict = auth[1]
77+
else:
78+
raise HttpSigException("Invalid authorization header.")
7379
else:
74-
raise HttpSigException("Invalid authorization header.")
80+
self.auth_dict = parse_signature_header(self.headers[sign_header])
7581

76-
self.headers = CaseInsensitiveDict(headers)
7782
self.required_headers = [s.lower() for s in required_headers]
7883
self.method = method
7984
self.path = path

0 commit comments

Comments
 (0)