From dd4328ae523bb375dd871e85d1bacd9311e87a89 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sat, 12 Nov 2022 15:10:15 -0700 Subject: Add JSON-LD signatures and tests for sig stuff --- users/admin.py | 6 +++ users/migrations/0002_identity_public_key_id.py | 18 +++++++ users/models/follow.py | 9 ++-- users/models/identity.py | 36 +------------ users/views/identity.py | 71 +++++++++++-------------- 5 files changed, 63 insertions(+), 77 deletions(-) create mode 100644 users/migrations/0002_identity_public_key_id.py (limited to 'users') diff --git a/users/admin.py b/users/admin.py index a4cea27..61a2bc8 100644 --- a/users/admin.py +++ b/users/admin.py @@ -22,6 +22,12 @@ class UserEventAdmin(admin.ModelAdmin): class IdentityAdmin(admin.ModelAdmin): list_display = ["id", "handle", "actor_uri", "state", "local"] raw_id_fields = ["users"] + actions = ["force_update"] + + @admin.action(description="Force Update") + def force_update(self, request, queryset): + for instance in queryset: + instance.transition_perform("outdated") @admin.register(Follow) diff --git a/users/migrations/0002_identity_public_key_id.py b/users/migrations/0002_identity_public_key_id.py new file mode 100644 index 0000000..3648c20 --- /dev/null +++ b/users/migrations/0002_identity_public_key_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2022-11-12 21:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="identity", + name="public_key_id", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/users/models/follow.py b/users/models/follow.py index 81ffcd9..238081e 100644 --- a/users/models/follow.py +++ b/users/models/follow.py @@ -37,7 +37,8 @@ class FollowStates(StateGraph): await HttpSignature.signed_request( uri=follow.target.inbox_uri, body=canonicalise(follow.to_ap()), - identity=follow.source, + private_key=follow.source.public_key, + key_id=follow.source.public_key_id, ) return cls.local_requested @@ -56,7 +57,8 @@ class FollowStates(StateGraph): await HttpSignature.signed_request( uri=follow.source.inbox_uri, body=canonicalise(follow.to_accept_ap()), - identity=follow.target, + private_key=follow.target.public_key, + key_id=follow.target.public_key_id, ) return cls.accepted @@ -69,7 +71,8 @@ class FollowStates(StateGraph): await HttpSignature.signed_request( uri=follow.target.inbox_uri, body=canonicalise(follow.to_undo_ap()), - identity=follow.source, + private_key=follow.source.public_key, + key_id=follow.source.public_key_id, ) return cls.undone_remotely diff --git a/users/models/identity.py b/users/models/identity.py index 4ec0342..96e09c8 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -11,7 +11,6 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from django.db import models from django.utils import timezone -from OpenSSL import crypto from core.ld import canonicalise from stator.models import State, StateField, StateGraph, StatorModel @@ -89,6 +88,7 @@ class Identity(StatorModel): private_key = models.TextField(null=True, blank=True) public_key = models.TextField(null=True, blank=True) + public_key_id = models.TextField(null=True, blank=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) @@ -182,10 +182,6 @@ class Identity(StatorModel): # TODO: Setting return self.data_age > 60 * 24 * 24 - @property - def key_id(self): - return self.actor_uri + "#main-key" - ### Actor/Webfinger fetching ### @classmethod @@ -242,6 +238,7 @@ class Identity(StatorModel): "as:manuallyApprovesFollowers" ) self.public_key = document.get("publicKey", {}).get("publicKeyPem") + self.public_key_id = document.get("publicKey", {}).get("id") self.icon_uri = document.get("icon", {}).get("url") self.image_uri = document.get("image", {}).get("url") # Now go do webfinger with that info to see if we can get a canonical domain @@ -286,32 +283,3 @@ class Identity(StatorModel): .decode("ascii") ) self.save() - - def sign(self, cleartext: str) -> bytes: - if not self.private_key: - raise ValueError("Cannot sign - no private key") - pkey = crypto.load_privatekey( - crypto.FILETYPE_PEM, - self.private_key.encode("ascii"), - ) - return crypto.sign( - pkey, - cleartext.encode("ascii"), - "sha256", - ) - - def verify_signature(self, signature: bytes, cleartext: str) -> bool: - if not self.public_key: - raise ValueError("Cannot verify - no public key") - x509 = crypto.X509() - x509.set_pubkey( - crypto.load_publickey( - crypto.FILETYPE_PEM, - self.public_key.encode("ascii"), - ) - ) - try: - crypto.verify(x509, signature, cleartext.encode("ascii"), "sha256") - except crypto.Error: - return False - return True diff --git a/users/views/identity.py b/users/views/identity.py index 3bcf5c5..d4e1155 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -7,15 +7,18 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse from django.shortcuts import redirect -from django.utils import timezone from django.utils.decorators import method_decorator -from django.utils.http import parse_http_date from django.views.decorators.csrf import csrf_exempt from django.views.generic import FormView, TemplateView, View from core.forms import FormHelper from core.ld import canonicalise -from core.signatures import HttpSignature +from core.signatures import ( + HttpSignature, + LDSignature, + VerificationError, + VerificationFormatError, +) from users.decorators import identity_required from users.models import Domain, Follow, Identity, IdentityStates, InboxMessage from users.shortcuts import by_handle_or_404 @@ -167,7 +170,7 @@ class Actor(View): "inbox": identity.actor_uri + "inbox/", "preferredUsername": identity.username, "publicKey": { - "id": identity.key_id, + "id": identity.public_key_id, "owner": identity.actor_uri, "publicKeyPem": identity.public_key, }, @@ -188,37 +191,8 @@ class Inbox(View): """ def post(self, request, handle): - # Verify body digest - if "HTTP_DIGEST" in request.META: - expected_digest = HttpSignature.calculate_digest(request.body) - if request.META["HTTP_DIGEST"] != expected_digest: - print("Wrong digest") - return HttpResponseBadRequest("Digest is incorrect") - # Verify date header - if "HTTP_DATE" in request.META: - header_date = parse_http_date(request.META["HTTP_DATE"]) - if abs(timezone.now().timestamp() - header_date) > 60: - print( - f"Date mismatch - they sent {header_date}, now is {timezone.now().timestamp()}" - ) - return HttpResponseBadRequest("Date is too far away") - # Get the signature details - if "HTTP_SIGNATURE" not in request.META: - print("No signature") - return HttpResponseBadRequest("No signature present") - signature_details = HttpSignature.parse_signature( - request.META["HTTP_SIGNATURE"] - ) - # Reject unknown algorithms - if signature_details["algorithm"] != "rsa-sha256": - print("Unknown sig algo") - return HttpResponseBadRequest("Unknown signature algorithm") - # Create the signature payload - headers_string = HttpSignature.headers_from_request( - request, signature_details["headers"] - ) # Load the LD - document = canonicalise(json.loads(request.body)) + document = canonicalise(json.loads(request.body), include_security=True) # Find the Identity by the actor on the incoming item # This ensures that the signature used for the headers matches the actor # described in the payload. @@ -229,12 +203,29 @@ class Inbox(View): if not identity.public_key: print("Cannot get actor") return HttpResponseBadRequest("Cannot retrieve actor") - if not identity.verify_signature( - signature_details["signature"], headers_string - ): - print("Bad signature!") - print(document) - return HttpResponseUnauthorized("Bad signature") + # If there's a "signature" payload, verify against that + if "signature" in document: + try: + LDSignature.verify_signature(document, identity.public_key) + except VerificationFormatError as e: + print("Bad LD signature format:", e.args[0]) + return HttpResponseBadRequest(e.args[0]) + except VerificationError: + print("Bad LD signature") + return HttpResponseUnauthorized("Bad signature") + # Otherwise, verify against the header (assuming it's the same actor) + else: + try: + HttpSignature.verify_request( + request, + identity.public_key, + ) + except VerificationFormatError as e: + print("Bad HTTP signature format:", e.args[0]) + return HttpResponseBadRequest(e.args[0]) + except VerificationError: + print("Bad HTTP signature") + return HttpResponseUnauthorized("Bad signature") # Hand off the item to be processed by the queue InboxMessage.objects.create(message=document) return HttpResponse(status=202) -- cgit v1.2.3