From fb6c409a9af5b8a686e977ee2251c359071e0ec3 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sun, 6 Nov 2022 21:30:07 -0700 Subject: Rework task system and fetching. I can taste how close follow is to working. --- core/signatures.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) (limited to 'core/signatures.py') diff --git a/core/signatures.py b/core/signatures.py index a5e4fed..6f4d9ef 100644 --- a/core/signatures.py +++ b/core/signatures.py @@ -1,8 +1,14 @@ import base64 -from typing import List, TypedDict +import json +from typing import Dict, List, Literal, TypedDict +from urllib.parse import urlparse +import httpx from cryptography.hazmat.primitives import hashes from django.http import HttpRequest +from django.utils.http import http_date + +from users.models import Identity class HttpSignature: @@ -25,7 +31,8 @@ class HttpSignature: @classmethod def headers_from_request(cls, request: HttpRequest, header_names: List[str]) -> str: """ - Creates the to-be-signed header payload from a Django request""" + Creates the to-be-signed header payload from a Django request + """ headers = {} for header_name in header_names: if header_name == "(request-target)": @@ -38,7 +45,7 @@ class HttpSignature: return "\n".join(f"{name.lower()}: {value}" for name, value in headers.items()) @classmethod - def parse_signature(cls, signature) -> "SignatureDetails": + def parse_signature(cls, signature: str) -> "SignatureDetails": bits = {} for item in signature.split(","): name, value = item.split("=", 1) @@ -52,6 +59,60 @@ class HttpSignature: } return signature_details + @classmethod + def compile_signature(cls, details: "SignatureDetails") -> str: + value = f'keyId="{details["keyid"]}",headers="' + value += " ".join(h.lower() for h in details["headers"]) + value += '",signature="' + value += base64.b64encode(details["signature"]).decode("ascii") + value += f'",algorithm="{details["algorithm"]}"' + return value + + @classmethod + async def signed_request( + self, + uri: str, + body: Dict, + identity: Identity, + content_type: str = "application/json", + method: Literal["post"] = "post", + ): + """ + Performs an async request to the given path, with a document, signed + as an identity. + """ + 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, + } + signed_string = "\n".join( + f"{name.lower()}: {value}" for name, value in headers.items() + ) + headers["Signature"] = self.compile_signature( + { + "keyid": identity.urls.key.full(), # type:ignore + "headers": list(headers.keys()), + "signature": identity.sign(signed_string), + "algorithm": "rsa-sha256", + } + ) + del headers["(request-target)"] + async with httpx.AsyncClient() as client: + print(f"Calling {method} {uri}") + print(body) + return await client.request( + method, + uri, + headers=headers, + content=body_bytes, + ) + class SignatureDetails(TypedDict): algorithm: str -- cgit v1.2.3