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

Commit

Permalink
Merge pull request #1 from fulder/hs2019-support
Browse files Browse the repository at this point in the history
Hs2019 support
  • Loading branch information
fulder authored Aug 25, 2020
2 parents 8712e9b + f97f6c0 commit 7198be9
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 96 deletions.
24 changes: 13 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ httpsig
.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=develop
:target: https://travis-ci.org/ahknight/httpsig

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.
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.

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

.. _project: https://github.com/joyent/node-http-signature
.. _module: https://github.com/zzsnzmn/py-http-signature
.. _spec: https://github.com/joyent/node-http-signature/blob/master/http_signing.md
.. _`current IETF draft`: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
.. _`Draft 8`: http://tools.ietf.org/html/draft-cavage-http-signatures-08
.. _`Draft 12`: http://tools.ietf.org/html/draft-cavage-http-signatures-12

Requirements
------------
Expand Down Expand Up @@ -49,7 +49,7 @@ For simple raw signing:
secret = open('rsa_private.pem', 'rb').read()
sig_maker = httpsig.Signer(secret=secret, algorithm='rsa-sha256')
sig_maker = httpsig.Signer(secret=secret, algorithm='hs2019', sign_algorithm=httpsig.PSS())
sig_maker.sign('hello world!')
For general use with web frameworks:
Expand All @@ -59,9 +59,9 @@ For general use with web frameworks:
import httpsig
key_id = "Some Key ID"
secret = b'some big secret'
secret = open('rsa_private.pem', 'rb').read()
hs = httpsig.HeaderSigner(key_id, secret, algorithm="hmac-sha256", headers=['(request-target)', 'host', 'date'])
hs = httpsig.HeaderSigner(key_id, secret, algorithm="hs2019", sign_algorithm=httpsig.PSS(), headers=['(request-target)', 'host', 'date'])
signed_headers_dict = hs.sign({"Date": "Tue, 01 Jan 2014 01:01:01 GMT", "Host": "example.com"}, method="GET", path="/api/1/object/1")
For use with requests:
Expand All @@ -74,9 +74,9 @@ For use with requests:
secret = open('rsa_private.pem', 'rb').read()
auth = HTTPSignatureAuth(key_id='Test', secret=secret)
auth = HTTPSignatureAuth(key_id='Test', secret=secret, sign_algorithm=httpsig.PSS())
z = requests.get('https://api.example.com/path/to/endpoint',
auth=auth, headers={'X-Api-Version': '~6.5'})
auth=auth, headers={'X-Api-Version': '~6.5', 'Date': 'Tue, 01 Jan 2014 01:01:01 GMT')
Class initialization parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -85,20 +85,22 @@ Note that keys and secrets should be bytes objects. At attempt will be made to
.. code:: python
httpsig.Signer(secret, algorithm='rsa-sha256')
httpsig.Signer(secret, algorithm='hs2019', sign_algorithm=httpsig.PSS())
``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.
``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``,
``algorithm`` should be set to 'hs2019' the other six signatures are now deprecated: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``,
``hmac-sha512``.
``sign_algorithm`` The digital signature algorithm derived from ``keyId``. Currently supported algorithms: ``httpsig.PSS``
.. code:: python
httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None)
httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='hs2019', sign_algorithm=httpsig.PSS(), headers=None)
``key_id`` is the label by which the server system knows your RSA signature or password.
``key_id`` is the label by which the server system knows your secret.
``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.
``secret`` and ``algorithm`` are as above.
``sign_algorithm`` The digital signature algorithm derived from ``keyId``. Currently supported algorithms: ``httpsig.PSS``
Tests
-----
Expand Down
1 change: 1 addition & 0 deletions httpsig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .sign import Signer, HeaderSigner
from .verify import Verifier, HeaderVerifier
from .sign_algorithms import *

try:
__version__ = get_distribution(__name__).version
Expand Down
4 changes: 2 additions & 2 deletions httpsig/requests_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase):
headers is a list of http headers to be included in the signing string,
defaulting to "Date" alone.
"""
def __init__(self, key_id='', secret='', algorithm=None, headers=None):
def __init__(self, key_id='', secret='', algorithm=None, sign_algorithm=None, headers=None):
headers = headers or []
self.header_signer = HeaderSigner(
key_id=key_id, secret=secret,
algorithm=algorithm, headers=headers)
algorithm=algorithm, sign_algorithm=sign_algorithm, headers=headers)
self.uses_host = 'host' in [h.lower() for h in headers]

def __call__(self, r):
Expand Down
51 changes: 33 additions & 18 deletions httpsig/sign.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from __future__ import print_function
import base64
import six

from Crypto.Hash import HMAC
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

from .sign_algorithms import SignAlgorithm
from .utils import *


DEFAULT_SIGN_ALGORITHM = "hmac-sha256"
DEFAULT_ALGORITHM = "hs2019"


class Signer(object):
Expand All @@ -18,17 +18,32 @@ class Signer(object):
Password-protected keyfiles are not supported.
"""
def __init__(self, secret, algorithm=None):

