From 52c83c67bbb7c3597d2fcc8fd3554927a252fedb Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sun, 6 Nov 2022 14:14:08 -0700 Subject: Signing works with OpenSSL. Will have to ask the cryptography peeps what I was doing wrong. --- .pre-commit-config.yaml | 1 + core/signatures.py | 22 +++++++++++++++++----- requirements.txt | 1 + users/models/identity.py | 42 ++++++++++++++++++++++-------------------- users/views/identity.py | 4 ++-- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fa3f15..74f9cbe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,3 +35,4 @@ repos: rev: v0.982 hooks: - id: mypy + additional_dependencies: [types-pyopenssl] diff --git a/core/signatures.py b/core/signatures.py index bcacb68..a5e4fed 100644 --- a/core/signatures.py +++ b/core/signatures.py @@ -1,5 +1,5 @@ import base64 -from typing import Any, Dict, List +from typing import List, TypedDict from cryptography.hazmat.primitives import hashes from django.http import HttpRequest @@ -38,11 +38,23 @@ class HttpSignature: return "\n".join(f"{name.lower()}: {value}" for name, value in headers.items()) @classmethod - def parse_signature(cls, signature) -> Dict[str, Any]: - signature_details = {} + def parse_signature(cls, signature) -> "SignatureDetails": + bits = {} for item in signature.split(","): name, value = item.split("=", 1) value = value.strip('"') - signature_details[name.lower()] = value - signature_details["headers"] = signature_details["headers"].split() + bits[name.lower()] = value + signature_details: SignatureDetails = { + "headers": bits["headers"].split(), + "signature": base64.b64decode(bits["signature"]), + "algorithm": bits["algorithm"], + "keyid": bits["keyid"], + } return signature_details + + +class SignatureDetails(TypedDict): + algorithm: str + headers: List[str] + signature: bytes + keyid: str diff --git a/requirements.txt b/requirements.txt index 2a08e47..09de1e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ urlman~=2.0.1 django-crispy-forms~=1.14 cryptography~=38.0 httpx~=0.23 +pyOpenSSL~=22.1.0 diff --git a/users/models/identity.py b/users/models/identity.py index b5f9897..4939535 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -7,13 +7,12 @@ from urllib.parse import urlparse import httpx import urlman from asgiref.sync import sync_to_async -from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa -from django.conf import settings from django.db import models from django.utils import timezone from django.utils.http import http_date +from OpenSSL import crypto from core.ld import canonicalise from users.models.domain import Domain @@ -96,14 +95,19 @@ class Identity(models.Model): return None @classmethod - def by_actor_uri(cls, uri, create=False): + def by_actor_uri(cls, uri) -> Optional["Identity"]: try: return cls.objects.get(actor_uri=uri) except cls.DoesNotExist: - if create: - return cls.objects.create(actor_uri=uri, local=False) return None + @classmethod + def by_actor_uri_with_create(cls, uri) -> "Identity": + try: + return cls.objects.get(actor_uri=uri) + except cls.DoesNotExist: + return cls.objects.create(actor_uri=uri, local=False) + @property def handle(self): return f"{self.username}@{self.domain_id}" @@ -219,7 +223,7 @@ class Identity(models.Model): ) return base64.b64encode( private_key.sign( - cleartext.encode("utf8"), + cleartext.encode("ascii"), padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH, @@ -228,22 +232,19 @@ class Identity(models.Model): ) ).decode("ascii") - def verify_signature(self, crypttext: str, cleartext: str) -> bool: + def verify_signature(self, signature: bytes, cleartext: str) -> bool: if not self.public_key: raise ValueError("Cannot verify - no public key") - public_key = serialization.load_pem_public_key(self.public_key.encode("ascii")) - print("sig??", crypttext, cleartext) - try: - public_key.verify( - crypttext.encode("utf8"), - cleartext.encode("utf8"), - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH, - ), - hashes.SHA256(), + x509 = crypto.X509() + x509.set_pubkey( + crypto.load_publickey( + crypto.FILETYPE_PEM, + self.public_key.encode("ascii"), ) - except InvalidSignature: + ) + try: + crypto.verify(x509, signature, cleartext.encode("ascii"), "sha256") + except crypto.Error: return False return True @@ -264,7 +265,7 @@ class Identity(models.Model): del headers["(request-target)"] headers[ "Signature" - ] = f'keyId="https://{settings.DEFAULT_DOMAIN}{self.urls.actor}",headers="{headers_string}",signature="{signature}"' + ] = f'keyId="{self.urls.key.full()}",headers="{headers_string}",signature="{signature}"' async with httpx.AsyncClient() as client: return await client.request( method, @@ -288,6 +289,7 @@ class Identity(models.Model): view = "/@{self.username}@{self.domain_id}/" view_short = "/@{self.username}/" actor = "{view}actor/" + key = "{actor}#main-key" inbox = "{actor}inbox/" outbox = "{actor}outbox/" activate = "{view}activate/" diff --git a/users/views/identity.py b/users/views/identity.py index 1beef2a..7134cf9 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -133,7 +133,7 @@ class Actor(View): "inbox": identity.urls.inbox.full(), "preferredUsername": identity.username, "publicKey": { - "id": identity.urls.actor.full() + "#main-key", + "id": identity.urls.key.full(), "owner": identity.urls.actor.full(), "publicKeyPem": identity.public_key, }, @@ -181,7 +181,7 @@ class Inbox(View): print(headers_string) print(document) # Find the Identity by the actor on the incoming item - identity = Identity.by_actor_uri(document["actor"], create=True) + identity = Identity.by_actor_uri_with_create(document["actor"]) if not identity.public_key: # See if we can fetch it right now async_to_sync(identity.fetch_actor)() -- cgit v1.2.3