diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py index 7972e78a01195e6ba0fc5f481b09551fdf5af1c4..980b7006bc2ca6394da8aa3fff10065214c73745 100644 --- a/api/funkwhale_api/federation/authentication.py +++ b/api/funkwhale_api/federation/authentication.py @@ -9,15 +9,17 @@ from . import actors from . import keys from . import serializers from . import signing +from . import utils class SignatureAuthentication(authentication.BaseAuthentication): - def authenticate(self, request): + def authenticate_actor(self, request): + headers = utils.clean_wsgi_headers(request.META) try: - signature = request.META['headers']['Signature'] + signature = headers['Signature'] key_id = keys.get_key_id_from_signature_header(signature) except KeyError: - raise exceptions.AuthenticationFailed('No signature') + return except ValueError as e: raise exceptions.AuthenticationFailed(str(e)) @@ -33,14 +35,17 @@ class SignatureAuthentication(authentication.BaseAuthentication): serializer = serializers.ActorSerializer(data=actor_data) if not serializer.is_valid(): - raise exceptions.AuthenticationFailed('Invalid actor payload') + raise exceptions.AuthenticationFailed('Invalid actor payload: {}'.format(serializer.errors)) try: signing.verify_django(request, public_key.encode('utf-8')) except cryptography.exceptions.InvalidSignature: raise exceptions.AuthenticationFailed('Invalid signature') + return serializer.build() + + def authenticate(self, request): + actor = self.authenticate_actor(request) user = AnonymousUser() - ac = serializer.build() - setattr(request, 'actor', ac) + setattr(request, 'actor', actor) return (user, None) diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py index 6f77cbd425813381388f6d368ca5df5fc35b6691..7e4d2aa5ae08748ea5b6975aa7345d33f993ab1c 100644 --- a/api/funkwhale_api/federation/signing.py +++ b/api/funkwhale_api/federation/signing.py @@ -1,6 +1,12 @@ +import logging import requests import requests_http_signature +from . import exceptions +from . import utils + +logger = logging.getLogger(__name__) + def verify(request, public_key): return requests_http_signature.HTTPSignatureAuth.verify( @@ -15,21 +21,35 @@ def verify_django(django_request, public_key): Given a django WSGI request, create an underlying requests.PreparedRequest instance we can verify """ - headers = django_request.META.get('headers', {}).copy() + headers = utils.clean_wsgi_headers(django_request.META) for h, v in list(headers.items()): # we include lower-cased version of the headers for compatibility # with requests_http_signature headers[h.lower()] = v try: - signature = headers['signature'] + signature = headers['Signature'] except KeyError: raise exceptions.MissingSignature - + url = 'http://noop{}'.format(django_request.path) + query = django_request.META['QUERY_STRING'] + if query: + url += '?{}'.format(query) + signature_headers = signature.split('headers="')[1].split('",')[0] + expected = signature_headers.split(' ') + logger.debug('Signature expected headers: %s', expected) + for header in expected: + try: + headers[header] + except KeyError: + logger.debug('Missing header: %s', header) request = requests.Request( method=django_request.method, - url='http://noop', + url=url, data=django_request.body, headers=headers) - + for h in request.headers.keys(): + v = request.headers[h] + if v: + request.headers[h] = str(v) prepared_request = request.prepare() return verify(request, public_key) diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py index 0c4a25a7f07df8afb0dcb24855770641c0acbaa7..1837b3950f471ba549d8f45077d5b773cc010da3 100644 --- a/api/tests/federation/test_authentication.py +++ b/api/tests/federation/test_authentication.py @@ -20,14 +20,17 @@ def test_authenticate(nodb_factories, mocker, api_request): }) signed_request = nodb_factories['federation.SignedRequest']( auth__key=private, - auth__key_id=actor_url + '#main-key' + auth__key_id=actor_url + '#main-key', + auth__headers=[ + 'date', + ] ) prepared = signed_request.prepare() django_request = api_request.get( '/', - headers={ - 'Date': prepared.headers['date'], - 'Signature': prepared.headers['signature'], + **{ + 'HTTP_DATE': prepared.headers['date'], + 'HTTP_SIGNATURE': prepared.headers['signature'], } ) authenticator = authentication.SignatureAuthentication() diff --git a/api/tests/federation/test_signing.py b/api/tests/federation/test_signing.py index 9da7a0f872a03da619543bccddfe9921a3f6e7c7..0c1ec2e0ba1dcd16722c2615eeeb48a5f01d7e87 100644 --- a/api/tests/federation/test_signing.py +++ b/api/tests/federation/test_signing.py @@ -44,56 +44,67 @@ def test_verify_fails_with_wrong_key(nodb_factories): signing.verify(prepared_request, wrong_public) -def test_can_verify_django_request(factories, api_request): +def test_can_verify_django_request(factories, fake_request): private_key, public_key = keys.get_key_pair() signed_request = factories['federation.SignedRequest']( - auth__key=private_key + auth__key=private_key, + auth__headers=[ + 'date', + ] ) prepared = signed_request.prepare() - django_request = api_request.get( + django_request = fake_request.get( '/', - headers={ - 'Date': prepared.headers['date'], - 'Signature': prepared.headers['signature'], + **{ + 'HTTP_DATE': prepared.headers['date'], + 'HTTP_SIGNATURE': prepared.headers['signature'], } ) assert signing.verify_django(django_request, public_key) is None -def test_can_verify_django_request_digest(factories, api_request): +def test_can_verify_django_request_digest(factories, fake_request): private_key, public_key = keys.get_key_pair() signed_request = factories['federation.SignedRequest']( auth__key=private_key, method='post', - data=b'hello=world' + data=b'hello=world', + auth__headers=[ + 'date', + 'digest', + ] ) prepared = signed_request.prepare() - django_request = api_request.post( + django_request = fake_request.post( '/', - headers={ - 'Date': prepared.headers['date'], - 'Digest': prepared.headers['digest'], - 'Signature': prepared.headers['signature'], + **{ + 'HTTP_DATE': prepared.headers['date'], + 'HTTP_DIGEST': prepared.headers['digest'], + 'HTTP_SIGNATURE': prepared.headers['signature'], } ) assert signing.verify_django(django_request, public_key) is None -def test_can_verify_django_request_digest_failure(factories, api_request): +def test_can_verify_django_request_digest_failure(factories, fake_request): private_key, public_key = keys.get_key_pair() signed_request = factories['federation.SignedRequest']( auth__key=private_key, method='post', - data=b'hello=world' + data=b'hello=world', + auth__headers=[ + 'date', + 'digest', + ] ) prepared = signed_request.prepare() - django_request = api_request.post( + django_request = fake_request.post( '/', - headers={ - 'Date': prepared.headers['date'], - 'Digest': prepared.headers['digest'] + 'noop', - 'Signature': prepared.headers['signature'], + **{ + 'HTTP_DATE': prepared.headers['date'], + 'HTTP_DIGEST': prepared.headers['digest'] + 'noop', + 'HTTP_SIGNATURE': prepared.headers['signature'], } ) @@ -101,17 +112,20 @@ def test_can_verify_django_request_digest_failure(factories, api_request): signing.verify_django(django_request, public_key) -def test_can_verify_django_request_failure(factories, api_request): +def test_can_verify_django_request_failure(factories, fake_request): private_key, public_key = keys.get_key_pair() signed_request = factories['federation.SignedRequest']( - auth__key=private_key + auth__key=private_key, + auth__headers=[ + 'date', + ] ) prepared = signed_request.prepare() - django_request = api_request.get( + django_request = fake_request.get( '/', - headers={ - 'Date': 'Wrong', - 'Signature': prepared.headers['signature'], + **{ + 'HTTP_DATE': 'Wrong', + 'HTTP_SIGNATURE': prepared.headers['signature'], } ) with pytest.raises(cryptography.exceptions.InvalidSignature):