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

Commit 7198be9

Browse files
authored
Merge pull request #1 from fulder/hs2019-support
Hs2019 support
2 parents 8712e9b + f97f6c0 commit 7198be9

14 files changed

+282
-96
lines changed

README.rst

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ 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 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.
10+
Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 12`_). 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 8`: http://tools.ietf.org/html/draft-cavage-http-signatures-08
18+
.. _`Draft 12`: http://tools.ietf.org/html/draft-cavage-http-signatures-12
1919

2020
Requirements
2121
------------
@@ -49,7 +49,7 @@ For simple raw signing:
4949
5050
secret = open('rsa_private.pem', 'rb').read()
5151
52-
sig_maker = httpsig.Signer(secret=secret, algorithm='rsa-sha256')
52+
sig_maker = httpsig.Signer(secret=secret, algorithm='hs2019', sign_algorithm=httpsig.PSS())
5353
sig_maker.sign('hello world!')
5454
5555
For general use with web frameworks:
@@ -59,9 +59,9 @@ For general use with web frameworks:
5959
import httpsig
6060
6161
key_id = "Some Key ID"
62-
secret = b'some big secret'
62+
secret = open('rsa_private.pem', 'rb').read()
6363
64-
hs = httpsig.HeaderSigner(key_id, secret, algorithm="hmac-sha256", headers=['(request-target)', 'host', 'date'])
64+
hs = httpsig.HeaderSigner(key_id, secret, algorithm="hs2019", sign_algorithm=httpsig.PSS(), headers=['(request-target)', 'host', 'date'])
6565
signed_headers_dict = hs.sign({"Date": "Tue, 01 Jan 2014 01:01:01 GMT", "Host": "example.com"}, method="GET", path="/api/1/object/1")
6666
6767
For use with requests:
@@ -74,9 +74,9 @@ For use with requests:
7474
7575
secret = open('rsa_private.pem', 'rb').read()
7676
77-
auth = HTTPSignatureAuth(key_id='Test', secret=secret)
77+
auth = HTTPSignatureAuth(key_id='Test', secret=secret, sign_algorithm=httpsig.PSS())
7878
z = requests.get('https://api.example.com/path/to/endpoint',
79-
auth=auth, headers={'X-Api-Version': '~6.5'})
79+
auth=auth, headers={'X-Api-Version': '~6.5', 'Date': 'Tue, 01 Jan 2014 01:01:01 GMT')
8080
8181
Class initialization parameters
8282
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -85,20 +85,22 @@ Note that keys and secrets should be bytes objects. At attempt will be made to
8585
8686
.. code:: python
8787
88-
httpsig.Signer(secret, algorithm='rsa-sha256')
88+
httpsig.Signer(secret, algorithm='hs2019', sign_algorithm=httpsig.PSS())
8989
9090
``secret``, in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password.
91-
``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``,
91+
``algorithm`` should be set to 'hs2019' the other six signatures are now deprecated: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``,
9292
``hmac-sha512``.
93+
``sign_algorithm`` The digital signature algorithm derived from ``keyId``. Currently supported algorithms: ``httpsig.PSS``
9394
9495
9596
.. code:: python
9697
97-
httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None)
98+
httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='hs2019', sign_algorithm=httpsig.PSS(), headers=None)
9899
99-
``key_id`` is the label by which the server system knows your RSA signature or password.
100+
``key_id`` is the label by which the server system knows your secret.
100101
``headers`` is the list of HTTP headers that are concatenated and used as signing objects. By default it is the specification's minimum, the ``Date`` HTTP header.
101102
``secret`` and ``algorithm`` are as above.
103+
``sign_algorithm`` The digital signature algorithm derived from ``keyId``. Currently supported algorithms: ``httpsig.PSS``
102104
103105
Tests
104106
-----

httpsig/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .sign import Signer, HeaderSigner
44
from .verify import Verifier, HeaderVerifier
5+
from .sign_algorithms import *
56

67
try:
78
__version__ = get_distribution(__name__).version

