diff --git a/federation/entities/activitypub/django/views.py b/federation/entities/activitypub/django/views.py index 19ec6b5..876bdfc 100644 --- a/federation/entities/activitypub/django/views.py +++ b/federation/entities/activitypub/django/views.py @@ -24,15 +24,10 @@ def get_and_verify_signer(request): body=request.body, method=request.method, headers=request.headers) - sig = HTTPSignatureHeaderAuth.get_sig_struct(req) - signer = sig.get('keyId', '').split('#')[0] - key = get_public_key(signer) - if key: - try: - verify_request_signature(req, key) - return signer - except InvalidSignature: - return None + try: + return verify_request_signature(req) + except ValueError: + return None def activitypub_object_view(func): diff --git a/federation/protocols/activitypub/protocol.py b/federation/protocols/activitypub/protocol.py index 84f695b..a6066bb 100644 --- a/federation/protocols/activitypub/protocol.py +++ b/federation/protocols/activitypub/protocol.py @@ -5,7 +5,6 @@ from typing import Callable, Tuple, Union, Dict from cryptography.exceptions import InvalidSignature from Crypto.PublicKey.RSA import RsaKey -from requests_http_signature import HTTPSignatureHeaderAuth from federation.entities.activitypub.enums import ActorType from federation.entities.mixins import BaseEntity @@ -88,16 +87,11 @@ class Protocol: if not skip_author_verification: try: self.verify_signature() - except (KeyError, InvalidSignature) as exc: + except (ValueError, KeyError, InvalidSignature) as exc: logger.warning(f'Signature verification failed: {exc}') return self.actor, {} return self.actor, self.payload def verify_signature(self): # Verify the HTTP signature - sig = HTTPSignatureHeaderAuth.get_sig_struct(self.request) - signer = sig.get('keyId', '').split('#')[0] if sig.get('keyId') else self.actor - key = self.get_contact_key(signer) - if self.request.headers.get('Signature') and not key: - raise KeyError(f'No public key found for {signer}') - verify_request_signature(self.request, key) + verify_request_signature(self.request) diff --git a/federation/protocols/activitypub/signing.py b/federation/protocols/activitypub/signing.py index c569cf2..0e808d3 100644 --- a/federation/protocols/activitypub/signing.py +++ b/federation/protocols/activitypub/signing.py @@ -6,10 +6,14 @@ https://funkwhale.audio/ import datetime import logging from typing import Union +from urllib.parse import urlsplit import pytz from Crypto.PublicKey.RSA import RsaKey -from requests_http_signature import HTTPSignatureHeaderAuth +from httpsig.sign_algorithms import PSS +from httpsig.requests_auth import HTTPSignatureAuth +from httpsig.verify import HeaderVerifier + from federation.types import RequestType from federation.utils.network import parse_http_date @@ -18,24 +22,38 @@ from federation.utils.text import encode_if_text logger = logging.getLogger("federation") -def get_http_authentication(private_key: RsaKey, private_key_id: str) -> HTTPSignatureHeaderAuth: +def get_http_authentication(private_key: RsaKey, private_key_id: str, digest: bool=True) -> HTTPSignatureAuth: """ Get HTTP signature authentication for a request. """ key = private_key.exportKey() - return HTTPSignatureHeaderAuth( - headers=["(request-target)", "user-agent", "host", "date"], + headers = ["(request-target)", "user-agent", "host", "date"] + if digest: headers.append('digest') + return HTTPSignatureAuth( + headers=headers, algorithm="rsa-sha256", - key=key, + secret=key, key_id=private_key_id, ) -def verify_request_signature(request: RequestType, public_key: Union[str, bytes]): +def verify_request_signature(request: RequestType): """ Verify HTTP signature in request against a public key. """ - key = encode_if_text(public_key) + from federation.utils.activitypub import retrieve_and_parse_document + + sig_struct = request.headers["Signature"] + sig = {i.split("=", 1)[0]: i.split("=", 1)[1].strip('"') for i in sig_struct.split(",")} + signer = retrieve_and_parse_document(sig.get('keyId')) + if not signer: + raise ValueError("Failed to retrieve keyId") + + if not getattr(signer, 'public_key_dict', None): + raise ValueError("Failed to retrieve public key") + + key = encode_if_text(signer.public_key_dict['publicKeyPem']) + date_header = request.headers.get("Date") if not date_header: raise ValueError("Request Date header is missing") @@ -45,7 +63,13 @@ def verify_request_signature(request: RequestType, public_key: Union[str, bytes] past_delta = datetime.timedelta(hours=24) future_delta = datetime.timedelta(seconds=30) now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) - if dt < now - past_delta or dt > now + future_delta: - raise ValueError("Request Date is too far in future or past") + #if dt < now - past_delta or dt > now + future_delta: + # raise ValueError("Request Date is too far in future or past") - HTTPSignatureHeaderAuth.verify(request, key_resolver=lambda **kwargs: key) + path = getattr(request, 'path', urlsplit(request.url).path) + if not HeaderVerifier(request.headers, key, method=request.method, + path=path, sign_header='signature', + sign_algorithm=PSS() if sig.get('algorithm',None) == 'hs2019' else None).verify(): + raise ValueError("Invalid signature") + + return signer.id diff --git a/federation/tests/protocols/activitypub/test_signing.py b/federation/tests/protocols/activitypub/test_signing.py index a3b64a0..c4a4255 100644 --- a/federation/tests/protocols/activitypub/test_signing.py +++ b/federation/tests/protocols/activitypub/test_signing.py @@ -5,13 +5,13 @@ from federation.tests.fixtures.keys import get_dummy_private_key def test_signing_request(): key = get_dummy_private_key() auth = get_http_authentication(key, "dummy_key_id") - assert auth.algorithm == 'rsa-sha256' - assert auth.headers == [ + assert auth.header_signer.headers == [ '(request-target)', 'user-agent', 'host', 'date', + 'digest', ] - assert auth.key == key.exportKey() - assert auth.key_id == 'dummy_key_id' + assert auth.header_signer.secret == key.exportKey() + assert 'dummy_key_id' in auth.header_signer.signature_template diff --git a/federation/utils/activitypub.py b/federation/utils/activitypub.py index 114cce0..8ea1e3c 100644 --- a/federation/utils/activitypub.py +++ b/federation/utils/activitypub.py @@ -3,17 +3,15 @@ import logging from typing import Optional, Any from federation.protocols.activitypub.signing import get_http_authentication +from federation.utils.django import get_federation_user from federation.utils.network import fetch_document, try_retrieve_webfinger_document from federation.utils.text import decode_if_bytes, validate_handle logger = logging.getLogger('federation') -try: - from federation.utils.django import get_federation_user - federation_user = get_federation_user() -except (ImportError, AttributeError): - federation_user = None - logger.warning("django is required for get requests signing") +federation_user = get_federation_user() +if not federation_user: logger.warning("django is required for get requests signing") + def get_profile_id_from_webfinger(handle: str) -> Optional[str]: """ @@ -43,7 +41,7 @@ def retrieve_and_parse_document(fid: str, cache: bool=True) -> Optional[Any]: """ from federation.entities.activitypub.models import element_to_objects # Circulars document, status_code, ex = fetch_document(fid, extra_headers={'accept': 'application/activity+json'}, cache=cache, - auth=get_http_authentication(federation_user.rsa_private_key,f'{federation_user.id}#main-key') if federation_user else None) + auth=get_http_authentication(federation_user.rsa_private_key,f'{federation_user.id}#main-key', digest=False) if federation_user else None) if document: try: document = json.loads(decode_if_bytes(document)) diff --git a/setup.py b/setup.py index cefc9de..9126a63 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,9 @@ setup( name='federation', version=__version__, description=description, + dependency_links=[ + "https://github.com/tripougnif/python-httpsig-socialhome/tarball/master#egg=f04c890ecca4d8921cd838a96db3e3345a80b4f0-0.1" + ], long_description=get_long_description(), author='Jason Robinson', author_email='mail@jasonrobinson.me', @@ -31,7 +34,7 @@ setup( "bleach>3.0", "calamus", "commonmark", - "cryptography<=3.4.7", + "cryptography", "cssselect>=0.9.2", "dirty-validators>=0.3.0", "lxml>=3.4.0", @@ -47,7 +50,7 @@ setup( "redis", "requests>=2.8.0", "requests-cache", - "requests-http-signature-jaywink>=0.1.0.dev0", + "f04c890ecca4d8921cd838a96db3e3345a80b4f0-0.1", ], include_package_data=True, classifiers=[