summaryrefslogtreecommitdiffstats
path: root/users
diff options
context:
space:
mode:
authorAndrew Godwin2022-11-12 15:10:15 -0700
committerAndrew Godwin2022-11-12 15:10:15 -0700
commitdd4328ae523bb375dd871e85d1bacd9311e87a89 (patch)
tree6a4ec8bc83be3bdd18421b3f0c221b7a6091cf9e /users
parent8fd5a9292c7d3aac352d3c0e96288bff8a79cb47 (diff)
downloadtakahe-dd4328ae523bb375dd871e85d1bacd9311e87a89.tar.gz
takahe-dd4328ae523bb375dd871e85d1bacd9311e87a89.tar.bz2
takahe-dd4328ae523bb375dd871e85d1bacd9311e87a89.zip
Add JSON-LD signatures and tests for sig stuff
Diffstat (limited to 'users')
-rw-r--r--users/admin.py6
-rw-r--r--users/migrations/0002_identity_public_key_id.py18
-rw-r--r--users/models/follow.py9
-rw-r--r--users/models/identity.py36
-rw-r--r--users/views/identity.py71
5 files changed, 63 insertions, 77 deletions
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)