diff --git a/federation/entities/activitypub/django/views.py b/federation/entities/activitypub/django/views.py index 19ec6b5..93f53c1 100644 --- a/federation/entities/activitypub/django/views.py +++ b/federation/entities/activitypub/django/views.py @@ -1,6 +1,5 @@ from cryptography.exceptions import InvalidSignature from django.http import JsonResponse, HttpResponse, HttpResponseNotFound -from requests_http_signature import HTTPSignatureHeaderAuth from federation.entities.activitypub.mappers import get_outbound_entity from federation.protocols.activitypub.signing import verify_request_signature @@ -24,15 +23,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, required=False) + except ValueError: + return None def activitypub_object_view(func): diff --git a/federation/entities/activitypub/models.py b/federation/entities/activitypub/models.py index dffc493..f891678 100644 --- a/federation/entities/activitypub/models.py +++ b/federation/entities/activitypub/models.py @@ -256,7 +256,7 @@ OBJECTS = [ def set_public(entity): - for attr in [getattr(entity, 'to', []), getattr(entity, 'cc' ,[])]: + for attr in [entity.to, entity.cc]: if isinstance(attr, list): if NAMESPACE_PUBLIC in attr: entity.public = True elif attr == NAMESPACE_PUBLIC: entity.public = True @@ -1318,8 +1318,8 @@ def extract_replies(replies): visited = [] def walk_reply_collection(replies): - items = getattr(replies, 'items', []) - if items and not isinstance(items, list): items = [items] + items = replies.items if replies.items is not missing else [] + if not isinstance(items, list): items = [items] for obj in items: if isinstance(obj, Note): try: @@ -1330,7 +1330,7 @@ def extract_replies(replies): continue elif not isinstance(obj, str): continue objs.append(obj) - if getattr(replies, 'next_', None): + if replies.next_ is not missing: if (replies.id != replies.next_) and (replies.next_ not in visited): resp = retrieve_and_parse_document(replies.next_) if resp: 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..9620c9c 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,46 @@ 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, required: bool=True): """ 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.get("Signature", None) + if not sig_struct: + if required: + raise ValueError("A signature is required but was not provided") + else: + return None + + # this should return a dict populated with the following keys: + # keyId, algorithm, headers and 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(f"Failed to retrieve keyId for {sig.get('keyId')}") + + if not getattr(signer, 'public_key_dict', None): + raise ValueError(f"Failed to retrieve public key for {sig.get('keyId')}") + + 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") @@ -48,4 +74,10 @@ def verify_request_signature(request: RequestType, public_key: Union[str, bytes] 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..85084dd 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,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 +47,7 @@ setup( "redis", "requests>=2.8.0", "requests-cache", - "requests-http-signature-jaywink>=0.1.0.dev0", + "httpsig @ git+https://github.com/tripougnif/python-httpsig-socialhome.git@ce03fa7b25acfacc14fba2670c33246025db7be0#egg=httpsig==0.1", ], include_package_data=True, classifiers=[