Merge pull request #173 from cthulahoops/signature_refactor

Refactor signatures into own module.
This commit is contained in:
Mouse Reeve 2020-05-17 12:11:46 -07:00 committed by GitHub
commit 845401bd62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 90 deletions

View file

@ -1,15 +1,11 @@
''' send out activitypub messages '''
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
import requests
from fedireads import models
from fedireads.tasks import app
from fedireads.signatures import make_signature
def get_public_recipients(user, software=None):
@ -63,24 +59,6 @@ def broadcast_task(sender_id, activity, recipients):
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):
''' crpyto whatever and http junk '''
now = http_date()

View file

@ -1,21 +1,18 @@
''' handles all of the activity coming in to the server '''
import json
from base64 import b64decode
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
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
import requests
from fedireads import books_manager, models, outgoing
from fedireads import status as status_builder
from fedireads.remote_user import get_or_create_remote_user
from fedireads.tasks import app
from fedireads.signatures import Signature
@csrf_exempt
@ -48,7 +45,15 @@ def shared_inbox(request):
return HttpResponseBadRequest()
try:
verify_signature(activity.get('actor'), request)
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get('actor'):
raise ValueError("Wrong actor created signature.")
key = get_public_key(key_actor)
signature.verify(key, request)
except ValueError:
return HttpResponse(status=401)
@ -88,7 +93,6 @@ def get_public_key(key_actor):
try:
user = models.User.objects.get(remote_id=key_actor)
public_key = user.public_key
actor = user.remote_id
except models.User.DoesNotExist:
response = requests.get(
key_actor,
@ -99,49 +103,7 @@ def get_public_key(key_actor):
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(','):
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)
return public_key
@app.task
def handle_follow(activity):

View file

@ -1,6 +1,4 @@
''' database schema for user data '''
from Crypto import Random
from Crypto.PublicKey import RSA
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.dispatch import receiver
@ -8,6 +6,7 @@ from django.dispatch import receiver
from fedireads import activitypub
from fedireads.models.shelf import Shelf
from fedireads.settings import DOMAIN
from fedireads.signatures import create_key_pair
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.outbox = '%s/outbox' % instance.remote_id
if not instance.private_key:
random_generator = Random.new().read
key = RSA.generate(1024, random_generator)
instance.private_key = key.export_key().decode('utf8')
instance.public_key = key.publickey().export_key().decode('utf8')
instance.private_key, instance.public_key = create_key_pair()
@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 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
from fedireads.signatures import create_key_pair, make_signature
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.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')
private_key, public_key = create_key_pair()
self.fake_remote = Sender(
'http://localhost/user/remote',
@ -76,7 +70,8 @@ class Signature(TestCase):
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
response = self.send_follow(sender, signature, now)
self.assertEqual(response.status_code, 200)
@responses.activate
def test_nonexistent_signer(self):
@ -89,4 +84,5 @@ class Signature(TestCase):
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
response = self.send_follow(sender, signature, now)
self.assertEqual(response.status_code, 401)

3
fr-dev
View file

@ -38,7 +38,8 @@ case "$1" in
docker-compose restart celery_worker
;;
test)
docker-compose exec web coverage run --source='.' manage.py test
shift 1
docker-compose exec web coverage run --source='.' manage.py test "$@"
;;
test_report)
docker-compose exec web coverage report