httpsig/requests_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
2121
headers is a list of http headers to be included in the signing string,
2222
defaulting to "Date" alone.
2323
"""
24-
def __init__(self, key_id='', secret='', algorithm=None, headers=None):
24+
def __init__(self, key_id='', secret='', algorithm=None, sign_algorithm=None, headers=None):
2525
headers = headers or []
2626
self.header_signer = HeaderSigner(
2727
key_id=key_id, secret=secret,
28-
algorithm=algorithm, headers=headers)
28+
algorithm=algorithm, sign_algorithm=sign_algorithm, headers=headers)
2929
self.uses_host = 'host' in [h.lower() for h in headers]
3030

3131
def __call__(self, r):

httpsig/sign.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
from __future__ import print_function
12
import base64
23
import six
34

45
from Crypto.Hash import HMAC
56
from Crypto.PublicKey import RSA
67
from Crypto.Signature import PKCS1_v1_5
7-
8+
from .sign_algorithms import SignAlgorithm
89
from .utils import *
910

10-
11-
DEFAULT_SIGN_ALGORITHM = "hmac-sha256"
11+
DEFAULT_ALGORITHM = "hs2019"
1212

1313

1414
class Signer(object):
@@ -18,17 +18,32 @@ class Signer(object):
1818
1919
Password-protected keyfiles are not supported.
2020
"""
21-
def __init__(self, secret, algorithm=None):
21+
22+
def __init__(self, secret, algorithm=None, sign_algorithm=None):
2223
if algorithm is None:
23-
algorithm = DEFAULT_SIGN_ALGORITHM
24+
algorithm = DEFAULT_ALGORITHM
2425

2526
assert algorithm in ALGORITHMS, "Unknown algorithm"
27+
28+
if sign_algorithm is not None and not issubclass(type(sign_algorithm), SignAlgorithm):
29+
raise HttpSigException("Unsupported digital signature algorithm")
30+
31+
if algorithm != DEFAULT_ALGORITHM:
32+
print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_ALGORITHM))
33+
2634
if isinstance(secret, six.string_types):
2735
secret = secret.encode("ascii")
2836

2937
self._rsa = None
3038
self._hash = None
31-
self.sign_algorithm, self.hash_algorithm = algorithm.split('-')
39+
self.algorithm = algorithm
40+
self.secret = secret
41+
42+
if "-" in algorithm:
43+
self.sign_algorithm, self.hash_algorithm = algorithm.split('-')
44+
elif algorithm == "hs2019":
45+
assert sign_algorithm is not None, "Required digital signature algorithm not specified"
46+
self.sign_algorithm = sign_algorithm
3247

3348
if self.sign_algorithm == 'rsa':
3449
try:
@@ -42,10 +57,6 @@ def __init__(self, secret, algorithm=None):
4257
self._hash = HMAC.new(secret,
4358
digestmod=HASHES[self.hash_algorithm])
4459

