Signing works with OpenSSL.

Will have to ask the cryptography peeps what I was doing wrong.
This commit is contained in:
Andrew Godwin 2022-11-06 14:14:08 -07:00
parent dbe57075d3
commit 52c83c67bb
5 changed files with 43 additions and 27 deletions

View file

@ -35,3 +35,4 @@ repos:
rev: v0.982 rev: v0.982
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: [types-pyopenssl]

View file

@ -1,5 +1,5 @@
import base64 import base64
from typing import Any, Dict, List from typing import List, TypedDict
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from django.http import HttpRequest from django.http import HttpRequest
@ -38,11 +38,23 @@ class HttpSignature:
return "\n".join(f"{name.lower()}: {value}" for name, value in headers.items()) return "\n".join(f"{name.lower()}: {value}" for name, value in headers.items())
@classmethod @classmethod
def parse_signature(cls, signature) -> Dict[str, Any]: def parse_signature(cls, signature) -> "SignatureDetails":
signature_details = {} bits = {}
for item in signature.split(","): for item in signature.split(","):
name, value = item.split("=", 1) name, value = item.split("=", 1)
value = value.strip('"') value = value.strip('"')
signature_details[name.lower()] = value bits[name.lower()] = value
signature_details["headers"] = signature_details["headers"].split() signature_details: SignatureDetails = {
"headers": bits["headers"].split(),
"signature": base64.b64decode(bits["signature"]),
"algorithm": bits["algorithm"],
"keyid": bits["keyid"],
}
return signature_details return signature_details
class SignatureDetails(TypedDict):
algorithm: str
headers: List[str]
signature: bytes
keyid: str

View file

@ -5,3 +5,4 @@ urlman~=2.0.1
django-crispy-forms~=1.14 django-crispy-forms~=1.14
cryptography~=38.0 cryptography~=38.0
httpx~=0.23 httpx~=0.23
pyOpenSSL~=22.1.0

View file

@ -7,13 +7,12 @@ from urllib.parse import urlparse
import httpx import httpx
import urlman import urlman
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives.asymmetric import padding, rsa
from django.conf import settings
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.http import http_date from django.utils.http import http_date
from OpenSSL import crypto
from core.ld import canonicalise from core.ld import canonicalise
from users.models.domain import Domain from users.models.domain import Domain
@ -96,14 +95,19 @@ class Identity(models.Model):
return None return None
@classmethod @classmethod
def by_actor_uri(cls, uri, create=False): def by_actor_uri(cls, uri) -> Optional["Identity"]:
try: try:
return cls.objects.get(actor_uri=uri) return cls.objects.get(actor_uri=uri)
except cls.DoesNotExist: except cls.DoesNotExist:
if create:
return cls.objects.create(actor_uri=uri, local=False)
return None 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 @property
def handle(self): def handle(self):
return f"{self.username}@{self.domain_id}" return f"{self.username}@{self.domain_id}"
@ -219,7 +223,7 @@ class Identity(models.Model):
) )
return base64.b64encode( return base64.b64encode(
private_key.sign( private_key.sign(
cleartext.encode("utf8"), cleartext.encode("ascii"),
padding.PSS( padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH, salt_length=padding.PSS.MAX_LENGTH,
@ -228,22 +232,19 @@ class Identity(models.Model):
) )
).decode("ascii") ).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: if not self.public_key:
raise ValueError("Cannot verify - no public key") raise ValueError("Cannot verify - no public key")
public_key = serialization.load_pem_public_key(self.public_key.encode("ascii")) x509 = crypto.X509()
print("sig??", crypttext, cleartext) x509.set_pubkey(
try: crypto.load_publickey(
public_key.verify( crypto.FILETYPE_PEM,
crypttext.encode("utf8"), self.public_key.encode("ascii"),
cleartext.encode("utf8"),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
) )
except InvalidSignature: )
try:
crypto.verify(x509, signature, cleartext.encode("ascii"), "sha256")
except crypto.Error:
return False return False
return True return True
@ -264,7 +265,7 @@ class Identity(models.Model):
del headers["(request-target)"] del headers["(request-target)"]
headers[ headers[
"Signature" "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: async with httpx.AsyncClient() as client:
return await client.request( return await client.request(
method, method,
@ -288,6 +289,7 @@ class Identity(models.Model):
view = "/@{self.username}@{self.domain_id}/" view = "/@{self.username}@{self.domain_id}/"
view_short = "/@{self.username}/" view_short = "/@{self.username}/"
actor = "{view}actor/" actor = "{view}actor/"
key = "{actor}#main-key"
inbox = "{actor}inbox/" inbox = "{actor}inbox/"
outbox = "{actor}outbox/" outbox = "{actor}outbox/"
activate = "{view}activate/" activate = "{view}activate/"

View file

@ -133,7 +133,7 @@ class Actor(View):
"inbox": identity.urls.inbox.full(), "inbox": identity.urls.inbox.full(),
"preferredUsername": identity.username, "preferredUsername": identity.username,
"publicKey": { "publicKey": {
"id": identity.urls.actor.full() + "#main-key", "id": identity.urls.key.full(),
"owner": identity.urls.actor.full(), "owner": identity.urls.actor.full(),
"publicKeyPem": identity.public_key, "publicKeyPem": identity.public_key,
}, },
@ -181,7 +181,7 @@ class Inbox(View):
print(headers_string) print(headers_string)
print(document) print(document)
# Find the Identity by the actor on the incoming item # 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: if not identity.public_key:
# See if we can fetch it right now # See if we can fetch it right now
async_to_sync(identity.fetch_actor)() async_to_sync(identity.fetch_actor)()