mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-30 13:31:08 +00:00
commit
4190793f17
4 changed files with 133 additions and 24 deletions
|
@ -63,37 +63,38 @@ def broadcast_task(sender_id, activity, recipients):
|
|||
return errors
|
||||
|
||||
|
||||
def sign_and_send(sender, activity, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
def make_signature(sender, destination, date):
|
||||
inbox_parts = urlparse(destination)
|
||||
now = http_date()
|
||||
signature_headers = [
|
||||
'(request-target): post %s' % inbox_parts.path,
|
||||
'host: %s' % inbox_parts.netloc,
|
||||
'date: %s' % now
|
||||
'date: %s' % date,
|
||||
]
|
||||
message_to_sign = '\n'.join(signature_headers)
|
||||
|
||||
if not sender.private_key:
|
||||
# this shouldn't happen. it would be bad if it happened.
|
||||
raise ValueError('No private key found for sender')
|
||||
signer = pkcs1_15.new(RSA.import_key(sender.private_key))
|
||||
signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8')))
|
||||
|
||||
signature = {
|
||||
'keyId': '%s#main-key' % sender.actor,
|
||||
'algorithm': 'rsa-sha256',
|
||||
'headers': '(request-target) host date',
|
||||
'signature': b64encode(signed_message).decode('utf8'),
|
||||
}
|
||||
signature = ','.join('%s="%s"' % (k, v) for (k, v) in signature.items())
|
||||
return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items())
|
||||
|
||||
def sign_and_send(sender, activity, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
now = http_date()
|
||||
|
||||
if not sender.private_key:
|
||||
# this shouldn't happen. it would be bad if it happened.
|
||||
raise ValueError('No private key found for sender')
|
||||
|
||||
response = requests.post(
|
||||
destination,
|
||||
data=json.dumps(activity),
|
||||
headers={
|
||||
'Date': now,
|
||||
'Signature': signature,
|
||||
'Signature': make_signature(sender, destination, now),
|
||||
'Content-Type': 'application/activity+json; charset=utf-8',
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
''' handles all of the activity coming in to the server '''
|
||||
import json
|
||||
from base64 import b64decode
|
||||
from urllib.parse import urldefrag
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
|
@ -46,7 +48,7 @@ def shared_inbox(request):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
verify_signature(request)
|
||||
verify_signature(activity.get('actor'), request)
|
||||
except ValueError:
|
||||
return HttpResponse(status=401)
|
||||
|
||||
|
@ -82,7 +84,24 @@ def shared_inbox(request):
|
|||
return HttpResponse()
|
||||
|
||||
|
||||
def verify_signature(request):
|
||||
def get_public_key(key_actor):
|
||||
try:
|
||||
user = models.User.objects.get(actor=key_actor)
|
||||
public_key = user.public_key
|
||||
actor = user.actor
|
||||
except models.User.DoesNotExist:
|
||||
response = requests.get(
|
||||
key_actor,
|
||||
headers={'Accept': 'application/activity+json'}
|
||||
)
|
||||
if not response.ok:
|
||||
raise ValueError('Could not load public key')
|
||||
user_data = response.json()
|
||||
public_key = user_data['publicKey']['publicKeyPem']
|
||||
|
||||
return RSA.import_key(public_key)
|
||||
|
||||
def verify_signature(required_actor, request):
|
||||
''' verify rsa signature '''
|
||||
signature_dict = {}
|
||||
for pair in request.headers['Signature'].split(','):
|
||||
|
@ -97,15 +116,13 @@ def verify_signature(request):
|
|||
except KeyError:
|
||||
raise ValueError('Invalid auth header')
|
||||
|
||||
response = requests.get(
|
||||
key_id,
|
||||
headers={'Accept': 'application/activity+json'}
|
||||
)
|
||||
if not response.ok:
|
||||
raise ValueError('Could not load public key')
|
||||
# TODO Use the fragment - actors can have multiple keys?
|
||||
key_actor = urldefrag(key_id).url
|
||||
|
||||
actor = response.json()
|
||||
key = RSA.import_key(actor['publicKey']['publicKeyPem'])
|
||||
if key_actor != required_actor:
|
||||
raise ValueError("Wrong actor created signature.")
|
||||
|
||||
key = get_public_key(key_actor)
|
||||
|
||||
comparison_string = []
|
||||
for signed_header_name in headers.split(' '):
|
||||
|
@ -125,8 +142,6 @@ def verify_signature(request):
|
|||
# raises a ValueError if it fails
|
||||
signer.verify(digest, signature)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_follow(activity):
|
||||
|
|
92
fedireads/tests/test_signing.py
Normal file
92
fedireads/tests/test_signing.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
from collections import namedtuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from Crypto import Random
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
import responses
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.utils.http import http_date
|
||||
|
||||
from fedireads.models import User
|
||||
from fedireads.broadcast import make_signature
|
||||
from fedireads.activitypub import get_follow_request
|
||||
from fedireads.settings import DOMAIN
|
||||
|
||||
Sender = namedtuple('Sender', ('actor', 'private_key', 'public_key'))
|
||||
|
||||
class Signature(TestCase):
|
||||
def setUp(self):
|
||||
self.mouse = User.objects.create_user('mouse', 'mouse@example.com', '')
|
||||
self.rat = User.objects.create_user('rat', 'rat@example.com', '')
|
||||
self.cat = User.objects.create_user('cat', 'cat@example.com', '')
|
||||
|
||||
random_generator = Random.new().read
|
||||
key = RSA.generate(1024, random_generator)
|
||||
private_key = key.export_key().decode('utf8')
|
||||
public_key = key.publickey().export_key().decode('utf8')
|
||||
|
||||
self.fake_remote = Sender(
|
||||
'http://localhost/user/remote',
|
||||
private_key,
|
||||
public_key,
|
||||
)
|
||||
|
||||
def send_follow(self, sender, signature, now):
|
||||
c = Client()
|
||||
return c.post(
|
||||
urlsplit(self.rat.inbox).path,
|
||||
data=get_follow_request(
|
||||
sender,
|
||||
self.rat,
|
||||
),
|
||||
content_type='application/json',
|
||||
**{
|
||||
'HTTP_DATE': now,
|
||||
'HTTP_SIGNATURE': signature,
|
||||
'HTTP_CONTENT_TYPE': 'application/activity+json; charset=utf-8',
|
||||
'HTTP_HOST': DOMAIN,
|
||||
}
|
||||
)
|
||||
|
||||
def test_correct_signature(self):
|
||||
now = http_date()
|
||||
signature = make_signature(self.mouse, self.rat.inbox, now)
|
||||
return self.send_follow(self.mouse, signature, now).status_code == 200
|
||||
|
||||
def test_wrong_signature(self):
|
||||
''' Messages must be signed by the right actor.
|
||||
(cat cannot sign messages on behalf of mouse)
|
||||
'''
|
||||
now = http_date()
|
||||
signature = make_signature(self.cat, self.rat.inbox, now)
|
||||
assert self.send_follow(self.mouse, signature, now).status_code == 401
|
||||
|
||||
@responses.activate
|
||||
def test_remote_signer(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
self.fake_remote.actor,
|
||||
json={'publicKey': {
|
||||
'publicKeyPem': self.fake_remote.public_key
|
||||
}},
|
||||
status=200)
|
||||
|
||||
now = http_date()
|
||||
sender = self.fake_remote
|
||||
signature = make_signature(sender, self.rat.inbox, now)
|
||||
assert self.send_follow(sender, signature, now).status_code == 200
|
||||
|
||||
@responses.activate
|
||||
def test_nonexistent_signer(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
self.fake_remote.actor,
|
||||
json={'error': 'not found'},
|
||||
status=404)
|
||||
|
||||
now = http_date()
|
||||
sender = self.fake_remote
|
||||
signature = make_signature(sender, self.rat.inbox, now)
|
||||
assert self.send_follow(sender, signature, now).status_code == 401
|
|
@ -1,5 +1,5 @@
|
|||
celery==4.4.2
|
||||
coverage=5.1
|
||||
coverage==5.1
|
||||
Django==3.0.3
|
||||
django-model-utils==4.0.0
|
||||
environs==7.2.0
|
||||
|
@ -10,3 +10,4 @@ pycryptodome==3.9.4
|
|||
python-dateutil==2.8.1
|
||||
redis==3.4.1
|
||||
requests==2.22.0
|
||||
responses==0.10.14
|
||||
|
|
Loading…
Reference in a new issue