From 9017acdb393f955b50f8f6204426ebb0ad67bed2 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 24 Jun 2018 19:17:56 +0200 Subject: [PATCH] Fix #328: Validate Date header in HTTP Signatures --- api/funkwhale_api/federation/factories.py | 3 ++- api/funkwhale_api/federation/signing.py | 31 +++++++++++++++++++++++ api/tests/federation/test_signing.py | 27 +++++++++++++++++--- changes/changelog.d/328.bugfix | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 changes/changelog.d/328.bugfix diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 7370ebd77..7dad1daa9 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -5,6 +5,7 @@ import requests import requests_http_signature from django.conf import settings from django.utils import timezone +from django.utils.http import http_date from funkwhale_api.factories import registry @@ -39,7 +40,7 @@ class SignedRequestFactory(factory.Factory): default_headers = { "User-Agent": "Test", "Host": "test.host", - "Date": "Right now", + "Date": http_date(timezone.now().timestamp()), "Content-Type": "application/activity+json", } if extracted: diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py index 15525b3e5..5b74b8577 100644 --- a/api/funkwhale_api/federation/signing.py +++ b/api/funkwhale_api/federation/signing.py @@ -1,4 +1,10 @@ +import datetime import logging +import pytz + +from django import forms +from django.utils import timezone +from django.utils.http import parse_http_date import requests import requests_http_signature @@ -7,8 +13,33 @@ from . import exceptions, utils logger = logging.getLogger(__name__) +#  the request Date should be between now - 30s and now + 30s +DATE_HEADER_VALID_FOR = 30 + + +def verify_date(raw_date): + if not raw_date: + raise forms.ValidationError("Missing date header") + + try: + ts = parse_http_date(raw_date) + except ValueError as e: + raise forms.ValidationError(str(e)) + dt = datetime.datetime.utcfromtimestamp(ts) + dt = dt.replace(tzinfo=pytz.utc) + delta = datetime.timedelta(seconds=DATE_HEADER_VALID_FOR) + now = timezone.now() + if dt < now - delta or dt > now + delta: + raise forms.ValidationError( + "Request Date is too far in the future or in the past" + ) + + return dt + def verify(request, public_key): + verify_date(request.headers.get("Date")) + return requests_http_signature.HTTPSignatureAuth.verify( request, key_resolver=lambda **kwargs: public_key, use_auth_header=False ) diff --git a/api/tests/federation/test_signing.py b/api/tests/federation/test_signing.py index 159f31cd9..298462142 100644 --- a/api/tests/federation/test_signing.py +++ b/api/tests/federation/test_signing.py @@ -1,4 +1,7 @@ import cryptography.exceptions +import datetime +from django.utils.http import http_date +from django import forms import pytest from funkwhale_api.federation import keys, signing @@ -36,6 +39,20 @@ def test_verify_fails_with_wrong_key(nodb_factories): signing.verify(prepared_request, wrong_public) +def test_verify_fails_with_wrong_date(nodb_factories, now): + too_old = now - datetime.timedelta(seconds=31) + too_old = http_date(too_old.timestamp()) + private, public = nodb_factories["federation.KeyPair"]() + auth = nodb_factories["federation.SignatureAuth"](key=private) + request = nodb_factories["federation.SignedRequest"]( + auth=auth, headers={"Date": too_old} + ) + prepared_request = request.prepare() + + with pytest.raises(forms.ValidationError): + signing.verify(prepared_request, public) + + def test_can_verify_django_request(factories, fake_request): private_key, public_key = keys.get_key_pair() signed_request = factories["federation.SignedRequest"]( @@ -95,14 +112,18 @@ def test_can_verify_django_request_digest_failure(factories, fake_request): signing.verify_django(django_request, public_key) -def test_can_verify_django_request_failure(factories, fake_request): +def test_can_verify_django_request_failure(factories, fake_request, now): private_key, public_key = keys.get_key_pair() signed_request = factories["federation.SignedRequest"]( auth__key=private_key, auth__headers=["date"] ) prepared = signed_request.prepare() django_request = fake_request.get( - "/", **{"HTTP_DATE": "Wrong", "HTTP_SIGNATURE": prepared.headers["signature"]} + "/", + **{ + "HTTP_DATE": http_date((now + datetime.timedelta(seconds=31)).timestamp()), + "HTTP_SIGNATURE": prepared.headers["signature"], + } ) - with pytest.raises(cryptography.exceptions.InvalidSignature): + with pytest.raises(forms.ValidationError): signing.verify_django(django_request, public_key) diff --git a/changes/changelog.d/328.bugfix b/changes/changelog.d/328.bugfix new file mode 100644 index 000000000..cb28e4c97 --- /dev/null +++ b/changes/changelog.d/328.bugfix @@ -0,0 +1 @@ +Validate Date header in HTTP Signatures (#328)