From 58630a053fc05475f9c82e994b82edf67ea76632 Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Tue, 19 May 2020 21:33:47 +0100 Subject: [PATCH] Add digests for outgoing messages, and testing. --- fedireads/broadcast.py | 10 ++++-- fedireads/signatures.py | 10 ++++-- fedireads/tests/test_signing.py | 59 ++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/fedireads/broadcast.py b/fedireads/broadcast.py index 05b6904e..53da0c29 100644 --- a/fedireads/broadcast.py +++ b/fedireads/broadcast.py @@ -5,7 +5,7 @@ import requests from fedireads import models from fedireads.tasks import app -from fedireads.signatures import make_signature +from fedireads.signatures import make_signature, make_digest def get_public_recipients(user, software=None): @@ -67,12 +67,16 @@ def sign_and_send(sender, activity, destination): # this shouldn't happen. it would be bad if it happened. raise ValueError('No private key found for sender') + data = json.dumps(activity).encode('utf-8') + digest = make_digest(data) + response = requests.post( destination, - data=json.dumps(activity), + data=data, headers={ 'Date': now, - 'Signature': make_signature(sender, destination, now), + 'Digest': digest, + 'Signature': make_signature(sender, destination, now, digest), 'Content-Type': 'application/activity+json; charset=utf-8', }, ) diff --git a/fedireads/signatures.py b/fedireads/signatures.py index 8463f119..8fab5c31 100644 --- a/fedireads/signatures.py +++ b/fedireads/signatures.py @@ -17,12 +17,13 @@ def create_key_pair(): return private_key, public_key -def make_signature(sender, destination, date): +def make_signature(sender, destination, date, digest): inbox_parts = urlparse(destination) signature_headers = [ '(request-target): post %s' % inbox_parts.path, 'host: %s' % inbox_parts.netloc, 'date: %s' % date, + 'digest: %s' % digest, ] message_to_sign = '\n'.join(signature_headers) signer = pkcs1_15.new(RSA.import_key(sender.private_key)) @@ -30,11 +31,14 @@ def make_signature(sender, destination, date): signature = { 'keyId': '%s#main-key' % sender.remote_id, 'algorithm': 'rsa-sha256', - 'headers': '(request-target) host date', + 'headers': '(request-target) host date digest', 'signature': b64encode(signed_message).decode('utf8'), } return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items()) +def make_digest(data): + return 'SHA-256=' + b64encode(hashlib.sha512(data).digest()).decode('utf-8') + def verify_digest(request): algorithm, digest = request.headers['digest'].split('=', 1) if algorithm == 'SHA-256': @@ -42,7 +46,7 @@ def verify_digest(request): elif algorithm == 'SHA-512': hash_function = hashlib.sha512 else: - raise ValueError("Unsupported hash function.") + raise ValueError("Unsupported hash function: {}".format(algorithm)) expected = hash_function(request.body).digest() if b64decode(digest) != expected: diff --git a/fedireads/tests/test_signing.py b/fedireads/tests/test_signing.py index 9cfa1311..7310a445 100644 --- a/fedireads/tests/test_signing.py +++ b/fedireads/tests/test_signing.py @@ -1,6 +1,7 @@ from collections import namedtuple from urllib.parse import urlsplit +import json import responses from django.test import TestCase, Client @@ -9,7 +10,10 @@ from django.utils.http import http_date from fedireads.models import User from fedireads.activitypub import get_follow_request from fedireads.settings import DOMAIN -from fedireads.signatures import create_key_pair, make_signature +from fedireads.signatures import create_key_pair, make_signature, make_digest + +def get_follow_data(follower, followee): + return json.dumps(get_follow_request(follower, followee)).encode('utf-8') Sender = namedtuple('Sender', ('remote_id', 'private_key', 'public_key')) @@ -27,35 +31,43 @@ class Signature(TestCase): public_key, ) - def send_follow(self, sender, signature, now): + def send(self, signature, now, data): c = Client() return c.post( urlsplit(self.rat.inbox).path, - data=get_follow_request( - sender, - self.rat, - ), + data=data, content_type='application/json', **{ 'HTTP_DATE': now, 'HTTP_SIGNATURE': signature, + 'HTTP_DIGEST': make_digest(data), 'HTTP_CONTENT_TYPE': 'application/activity+json; charset=utf-8', 'HTTP_HOST': DOMAIN, } ) - def test_correct_signature(self): + def send_test_request( + self, + sender, + signer=None, + send_data=None, + digest=None): now = http_date() - signature = make_signature(self.mouse, self.rat.inbox, now) - return self.send_follow(self.mouse, signature, now).status_code == 200 + data = get_follow_data(sender, self.rat) + signature = make_signature( + signer or sender, self.rat.inbox, now, digest or make_digest(data)) + return self.send(signature, now, send_data or data) + + def test_correct_signature(self): + response = self.send_test_request(sender=self.mouse) + self.assertEqual(response.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 + response = self.send_test_request(sender=self.mouse, signer=self.cat) + self.assertEqual(response.status_code, 401) @responses.activate def test_remote_signer(self): @@ -67,10 +79,7 @@ class Signature(TestCase): }}, status=200) - now = http_date() - sender = self.fake_remote - signature = make_signature(sender, self.rat.inbox, now) - response = self.send_follow(sender, signature, now) + response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) @responses.activate @@ -81,8 +90,18 @@ class Signature(TestCase): json={'error': 'not found'}, status=404) - now = http_date() - sender = self.fake_remote - signature = make_signature(sender, self.rat.inbox, now) - response = self.send_follow(sender, signature, now) + response = self.send_test_request(sender=self.fake_remote) + self.assertEqual(response.status_code, 401) + + def test_changed_data(self): + '''Message data must match the digest header.''' + response = self.send_test_request( + self.mouse, + send_data=get_follow_data(self.mouse, self.cat)) + self.assertEqual(response.status_code, 401) + + def test_invalid_digest(self): + response = self.send_test_request( + self.mouse, + digest='SHA-256=AAAAAAAAAAAAAAAAAA') self.assertEqual(response.status_code, 401)