Move signature code into fedireads.signatures.

This commit is contained in:
Adam Kelly 2020-05-14 14:24:37 +01:00
parent 3b16bb748c
commit b212456d0d
5 changed files with 104 additions and 89 deletions

View File

@ -1,15 +1,11 @@
''' send out activitypub messages ''' ''' send out activitypub messages '''
import json import json
from urllib.parse import urlparse
from base64 import b64encode
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from django.utils.http import http_date from django.utils.http import http_date
import requests import requests
from fedireads import models from fedireads import models
from fedireads.tasks import app from fedireads.tasks import app
from fedireads.signatures import make_signature
def get_public_recipients(user, software=None): def get_public_recipients(user, software=None):
@ -63,24 +59,6 @@ def broadcast_task(sender_id, activity, recipients):
return errors return errors
def make_signature(sender, destination, date):
inbox_parts = urlparse(destination)
signature_headers = [
'(request-target): post %s' % inbox_parts.path,
'host: %s' % inbox_parts.netloc,
'date: %s' % date,
]
message_to_sign = '\n'.join(signature_headers)
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.remote_id,
'algorithm': 'rsa-sha256',
'headers': '(request-target) host date',
'signature': b64encode(signed_message).decode('utf8'),
}
return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items())
def sign_and_send(sender, activity, destination): def sign_and_send(sender, activity, destination):
''' crpyto whatever and http junk ''' ''' crpyto whatever and http junk '''
now = http_date() now = http_date()

View File

