Skip to content
This repository was archived by the owner on Oct 25, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6958fe3
Add hs2019 to algorithm list and set to default sign algorithm
fulder Aug 24, 2020
0e18601
Print deprecation message on old algorithms
fulder Aug 24, 2020
39298a0
Requre digital signature algorithm while using 'hs2019' algorithm
fulder Aug 24, 2020
3e74497
Set algorithm class variable instead of property
fulder Aug 24, 2020
813d2a1
Remove duplicated default sign algorithm
fulder Aug 24, 2020
c2654c7
Rename long digital signature algorithm to just sign_algorithm
fulder Aug 24, 2020
bd001d8
Add sign_algorithm to HeaderSigner docstring
fulder Aug 24, 2020
79ca3a2
Require sign_algorithm for 'hs2019' algorith in Verifier
fulder Aug 24, 2020
86ef109
Accept None sign_algorithm for backward compability
fulder Aug 24, 2020
4052954
Take back setting algorithm to default twice
fulder Aug 24, 2020
1e01f5c
Update tests to hs2019 and add one deprecated test using old keys
fulder Aug 24, 2020
619a13c
Make PSS salt configurable
fulder Aug 24, 2020
0a57e36
Set default salt to None, i.e. digest size
fulder Aug 24, 2020
c4e36fb
Create sign_algorithms with first PSS class
fulder Aug 25, 2020
68b7369
Test creating superclass for sign algorithm
fulder Aug 25, 2020
2060e83
Check for subclasses of SignAlgorithms instead of hardcoded list
fulder Aug 25, 2020
dc2b90b
Rename default sign algorithm to default algorithm
fulder Aug 25, 2020
76c26c7
Fix failing test due to invalid signature
fulder Aug 25, 2020
179ab94
Import sign_algorithms in httpsig init
fulder Aug 25, 2020
4613083
Update README with newest algorithm
fulder Aug 25, 2020
1b061f9
Add sign_algorithm parameter to HTTPSignatureAuth
fulder Aug 25, 2020
77c42ad
Use issubclass instead of isinstance in Singer construct
fulder Aug 25, 2020
08ca125
Remove old unused HeaderVerfier salt_length parameter
fulder Aug 25, 2020
fb263d1
Enforce parameters for superclass sign/verify funcs
fulder Aug 25, 2020
f97f6c0
Bump README draft to version 12
fulder Aug 25, 2020
c5163f8
Throw exception on algorithm signature parameter and dervice algorith…
fulder Aug 26, 2020
912ff85
Return false in verify on algorith mismatch
fulder Aug 26, 2020
822cad0
Add test for correct derived algorithm
fulder Aug 26, 2020
251cce9
Move algorithm checks to Signer
fulder Aug 27, 2020
c6951c2
Fix typo in test name
fulder Aug 27, 2020
f517f39
Default to hash lenght for salt in PSS
fulder Aug 28, 2020
d2be3fb
Merge pull request #5 from fulder/default-to-hash-salt-length
fulder Aug 28, 2020
f419b34
Get hash digest_size in PSS construct
fulder Aug 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
53 changes: 35 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,34 @@ 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))
elif algorithm == DEFAULT_ALGORITHM and sign_algorithm is None:
raise HttpSigException("Required sign algorithm for {} algorithm not set".format(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 +59,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 +81,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 +98,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 +129,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
66 changes: 66 additions & 0 deletions httpsig/sign_algorithms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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

if self.salt_length is None:
self.salt_length = self.hash_algorithm.digest_size

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):
if isinstance(data, six.string_types):
data = data.encode("ascii")

h = self.hash_algorithm.new()
h.update(data)

pss = self._create_pss(private_key)

return pss.sign(h)

def verify(self, public_key, data, signature):
h = self.hash_algorithm.new()
h.update(data)

pss = self._create_pss(public_key)

return pss.verify(h, base64.b64decode(signature))
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