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
hooks:
- id: mypy
additional_dependencies: [types-pyopenssl]

View file

@ -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

View file

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

View file

@ -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/"

View file

@ -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)()