def __init__(self, secret, algorithm=None, sign_algorithm=None):
if algorithm is None:
algorithm = DEFAULT_SIGN_ALGORITHM
algorithm = DEFAULT_ALGORITHM

assert algorithm in ALGORITHMS, "Unknown algorithm"

if sign_algorithm is not None and not issubclass(type(sign_algorithm), SignAlgorithm):
raise HttpSigException("Unsupported digital signature algorithm")

if algorithm != DEFAULT_ALGORITHM:
print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_ALGORITHM))

if isinstance(secret, six.string_types):
secret = secret.encode("ascii")

self._rsa = None
self._hash = None
self.sign_algorithm, self.hash_algorithm = algorithm.split('-')
self.algorithm = algorithm
self.secret = secret

if "-" in algorithm:
self.sign_algorithm, self.hash_algorithm = algorithm.split('-')
elif algorithm == "hs2019":
assert sign_algorithm is not None, "Required digital signature algorithm not specified"
self.sign_algorithm = sign_algorithm

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

@property
def algorithm(self):
return '%s-%s' % (self.sign_algorithm, self.hash_algorithm)

def _sign_rsa(self, data):
if isinstance(data, six.string_types):
data = data.encode("ascii")
Expand All @@ -68,6 +79,8 @@ def sign(self, data):
signed = self._sign_rsa(data)
elif self._hash:
signed = self._sign_hmac(data)
elif issubclass(type(self.sign_algorithm), SignAlgorithm):
signed = self.sign_algorithm.sign(self.secret, data)
if not signed:
raise SystemError('No valid encryptor found.')
return base64.b64encode(signed).decode("ascii")
Expand All @@ -83,20 +96,22 @@ class HeaderSigner(Signer):
to use
:arg secret: a PEM-encoded RSA private key or an HMAC secret (must
match the algorithm)
:arg algorithm: one of the six specified algorithms
:arg headers: a list of http headers to be included in the signing
:param algorithm: one of the seven specified algorithms
:param sign_algorithm: required for 'hs2019' algorithm. Sign algorithm for the secret
:param headers: a list of http headers to be included in the signing
string, defaulting to ['date'].
:arg sign_header: header used to include signature, defaulting to
:param sign_header: header used to include signature, defaulting to
'authorization'.
"""
def __init__(self, key_id, secret, algorithm=None, headers=None, sign_header='authorization'):

def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'):
if algorithm is None:
algorithm = DEFAULT_SIGN_ALGORITHM
algorithm = DEFAULT_ALGORITHM

super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm)
super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm)
self.headers = headers or ['date']
self.signature_template = build_signature_template(
key_id, algorithm, headers, sign_header)
key_id, algorithm, headers, sign_header)
self.sign_header = sign_header

def sign(self, headers, host=None, method=None, path=None):
Expand All @@ -112,7 +127,7 @@ def sign(self, headers, host=None, method=None, path=None):
headers = CaseInsensitiveDict(headers)
required_headers = self.headers or ['date']
signable = generate_message(
required_headers, headers, host, method, path)
required_headers, headers, host, method, path)

signature = super(HeaderSigner, self).sign(signable)
headers[self.sign_header] = self.signature_template % signature
Expand Down
61 changes: 61 additions & 0 deletions httpsig/sign_algorithms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import base64

import six
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_PSS
from httpsig.utils import HttpSigException, HASHES
from abc import ABCMeta, abstractmethod

DEFAULT_HASH_ALGORITHM = "sha512"


class SignAlgorithm(object):
__metaclass__ = ABCMeta

@abstractmethod
def sign(self, private, data):
raise NotImplementedError()

@abstractmethod
def verify(self, public, data, signature):
raise NotImplementedError()


class PSS(SignAlgorithm):

def __init__(self, hash_algorithm=DEFAULT_HASH_ALGORITHM, salt_length=None, mgfunc=None):
if hash_algorithm not in HASHES:
raise HttpSigException("Unsupported hash algorithm")

if hash_algorithm != DEFAULT_HASH_ALGORITHM:
raise HttpSigException(
"Hash algorithm: {} is deprecated. Please use: {}".format(hash_algorithm, DEFAULT_HASH_ALGORITHM))

self.hash_algorithm = HASHES[hash_algorithm]
self.salt_length = salt_length
self.mgfunc = mgfunc

def _create_pss(self, key):
try:
rsa_key = RSA.importKey(key)
pss = PKCS1_PSS.new(rsa_key, saltLen=self.salt_length, mgfunc=self.mgfunc)
except ValueError:
raise HttpSigException("Invalid key.")
return pss

def sign(self, private_key, data):
pss = self._create_pss(private_key)

if isinstance(data, six.string_types):
data = data.encode("ascii")

h = self.hash_algorithm.new()
h.update(data)
return pss.sign(h)

def verify(self, public_key, data, signature):
pss = self._create_pss(public_key)

h = self.hash_algorithm.new()
h.update(data)
return pss.verify(h, base64.b64decode(signature))
File renamed without changes.
27 changes: 27 additions & 0 deletions httpsig/tests/rsa_private_2048.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQB7eXXK+gSpDXsvZkcXd19X85iemJd0KywRH+/W+1J1j8pd+O1l
H2He8GLaDFCwFijTvTmptfMYB2XyvG8/tPpaSzaIbSBlKXWxSo1fdUMf2e7SbqVr
Fi5DolPrIfRpVqw4iqnTZZ46Y2vfa57Ee3NRF5zoagMS9BM7nfuCKvzZcUK81V75
hup5kpMHW1ofBZAPwQMm8CoXD1bpM+acN1N+63vgTY2QyUq2yJOI3HJvyFZTw+Sj
/ialYtDvDTluBH98i4504OIA6z0SCijF11irvAOSPc0GVXB8HjtUlqbD0BD6Hyqg
MeXgi9nGJhJDnJDiCVlPwg6Ni+h3nW/sXXopAgMBAAECggEANkOg8v2CAtG7647l
e3io3DxgPIMPPKykhzoj67Uz/hqdc0MtAZ4TIyk+KFn1NA3pD3U/3EfseAj4Uv9h
XPwqcnhPlRFwhUT9RldfXi5ou5zJio26ASAUYQD8JIAdrBW9RnQaQp+MNFjxVZU0
h2FBwse/25yLkU7XDQJXQFOoH988Dpozz1y8q11NxurakR67+xtqO5KG7FZdwCsN
W2Z7gTm7T59NYdHevFi2b91hdBdLWCn9RPduEvRViQY5KzzkT6cg493G3vCPXxCy
9C9aCNF7PXghy/im7dLz+H28xYls3KPOJve2dmvox2+aPH66TgXkfj/kfULJmHZq
el3dIQKBgQDAxiqPcEF1Fq4UOoipCvcpiyz0gdFFw1x58km9GOpDdDK1bqcFc2z/
GEoauWVl/PZZJdmht1zzkg4R3Izpbsg1IFxd3m7KbcfOK2bA9h2QPmjW8OwSu4/h
/l8mDsNF5crOdBnUHacgHhL1SJx323Yu3z9PmiN9wLW1gyYkh82SzQKBgQCj+LWP
1DZdsHOs224CjGjfj02PsaV5RNgD7Qqk5VcQFHzmJTAqoroPzJNjUD1sUnXXJHI0
JL533giIsxQxnyca1qtxaO6KA4baykQtKKQqKTWhE2oowS1howHRbLShq1Hxvw9S
QSS0ZAo5DyjZLMkVnlB+v7sXJR8X0Ru8qHKczQKBgQCBMEy1c/VqEpj21YNgRgj9
vleSRK2KozIGR2lDYL8eFXEmRdGIxaH2EsEWx8g8YRp3A/aleczBLtBfB/8nMSba
86TzA24cGxYcBNoH1uhZEnoQEcUjiK8UNPRu/NXAsg8H7KaikHy/+WebGd5CNMEv
CE3VeubuD4e27P1S3e/WwQKBgDzgGjASvjhcSSXUtWv2yvyszEPb1S5Hk9cpSvlb
N859fL1I8y/xCBjTf6iwYo1zs9Iy8r9PIPOJmCuAKLAfgToilrXdGipdEtTpoRQO
8ZvBfuqVNaV5yqpkBUnGDO20mBCjOUH1c3YRagYzDZxLV0BSbVoRPpliK8AA30ZU
V3DFAoGAfaPc8p6o7tCaPMpRxynIAvgIqg4sIBJdX/G4Q+SZeZR/mFlfpuhY4kzh
CL+RKAhOyOaYsSxlk4vB954y4UZFl6/t2W6gNxouelA77TgV2/rjx/fLk06J+RIF
QQkiAXwUZ2xpmdnUk+UREBwrB3LoU9kZM6fKX/LB4QEZuOmbERQ=
-----END RSA PRIVATE KEY-----
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
oYi+1hqp1fIekaxsyQIDAQAB
-----END PUBLIC KEY-----
-----END PUBLIC KEY-----
9 changes: 9 additions & 0 deletions httpsig/tests/rsa_public_2048.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQB7eXXK+gSpDXsvZkcXd19X
85iemJd0KywRH+/W+1J1j8pd+O1lH2He8GLaDFCwFijTvTmptfMYB2XyvG8/tPpa
SzaIbSBlKXWxSo1fdUMf2e7SbqVrFi5DolPrIfRpVqw4iqnTZZ46Y2vfa57Ee3NR
F5zoagMS9BM7nfuCKvzZcUK81V75hup5kpMHW1ofBZAPwQMm8CoXD1bpM+acN1N+
63vgTY2QyUq2yJOI3HJvyFZTw+Sj/ialYtDvDTluBH98i4504OIA6z0SCijF11ir
vAOSPc0GVXB8HjtUlqbD0BD6HyqgMeXgi9nGJhJDnJDiCVlPwg6Ni+h3nW/sXXop
AgMBAAE=
-----END PUBLIC KEY-----
Loading

0 comments on commit 7198be9

Please sign in to comment.