@ -1,21 +1,18 @@
''' handles all of the activity coming in to the server ''' ''' handles all of the activity coming in to the server '''
import json import json
from base64 import b64decode
from urllib.parse import urldefrag from urllib.parse import urldefrag
import requests
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
import django.db.utils import django.db.utils
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
import requests
from fedireads import books_manager, models, outgoing from fedireads import books_manager, models, outgoing
from fedireads import status as status_builder from fedireads import status as status_builder
from fedireads.remote_user import get_or_create_remote_user from fedireads.remote_user import get_or_create_remote_user
from fedireads.tasks import app from fedireads.tasks import app
from fedireads.signatures import Signature
@csrf_exempt @csrf_exempt
@ -48,7 +45,16 @@ def shared_inbox(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
verify_signature(activity.get('actor'), request) signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get('actor'):
print("Wrong sig: ", key_actor, activity.get('actor'))
raise ValueError("Wrong actor created signature.")
key = get_public_key(key_actor)
signature.verify(key, request)
except ValueError: except ValueError:
return HttpResponse(status=401) return HttpResponse(status=401)
@ -88,7 +94,6 @@ def get_public_key(key_actor):
try: try:
user = models.User.objects.get(remote_id=key_actor) user = models.User.objects.get(remote_id=key_actor)
public_key = user.public_key public_key = user.public_key
actor = user.remote_id
except models.User.DoesNotExist: except models.User.DoesNotExist:
response = requests.get( response = requests.get(
key_actor, key_actor,
@ -99,49 +104,7 @@ def get_public_key(key_actor):
user_data = response.json() user_data = response.json()
public_key = user_data['publicKey']['publicKeyPem'] public_key = user_data['publicKey']['publicKeyPem']
return RSA.import_key(public_key) return public_key
def verify_signature(required_actor, request):
''' verify rsa signature '''
signature_dict = {}
for pair in request.headers['Signature'].split(','):
k, v = pair.split('=', 1)
v = v.replace('"', '')
signature_dict[k] = v
try:
key_id = signature_dict['keyId']
headers = signature_dict['headers']
signature = b64decode(signature_dict['signature'])
except KeyError:
raise ValueError('Invalid auth header')
# TODO Use the fragment - actors can have multiple keys?
key_actor = urldefrag(key_id).url
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(' '):
if signed_header_name == '(request-target)':
comparison_string.append('(request-target): post %s' % request.path)
else:
comparison_string.append('%s: %s' % (
signed_header_name,
request.headers[signed_header_name]
))
comparison_string = '\n'.join(comparison_string)
signer = pkcs1_15.new(key)
digest = SHA256.new()
digest.update(comparison_string.encode())
# raises a ValueError if it fails
signer.verify(digest, signature)
@app.task @app.task
def handle_follow(activity): def handle_follow(activity):

View File

@ -1,6 +1,4 @@
''' database schema for user data ''' ''' database schema for user data '''
from Crypto import Random
from Crypto.PublicKey import RSA
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
@ -8,6 +6,7 @@ from django.dispatch import receiver
from fedireads import activitypub from fedireads import activitypub
from fedireads.models.shelf import Shelf from fedireads.models.shelf import Shelf
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.signatures import create_key_pair
from .base_model import FedireadsModel from .base_model import FedireadsModel
@ -158,10 +157,7 @@ def execute_before_save(sender, instance, *args, **kwargs):
instance.shared_inbox = 'https://%s/inbox' % DOMAIN instance.shared_inbox = 'https://%s/inbox' % DOMAIN
instance.outbox = '%s/outbox' % instance.remote_id instance.outbox = '%s/outbox' % instance.remote_id
if not instance.private_key: if not instance.private_key:
random_generator = Random.new().read instance.private_key, instance.public_key = create_key_pair()
key = RSA.generate(1024, random_generator)
instance.private_key = key.export_key().decode('utf8')
instance.public_key = key.publickey().export_key().decode('utf8')
@receiver(models.signals.post_save, sender=User) @receiver(models.signals.post_save, sender=User)

82
fedireads/signatures.py Normal file
View File

@ -0,0 +1,82 @@
from urllib.parse import urlparse
from base64 import b64encode, b64decode
from Crypto import Random
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15 #pylint: disable=no-name-in-module
from Crypto.Hash import SHA256
def create_key_pair():
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')
return private_key, public_key
def make_signature(sender, destination, date):
inbox_parts = urlparse(destination)
signature_headers = [
'(request-target): post %s' % inbox_parts.path,
'host: %s' % inbox_parts.netloc,
'date: %s' % date,
]
message_to_sign = '\n'.join(signature_headers)
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.remote_id,
'algorithm': 'rsa-sha256',
'headers': '(request-target) host date',
'signature': b64encode(signed_message).decode('utf8'),
}
return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items())
class Signature:
def __init__(self, key_id, headers, signature):
self.key_id = key_id
self.headers = headers
self.signature = signature
@classmethod
def parse(cls, request):
signature_dict = {}
for pair in request.headers['Signature'].split(','):
k, v = pair.split('=', 1)
v = v.replace('"', '')
signature_dict[k] = v
try:
key_id = signature_dict['keyId']
headers = signature_dict['headers']
signature = b64decode(signature_dict['signature'])
except KeyError:
raise ValueError('Invalid auth header')
return cls(key_id, headers, signature)
def verify(self, public_key, request):
''' verify rsa signature '''
public_key = RSA.import_key(public_key)
comparison_string = []
for signed_header_name in self.headers.split(' '):
if signed_header_name == '(request-target)':
comparison_string.append(
'(request-target): post %s' % request.path)
else:
comparison_string.append('%s: %s' % (
signed_header_name,
request.headers[signed_header_name]
))
comparison_string = '\n'.join(comparison_string)
signer = pkcs1_15.new(public_key)
digest = SHA256.new()
digest.update(comparison_string.encode())
# raises a ValueError if it fails
signer.verify(digest, self.signature)

View File

@ -1,18 +1,15 @@
from collections import namedtuple from collections import namedtuple
from urllib.parse import urlsplit from urllib.parse import urlsplit
from Crypto import Random
from Crypto.PublicKey import RSA
import responses import responses
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils.http import http_date from django.utils.http import http_date
from fedireads.models import User from fedireads.models import User
from fedireads.broadcast import make_signature
from fedireads.activitypub import get_follow_request from fedireads.activitypub import get_follow_request
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.signatures import create_key_pair, make_signature
Sender = namedtuple('Sender', ('remote_id', 'private_key', 'public_key')) Sender = namedtuple('Sender', ('remote_id', 'private_key', 'public_key'))
@ -22,10 +19,7 @@ class Signature(TestCase):
self.rat = User.objects.create_user('rat', 'rat@example.com', '') self.rat = User.objects.create_user('rat', 'rat@example.com', '')
self.cat = User.objects.create_user('cat', 'cat@example.com', '') self.cat = User.objects.create_user('cat', 'cat@example.com', '')
random_generator = Random.new().read private_key, public_key = create_key_pair()
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( self.fake_remote = Sender(
'http://localhost/user/remote', 'http://localhost/user/remote',
@ -76,7 +70,8 @@ class Signature(TestCase):
now = http_date() now = http_date()
sender = self.fake_remote sender = self.fake_remote
signature = make_signature(sender, self.rat.inbox, now) signature = make_signature(sender, self.rat.inbox, now)
assert self.send_follow(sender, signature, now).status_code == 200 response = self.send_follow(sender, signature, now)
self.assertEqual(response.status_code, 200)
@responses.activate @responses.activate
def test_nonexistent_signer(self): def test_nonexistent_signer(self):
@ -89,4 +84,5 @@ class Signature(TestCase):
now = http_date() now = http_date()
sender = self.fake_remote sender = self.fake_remote
signature = make_signature(sender, self.rat.inbox, now) signature = make_signature(sender, self.rat.inbox, now)
assert self.send_follow(sender, signature, now).status_code == 401 response = self.send_follow(sender, signature, now)
self.assertEqual(response.status_code, 401)