Merge pull request #172 from cthulahoops/signing

Signing
This commit is contained in:
Mouse Reeve 2020-05-13 08:38:15 -07:00 committed by GitHub
commit 4190793f17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 24 deletions

View file

@ -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',
},
)

View file

@ -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):

View 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

View file

@ -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