diff --git a/rest_framework_httpsignature/authentication.py b/rest_framework_httpsignature/authentication.py index 5e31600..77526b5 100644 --- a/rest_framework_httpsignature/authentication.py +++ b/rest_framework_httpsignature/authentication.py @@ -1,6 +1,8 @@ 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 @@ -8,9 +10,11 @@ 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.""" @@ -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. @@ -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: @@ -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) diff --git a/rest_framework_httpsignature/tests/__init__.py b/rest_framework_httpsignature/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rest_framework_httpsignature/tests/private_key.pem b/rest_framework_httpsignature/tests/private_key.pem new file mode 100644 index 0000000..413f1f4 --- /dev/null +++ b/rest_framework_httpsignature/tests/private_key.pem @@ -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----- diff --git a/rest_framework_httpsignature/tests/private_key2.pem b/rest_framework_httpsignature/tests/private_key2.pem new file mode 100644 index 0000000..88ba2cf --- /dev/null +++ b/rest_framework_httpsignature/tests/private_key2.pem @@ -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----- diff --git a/rest_framework_httpsignature/tests/public_key.pem b/rest_framework_httpsignature/tests/public_key.pem new file mode 100644 index 0000000..9d3d24c --- /dev/null +++ b/rest_framework_httpsignature/tests/public_key.pem @@ -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----- diff --git a/rest_framework_httpsignature/tests.py b/rest_framework_httpsignature/tests/test_signatures.py similarity index 62% rename from rest_framework_httpsignature/tests.py rename to rest_framework_httpsignature/tests/test_signatures.py index e2eda6e..a03db8f 100644 --- a/rest_framework_httpsignature/tests.py +++ b/rest_framework_httpsignature/tests/test_signatures.py @@ -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' @@ -25,7 +26,6 @@ def build_signature(headers, key_id=KEYID, signature=SIGNATURE): class HeadersUnitTestCase(SimpleTestCase): - request = RequestFactory() def setUp(self): @@ -69,7 +69,6 @@ def test_build_signature_for_request_line(self): class SignatureTestCase(SimpleTestCase): - def setUp(self): self.auth = SignatureAuthentication() @@ -105,7 +104,6 @@ def test_get_signature_without_headers(self): class BuildSignatureTestCase(SimpleTestCase): - request = RequestFactory() KEYID = 'su-key' @@ -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.""" @@ -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( @@ -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)