summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorAndrew Godwin2022-11-20 18:29:19 -0700
committerAndrew Godwin2022-11-20 18:29:19 -0700
commit5ddce16213a8e7b4e9d052a14ed8d7e37ac5f068 (patch)
treef6bfb8d8e0fe6e00a30125ba4b6076426c56bcf2 /core
parentbed5c7ffaa184fd6146df17279fc2b96f9d02944 (diff)
downloadtakahe-5ddce16213a8e7b4e9d052a14ed8d7e37ac5f068.tar.gz
takahe-5ddce16213a8e7b4e9d052a14ed8d7e37ac5f068.tar.bz2
takahe-5ddce16213a8e7b4e9d052a14ed8d7e37ac5f068.zip
Add a system actor to sign outgoing S2S GETs
Diffstat (limited to 'core')
-rw-r--r--core/ld.py6
-rw-r--r--core/models/config.py3
-rw-r--r--core/signatures.py56
3 files changed, 56 insertions, 9 deletions
diff --git a/core/ld.py b/core/ld.py
index fff0526..1d79abf 100644
--- a/core/ld.py
+++ b/core/ld.py
@@ -358,6 +358,12 @@ schemas = {
]
},
},
+ "joinmastodon.org/ns": {
+ "contentType": "application/ld+json",
+ "documentUrl": "http://joinmastodon.org/ns",
+ "contextUrl": None,
+ "document": {},
+ },
}
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
diff --git a/core/models/config.py b/core/models/config.py
index ffe7172..d69205c 100644
--- a/core/models/config.py
+++ b/core/models/config.py
@@ -154,6 +154,9 @@ class Config(models.Model):
version: str = __version__
+ system_actor_public_key: str = ""
+ system_actor_private_key: str = ""
+
site_name: str = "Takahē"
highlight_color: str = "#449c8c"
site_about: str = "<h2>Welcome!</h2>\n\nThis is a community running Takahē."
diff --git a/core/signatures.py b/core/signatures.py
index d981f87..df3ca61 100644
--- a/core/signatures.py
+++ b/core/signatures.py
@@ -1,10 +1,11 @@
import base64
import json
-from typing import Dict, List, Literal, TypedDict
+from typing import Dict, List, Literal, Optional, Tuple, TypedDict
from urllib.parse import urlparse
import httpx
-from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
from django.http import HttpRequest
from django.utils import timezone
from django.utils.http import http_date, parse_http_date
@@ -30,6 +31,32 @@ class VerificationFormatError(VerificationError):
pass
+class RsaKeys:
+ @classmethod
+ def generate_keypair(cls) -> Tuple[str, str]:
+ """
+ Generates a new RSA keypair
+ """
+ private_key = rsa.generate_private_key(
+ public_exponent=65537,
+ key_size=2048,
+ )
+ private_key_serialized = private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption(),
+ ).decode("ascii")
+ public_key_serialized = (
+ private_key.public_key()
+ .public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+ .decode("ascii")
+ )
+ return private_key_serialized, public_key_serialized
+
+
class HttpSignature:
"""
Allows for calculation and verification of HTTP signatures
@@ -138,28 +165,37 @@ class HttpSignature:
@classmethod
async def signed_request(
- self,
+ cls,
uri: str,
- body: Dict,
+ body: Optional[Dict],
private_key: str,
key_id: str,
content_type: str = "application/json",
- method: Literal["post"] = "post",
+ method: Literal["get", "post"] = "post",
):
"""
Performs an async request to the given path, with a document, signed
as an identity.
"""
+ # Create the core header field set
uri_parts = urlparse(uri)
date_string = http_date()
- body_bytes = json.dumps(body).encode("utf8")
headers = {
"(request-target)": f"{method} {uri_parts.path}",
"Host": uri_parts.hostname,
"Date": date_string,
- "Digest": self.calculate_digest(body_bytes),
- "Content-Type": content_type,
}
+ # If we have a body, add a digest and content type
+ if body is not None:
+ body_bytes = json.dumps(body).encode("utf8")
+ headers["Digest"] = cls.calculate_digest(body_bytes)
+ headers["Content-Type"] = content_type
+ else:
+ body_bytes = b""
+ # GET requests get implicit accept headers added
+ if method == "get":
+ headers["Accept"] = "application/activity+json, application/ld+json"
+ # Sign the headers
signed_string = "\n".join(
f"{name.lower()}: {value}" for name, value in headers.items()
)
@@ -172,7 +208,7 @@ class HttpSignature:
signed_string.encode("ascii"),
"sha256",
)
- headers["Signature"] = self.compile_signature(
+ headers["Signature"] = cls.compile_signature(
{
"keyid": key_id,
"headers": list(headers.keys()),
@@ -180,6 +216,7 @@ class HttpSignature:
"algorithm": "rsa-sha256",
}
)
+ # Send the request with all those headers except the pseudo one
del headers["(request-target)"]
async with httpx.AsyncClient() as client:
response = await client.request(
@@ -187,6 +224,7 @@ class HttpSignature:
uri,
headers=headers,
content=body_bytes,
+ follow_redirects=method == "get",
)
if response.status_code >= 400:
raise ValueError(