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
+        )