From aa7365b71fa96da8808a462d9afdc664e8e2d41f Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Sat, 24 Mar 2018 15:20:15 +0100
Subject: [PATCH] Basic logic for signing/verifying requests

---
 api/config/settings/common.py             |  1 +
 api/funkwhale_api/federation/__init__.py  |  0
 api/funkwhale_api/federation/factories.py | 30 ++++++++++++++++++++
 api/funkwhale_api/federation/signing.py   | 21 ++++++++++++++
 api/requirements/base.txt                 |  2 ++
 api/tests/conftest.py                     | 12 ++++++--
 api/tests/federation/test_signing.py      | 34 +++++++++++++++++++++++
 7 files changed, 98 insertions(+), 2 deletions(-)
 create mode 100644 api/funkwhale_api/federation/__init__.py
 create mode 100644 api/funkwhale_api/federation/factories.py
 create mode 100644 api/funkwhale_api/federation/signing.py
 create mode 100644 api/tests/federation/test_signing.py

diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 077566d1c..1144e8891 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 000000000..e69de29bb
diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
new file mode 100644
index 000000000..29aed4baf
--- /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 000000000..626dfbe86
--- /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 efcc4eea4..d4408dbdc 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 62bc5ada6..d5b3b24f4 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 000000000..712f2bd35
--- /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
+        )
-- 
GitLab