Skip to content

RSA public key signature added #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 50 additions & 14 deletions rest_framework_httpsignature/authentication.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from rest_framework import authentication
from rest_framework import exceptions
from httpsig import HeaderSigner
from httpsig import HeaderSigner, HeaderVerifier
from httpsig.utils import HttpSigException

import re


class SignatureAuthentication(authentication.BaseAuthentication):

SIGNATURE_RE = re.compile('signature="(.+?)"')
SIGNATURE_HEADERS_RE = re.compile('headers="([\(\)\sa-z0-9-]+?)"')
KEYID_RE = re.compile('.*keyId="(.*?)".*')

API_KEY_HEADER = 'X-Api-Key'
ALGORITHM = 'hmac-sha256'
REQUIRED_HEADERS = ['date']

def get_signature_from_signature_string(self, signature):
"""Return the signature from the signature header or None."""
Expand All @@ -19,6 +23,13 @@ def get_signature_from_signature_string(self, signature):
return None
return match.group(1)

def get_keyid_from_auth_string(self, auth_header):
"""Return the signature from the signature header or None."""
match = self.KEYID_RE.search(auth_header)
if not match:
return None
return match.group(1)

def get_headers_from_signature(self, signature):
"""Returns a list of headers fields to sign.

Expand Down Expand Up @@ -75,18 +86,23 @@ def fetch_user_data(self, api_key):
return None

def authenticate(self, request):
# Check for API key header.
api_key_header = self.header_canonical(self.API_KEY_HEADER)
api_key = request.META.get(api_key_header)
if not api_key:
return None

# Check if request has a "Signature" request header.
authorization_header = self.header_canonical('Authorization')
sent_string = request.META.get(authorization_header)
if not sent_string:
auth_string = request.META.get(authorization_header)
if not auth_string:
raise exceptions.AuthenticationFailed('No signature provided')
sent_signature = self.get_signature_from_signature_string(sent_string)

# Check for API key header.
api_key = None
if self.ALGORITHM.lower().startswith('rsa'):
api_key = self.get_keyid_from_auth_string(auth_string)
else:
api_key_header = self.header_canonical(self.API_KEY_HEADER)
api_key = request.META.get(api_key_header)

if not api_key:
raise exceptions.AuthenticationFailed('No api key provided')

# Fetch credentials for API key from the data store.
try:
Expand All @@ -95,11 +111,31 @@ def authenticate(self, request):
raise exceptions.AuthenticationFailed('Bad API key')

# Build string to sign from "headers" part of Signature value.
computed_string = self.build_signature(api_key, secret, request)
computed_signature = self.get_signature_from_signature_string(
computed_string)
path = request.get_full_path()
sent_signature = request.META.get(
self.header_canonical('Authorization'))
host = request.META.get(self.header_canonical('Host'))
signature_headers = self.get_headers_from_signature(sent_signature)
unsigned = self.build_dict_to_sign(request, signature_headers)

if computed_signature != sent_signature:
raise exceptions.AuthenticationFailed('Bad signature')
unsigned.update({'authorization': auth_string})

#unsigned['date'] = unsigned['date'] + 'd'

try:
hv = HeaderVerifier(headers=unsigned,
secret=secret,
required_headers=self.REQUIRED_HEADERS,
method=request.method,
path=path,
host=host)
except (HttpSigException, KeyError, Exception) as e:
raise exceptions.AuthenticationFailed(str(e))

try:
if not hv.verify():
raise exceptions.AuthenticationFailed('Bad signature')
except Exception as e:
raise exceptions.AuthenticationFailed(str(e))

return (user, api_key)
Empty file.
28 changes: 28 additions & 0 deletions rest_framework_httpsignature/tests/private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHLvR6pSVDn90y
KmUsmq0W5wraCM0U8SdltKgrfmoVpcPFz555LiNy1yKQAZRUg8GAAdtPL1Wp/NvT
ddYohhK2Pg0Aux/Zwkhh44JVIYH8dfgFCQhvcr1GzVij57vxfaNQkL/0ZyjzfyfX
hi8T/mFz/C2D8GxMfoFPvgTAiG1kprgt7ZnEPt3efHDX6qffs+9Ke4/Glb01redP
bb0BUTwwx8TjlcZ6Tho0U2NGspqGLznuHC7rL7G/uYieIFJooi0mP6MX1W/aUnVD
pZcdIkoPR3PznVzjjBSt6mnAUkXxX0aZy5sw9zWAGJub/FYzytFudiZUXSDfgwBo
/6kxrW2tAgMBAAECggEAW63UH6NlzIOHl3CGEwq6wsDjcMn+QzZgcOK/SQ2tnHso
6iKPCa3f6Rr2sJvZfzEJ3nZ8UC00W8KkF+e0BAD6GeHjsENw/JT9JflG4xJCN0bB
OugWdt20GyOnOgIOsq+mfQ2zHLZi1fjgCMadYrGCf5VCCemen3LW6DJJE6l32IxI
QN0c0nWoBMzTraL2VzcbqNbJas4RoOqCwY5H+ApqaHiQ34bpLQQffH8mMmGW+Cms
jpp6vNY3td8j1fMI2IENZfikEN1m/R2AJxJXSIrEWbilUAZkYyGAFZkybwjA2qzm
oL0xAK9+/EXIDJczE7r96U/OIgICSALTHOHtrwUkQQKBgQDsWauNlDCct9mvsbNV
gK2hYehyvsaBVIEKxizu8ING9KdBfSQwoHj3tmTgBOcQ2cTirO3TamBraPxRrsrS
GRMGUHKGFIDxxpRjyd2zYs0oK5txuwOCWEsuQdyNyhBpbwnJtRdlrK+SnfLceNiV
f38ShR++GiRRuagG3Lds+Uj2MQKBgQDXvj+97WoVpPpZhfUGMYYbH/JBUH5cYjSH
F4gYzg2/kx+6xlptg7Lt4ui14BtiwXt4I1d47qT0pUAdghPvXv9NvwxUgu5zPmaV
5YpKV00jjHAGUqyscKPrqn8fMyOSzIJnuOOMwIoSljPMPM0bV6V+xEmFR2It3gX+
G5iS2BIEPQKBgQCADvHRqypPr5mmBV1KhYcOOtNMYKuDZXrpkIjGCdDHQEXjSN+z
7S694Lh1XJKp4aQ4wUO22htV9zNHOrKv9WAGes4icbePyG2cR8L0sCLCkiYOECsN
k7NgY9URihssVTpzbMg5kcAra6Mr69pF3ifGrBSP1vA4y6QL28kSpVrv8QKBgE0S
7Yi3qX+ECeAzqB6HUMad+hj1Xb85YlSkxn0+F9FKCTrbo/Cd7S1pNAPNxVrZjneU
AKr2br3rz2T7VI3enUy0JP6ILBHFyDZi4629VJSPlnHb1U5hi14k8fc+eMX4A9p0
Re7B1lHfkS+0xP2wqTIJg852ew+x0ug+CZrkUENtAoGATbP0QXNbiGuECq/D/yht
GfNcP7owme7zotiN714kOCy9Kf3yyOt+Sb4etbSxaQRYeRhsFc4ijUYWD31V6vG1
yYES3T56fqLsT3Ia41AIEGneUAlzK/y0ZSSyOT8ftcfZdDTVHgHic1f0Tk/GlSAs
KEwzD83pe2R9r0wI6+5SWQk=
-----END PRIVATE KEY-----
28 changes: 28 additions & 0 deletions rest_framework_httpsignature/tests/private_key2.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG26OoL5SAXGj7
bOvf2WGSERXvf3qgRmFpeO65bI988r/xWv8odFs2iGZAFSDbZ6vMGx6HsLXJ731a
jIC3biSdIx55+zOx0puU3AxycLTLoSQjyzbNaY70df92qF62MYkPcPfC2gzj91uu
9W+9ZPZEVCIhrJD7oDEhnIKFKGITjQbSB3pNxTylAByRflYfkcD6uPcd7+hbOg2s
8L9Gm1ztixYAoOY0AGKY90PRKv4Duy0dTEbNuPCxLhO5pP/Vle7tovukZe/aoZu4
FoRRO+TIsDhJEx93eWXgPuiybFhL02YhgXw6qxawzXs4Fn2c4wBL0t7QeO67HlPl
bEqioCtLAgMBAAECggEAUZU0jECQ9SR0cYobLygYznsx+6LaJT0ao9HYZrwyFfnl
Y1iIzAkIjtPg1zOT2k+q/L63hMWrnyAg1nBEMnz+inUpALRdXfvglm68sIqqscv3
brPlVNqUqphqaTzkNm0WJP6ctxUMKs6Fj77jy9jK6/d0VUpd5M2wunBiX8zUh94f
osnJrsjA45KLBdUIP2Zk42wnHbursovIfxcnDOFQzQt4lNmIN/J3gfSk6pfJU0cX
HLH5SiNtIoWEQ92CzQLrmu2x7VQPocuHL06iC0LzOg/sbbIFBErY650U/xZmJf8N
no8vhyhzwqfYUKLXe2Xah/iUnPGPO2mq1opc2u5UCQKBgQDkteaS0V0aQmkZ46LK
mBgk9UUawf6Gm5SxYZfMviavMt31gKKrFWzawY8OwJOUibaKuE1Hq4umai7QHkik
rHGNxB7eeVOGz2GG6OdUMZtd+btnIlVhZMHO53JipbygmCzbVxvgzBFRzPPpDt1k
RX+H2o6ibXXyCF1i1BMfEEmmzwKBgQDeld+tu9COz5hxSOvkdE1v8HUo+ncuVNTB
3nvhO4KrIs74JUD0rRNUw9bbGuMH/LQ0NznqLQ2P/jW6Zp/O8dNp7skgO+COmPH6
XsIZSGqfJ4NFPTAunL1ZHe6dG81w4Nsc8lwaojwqajpppKf0Z+U1c7jUGqq94uT8
rosnRlPSxQKBgEdZS9YPhGj1wM33yshDDH0zGtzPGjUqAggYNwADbhQH3WCCQbz3
kR7pdVSX1TJoh87c0hcCuC0xQOtiFy1wMniUb0DePqV2uqkYrVoBo8N8be8tsc8R
XLjMUU3fAGplLtE6apMFdn27X3gcUArA95kNIKQhW8MmwuNa36A4N5HXAoGAapcq
/m+qeDlBrz5UeJqZWrmz4WPQHwfQuuZoPHvbH0kUBBETAhi/4R/HjDVb8z84rKil
u1bH3+TEpfbvIJL9wwTum9kQuDjV6Cfom2LqbDznyAh9QlUc98g1tFbUEvIa+8m0
Aa0fUtB8GIsZQxld0jMQl8INcdFuBvMvACfVjGECgYAbQi6Ia/dbUHO3MIbZNway
g9HSu8lshfTJ4xJCGKfzxdGlo5b2iGYFhgB1ynKxebnXCvz4IA1YfJT0LArCjuui
gkXy8MNhsXTAunR5LK3GFo1+GLJBw4dowp/Zc9ILHXGX5K6qFeh7eZQUiGfqVXo0
fuK7XVI3YRbZM4YDuIhFQA==
-----END PRIVATE KEY-----
9 changes: 9 additions & 0 deletions rest_framework_httpsignature/tests/public_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxy70eqUlQ5/dMiplLJqt
FucK2gjNFPEnZbSoK35qFaXDxc+eeS4jctcikAGUVIPBgAHbTy9Vqfzb03XWKIYS
tj4NALsf2cJIYeOCVSGB/HX4BQkIb3K9Rs1Yo+e78X2jUJC/9Gco838n14YvE/5h
c/wtg/BsTH6BT74EwIhtZKa4Le2ZxD7d3nxw1+qn37PvSnuPxpW9Na3nT229AVE8
MMfE45XGek4aNFNjRrKahi857hwu6y+xv7mIniBSaKItJj+jF9Vv2lJ1Q6WXHSJK
D0dz851c44wUreppwFJF8V9GmcubMPc1gBibm/xWM8rRbnYmVF0g34MAaP+pMa1t
rQIDAQAB
-----END PUBLIC KEY-----
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
from django.contrib.auth import get_user_model
from rest_framework_httpsignature.authentication import SignatureAuthentication
from rest_framework.exceptions import AuthenticationFailed
import re
import re, os
import six

User = get_user_model()

ENDPOINT = '/api'
METHOD = 'GET'
KEYID = 'some-key'
KEYID = 'somekey'
SECRET = 'my secret string'
SIGNATURE = 'some.signature'

Expand All @@ -25,7 +26,6 @@ def build_signature(headers, key_id=KEYID, signature=SIGNATURE):


class HeadersUnitTestCase(SimpleTestCase):

request = RequestFactory()

def setUp(self):
Expand Down Expand Up @@ -69,7 +69,6 @@ def test_build_signature_for_request_line(self):


class SignatureTestCase(SimpleTestCase):

def setUp(self):
self.auth = SignatureAuthentication()

Expand Down Expand Up @@ -105,7 +104,6 @@ def test_get_signature_without_headers(self):


class BuildSignatureTestCase(SimpleTestCase):

request = RequestFactory()
KEYID = 'su-key'

Expand Down Expand Up @@ -137,12 +135,11 @@ def test_build_signature(self):
signature_string = self.auth.build_signature(
self.KEYID, SECRET, req)
signature = re.match(
'.*signature="(.+)",?.*', signature_string).group(1)
'.*signature="(.+?)"', signature_string).group(1)
self.assertEqual(expected_signature, signature)


class SignatureAuthenticationTestCase(TestCase):

class APISignatureAuthentication(SignatureAuthentication):
"""Extend the SignatureAuthentication to test it."""

Expand All @@ -167,8 +164,8 @@ def setUp(self):

def test_no_credentials(self):
request = RequestFactory().get(ENDPOINT)
res = self.auth.authenticate(request)
self.assertIsNone(res)
self.assertRaises(AuthenticationFailed,
self.auth.authenticate, request)

def test_only_api_key(self):
request = RequestFactory().get(
Expand Down Expand Up @@ -204,3 +201,114 @@ def test_can_authenticate(self):
self.assertIsNotNone(result)
self.assertEqual(result[0], self.test_user)
self.assertEqual(result[1], KEYID)


class SignatureAuthenticationRSATestCase(TestCase):
class APISignatureAuthentication(SignatureAuthentication):
"""Extend the SignatureAuthentication to test it.
TODO: CLEANUP this test code
"""
ALGORITHM = 'rsa-sha256'

def __init__(self, user):
self.user = user

def fetch_user_data(self, api_key):
import os

if api_key != KEYID:
return None
public_key_path = os.path.join(os.path.dirname(__file__),
'public_key.pem')
with open(public_key_path, 'rb') as f:
public_key = f.read()
return (self.user, public_key)

TEST_USERNAME = 'test-user'
TEST_PASSWORD = 'test-password'

def setUp(self):
self.test_user = User(username=self.TEST_USERNAME)
self.test_user.set_password(self.TEST_PASSWORD)
self.auth = self.APISignatureAuthentication(self.test_user)

def test_rsa_pubkey_pass(self):

from httpsig.sign import HeaderSigner

private_key_path = os.path.join(os.path.dirname(__file__),
'private_key.pem')
with open(private_key_path, 'rb') as f:
private_key = f.read()

HOST = "example.com"
METHOD = "GET"
PATH = '/foo?param=value&pet=dog'
hs = HeaderSigner(key_id=KEYID, secret=private_key,
algorithm=self.auth.ALGORITHM,
headers=[
'(request-target)',
'host',
'date',
'content-type',
'content-md5',
'content-length'
])
unsigned = {
'Host': HOST,
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT',
'Content-Type': 'application/json',
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
'Content-Length': '18',
}
signed = hs.sign(unsigned, method=METHOD, path=PATH)

# convert headers to DJANGO format and create request
DJ_HEADERS = {}
for key, value in six.iteritems(signed):
DJ_HEADERS.update({self.auth.header_canonical(key): value})
request = RequestFactory().get(PATH, {}, **DJ_HEADERS)

result = self.auth.authenticate(request)
self.assertIsNotNone(result)
self.assertEqual(result[0], self.test_user)
self.assertEqual(result[1], KEYID)

def test_rsa_pubkey_fail(self):

from httpsig.sign import HeaderSigner

private_key_path = os.path.join(os.path.dirname(__file__),
'private_key2.pem')
with open(private_key_path, 'rb') as f:
private_key = f.read()

HOST = "example.com"
METHOD = "GET"
PATH = '/foo?param=value&pet=dog'
hs = HeaderSigner(key_id=KEYID, secret=private_key,
algorithm=self.auth.ALGORITHM,
headers=[
'(request-target)',
'host',
'date',
'content-type',
'content-md5',
'content-length'
])
unsigned = {
'Host': HOST,
'Date': 'Thu, 05 Jan 2012 21:31:40 GMT',
'Content-Type': 'application/json',
'Content-MD5': 'Sd/dVLAcvNLSq16eXua5uQ==',
'Content-Length': '18',
}
signed = hs.sign(unsigned, method=METHOD, path=PATH)

# convert headers to DJANGO format and create request
DJ_HEADERS = {}
for key, value in six.iteritems(signed):
DJ_HEADERS.update({self.auth.header_canonical(key): value})
request = RequestFactory().get(PATH, {}, **DJ_HEADERS)
self.assertRaises(AuthenticationFailed,
self.auth.authenticate, request)