diff options
Diffstat (limited to 'core')
| -rw-r--r-- | core/ld.py | 6 | ||||
| -rw-r--r-- | core/models/config.py | 3 | ||||
| -rw-r--r-- | core/signatures.py | 56 | 
3 files changed, 56 insertions, 9 deletions
| @@ -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( | 
