mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-22 17:41:08 +00:00
Merge pull request #2812 from hughrun/gts
Fix federation with GoToSocial and inconsistent KeyId in headers
This commit is contained in:
commit
a4ccd45537
7 changed files with 57 additions and 18 deletions
|
@ -529,7 +529,7 @@ async def async_broadcast(recipients: List[str], sender, data: str):
|
||||||
|
|
||||||
|
|
||||||
async def sign_and_send(
|
async def sign_and_send(
|
||||||
session: aiohttp.ClientSession, sender, data: str, destination: str
|
session: aiohttp.ClientSession, sender, data: str, destination: str, **kwargs
|
||||||
):
|
):
|
||||||
"""Sign the messages and send them in an asynchronous bundle"""
|
"""Sign the messages and send them in an asynchronous bundle"""
|
||||||
now = http_date()
|
now = http_date()
|
||||||
|
@ -539,11 +539,19 @@ async def sign_and_send(
|
||||||
raise ValueError("No private key found for sender")
|
raise ValueError("No private key found for sender")
|
||||||
|
|
||||||
digest = make_digest(data)
|
digest = make_digest(data)
|
||||||
|
signature = make_signature(
|
||||||
|
"post",
|
||||||
|
sender,
|
||||||
|
destination,
|
||||||
|
now,
|
||||||
|
digest=digest,
|
||||||
|
use_legacy_key=kwargs.get("use_legacy_key"),
|
||||||
|
)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Date": now,
|
"Date": now,
|
||||||
"Digest": digest,
|
"Digest": digest,
|
||||||
"Signature": make_signature("post", sender, destination, now, digest),
|
"Signature": signature,
|
||||||
"Content-Type": "application/activity+json; charset=utf-8",
|
"Content-Type": "application/activity+json; charset=utf-8",
|
||||||
"User-Agent": USER_AGENT,
|
"User-Agent": USER_AGENT,
|
||||||
}
|
}
|
||||||
|
@ -554,6 +562,14 @@ async def sign_and_send(
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to send broadcast to %s: %s", destination, response.reason
|
"Failed to send broadcast to %s: %s", destination, response.reason
|
||||||
)
|
)
|
||||||
|
if kwargs.get("use_legacy_key") is not True:
|
||||||
|
logger.info("Trying again with legacy keyId header value")
|
||||||
|
asyncio.ensure_future(
|
||||||
|
sign_and_send(
|
||||||
|
session, sender, data, destination, use_legacy_key=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.info("Connection timed out for url: %s", destination)
|
logger.info("Connection timed out for url: %s", destination)
|
||||||
|
|
|
@ -371,7 +371,7 @@ class TagField(ManyToManyField):
|
||||||
tags.append(
|
tags.append(
|
||||||
activitypub.Link(
|
activitypub.Link(
|
||||||
href=item.remote_id,
|
href=item.remote_id,
|
||||||
name=getattr(item, item.name_field),
|
name=f"@{getattr(item, item.name_field)}",
|
||||||
type=activity_type,
|
type=activity_type,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -379,7 +379,12 @@ class TagField(ManyToManyField):
|
||||||
|
|
||||||
def field_from_activity(self, value, allow_external_connections=True):
|
def field_from_activity(self, value, allow_external_connections=True):
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
return None
|
# GoToSocial DMs and single-user mentions are
|
||||||
|
# sent as objects, not as an array of objects
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = [value]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
items = []
|
items = []
|
||||||
for link_json in value:
|
for link_json in value:
|
||||||
link = activitypub.Link(**link_json)
|
link = activitypub.Link(**link_json)
|
||||||
|
|
|
@ -142,10 +142,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
# keep notes if they mention local users
|
# keep notes if they mention local users
|
||||||
if activity.tag == MISSING or activity.tag is None:
|
if activity.tag == MISSING or activity.tag is None:
|
||||||
return True
|
return True
|
||||||
tags = [l["href"] for l in activity.tag if l["type"] == "Mention"]
|
# GoToSocial sends single tags as objects
|
||||||
|
# not wrapped in a list
|
||||||
|
tags = activity.tag if isinstance(activity.tag, list) else [activity.tag]
|
||||||
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if user_model.objects.filter(remote_id=tag, local=True).exists():
|
if (
|
||||||
|
tag["type"] == "Mention"
|
||||||
|
and user_model.objects.filter(
|
||||||
|
remote_id=tag["href"], local=True
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
# we found a mention of a known use boost
|
# we found a mention of a known use boost
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -22,7 +22,7 @@ def create_key_pair():
|
||||||
return private_key, public_key
|
return private_key, public_key
|
||||||
|
|
||||||
|
|
||||||
def make_signature(method, sender, destination, date, digest=None):
|
def make_signature(method, sender, destination, date, **kwargs):
|
||||||
"""uses a private key to sign an outgoing message"""
|
"""uses a private key to sign an outgoing message"""
|
||||||
inbox_parts = urlparse(destination)
|
inbox_parts = urlparse(destination)
|
||||||
signature_headers = [
|
signature_headers = [
|
||||||
|
@ -31,6 +31,7 @@ def make_signature(method, sender, destination, date, digest=None):
|
||||||
f"date: {date}",
|
f"date: {date}",
|
||||||
]
|
]
|
||||||
headers = "(request-target) host date"
|
headers = "(request-target) host date"
|
||||||
|
digest = kwargs.get("digest")
|
||||||
if digest is not None:
|
if digest is not None:
|
||||||
signature_headers.append(f"digest: {digest}")
|
signature_headers.append(f"digest: {digest}")
|
||||||
headers = "(request-target) host date digest"
|
headers = "(request-target) host date digest"
|
||||||
|
@ -38,8 +39,14 @@ def make_signature(method, sender, destination, date, digest=None):
|
||||||
message_to_sign = "\n".join(signature_headers)
|
message_to_sign = "\n".join(signature_headers)
|
||||||
signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key))
|
signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key))
|
||||||
signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8")))
|
signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8")))
|
||||||
|
# For legacy reasons we need to use an incorrect keyId for older Bookwyrm versions
|
||||||
|
key_id = (
|
||||||
|
f"{sender.remote_id}#main-key"
|
||||||
|
if kwargs.get("use_legacy_key")
|
||||||
|
else f"{sender.remote_id}/#main-key"
|
||||||
|
)
|
||||||
signature = {
|
signature = {
|
||||||
"keyId": f"{sender.remote_id}#main-key",
|
"keyId": key_id,
|
||||||
"algorithm": "rsa-sha256",
|
"algorithm": "rsa-sha256",
|
||||||
"headers": headers,
|
"headers": headers,
|
||||||
"signature": b64encode(signed_message).decode("utf8"),
|
"signature": b64encode(signed_message).decode("utf8"),
|
||||||
|
|
|
@ -404,7 +404,7 @@ class ModelFields(TestCase):
|
||||||
self.assertIsInstance(result, list)
|
self.assertIsInstance(result, list)
|
||||||
self.assertEqual(len(result), 1)
|
self.assertEqual(len(result), 1)
|
||||||
self.assertEqual(result[0].href, "https://e.b/c")
|
self.assertEqual(result[0].href, "https://e.b/c")
|
||||||
self.assertEqual(result[0].name, "Name")
|
self.assertEqual(result[0].name, "@Name")
|
||||||
self.assertEqual(result[0].type, "Serializable")
|
self.assertEqual(result[0].type, "Serializable")
|
||||||
|
|
||||||
def test_tag_field_from_activity(self, *_):
|
def test_tag_field_from_activity(self, *_):
|
||||||
|
|
|
@ -87,7 +87,7 @@ class Signature(TestCase):
|
||||||
data = json.dumps(get_follow_activity(sender, self.rat))
|
data = json.dumps(get_follow_activity(sender, self.rat))
|
||||||
digest = digest or make_digest(data)
|
digest = digest or make_digest(data)
|
||||||
signature = make_signature(
|
signature = make_signature(
|
||||||
"post", signer or sender, self.rat.inbox, now, digest
|
"post", signer or sender, self.rat.inbox, now, digest=digest
|
||||||
)
|
)
|
||||||
with patch("bookwyrm.views.inbox.activity_task.apply_async"):
|
with patch("bookwyrm.views.inbox.activity_task.apply_async"):
|
||||||
with patch("bookwyrm.models.user.set_remote_server.delay"):
|
with patch("bookwyrm.models.user.set_remote_server.delay"):
|
||||||
|
@ -111,6 +111,7 @@ class Signature(TestCase):
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
|
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
|
||||||
data = json.loads(datafile.read_bytes())
|
data = json.loads(datafile.read_bytes())
|
||||||
data["id"] = self.fake_remote.remote_id
|
data["id"] = self.fake_remote.remote_id
|
||||||
|
data["publicKey"]["id"] = f"{self.fake_remote.remote_id}/#main-key"
|
||||||
data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key
|
data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key
|
||||||
del data["icon"] # Avoid having to return an avatar.
|
del data["icon"] # Avoid having to return an avatar.
|
||||||
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
|
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
|
||||||
|
@ -138,6 +139,7 @@ class Signature(TestCase):
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
|
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
|
||||||
data = json.loads(datafile.read_bytes())
|
data = json.loads(datafile.read_bytes())
|
||||||
data["id"] = self.fake_remote.remote_id
|
data["id"] = self.fake_remote.remote_id
|
||||||
|
data["publicKey"]["id"] = f"{self.fake_remote.remote_id}/#main-key"
|
||||||
data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key
|
data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key
|
||||||
del data["icon"] # Avoid having to return an avatar.
|
del data["icon"] # Avoid having to return an avatar.
|
||||||
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
|
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
|
||||||
|
@ -157,7 +159,7 @@ class Signature(TestCase):
|
||||||
"bookwyrm.models.relationship.UserFollowRequest.accept"
|
"bookwyrm.models.relationship.UserFollowRequest.accept"
|
||||||
) as accept_mock:
|
) as accept_mock:
|
||||||
response = self.send_test_request(sender=self.fake_remote)
|
response = self.send_test_request(sender=self.fake_remote)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200) # BUG this is 401
|
||||||
self.assertTrue(accept_mock.called)
|
self.assertTrue(accept_mock.called)
|
||||||
|
|
||||||
# Old key is cached, so still works:
|
# Old key is cached, so still works:
|
||||||
|
|
|
@ -3,7 +3,6 @@ import json
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from urllib.parse import urldefrag
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from django.http import HttpResponse, Http404
|
from django.http import HttpResponse, Http404
|
||||||
|
@ -130,15 +129,18 @@ def has_valid_signature(request, activity):
|
||||||
"""verify incoming signature"""
|
"""verify incoming signature"""
|
||||||
try:
|
try:
|
||||||
signature = Signature.parse(request)
|
signature = Signature.parse(request)
|
||||||
|
remote_user = activitypub.resolve_remote_id(
|
||||||
key_actor = urldefrag(signature.key_id).url
|
activity.get("actor"), model=models.User
|
||||||
if key_actor != activity.get("actor"):
|
)
|
||||||
raise ValueError("Wrong actor created signature.")
|
|
||||||
|
|
||||||
remote_user = activitypub.resolve_remote_id(key_actor, model=models.User)
|
|
||||||
if not remote_user:
|
if not remote_user:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if signature.key_id != remote_user.key_pair.remote_id:
|
||||||
|
if (
|
||||||
|
signature.key_id != f"{remote_user.remote_id}#main-key"
|
||||||
|
): # legacy Bookwyrm
|
||||||
|
raise ValueError("Wrong actor created signature.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
signature.verify(remote_user.key_pair.public_key, request)
|
signature.verify(remote_user.key_pair.public_key, request)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
Loading…
Reference in a new issue