kopia lustrzana https://gitlab.com/jaywink/federation
Implement automatic remote profile updates. Reduce remote requests frequency. WIP: implement profile retractions.
rodzic
a0c4e7fb6e
commit
6b9c74b793
|
@ -24,7 +24,7 @@ def get_and_verify_signer(request):
|
||||||
method=request.method,
|
method=request.method,
|
||||||
headers=request.headers)
|
headers=request.headers)
|
||||||
try:
|
try:
|
||||||
return verify_request_signature(req, required=False)
|
return verify_request_signature(req)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from Crypto.Hash import SHA256
|
||||||
from Crypto.PublicKey.RSA import import_key
|
from Crypto.PublicKey.RSA import import_key
|
||||||
from Crypto.Signature import pkcs1_15
|
from Crypto.Signature import pkcs1_15
|
||||||
|
|
||||||
|
from federation.entities.utils import get_profile
|
||||||
from federation.utils.activitypub import retrieve_and_parse_document
|
from federation.utils.activitypub import retrieve_and_parse_document
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,9 +54,10 @@ def verify_ld_signature(payload):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# retrieve the author's public key
|
# retrieve the author's public key
|
||||||
|
profile = get_profile(key_id=signature.get('creator'))
|
||||||
|
if not profile:
|
||||||
profile = retrieve_and_parse_document(signature.get('creator'))
|
profile = retrieve_and_parse_document(signature.get('creator'))
|
||||||
if not profile:
|
if not profile:
|
||||||
|
|
||||||
logger.warning('ld_signature - Failed to retrieve profile for %s', signature.get("creator"))
|
logger.warning('ld_signature - Failed to retrieve profile for %s', signature.get("creator"))
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -617,11 +617,16 @@ class Person(Object, base.Profile):
|
||||||
self.inbox = value.get('private', None)
|
self.inbox = value.get('private', None)
|
||||||
self.endpoints = {'sharedInbox': value.get('public', 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
|
@property
|
||||||
def public_key(self):
|
def public_key(self):
|
||||||
if self._cached_public_key: return self._cached_public_key
|
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)
|
self._cached_public_key = self.public_key_dict.get('publicKeyPem', None)
|
||||||
|
|
||||||
return self._cached_public_key
|
return self._cached_public_key
|
||||||
|
@ -982,7 +987,7 @@ class Video(Document, base.Video):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.__dict__.update({'schema': True})
|
self.__dict__.update({'schema': True})
|
||||||
if hasattr(self, 'content_map'):
|
if self.content_map is not missing:
|
||||||
text = self.content_map['orig']
|
text = self.content_map['orig']
|
||||||
if getattr(self, 'media_type', None) == 'text/markdown':
|
if getattr(self, 'media_type', None) == 'text/markdown':
|
||||||
url = ""
|
url = ""
|
||||||
|
@ -994,7 +999,7 @@ class Video(Document, base.Video):
|
||||||
self.raw_content = text.strip()
|
self.raw_content = text.strip()
|
||||||
self._media_type = self.media_type
|
self._media_type = self.media_type
|
||||||
|
|
||||||
if hasattr(self, 'actor_id'):
|
if self.actor_id is not missing:
|
||||||
act = self.actor_id
|
act = self.actor_id
|
||||||
new_act = []
|
new_act = []
|
||||||
if not isinstance(act, list): act = [act]
|
if not isinstance(act, list): act = [act]
|
||||||
|
@ -1225,7 +1230,7 @@ class Delete(Create, base.Retraction):
|
||||||
signable = True
|
signable = True
|
||||||
|
|
||||||
def to_base(self):
|
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.target_id = self.object_
|
||||||
self.entity_type = 'Object'
|
self.entity_type = 'Object'
|
||||||
return self
|
return self
|
||||||
|
@ -1253,7 +1258,7 @@ class View(Create):
|
||||||
def process_followers(obj, base_url):
|
def process_followers(obj, base_url):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def extract_receiver(profile, receiver):
|
def extract_receiver(author, receiver):
|
||||||
"""
|
"""
|
||||||
Transform a single receiver ID to a UserType.
|
Transform a single receiver ID to a UserType.
|
||||||
"""
|
"""
|
||||||
|
@ -1267,11 +1272,18 @@ def extract_receiver(profile, receiver):
|
||||||
|
|
||||||
if isinstance(obj, base.Profile):
|
if isinstance(obj, base.Profile):
|
||||||
return [UserType(id=receiver, receiver_variant=ReceiverVariant.ACTOR)]
|
return [UserType(id=receiver, receiver_variant=ReceiverVariant.ACTOR)]
|
||||||
# This doesn't handle cases where the actor is sending to other actors
|
|
||||||
|
# This handles cases where the actor is sending to other actors
|
||||||
# followers (seen on PeerTube)
|
# followers (seen on PeerTube)
|
||||||
if profile.followers == receiver:
|
if isinstance(obj, base.Collection):
|
||||||
|
profile = get_profile(followers_fid=obj.id)
|
||||||
|
if profile:
|
||||||
return [UserType(id=profile.id, receiver_variant=ReceiverVariant.FOLLOWERS)]
|
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):
|
def extract_receivers(entity):
|
||||||
"""
|
"""
|
||||||
|
@ -1281,8 +1293,9 @@ def extract_receivers(entity):
|
||||||
profile = None
|
profile = None
|
||||||
# don't care about receivers for payloads without an actor_id
|
# don't care about receivers for payloads without an actor_id
|
||||||
if getattr(entity, 'actor_id'):
|
if getattr(entity, 'actor_id'):
|
||||||
profile = retrieve_and_parse_profile(entity.actor_id)
|
profile = get_profile_or_entity(entity.actor_id)
|
||||||
if not profile: return receivers
|
if not isinstance(profile, base.Profile):
|
||||||
|
return receivers
|
||||||
|
|
||||||
for attr in ("to", "cc"):
|
for attr in ("to", "cc"):
|
||||||
receiver = getattr(entity, attr, None)
|
receiver = getattr(entity, attr, None)
|
||||||
|
|
|
@ -31,7 +31,9 @@ class BaseEntity:
|
||||||
guid: str = ""
|
guid: str = ""
|
||||||
handle: str = ""
|
handle: str = ""
|
||||||
finger: str = ""
|
finger: str = ""
|
||||||
|
followers: str = ""
|
||||||
id: str = ""
|
id: str = ""
|
||||||
|
key_id: str = ""
|
||||||
mxid: str = ""
|
mxid: str = ""
|
||||||
signature: str = ""
|
signature: str = ""
|
||||||
# for AP
|
# for AP
|
||||||
|
|
|
@ -88,7 +88,8 @@ class Protocol:
|
||||||
if not skip_author_verification:
|
if not skip_author_verification:
|
||||||
try:
|
try:
|
||||||
# Verify the HTTP signature
|
# 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:
|
except (ValueError, KeyError, InvalidSignature) as exc:
|
||||||
logger.warning('HTTP signature verification failed: %s', exc)
|
logger.warning('HTTP signature verification failed: %s', exc)
|
||||||
return self.actor, {}
|
return self.actor, {}
|
||||||
|
|
|
@ -13,6 +13,7 @@ from httpsig.sign_algorithms import PSS
|
||||||
from httpsig.requests_auth import HTTPSignatureAuth
|
from httpsig.requests_auth import HTTPSignatureAuth
|
||||||
from httpsig.verify import HeaderVerifier
|
from httpsig.verify import HeaderVerifier
|
||||||
|
|
||||||
|
from federation.entities.utils import get_profile
|
||||||
from federation.types import RequestType
|
from federation.types import RequestType
|
||||||
from federation.utils.network import parse_http_date
|
from federation.utils.network import parse_http_date
|
||||||
from federation.utils.text import encode_if_text
|
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.
|
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)
|
sig_struct = request.headers.get("Signature", None)
|
||||||
if not sig_struct:
|
if not sig_struct:
|
||||||
if required:
|
|
||||||
raise ValueError("A signature is required but was not provided")
|
raise ValueError("A signature is required but was not provided")
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# this should return a dict populated with the following keys:
|
# this should return a dict populated with the following keys:
|
||||||
# keyId, algorithm, headers and signature
|
# keyId, algorithm, headers and signature
|
||||||
sig = {i.split("=", 1)[0]: i.split("=", 1)[1].strip('"') for i in sig_struct.split(",")}
|
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:
|
if not signer:
|
||||||
raise ValueError(f"Failed to retrieve keyId for {sig.get('keyId')}")
|
signer = retrieve_and_parse_document(sig.get('keyId'))
|
||||||
|
key = getattr(signer, 'public_key', None)
|
||||||
if not getattr(signer, 'public_key_dict', None):
|
if not key and pubkey:
|
||||||
raise ValueError(f"Failed to retrieve public key for {sig.get('keyId')}")
|
# 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 = encode_if_text(signer.public_key_dict['publicKeyPem'])
|
key = pubkey
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No public key for {sig.get('keyId')}")
|
||||||
|
|
||||||
|
key = encode_if_text(key)
|
||||||
date_header = request.headers.get("Date")
|
date_header = request.headers.get("Date")
|
||||||
if not date_header:
|
if not date_header:
|
||||||
raise ValueError("Request Date header is missing")
|
raise ValueError("Request Date header is missing")
|
||||||
|
|
Ładowanie…
Reference in New Issue