Signing works with OpenSSL.
Will have to ask the cryptography peeps what I was doing wrong.
This commit is contained in:
parent
dbe57075d3
commit
52c83c67bb
|
@ -35,3 +35,4 @@ repos:
|
|||
rev: v0.982
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-pyopenssl]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,3 +5,4 @@ urlman~=2.0.1
|
|||
django-crispy-forms~=1.14
|
||||
cryptography~=38.0
|
||||
httpx~=0.23
|
||||
pyOpenSSL~=22.1.0
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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)()
|
||||
|
|
Loading…
Reference in New Issue