45-
@property
46-
def algorithm(self):
47-
return '%s-%s' % (self.sign_algorithm, self.hash_algorithm)
48-
4960
def _sign_rsa(self, data):
5061
if isinstance(data, six.string_types):
5162
data = data.encode("ascii")
@@ -68,6 +79,8 @@ def sign(self, data):
6879
signed = self._sign_rsa(data)
6980
elif self._hash:
7081
signed = self._sign_hmac(data)
82+
elif issubclass(type(self.sign_algorithm), SignAlgorithm):
83+
signed = self.sign_algorithm.sign(self.secret, data)
7184
if not signed:
7285
raise SystemError('No valid encryptor found.')
7386
return base64.b64encode(signed).decode("ascii")
@@ -83,20 +96,22 @@ class HeaderSigner(Signer):
8396
to use
8497
:arg secret: a PEM-encoded RSA private key or an HMAC secret (must
8598
match the algorithm)
86-
:arg algorithm: one of the six specified algorithms
87-
:arg headers: a list of http headers to be included in the signing
99+
:param algorithm: one of the seven specified algorithms
100+
:param sign_algorithm: required for 'hs2019' algorithm. Sign algorithm for the secret
101+
:param headers: a list of http headers to be included in the signing
88102
string, defaulting to ['date'].
89-
:arg sign_header: header used to include signature, defaulting to
103+
:param sign_header: header used to include signature, defaulting to
90104
'authorization'.
91105
"""
92-
def __init__(self, key_id, secret, algorithm=None, headers=None, sign_header='authorization'):
106+
107+
def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'):
93108
if algorithm is None:
94-
algorithm = DEFAULT_SIGN_ALGORITHM
109+
algorithm = DEFAULT_ALGORITHM
95110

96-
super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm)
111+
super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm)
97112
self.headers = headers or ['date']
98113
self.signature_template = build_signature_template(
99-
key_id, algorithm, headers, sign_header)
114+
key_id, algorithm, headers, sign_header)
100115
self.sign_header = sign_header
101116

102117
def sign(self, headers, host=None, method=None, path=None):
@@ -112,7 +127,7 @@ def sign(self, headers, host=None, method=None, path=None):
112127
headers = CaseInsensitiveDict(headers)
113128
required_headers = self.headers or ['date']
114129
signable = generate_message(
115-
required_headers, headers, host, method, path)
130+
required_headers, headers, host, method, path)
116131

117132
signature = super(HeaderSigner, self).sign(signable)
118133
headers[self.sign_header] = self.signature_template % signature

httpsig/sign_algorithms.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import base64
2+
3+
import six
4+
from Crypto.PublicKey import RSA
5+
from Crypto.Signature import PKCS1_PSS
6+
from httpsig.utils import HttpSigException, HASHES
7+
from abc import ABCMeta, abstractmethod
8+
9+
DEFAULT_HASH_ALGORITHM = "sha512"
10+
11+
12+
class SignAlgorithm(object):
13+
__metaclass__ = ABCMeta
14+
15+
@abstractmethod
16+
def sign(self, private, data):
17+
raise NotImplementedError()
18+
19+
@abstractmethod
20+
def verify(self, public, data, signature):
21+
raise NotImplementedError()
22+
23+
24+
class PSS(SignAlgorithm):
25+
26+
def __init__(self, hash_algorithm=DEFAULT_HASH_ALGORITHM, salt_length=None, mgfunc=None):
27+
if hash_algorithm not in HASHES:
28+
raise HttpSigException("Unsupported hash algorithm")
29+
30+
if hash_algorithm != DEFAULT_HASH_ALGORITHM:
31+
raise HttpSigException(
32+
"Hash algorithm: {} is deprecated. Please use: {}".format(hash_algorithm, DEFAULT_HASH_ALGORITHM))
33+
34+
self.hash_algorithm = HASHES[hash_algorithm]
35+
self.salt_length = salt_length
36+
self.mgfunc = mgfunc
37+
38+
def _create_pss(self, key):
39+
try:
40+
rsa_key = RSA.importKey(key)
41+
pss = PKCS1_PSS.new(rsa_key, saltLen=self.salt_length, mgfunc=self.mgfunc)
42+
except ValueError:
43+
raise HttpSigException("Invalid key.")
44+
return pss
45+
46+
def sign(self, private_key, data):
47+
pss = self._create_pss(private_key)
48+
49+
if isinstance(data, six.string_types):
50+
data = data.encode("ascii")
51+
52+
h = self.hash_algorithm.new()
53+
h.update(data)
54+
return pss.sign(h)
55+
56+
def verify(self, public_key, data, signature):
57+
pss = self._create_pss(public_key)
58+
59+
h = self.hash_algorithm.new()
60+
h.update(data)
61+
return pss.verify(h, base64.b64decode(signature))

httpsig/tests/rsa_private_2048.pem

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEogIBAAKCAQB7eXXK+gSpDXsvZkcXd19X85iemJd0KywRH+/W+1J1j8pd+O1l
3+
H2He8GLaDFCwFijTvTmptfMYB2XyvG8/tPpaSzaIbSBlKXWxSo1fdUMf2e7SbqVr
4+
Fi5DolPrIfRpVqw4iqnTZZ46Y2vfa57Ee3NRF5zoagMS9BM7nfuCKvzZcUK81V75
5+
hup5kpMHW1ofBZAPwQMm8CoXD1bpM+acN1N+63vgTY2QyUq2yJOI3HJvyFZTw+Sj
6+
/ialYtDvDTluBH98i4504OIA6z0SCijF11irvAOSPc0GVXB8HjtUlqbD0BD6Hyqg
7+
MeXgi9nGJhJDnJDiCVlPwg6Ni+h3nW/sXXopAgMBAAECggEANkOg8v2CAtG7647l
8+
e3io3DxgPIMPPKykhzoj67Uz/hqdc0MtAZ4TIyk+KFn1NA3pD3U/3EfseAj4Uv9h
9+
XPwqcnhPlRFwhUT9RldfXi5ou5zJio26ASAUYQD8JIAdrBW9RnQaQp+MNFjxVZU0
10+
h2FBwse/25yLkU7XDQJXQFOoH988Dpozz1y8q11NxurakR67+xtqO5KG7FZdwCsN
11+
W2Z7gTm7T59NYdHevFi2b91hdBdLWCn9RPduEvRViQY5KzzkT6cg493G3vCPXxCy
12+
9C9aCNF7PXghy/im7dLz+H28xYls3KPOJve2dmvox2+aPH66TgXkfj/kfULJmHZq
13+
el3dIQKBgQDAxiqPcEF1Fq4UOoipCvcpiyz0gdFFw1x58km9GOpDdDK1bqcFc2z/
14+
GEoauWVl/PZZJdmht1zzkg4R3Izpbsg1IFxd3m7KbcfOK2bA9h2QPmjW8OwSu4/h
15+
/l8mDsNF5crOdBnUHacgHhL1SJx323Yu3z9PmiN9wLW1gyYkh82SzQKBgQCj+LWP
16+
1DZdsHOs224CjGjfj02PsaV5RNgD7Qqk5VcQFHzmJTAqoroPzJNjUD1sUnXXJHI0
17+
JL533giIsxQxnyca1qtxaO6KA4baykQtKKQqKTWhE2oowS1howHRbLShq1Hxvw9S
18+
QSS0ZAo5DyjZLMkVnlB+v7sXJR8X0Ru8qHKczQKBgQCBMEy1c/VqEpj21YNgRgj9
19+
vleSRK2KozIGR2lDYL8eFXEmRdGIxaH2EsEWx8g8YRp3A/aleczBLtBfB/8nMSba
20+
86TzA24cGxYcBNoH1uhZEnoQEcUjiK8UNPRu/NXAsg8H7KaikHy/+WebGd5CNMEv
21+
CE3VeubuD4e27P1S3e/WwQKBgDzgGjASvjhcSSXUtWv2yvyszEPb1S5Hk9cpSvlb
22+
N859fL1I8y/xCBjTf6iwYo1zs9Iy8r9PIPOJmCuAKLAfgToilrXdGipdEtTpoRQO
23+
8ZvBfuqVNaV5yqpkBUnGDO20mBCjOUH1c3YRagYzDZxLV0BSbVoRPpliK8AA30ZU
24+
V3DFAoGAfaPc8p6o7tCaPMpRxynIAvgIqg4sIBJdX/G4Q+SZeZR/mFlfpuhY4kzh
25+
CL+RKAhOyOaYsSxlk4vB954y4UZFl6/t2W6gNxouelA77TgV2/rjx/fLk06J+RIF
26+
QQkiAXwUZ2xpmdnUk+UREBwrB3LoU9kZM6fKX/LB4QEZuOmbERQ=
27+
-----END RSA PRIVATE KEY-----
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
33
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
44
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
55
oYi+1hqp1fIekaxsyQIDAQAB
6-
-----END PUBLIC KEY-----
6+
-----END PUBLIC KEY-----

httpsig/tests/rsa_public_2048.pem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQB7eXXK+gSpDXsvZkcXd19X
3+
85iemJd0KywRH+/W+1J1j8pd+O1lH2He8GLaDFCwFijTvTmptfMYB2XyvG8/tPpa
4+
SzaIbSBlKXWxSo1fdUMf2e7SbqVrFi5DolPrIfRpVqw4iqnTZZ46Y2vfa57Ee3NR
5+
F5zoagMS9BM7nfuCKvzZcUK81V75hup5kpMHW1ofBZAPwQMm8CoXD1bpM+acN1N+
6+
63vgTY2QyUq2yJOI3HJvyFZTw+Sj/ialYtDvDTluBH98i4504OIA6z0SCijF11ir
7+
vAOSPc0GVXB8HjtUlqbD0BD6HyqgMeXgi9nGJhJDnJDiCVlPwg6Ni+h3nW/sXXop
8+
AgMBAAE=
9+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)