diff --git a/federation/entities/activitypub/django/views.py b/federation/entities/activitypub/django/views.py index 93f53c1..e6eb688 100644 --- a/federation/entities/activitypub/django/views.py +++ b/federation/entities/activitypub/django/views.py @@ -24,7 +24,7 @@ def get_and_verify_signer(request): method=request.method, headers=request.headers) try: - return verify_request_signature(req, required=False) + return verify_request_signature(req) except ValueError: return None diff --git a/federation/entities/activitypub/ldsigning.py b/federation/entities/activitypub/ldsigning.py index c199324..381f419 100644 --- a/federation/entities/activitypub/ldsigning.py +++ b/federation/entities/activitypub/ldsigning.py @@ -11,6 +11,7 @@ from Crypto.Hash import SHA256 from Crypto.PublicKey.RSA import import_key from Crypto.Signature import pkcs1_15 +from federation.entities.utils import get_profile from federation.utils.activitypub import retrieve_and_parse_document @@ -53,9 +54,10 @@ def verify_ld_signature(payload): return None # retrieve the author's public key - profile = retrieve_and_parse_document(signature.get('creator')) + profile = get_profile(key_id=signature.get('creator')) + if not profile: + profile = retrieve_and_parse_document(signature.get('creator')) if not profile: - logger.warning('ld_signature - Failed to retrieve profile for %s', signature.get("creator")) return None try: diff --git a/federation/entities/activitypub/models.py b/federation/entities/activitypub/models.py index 380fbcb..23d31cf 100644 --- a/federation/entities/activitypub/models.py +++ b/federation/entities/activitypub/models.py @@ -617,11 +617,16 @@ class Person(Object, base.Profile): self.inbox = value.get('private', None) self.endpoints = {'sharedInbox': value.get('public', None)} + @property + def key_id(self): + if isinstance(self.public_key_dict, dict): + return self.public_key_dict.get('id', None) + @property def public_key(self): if self._cached_public_key: return self._cached_public_key - if hasattr(self, 'public_key_dict') and isinstance(self.public_key_dict, dict): + if isinstance(self.public_key_dict, dict): self._cached_public_key = self.public_key_dict.get('publicKeyPem', None) return self._cached_public_key @@ -982,7 +987,7 @@ class Video(Document, base.Video): """ self.__dict__.update({'schema': True}) - if hasattr(self, 'content_map'): + if self.content_map is not missing: text = self.content_map['orig'] if getattr(self, 'media_type', None) == 'text/markdown': url = "" @@ -994,7 +999,7 @@ class Video(Document, base.Video): self.raw_content = text.strip() self._media_type = self.media_type - if hasattr(self, 'actor_id'): + if self.actor_id is not missing: act = self.actor_id new_act = [] if not isinstance(act, list): act = [act] @@ -1225,7 +1230,7 @@ class Delete(Create, base.Retraction): signable = True def to_base(self): - if hasattr(self, 'object_') and not isinstance(self.object_, Tombstone): + if not isinstance(self.object_, Tombstone): self.target_id = self.object_ self.entity_type = 'Object' return self @@ -1253,7 +1258,7 @@ class View(Create): def process_followers(obj, base_url): pass -def extract_receiver(profile, receiver): +def extract_receiver(author, receiver): """ Transform a single receiver ID to a UserType. """ @@ -1267,11 +1272,18 @@ def extract_receiver(profile, receiver): if isinstance(obj, base.Profile): return [UserType(id=receiver, receiver_variant=ReceiverVariant.ACTOR)] - # This doesn't handle cases where the actor is sending to other actors - # followers (seen on PeerTube) - if profile.followers == receiver: - return [UserType(id=profile.id, receiver_variant=ReceiverVariant.FOLLOWERS)] + # This handles cases where the actor is sending to other actors + # followers (seen on PeerTube) + if isinstance(obj, base.Collection): + profile = get_profile(followers_fid=obj.id) + if profile: + return [UserType(id=profile.id, receiver_variant=ReceiverVariant.FOLLOWERS)] + + if author.followers == receiver: + return [UserType(id=author.id, receiver_variant=ReceiverVariant.FOLLOWERS)] + + return [] def extract_receivers(entity): """ @@ -1281,8 +1293,9 @@ def extract_receivers(entity): profile = None # don't care about receivers for payloads without an actor_id if getattr(entity, 'actor_id'): - profile = retrieve_and_parse_profile(entity.actor_id) - if not profile: return receivers + profile = get_profile_or_entity(entity.actor_id) + if not isinstance(profile, base.Profile): + return receivers for attr in ("to", "cc"): receiver = getattr(entity, attr, None) diff --git a/federation/entities/mixins.py b/federation/entities/mixins.py index bbc4075..30ef9d8 100644 --- a/federation/entities/mixins.py +++ b/federation/entities/mixins.py @@ -31,7 +31,9 @@ class BaseEntity: guid: str = "" handle: str = "" finger: str = "" + followers: str = "" id: str = "" + key_id: str = "" mxid: str = "" signature: str = "" # for AP diff --git a/federation/protocols/activitypub/protocol.py b/federation/protocols/activitypub/protocol.py index fc2e95f..9d6ce6f 100644 --- a/federation/protocols/activitypub/protocol.py +++ b/federation/protocols/activitypub/protocol.py @@ -88,7 +88,8 @@ class Protocol: if not skip_author_verification: try: # Verify the HTTP signature - self.sender = verify_request_signature(self.request) + pubkey = sender_key_fetcher(self.actor) if sender_key_fetcher else '' + self.sender = verify_request_signature(self.request, pubkey=pubkey) except (ValueError, KeyError, InvalidSignature) as exc: logger.warning('HTTP signature verification failed: %s', exc) return self.actor, {} diff --git a/federation/protocols/activitypub/signing.py b/federation/protocols/activitypub/signing.py index 29ac4fd..5f164f6 100644 --- a/federation/protocols/activitypub/signing.py +++ b/federation/protocols/activitypub/signing.py @@ -13,6 +13,7 @@ from httpsig.sign_algorithms import PSS from httpsig.requests_auth import HTTPSignatureAuth from httpsig.verify import HeaderVerifier +from federation.entities.utils import get_profile from federation.types import RequestType from federation.utils.network import parse_http_date from federation.utils.text import encode_if_text @@ -35,7 +36,7 @@ def get_http_authentication(private_key: RsaKey, private_key_id: str, digest: bo ) -def verify_request_signature(request: RequestType, required: bool=True): +def verify_request_signature(request: RequestType, pubkey: str=""): """ Verify HTTP signature in request against a public key. """ @@ -43,23 +44,23 @@ def verify_request_signature(request: RequestType, required: bool=True): 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 + raise ValueError("A signature is required but was not provided") # 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')) + signer = get_profile(key_id=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']) + signer = retrieve_and_parse_document(sig.get('keyId')) + key = getattr(signer, 'public_key', None) + if not key and pubkey: + # fallback to the author's key the client app may have provided + logger.warning("Failed to retrieve keyId for %s, trying the actor's key", sig.get('keyId')) + key = pubkey + else: + raise ValueError(f"No public key for {sig.get('keyId')}") + key = encode_if_text(key) date_header = request.headers.get("Date") if not date_header: raise ValueError("Request Date header is missing")