Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8f33aff
add version var
reverbc Jul 29, 2017
a0e45ff
pylinter
Jul 29, 2017
2a6f140
add py35 and py36 support
reverbc Jul 29, 2017
b676906
fix pep8 and pylint for sign.py
reverbc Jul 29, 2017
bde9766
fix pep8 and pylint for verify.py
reverbc Jul 29, 2017
6b35ec1
fix pep8 and pylint for requests_auth.py
reverbc Jul 29, 2017
7e8ed41
update travis-ci badge
reverbc Jul 29, 2017
26a34dd
support draft-01-07, generate correct message for sign
Jul 29, 2017
0725c18
Add test msg
Jul 29, 2017
9c79e8b
linter
Jul 29, 2017
324e1dc
remove unused methods
reverbc Jul 29, 2017
f3365f0
fix pep8 and pylint for utils.py
reverbc Jul 29, 2017
6083380
draft version check.
Jul 29, 2017
d305d15
Rename and add more case
Jul 29, 2017
0babf46
rename
Jul 29, 2017
bb19a9d
rename
Jul 29, 2017
6e2556d
do not run tests twice with tox
reverbc Jul 29, 2017
ec3929d
rename
Jul 29, 2017
50faa4e
gitignore .eggs
Jul 29, 2017
b5f686a
integrate coveralls.io
reverbc Jul 29, 2017
e43f654
Merge remote-tracking branch 'origin/dev' into dev
reverbc Jul 29, 2017
9cb9b8c
drop py32 support
reverbc Jul 29, 2017
40e799f
mv unittest
Jul 30, 2017
8f5d449
config change
Jul 30, 2017
7f9e22d
Add coveragerc. Omit unrelated python file.
Jul 30, 2017
768ad4e
mv tests
Jul 30, 2017
e6a9e63
set default algo
reverbc Jul 30, 2017
d03f699
add unit test for utils.py
reverbc Jul 30, 2017
b4b4dbd
test case for request_auth
Jul 30, 2017
e6d99fc
import cover ignore
Jul 30, 2017
f30bf7d
README update
Jul 30, 2017
7ea5f11
Update README.rst
jarronshih Jul 30, 2017
b4c13f3
Update CHANGELOG.rst
jarronshih Jul 30, 2017
88837af
fix test env
Jul 30, 2017
e973641
add test cases for verify.py
reverbc Jul 30, 2017
af22bf3
hide sign_hmac and sign_rsa methods
reverbc Jul 30, 2017
8594dd4
add test cases for sign.py
reverbc Jul 30, 2017
d28d092
Update README.rst
reverbc Jul 30, 2017
f6c75d9
revert readme for pr
reverbc Jul 31, 2017
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
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[run]
omit =
*/_version.py
*/__init__.py
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
*~
.noseids
.tox
.eggs
.coverage
build/
dist/
doc/__build/*
*_rsa
*_rsa.pub
locale/
pip-log.txt
MANIFEST
10 changes: 7 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
language: python
python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install .
- pip install nose
script: nosetests
- pip install coveralls
script:
coverage run --source=httpsig setup.py test
after_success:
coveralls
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
httpsig Changes
---------------

1.1.3 (2017-July)
-------------------

* Support IEFT ``draft-00`` to ``draft-07``
* Python version update: support ``py3.5``, ``py3.6`` and remove ``py3.2`` (due to coverage not supported)
* Python re-format by using linter.
* Complete unittest and enhance coverage to over 90%.
* Bugfix for python3 string encode and decode.


1.1.2 (2015-Feb-11)
-------------------

Expand Down
20 changes: 0 additions & 20 deletions MANIFEST

This file was deleted.

1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ include *.rst
include *.txt
include versioneer.py
include httpsig/_version.py
include httpsig/tests/*.pem
46 changes: 23 additions & 23 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ httpsig

.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=master
:target: https://travis-ci.org/ahknight/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 3`_). 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 from draft 00 to 07. 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 3`: http://tools.ietf.org/html/draft-cavage-http-signatures-03

Requirements
------------

* Python 2.7, 3.2, 3.3, 3.4
* Python 2.7, 3.3, 3.4, 3.5, 3.6
* PyCrypto_

Optional:
Expand All @@ -40,23 +39,23 @@ For simple raw signing:
.. code:: python

import httpsig

secret = open('rsa_private.pem', 'rb').read()

sig_maker = httpsig.Signer(secret=secret, algorithm='rsa-sha256')
sig_maker.sign('hello world!')

For general use with web frameworks:

.. code:: python

import httpsig
key_id = "Some Key ID"
secret = b'some big secret'
hs = httpsig.HeaderSigner(key_id, secret, algorithm="hmac-sha256", 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")

key_id = 'some key ID'
secret = 'some big secret'

hs = httpsig.HeaderSigner(key_id, secret, algorithm='hmac-sha256', 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 @@ -65,11 +64,11 @@ For use with requests:
import json
import requests
from httpsig.requests_auth import HTTPSignatureAuth

secret = open('rsa_private.pem', 'rb').read()

auth = HTTPSignatureAuth(key_id='Test', secret=secret)
z = requests.get('https://api.example.com/path/to/endpoint',
z = requests.get('https://api.example.com/path/to/endpoint',
auth=auth, headers={'X-Api-Version': '~6.5'})

Class initialization parameters
Expand All @@ -81,18 +80,19 @@ Note that keys and secrets should be bytes objects. At attempt will be made to

httpsig.Signer(secret, algorithm='rsa-sha256')

``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``,
``hmac-sha512``.
: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 hash-sign algorithm combinations: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``.


.. code:: python

httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None)
httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None, httpsig_version=None)

``key_id`` is the label by which the server system knows your RSA signature or password.
``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.
:key_id: is the label by which the server system knows your RSA signature or password.
: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.
:httpsig_version: is the IEFT version. By default it is ``draft-07`` and allowed: ``draft-00`` to ``draft-07``.
:secret: as above.
:algorithm: as above.

Tests
-----
Expand Down
1 change: 0 additions & 1 deletion httpsig/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,3 @@ def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
return (versions_from_vcs(tag_prefix, root, verbose)
or versions_from_parentdir(parentdir_prefix, root, verbose)
or default)

30 changes: 17 additions & 13 deletions httpsig/requests_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
try:
# Python 3
from urllib.parse import urlparse
except ImportError:
except ImportError: # pragma: no cover
# Python 2
from urlparse import urlparse

Expand All @@ -19,19 +19,23 @@ class HTTPSignatureAuth(AuthBase):
algorithm is one of the six specified algorithms
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, headers=None, httpsig_version=None):
headers = headers or []
self.header_signer = HeaderSigner(key_id=key_id, secret=secret,
algorithm=algorithm, headers=headers)
self.header_signer = HeaderSigner(
key_id=key_id,
secret=secret,
algorithm=algorithm,
headers=headers,
httpsig_version=httpsig_version)
self.uses_host = 'host' in [h.lower() for h in headers]

def __call__(self, r):
def __call__(self, request):
headers = self.header_signer.sign(
r.headers,
# 'Host' header unavailable in request object at this point
# if 'host' header is needed, extract it from the url
host=urlparse(r.url).netloc if self.uses_host else None,
method=r.method,
path=r.path_url)
r.headers.update(headers)
return r
request.headers,
# 'Host' header unavailable in request object at this point
# if 'host' header is needed, extract it from the url
host=urlparse(request.url).netloc if self.uses_host else None,
method=request.method,
path=request.path_url)
request.headers.update(headers)
return request
81 changes: 48 additions & 33 deletions httpsig/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,71 @@
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

from .utils import *
from .utils import ALGORITHMS, HASHES, HttpSigException, build_signature_template, CaseInsensitiveDict, generate_message


DEFAULT_SIGN_ALGORITHM = "hmac-sha256"
DEFAULT_SIGN_ALGORITHM = 'hmac-sha256'
DEFAULT_HTTPSIG_VERSION = 'draft-07'
SUPPORTED_HTTPSIG_VERSION = ['draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07']


class Signer(object):
"""
When using an RSA algo, the secret is a PEM-encoded private key.
When using an HMAC algo, the secret is the HMAC signing secret.

Password-protected keyfiles are not supported.
"""
def __init__(self, secret, algorithm=None):
if algorithm is None:
algorithm = DEFAULT_SIGN_ALGORITHM

assert algorithm in ALGORITHMS, "Unknown algorithm"
if isinstance(secret, six.string_types): secret = secret.encode("ascii")

def __init__(self, secret, algorithm=DEFAULT_SIGN_ALGORITHM):
assert algorithm in ALGORITHMS, 'Unknown 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('-')

if self.sign_algorithm == 'rsa':
try:
rsa_key = RSA.importKey(secret)
self._rsa = PKCS1_v1_5.new(rsa_key)
self._hash = HASHES[self.hash_algorithm]
except ValueError:
raise HttpSigException("Invalid key.")
raise HttpSigException('Invalid key.')

elif self.sign_algorithm == 'hmac':
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")
h = self._hash.new()
h.update(data)
return self._rsa.sign(h)
def __sign_rsa(self, data):
_hash = self._hash.new()
_hash.update(data)
return self._rsa.sign(_hash)

def _sign_hmac(self, data):
if isinstance(data, six.string_types): data = data.encode("ascii")
def __sign_hmac(self, data):
hmac = self._hash.copy()
hmac.update(data)
return hmac.digest()

def _sign(self, data):
if isinstance(data, six.string_types): data = data.encode("ascii")
'''return raw signed string'''
signed = None
if self._rsa:
signed = self._sign_rsa(data)
signed = self.__sign_rsa(data)
elif self._hash:
signed = self._sign_hmac(data)
signed = self.__sign_hmac(data)
if not signed:
raise SystemError('No valid encryptor found.')
return base64.b64encode(signed).decode("ascii")
return signed

def sign(self, data):
if isinstance(data, six.string_types):
data = data.encode('ascii')

return base64.b64encode(self._sign(data)).decode('ascii')

class HeaderSigner(Signer):
'''
Expand All @@ -78,13 +81,15 @@ class HeaderSigner(Signer):
:arg algorithm: one of the six specified algorithms
:arg headers: a list of http headers to be included in the signing string, defaulting to ['date'].
'''
def __init__(self, key_id, secret, algorithm=None, headers=None):
if algorithm is None:
algorithm = DEFAULT_SIGN_ALGORITHM

def __init__(self, key_id, secret, algorithm=DEFAULT_SIGN_ALGORITHM, headers=None, httpsig_version=DEFAULT_HTTPSIG_VERSION):
super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm)
self.headers = headers or ['date']
self.signature_template = build_signature_template(key_id, algorithm, headers)
if httpsig_version in SUPPORTED_HTTPSIG_VERSION:
self.httpsig_version = httpsig_version
else:
self.httpsig_version = DEFAULT_HTTPSIG_VERSION
self._verify_headers_by_draft_version()

def sign(self, headers, host=None, method=None, path=None):
"""
Expand All @@ -96,11 +101,21 @@ def sign(self, headers, host=None, method=None, path=None):
path is the HTTP path (required when using '(request-target)').
"""
headers = CaseInsensitiveDict(headers)
required_headers = self.headers or ['date']
signable = generate_message(required_headers, headers, host, method, path)

signature = self._sign(signable)
signable = generate_message(self.headers, headers, host, method, path)

signature = super(HeaderSigner, self).sign(signable)
headers['authorization'] = self.signature_template % signature

return headers

def _verify_headers_by_draft_version(self):
HEADER_MAPING = {
'request-line': ['draft-00', 'draft-01'],
'(request-line)': ['draft-02'],
'(request-target)': ['draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07']
}

for header, drafts in HEADER_MAPING.items():
if header in self.headers:
if self.httpsig_version not in drafts:
raise KeyError('%s is not supported by %s' % (header, self.httpsig_version))
3 changes: 0 additions & 3 deletions httpsig/tests/__init__.py

This file was deleted.

Loading