diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 077566d1c6a82e329f334a7fe94764cafbd92a70..1144e8891acbb160d9caa3314ef516d70628379b 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -89,6 +89,7 @@ LOCAL_APPS = ( 'funkwhale_api.music', 'funkwhale_api.requests', 'funkwhale_api.favorites', + 'funkwhale_api.federation', 'funkwhale_api.radios', 'funkwhale_api.history', 'funkwhale_api.playlists', diff --git a/api/funkwhale_api/federation/__init__.py b/api/funkwhale_api/federation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..29aed4baf728a316cb0398537a7cbbed0bb961ab --- /dev/null +++ b/api/funkwhale_api/federation/factories.py @@ -0,0 +1,30 @@ +import factory +import requests +import requests_http_signature + +from funkwhale_api.factories import registry + +from . import signing + + +registry.register(signing.get_key_pair, name='federation.KeyPair') + + +@registry.register(name='federation.SignatureAuth') +class SignatureAuthFactory(factory.Factory): + algorithm = 'rsa-sha256' + key = factory.LazyFunction(lambda: signing.get_key_pair()[0]) + key_id = factory.Faker('url') + + class Meta: + model = requests_http_signature.HTTPSignatureAuth + + +@registry.register(name='federation.SignedRequest') +class SignedRequestFactory(factory.Factory): + url = factory.Faker('url') + method = 'get' + auth = factory.SubFactory(SignatureAuthFactory) + + class Meta: + model = requests.Request diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py new file mode 100644 index 0000000000000000000000000000000000000000..626dfbe86b0931737b4459091eafa9de93ba6b28 --- /dev/null +++ b/api/funkwhale_api/federation/signing.py @@ -0,0 +1,21 @@ +from cryptography.hazmat.primitives import serialization as crypto_serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend as crypto_default_backend + + +def get_key_pair(size=2048): + key = rsa.generate_private_key( + backend=crypto_default_backend(), + public_exponent=65537, + key_size=size + ) + private_key = key.private_bytes( + crypto_serialization.Encoding.PEM, + crypto_serialization.PrivateFormat.PKCS8, + crypto_serialization.NoEncryption()) + public_key = key.public_key().public_bytes( + crypto_serialization.Encoding.PEM, + crypto_serialization.PublicFormat.PKCS1 + ) + + return private_key, public_key diff --git a/api/requirements/base.txt b/api/requirements/base.txt index efcc4eea40e66fadd74b25dc8cfa1c89f4e8abe7..d4408dbdc210ecd079ecc83235f25228c0873034 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -60,3 +60,5 @@ channels_redis>=2.1,<2.2 django-cacheops>=4,<4.1 daphne==2.0.4 +cryptography>=2,<3 +requests-http-signature==0.0.3 diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 62bc5ada676327aa1d5044c7bd31eaea45904dea..d5b3b24f46af5cc313217b18ae308168c4e0a6ad 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -31,7 +31,11 @@ def cache(): def factories(db): from funkwhale_api import factories for v in factories.registry.values(): - v._meta.strategy = factory.CREATE_STRATEGY + try: + v._meta.strategy = factory.CREATE_STRATEGY + except AttributeError: + # probably not a class based factory + pass yield factories.registry @@ -39,7 +43,11 @@ def factories(db): def nodb_factories(): from funkwhale_api import factories for v in factories.registry.values(): - v._meta.strategy = factory.BUILD_STRATEGY + try: + v._meta.strategy = factory.BUILD_STRATEGY + except AttributeError: + # probably not a class based factory + pass yield factories.registry diff --git a/api/tests/federation/test_signing.py b/api/tests/federation/test_signing.py new file mode 100644 index 0000000000000000000000000000000000000000..712f2bd35072d0b9e4a9b70400b0a28f9ec51210 --- /dev/null +++ b/api/tests/federation/test_signing.py @@ -0,0 +1,34 @@ +import cryptography.exceptions +import io +import pytest +import requests_http_signature + +from funkwhale_api.federation import signing + + +def test_can_sign_and_verify_request(factories): + private, public = factories['federation.KeyPair']() + auth = factories['federation.SignatureAuth'](key=private) + request = factories['federation.SignedRequest']( + auth=auth + ) + prepared_request = request.prepare() + assert 'date' in prepared_request.headers + assert 'authorization' in prepared_request.headers + assert prepared_request.headers['authorization'].startswith('Signature') + assert requests_http_signature.HTTPSignatureAuth.verify( + prepared_request, + key_resolver=lambda **kwargs: public + ) is None + + +def test_verify_fails_with_wrong_key(factories): + wrong_private, wrong_public = factories['federation.KeyPair']() + request = factories['federation.SignedRequest']() + prepared_request = request.prepare() + + with pytest.raises(cryptography.exceptions.InvalidSignature): + requests_http_signature.HTTPSignatureAuth.verify( + prepared_request, + key_resolver=lambda **kwargs: wrong_public + )