Move signature code into fedireads.signatures.
This commit is contained in:
parent
3b16bb748c
commit
b212456d0d
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue