From 38ea6823d59d937e99cb33f2eca9f10409b98e62 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 05:13:03 +0900 Subject: [PATCH 1/9] feat: add apsig's rfc9421 support --- .gitignore | 4 +- rfc9421.py | 401 +++++++++++++++++++++++++++++ src/apkit/_version.py | 4 +- src/apkit/client/_common.py | 86 +++++-- src/apkit/client/asyncio/actor.py | 4 +- src/apkit/client/asyncio/client.py | 7 +- src/apkit/client/sync/actor.py | 4 +- src/apkit/client/sync/client.py | 4 +- src/apkit/helper/inbox.py | 162 +++++++++++- 9 files changed, 636 insertions(+), 40 deletions(-) create mode 100644 rfc9421.py diff --git a/.gitignore b/.gitignore index 9fd7acd..c3d8f02 100644 --- a/.gitignore +++ b/.gitignore @@ -206,4 +206,6 @@ marimo/_static/ marimo/_lsp/ __marimo__/ -private_key.pem +private_key*.pem + +devel/ \ No newline at end of file diff --git a/rfc9421.py b/rfc9421.py new file mode 100644 index 0000000..633ef8d --- /dev/null +++ b/rfc9421.py @@ -0,0 +1,401 @@ +import base64 +import datetime as dt +import email.utils +import json +from typing import Any, List, Tuple, cast + +import pytz +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa +from http_sf import parse +from http_sf.types import ( + BareItemType, + InnerListType, + ItemType, + ParamsType, +) +from multiformats import multibase, multicodec + +from apsig.draft.tools import calculate_digest +from apsig.exceptions import MissingSignature, VerificationFailed + + +class RFC9421Signer: + def __init__(self, private_key: rsa.RSAPrivateKey, key_id: str): + self.private_key = private_key + self.key_id = key_id + self.sign_headers = [ + "date", + "@method", + "@path", + "@authority", + "content-type", + "content-length", + ] + + def __build_signature_base( + self, special_keys: dict[str, str], headers: dict[str, str] + ) -> bytes: + headers_new = [] + headers = headers.copy() + for h in self.sign_headers: + if h in ["@method", "@path", "@authority"]: + v = special_keys.get(h) + + if v: + headers_new.append(f'"{h}": {v}') + else: + raise ValueError(f"Missing Value: {h}") + elif h == "@signature-params": + v = special_keys.get(h) + + if v: + headers_new.append(f'"{h}": {self.__generate_sig_input()}') + else: + raise ValueError(f"Missing Value: {h}") + else: + v_raw = headers.get(h) + if v_raw is not None: + v = v_raw.strip() + headers_new.append(f'"{h}": {v}') + else: + raise ValueError(f"Missing Header Value: {h}") + headers_new.append( + f'"@signature-params": {self.__generate_sig_input()}' + ) + return ("\n".join(headers_new)).encode("utf-8") + + def generate_signature_header(self, signature: bytes) -> str: + return base64.b64encode(signature).decode("utf-8") + + def __generate_sig_input(self): + param = "(" + target_len = len(self.sign_headers) + timestamp = dt.datetime.now(dt.UTC) + for p in self.sign_headers: + param += f'"{p}"' + if p != self.sign_headers[target_len - 1]: + param += " " + param += ");" + param += f"created={int(timestamp.timestamp())};" + param += 'alg="rsa-v1_5-sha256";' + param += f'keyid="{self.key_id}"' + return param + + def sign( + self, + method: str, + path: str, + host: str, + headers: dict, + body: bytes | dict = b"", + ): + if isinstance(body, dict): + body = json.dumps(body).encode("utf-8") + + headers = {k.lower(): v for k, v in headers.items()} + if not headers.get("date"): + headers["date"] = email.utils.formatdate(usegmt=True) + if not headers.get("content-length"): + headers["content-length"] = str(len(body)) + + special_keys = { + "@method": method.upper(), + "@path": path, + "@authority": host, + } + + base = self.__build_signature_base(special_keys, headers) + signed = self.private_key.sign( + base, padding.PKCS1v15(), hashes.SHA256() + ) + headers_req = headers.copy() + headers_req["Signature"] = ( + f"sig1=:{self.generate_signature_header(signed)}:" + ) + headers_req["content-digest"] = f"sha-256=:{calculate_digest(body)}:" + headers_req["Signature-Input"] = f"sig1={self.__generate_sig_input()}" + return headers_req + + +class RFC9421Verifier: + def __init__( + self, + public_key: ed25519.Ed25519PublicKey + | rsa.RSAPublicKey + | ec.EllipticCurvePublicKey + | str, + method: str, + path: str, + host: str, + headers: dict[str, str], + body: bytes | dict | None = None, + clock_skew: int = 300, + ): + self.public_key: ( + ed25519.Ed25519PublicKey + | rsa.RSAPublicKey + | ec.EllipticCurvePublicKey + ) + + if isinstance(public_key, str): + codec, data = multicodec.unwrap(multibase.decode(public_key)) + match codec.name: + case "ed25519-pub": + self.public_key: ed25519.Ed25519PublicKey = ( + ed25519.Ed25519PublicKey.from_public_bytes(data) + ) + case "rsa-pub": + pubkey = serialization.load_pem_public_key(data) + if not isinstance(pubkey, rsa.RSAPublicKey): + raise TypeError( + "PublicKey must be ed25519 or RSA or ECDSA." + ) + self.public_key = pubkey + case "p256-pub": + pubkey = serialization.load_pem_public_key(data) + if not isinstance(pubkey, ec.EllipticCurvePublicKey): + raise TypeError( + "PublicKey must be ed25519 or RSA or ECDSA." + ) + self.public_key = pubkey + case "p384-pub": + pubkey = serialization.load_pem_public_key(data) + if not isinstance(pubkey, ec.EllipticCurvePublicKey): + raise TypeError( + "PublicKey must be ed25519 or RSA or ECDSA." + ) + self.public_key = pubkey + case _: + raise TypeError( + "PublicKey must be ed25519 or RSA or ECDSA." + ) + else: + self.public_key: ed25519.Ed25519PublicKey | rsa.RSAPublicKey = ( + public_key + ) + self.clock_skew = clock_skew + self.method = method.upper() + self.path = path + self.host = host + self.headers = {key.lower(): value for key, value in headers.items()} + + def __expect_value_and_params_member( + self, + member: Any, + ) -> Tuple[ItemType | InnerListType, ParamsType]: + if not isinstance(member, tuple) or len(member) != 2: + raise ValueError("expected a (value, params) tuple") + value, params = member + if not isinstance(params, dict): + raise ValueError("expected params to be a dict") + return cast( + Tuple[ItemType | InnerListType, ParamsType], (value, params) + ) + + def __generate_sig_input( + self, headers: List[BareItemType], params: ParamsType + ) -> str: + created = params.get("created") + alg = params.get("alg") + keyid = params.get("keyid") + + if isinstance(created, dt.datetime): + created_timestamp = created + elif isinstance(created, int): + created_timestamp = dt.datetime.fromtimestamp(created) + elif isinstance(created, str): + created_timestamp = dt.datetime.fromtimestamp(int(created)) + else: + raise ValueError("Unknown created value") + request_time = created_timestamp.astimezone(pytz.utc) + current_time = dt.datetime.now(dt.UTC) + if abs((current_time - request_time).total_seconds()) > self.clock_skew: + raise VerificationFailed( + f"property created is too far from current time ({current_time}): {request_time}" + ) + + param = "(" + target_len = len(headers) + for p in headers: + param += f'"{p}"' + if p != headers[target_len - 1]: + param += " " + param += ");" + param += f"created={int(created_timestamp.timestamp())};" + param += f'alg="{alg}";' + param += f'keyid="{keyid}"' + return param + + def __rebuild_sigbase( + self, headers: List[BareItemType], params: ParamsType + ) -> bytes: + special_keys = { + "@method": self.method, + "@path": self.path, + "@authority": self.host, + } + base = [] + for h in cast(List[str], headers): + if h in ["@method", "@path", "@authority"]: + base.append(f'"{h}": {special_keys.get(h)}') + else: + v_raw = self.headers.get(h) + if v_raw is not None: + v = v_raw.strip() + base.append(f'"{h}": {v}') + else: + raise ValueError(f"Missing Header Value: {h}") + base.append( + f'"@signature-params": {self.__generate_sig_input(headers=headers, params=params)}' + ) + return ("\n".join(base)).encode("utf-8") + + def verify(self, raise_on_fail: bool = False) -> str | None: + signature = self.headers.get("signature") + if not signature: + if raise_on_fail: + raise MissingSignature("Signature header is missing") + return None + + signature_input = self.headers.get("signature-input") + if not signature_input: + if raise_on_fail: + raise MissingSignature("Signature-Input header is missing") + return None + + signature_input_parsed = parse( + signature_input.encode("utf-8"), tltype="dictionary" + ) + signature_parsed = parse(signature.encode("utf-8"), tltype="dictionary") + + if not isinstance(signature_input_parsed, dict): + raise VerificationFailed( + f"Unsupported Signature-Input type: {type(signature_input_parsed)}" + ) + + if not isinstance(signature_parsed, dict): + raise VerificationFailed( + f"Unsupported Signature type: {type(signature_parsed)}" + ) + + for k, v in signature_input_parsed.items(): + try: + value, params = self.__expect_value_and_params_member(v) + if isinstance(value, list): + headers: List[BareItemType] = [ + itm[0] if isinstance(itm, tuple) else itm + for itm in value + ] + else: + raise ValueError( + "expected the value to be an inner-list (list of items)" + ) + + created = params.get("created") + key_id = str(params.get("keyid")) + alg = params.get("alg") + + if not created: + raise VerificationFailed("created not found.") + if not key_id: + raise VerificationFailed("keyid not found.") + if not alg: + raise VerificationFailed("alg not found.") + if alg not in [ + "ed25519", + "rsa-v1_5-sha256", + "rsa-v1_5-sha512", + "rsa-pss-sha512", + ]: + raise VerificationFailed(f"Unsupported algorithm: {alg}") + + sigi = self.__rebuild_sigbase(headers, params) + signature_bytes = signature_parsed.get(k) + if not isinstance(signature_bytes, tuple): + raise VerificationFailed( + f"Unknown Signature: {type(signature_bytes)}" + ) + + sig_val = None + for sig in cast(InnerListType, signature_bytes): + if isinstance(sig, bytes): + sig_val = sig + break + if sig_val is None: + raise ValueError("No Signature found.") + try: + match alg: + case "ed25519": + if not isinstance( + self.public_key, ed25519.Ed25519PublicKey + ): + raise VerificationFailed("Algorithm missmatch.") + self.public_key.verify(sig_val, sigi) + case "rsa-v1_5-sha256": + if not isinstance( + self.public_key, rsa.RSAPublicKey + ): + raise VerificationFailed("Algorithm missmatch.") + self.public_key.verify( + sig_val, + sigi, + padding.PKCS1v15(), + hashes.SHA256(), + ) + case "rsa-v1_5-sha512": + if not isinstance( + self.public_key, rsa.RSAPublicKey + ): + raise VerificationFailed("Algorithm missmatch.") + self.public_key.verify( + sig_val, + sigi, + padding.PKCS1v15(), + hashes.SHA512(), + ) + case "rsa-pss-sha512": + if not isinstance( + self.public_key, rsa.RSAPublicKey + ): + raise VerificationFailed("Algorithm missmatch.") + self.public_key.verify( + sig_val, + sigi, + padding.PSS( + mgf=padding.MGF1(hashes.SHA512()), + salt_length=hashes.SHA512().digest_size, + ), + hashes.SHA512(), + ) + case "ecdsa-p256-sha256": + if not isinstance( + self.public_key, ec.EllipticCurvePublicKey + ): + raise VerificationFailed("Algorithm missmatch.") + self.public_key.verify( + sig_val, + sigi, + ec.ECDSA(hashes.SHA256()), + ) + case "ecdsa-p384-sha384": + if not isinstance( + self.public_key, ec.EllipticCurvePublicKey + ): + raise VerificationFailed("Algorithm missmatch.") + self.public_key.verify( + sig_val, + sigi, + ec.ECDSA(hashes.SHA384()), + ) + return key_id + except Exception as e: + if raise_on_fail: + raise VerificationFailed(str(e)) + return None + except ValueError: + continue + + if raise_on_fail: + raise VerificationFailed("RFC9421 Signature verification failed.") + return None diff --git a/src/apkit/_version.py b/src/apkit/_version.py index 96b0a27..54c1fab 100644 --- a/src/apkit/_version.py +++ b/src/apkit/_version.py @@ -28,7 +28,7 @@ commit_id: COMMIT_ID __commit_id__: COMMIT_ID -__version__ = version = '0.3.2.post1.dev87+gb584b8cf1.d20251223' -__version_tuple__ = version_tuple = (0, 3, 2, 'post1', 'dev87', 'gb584b8cf1.d20251223') +__version__ = version = '0.3.3.post1.dev27+ge47c822fb.d20251228' +__version_tuple__ = version_tuple = (0, 3, 3, 'post1', 'dev27', 'ge47c822fb.d20251228') __commit_id__ = commit_id = None diff --git a/src/apkit/client/_common.py b/src/apkit/client/_common.py index 4f7a69d..677edf5 100644 --- a/src/apkit/client/_common.py +++ b/src/apkit/client/_common.py @@ -1,12 +1,13 @@ +import warnings import datetime import json -import warnings +import urllib.parse +from collections.abc import Mapping from typing import ( Any, Dict, Iterable, List, - Mapping, Optional, Tuple, Union, @@ -16,14 +17,16 @@ import apsig from apmodel.types import ActivityPubModel from apsig import draft +from apsig.rfc9421 import RFC9421Signer from cryptography.hazmat.primitives.asymmetric import ed25519, rsa from ..types import ActorKey -from .exceptions import NotImplementedWarning from .models import Resource, WebfingerResult -def ensure_user_agent_and_reconstruct(headers: Any, user_agent: str) -> Dict[str, str]: +def reconstruct_headers( + headers: Any, user_agent: str, json: Optional[dict | ActivityPubModel | Any] = None +) -> Dict[str, str]: processed_headers: Dict[str, Any] = {} match headers: @@ -48,6 +51,16 @@ def ensure_user_agent_and_reconstruct(headers: Any, user_agent: str) -> Dict[str processed_headers["user-agent"] = user_agent processed_headers["user-agent_original_key"] = "User-Agent" + if json: + if isinstance(json, ActivityPubModel): + if "content-type" not in processed_headers: + processed_headers["content-type"] = "application/activity+json; charset=UTF-8" + processed_headers["content-type_original_key"] = "Content-Type" + elif isinstance(json, dict): + if "content-type" not in processed_headers: + processed_headers["content-type"] = "application/json" + processed_headers["content-type_original_key"] = "Content-Type" + final_headers: Dict[str, str] = {} for key_lower, value in processed_headers.items(): if key_lower.endswith("_original_key"): @@ -87,14 +100,43 @@ def sign_request( for signature in signatures: if isinstance(signature.private_key, rsa.RSAPrivateKey): if "rfc9421" in sign_with and not signed_rfc9421: - warnings.warn( - 'This signature spec "rfc9421" is not implemented yet.', - category=NotImplementedWarning, - stacklevel=2, - ) - signed_rfc9421 = True + if "draft-cavage" in sign_with: + warnings.warn( + 'Draft and RFC9421 Signing is exclusive. ' + 'Legacy Draft mode is enabled to maintain compatibility. ' + 'The RFC 9421 (Structured Fields) signing logic will not be applied to this message.', + UserWarning + ) + signed_rfc9421 = True + else: + parsed_url = urllib.parse.urlparse(url) + if parsed_url.hostname: + # warnings.warn( + # 'This signature spec "rfc9421" is not implemented yet.', + # category=NotImplementedWarning, + # stacklevel=2, + # ) + rfc_signer = RFC9421Signer( + signature.private_key, signature.key_id + ) + headers = rfc_signer.sign( + headers=dict(headers) if headers else {}, + method="POST", + host=parsed_url.hostname, + path=parsed_url.path, + body=body if body else b"", + ) + signed_rfc9421 = True if "draft-cavage" in sign_with and not signed_cavage: + if "rfc9421" in sign_with: + warnings.warn( + 'Draft and RFC9421 Signing is exclusive. ' + 'Legacy Draft mode is enabled to maintain compatibility. ' + 'The RFC 9421 (Structured Fields) signing logic will not be applied to this message.', + UserWarning + ) + signed_rfc9421 = True signer = draft.Signer( headers=dict(headers) if headers else {}, method="POST", @@ -109,20 +151,32 @@ def sign_request( if "rsa2017" in sign_with and body and not signed_rsa2017: ld_signer = apsig.LDSignature() body = ld_signer.sign( - doc=(body if not isinstance(body, bytes) else json.loads(body)), + doc=( + body + if not isinstance(body, bytes) + else json.loads(body) + ), creator=signature.key_id, private_key=signature.private_key, ) signed_rsa2017 = True elif isinstance(signature.private_key, ed25519.Ed25519PrivateKey): + if "fep8b32" in sign_with and body and not signed_fep8b32: now = ( - datetime.datetime.now().isoformat(sep="T", timespec="seconds") + "Z" + datetime.datetime.now().isoformat( + sep="T", timespec="seconds" + ) + + "Z" + ) + fep_8b32_signer = apsig.ProofSigner( + private_key=signature.private_key ) - fep_8b32_signer = apsig.ProofSigner(private_key=signature.private_key) body = fep_8b32_signer.sign( unsecured_document=( - body if not isinstance(body, bytes) else json.loads(body) + body + if not isinstance(body, bytes) + else json.loads(body) ), options={ "type": "DataIntegrityProof", @@ -154,7 +208,9 @@ def validate_webfinger_result( ) -def _is_expected_content_type(actual_ctype: str, expected_ctype_prefix: str) -> bool: +def _is_expected_content_type( + actual_ctype: str, expected_ctype_prefix: str +) -> bool: mime_type = actual_ctype.split(";")[0].strip().lower() if mime_type == "application/json": diff --git a/src/apkit/client/asyncio/actor.py b/src/apkit/client/asyncio/actor.py index bb32d4f..8599c9f 100644 --- a/src/apkit/client/asyncio/actor.py +++ b/src/apkit/client/asyncio/actor.py @@ -19,7 +19,7 @@ async def resolve( headers: dict = {"Accept": "application/jrd+json"}, ) -> models.WebfingerResult: """Resolves an actor's profile from a remote server asynchronously.""" - headers = _common.ensure_user_agent_and_reconstruct( + headers = _common.reconstruct_headers( headers, self.__client.user_agent ) resource = models.Resource(username=username, host=host) @@ -37,7 +37,7 @@ async def resolve( async def fetch( self, url: str, headers: dict = {"Accept": "application/activity+json"} ) -> ActivityPubModel | dict | list | str | None: - headers = _common.ensure_user_agent_and_reconstruct( + headers = _common.reconstruct_headers( headers if headers else {}, self.__client.user_agent ) async with self.__client.get(url, headers=headers) as resp: diff --git a/src/apkit/client/asyncio/client.py b/src/apkit/client/asyncio/client.py index afbe139..917dcb3 100644 --- a/src/apkit/client/asyncio/client.py +++ b/src/apkit/client/asyncio/client.py @@ -38,7 +38,7 @@ from ..._version import __version__ from ...types import ActorKey -from .._common import ensure_user_agent_and_reconstruct, sign_request +from .._common import reconstruct_headers, sign_request from .actor import ActorFetcher from .types import ActivityPubClientResponse, _RequestContextManager @@ -164,8 +164,9 @@ async def _request( "fep8b32", ], ) -> ActivityPubClientResponse: - headers = ensure_user_agent_and_reconstruct( - headers if headers else {}, self.user_agent + headers = reconstruct_headers( + headers if headers else {}, self.user_agent, + json ) if signatures != [] and sign_with: j, headers = await asyncio.to_thread( diff --git a/src/apkit/client/sync/actor.py b/src/apkit/client/sync/actor.py index e915129..0167f24 100644 --- a/src/apkit/client/sync/actor.py +++ b/src/apkit/client/sync/actor.py @@ -19,7 +19,7 @@ def resolve( headers: dict = {"Accept": "application/jrd+json"}, ) -> models.WebfingerResult: """Resolves an actor's profile from a remote server.""" - headers = _common.ensure_user_agent_and_reconstruct( + headers = _common.reconstruct_headers( headers, self.__client.user_agent ) resource = models.Resource(username=username, host=host) @@ -37,7 +37,7 @@ def resolve( def fetch( self, url: str, headers: dict = {"Accept": "application/activity+json"} ) -> ActivityPubModel | dict | list | str | None: - headers = _common.ensure_user_agent_and_reconstruct( + headers = _common.reconstruct_headers( headers, self.__client.user_agent ) resp = self.__client.get( diff --git a/src/apkit/client/sync/client.py b/src/apkit/client/sync/client.py index 0639314..c8ee954 100644 --- a/src/apkit/client/sync/client.py +++ b/src/apkit/client/sync/client.py @@ -8,7 +8,7 @@ from ..._version import __version__ from ...types import ActorKey -from .._common import ensure_user_agent_and_reconstruct, sign_request +from .._common import reconstruct_headers, sign_request from .actor import ActorFetcher from .exceptions import TooManyRedirectsError from .types import Response @@ -59,7 +59,7 @@ def request( ) -> Response: if not self.__http: raise NotImplementedError - headers = ensure_user_agent_and_reconstruct(headers, self.user_agent) + headers = reconstruct_headers(headers, self.user_agent, content) if content is not None: content = self.__transform_to_bytes(content) if signatures != []: diff --git a/src/apkit/helper/inbox.py b/src/apkit/helper/inbox.py index 7a2db4d..d2f09d8 100644 --- a/src/apkit/helper/inbox.py +++ b/src/apkit/helper/inbox.py @@ -3,6 +3,7 @@ from typing import Any, Optional import apmodel +import http_sf from apmodel import Activity from apmodel.core.link import Link from apmodel.vocab.actor import Actor @@ -13,8 +14,9 @@ UnknownSignature, VerificationFailed, ) +from apsig.rfc9421 import RFC9421Verifier from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ed25519, rsa +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey @@ -49,7 +51,9 @@ def __get_draft_signature_parts(self, signature: str) -> dict[Any, Any]: signature_parts[key.strip()] = value.strip().strip('"') return signature_parts - async def __get_signature_from_kv(self, key_id: str) -> tuple[Optional[str], bool]: + async def __get_signature_from_kv( + self, key_id: str + ) -> tuple[Optional[str], bool]: cache = False public_key = await self.config.kv.async_get(f"signature:{key_id}") if public_key: @@ -57,6 +61,113 @@ async def __get_signature_from_kv(self, key_id: str) -> tuple[Optional[str], boo cache = True return public_key, cache + async def __verify_rfc9421( + self, + body: bytes, + url, + method: str, + headers: dict, + no_check_cache: bool = False, + ) -> bool: + body_json = json.loads(body) + activity = apmodel.load(body_json) + if not isinstance(activity, Activity): + raise ValueError("unsupported model type") + + signature_input_header = headers.get("signature-input") + if not signature_input_header: + raise MissingSignature("signature-input header is missing") + + signature_input_parsed = http_sf.parse( + signature_input_header.encode("utf-8"), tltype="dictionary" + ) + if not isinstance(signature_input_parsed, dict): + raise VerificationFailed( + f"Unsupported Signature-Input type: {type(signature_input_parsed)}" + ) + + async def _verify_with_key( + key_id: str, + public_key_obj: Any, + is_cache: bool, + ) -> bool: + try: + verifier = RFC9421Verifier( + public_key=public_key_obj, + method=method, + path=url.path, + host=url.netloc.decode("utf-8"), + headers=headers, + ) + verified_key_id = verifier.verify(raise_on_fail=True) + if verified_key_id: + if not is_cache: + if isinstance( + public_key_obj, + (rsa.RSAPublicKey, ec.EllipticCurvePublicKey), + ): + pem = public_key_obj.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + await self.config.kv.async_set( + f"signature:{key_id}", pem.decode("utf-8") + ) + elif isinstance(public_key_obj, ed25519.Ed25519PublicKey): + ku = KeyUtil(public_key_obj) + await self.config.kv.async_set( + f"signature:{key_id}", ku.encode_multibase() + ) + return True + except VerificationFailed as e: + if is_cache: + return False # Will be retried with fresh key + raise e + return False + + for _, v in signature_input_parsed.items(): + if not isinstance(v, tuple) or len(v) != 2: + continue + _, params = v + key_id = params.get("keyid") + if not key_id: + continue + key_id = str(key_id) + + # Try with cached key first + public_key_obj = None + cache = False + if not no_check_cache: + public_key_pem, cache = await self.__get_signature_from_kv(key_id) + if public_key_pem: + try: + ku = KeyUtil() + public_key_obj = ku.decode_multibase(public_key_pem) + except Exception: + try: + public_key_obj = serialization.load_pem_public_key( + public_key_pem.encode("utf-8") + ) + except ValueError: + self.logger.warning( + f"Failed to load cached key {key_id}" + ) + if public_key_obj and await _verify_with_key( + key_id, public_key_obj, True + ): + return True + + actor = await self.__fetch_actor(activity) + if actor: + key_from_actor = actor.get_key(key_id) + if key_from_actor and key_from_actor.public_key: + if await _verify_with_key( + key_id, key_from_actor.public_key, False + ): + return True + + return False + async def __verify_draft( self, body: bytes, @@ -90,17 +201,21 @@ async def __verify_draft( and isinstance(public_key.public_key, RSAPublicKey) ): verifier = Verifier( - public_key.public_key, method, url, headers, body + public_key.public_key, + method, + str(url), + headers, + body, ) try: verifier.verify(raise_on_fail=True) - if isinstance(public_key, rsa.RSAPublicKey): + if isinstance(public_key.public_key, rsa.RSAPublicKey): await self.config.kv.async_set( f"signature:{key_id}", - public_key.public_bytes( + public_key.public_key.public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo, - ), + ).decode("utf-8"), ) return True except Exception as e: @@ -117,7 +232,9 @@ async def __verify_draft( else: raise MissingSignature("this is not http signed activity.") - async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> bool: + async def __verify_proof( + self, body: bytes, no_check_cache: bool = False + ) -> bool: body_json = json.loads(body) proof_key = body_json.get("proof") @@ -135,7 +252,9 @@ async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> boo if not cache: public_keys = await self.__fetch_actor(activity) if public_keys: - public_key = public_keys.get_key(verification_method) + public_key = public_keys.get_key( + verification_method + ) if ( public_key and not isinstance(public_key, str) @@ -145,7 +264,9 @@ async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> boo try: proof.verify(body_json) if not cache: - if isinstance(public_key, ed25519.Ed25519PublicKey): + if isinstance( + public_key, ed25519.Ed25519PublicKey + ): ku = KeyUtil(public_key) await self.config.kv.async_set( f"signature:{verification_method}", @@ -168,7 +289,9 @@ async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> boo else: raise MissingSignature("this is not signed activity.") - async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: + async def __verify_ld( + self, body: bytes, no_check_cache: bool = False + ) -> bool: ld = LDSignature() body_json = json.loads(body) signature = body_json.get("signature") @@ -195,7 +318,9 @@ async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: if not isinstance(public_key, str) else public_key ) - if public_key and not isinstance(public_key, Ed25519PublicKey): + if public_key and not isinstance( + public_key, Ed25519PublicKey + ): ld.verify(body_json, public_key, raise_on_fail=True) if not cache: if isinstance(public_key, rsa.RSAPublicKey): @@ -223,12 +348,21 @@ async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: raise ValueError("unsupported model type") async def verify(self, body: bytes, url, method, headers: dict) -> bool: + try: + rfc9421 = await self.__verify_rfc9421(body, url, method, headers) + if rfc9421: + self.logger.debug("RFC9421 verification successful") + return True + except Exception as e: + self.logger.debug(f"RFC9421 verification failed; {str(e)}") try: proof = await self.__verify_proof(body) if proof: return True except Exception as e: - self.logger.debug(f"Object Integrity Proofs verification failed; {str(e)}") + self.logger.debug( + f"Object Integrity Proofs verification failed; {str(e)}" + ) try: ld = await self.__verify_ld(body) if ld: @@ -240,5 +374,7 @@ async def verify(self, body: bytes, url, method, headers: dict) -> bool: if draft: return True except Exception as e: - self.logger.debug(f"Draft HTTP signature verification failed; {str(e)}") + self.logger.debug( + f"Draft HTTP signature verification failed; {str(e)}" + ) return False From 852c7c665928369b044d956e238b1414c782df2c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:14:03 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rfc9421.py | 55 ++++++++---------------------- src/apkit/client/_common.py | 44 +++++++++--------------- src/apkit/client/asyncio/actor.py | 4 +-- src/apkit/client/asyncio/client.py | 5 +-- src/apkit/client/sync/actor.py | 8 ++--- src/apkit/helper/inbox.py | 44 ++++++------------------ 6 files changed, 45 insertions(+), 115 deletions(-) diff --git a/rfc9421.py b/rfc9421.py index 633ef8d..64c024e 100644 --- a/rfc9421.py +++ b/rfc9421.py @@ -60,9 +60,7 @@ def __build_signature_base( headers_new.append(f'"{h}": {v}') else: raise ValueError(f"Missing Header Value: {h}") - headers_new.append( - f'"@signature-params": {self.__generate_sig_input()}' - ) + headers_new.append(f'"@signature-params": {self.__generate_sig_input()}') return ("\n".join(headers_new)).encode("utf-8") def generate_signature_header(self, signature: bytes) -> str: @@ -106,13 +104,9 @@ def sign( } base = self.__build_signature_base(special_keys, headers) - signed = self.private_key.sign( - base, padding.PKCS1v15(), hashes.SHA256() - ) + signed = self.private_key.sign(base, padding.PKCS1v15(), hashes.SHA256()) headers_req = headers.copy() - headers_req["Signature"] = ( - f"sig1=:{self.generate_signature_header(signed)}:" - ) + headers_req["Signature"] = f"sig1=:{self.generate_signature_header(signed)}:" headers_req["content-digest"] = f"sha-256=:{calculate_digest(body)}:" headers_req["Signature-Input"] = f"sig1={self.__generate_sig_input()}" return headers_req @@ -133,9 +127,7 @@ def __init__( clock_skew: int = 300, ): self.public_key: ( - ed25519.Ed25519PublicKey - | rsa.RSAPublicKey - | ec.EllipticCurvePublicKey + ed25519.Ed25519PublicKey | rsa.RSAPublicKey | ec.EllipticCurvePublicKey ) if isinstance(public_key, str): @@ -148,32 +140,22 @@ def __init__( case "rsa-pub": pubkey = serialization.load_pem_public_key(data) if not isinstance(pubkey, rsa.RSAPublicKey): - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) + raise TypeError("PublicKey must be ed25519 or RSA or ECDSA.") self.public_key = pubkey case "p256-pub": pubkey = serialization.load_pem_public_key(data) if not isinstance(pubkey, ec.EllipticCurvePublicKey): - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) + raise TypeError("PublicKey must be ed25519 or RSA or ECDSA.") self.public_key = pubkey case "p384-pub": pubkey = serialization.load_pem_public_key(data) if not isinstance(pubkey, ec.EllipticCurvePublicKey): - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) + raise TypeError("PublicKey must be ed25519 or RSA or ECDSA.") self.public_key = pubkey case _: - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) + raise TypeError("PublicKey must be ed25519 or RSA or ECDSA.") else: - self.public_key: ed25519.Ed25519PublicKey | rsa.RSAPublicKey = ( - public_key - ) + self.public_key: ed25519.Ed25519PublicKey | rsa.RSAPublicKey = public_key self.clock_skew = clock_skew self.method = method.upper() self.path = path @@ -189,9 +171,7 @@ def __expect_value_and_params_member( value, params = member if not isinstance(params, dict): raise ValueError("expected params to be a dict") - return cast( - Tuple[ItemType | InnerListType, ParamsType], (value, params) - ) + return cast(Tuple[ItemType | InnerListType, ParamsType], (value, params)) def __generate_sig_input( self, headers: List[BareItemType], params: ParamsType @@ -284,8 +264,7 @@ def verify(self, raise_on_fail: bool = False) -> str | None: value, params = self.__expect_value_and_params_member(v) if isinstance(value, list): headers: List[BareItemType] = [ - itm[0] if isinstance(itm, tuple) else itm - for itm in value + itm[0] if isinstance(itm, tuple) else itm for itm in value ] else: raise ValueError( @@ -333,9 +312,7 @@ def verify(self, raise_on_fail: bool = False) -> str | None: raise VerificationFailed("Algorithm missmatch.") self.public_key.verify(sig_val, sigi) case "rsa-v1_5-sha256": - if not isinstance( - self.public_key, rsa.RSAPublicKey - ): + if not isinstance(self.public_key, rsa.RSAPublicKey): raise VerificationFailed("Algorithm missmatch.") self.public_key.verify( sig_val, @@ -344,9 +321,7 @@ def verify(self, raise_on_fail: bool = False) -> str | None: hashes.SHA256(), ) case "rsa-v1_5-sha512": - if not isinstance( - self.public_key, rsa.RSAPublicKey - ): + if not isinstance(self.public_key, rsa.RSAPublicKey): raise VerificationFailed("Algorithm missmatch.") self.public_key.verify( sig_val, @@ -355,9 +330,7 @@ def verify(self, raise_on_fail: bool = False) -> str | None: hashes.SHA512(), ) case "rsa-pss-sha512": - if not isinstance( - self.public_key, rsa.RSAPublicKey - ): + if not isinstance(self.public_key, rsa.RSAPublicKey): raise VerificationFailed("Algorithm missmatch.") self.public_key.verify( sig_val, diff --git a/src/apkit/client/_common.py b/src/apkit/client/_common.py index 677edf5..bcb4138 100644 --- a/src/apkit/client/_common.py +++ b/src/apkit/client/_common.py @@ -54,7 +54,9 @@ def reconstruct_headers( if json: if isinstance(json, ActivityPubModel): if "content-type" not in processed_headers: - processed_headers["content-type"] = "application/activity+json; charset=UTF-8" + processed_headers[ + "content-type" + ] = "application/activity+json; charset=UTF-8" processed_headers["content-type_original_key"] = "Content-Type" elif isinstance(json, dict): if "content-type" not in processed_headers: @@ -102,10 +104,10 @@ def sign_request( if "rfc9421" in sign_with and not signed_rfc9421: if "draft-cavage" in sign_with: warnings.warn( - 'Draft and RFC9421 Signing is exclusive. ' - 'Legacy Draft mode is enabled to maintain compatibility. ' - 'The RFC 9421 (Structured Fields) signing logic will not be applied to this message.', - UserWarning + "Draft and RFC9421 Signing is exclusive. " + "Legacy Draft mode is enabled to maintain compatibility. " + "The RFC 9421 (Structured Fields) signing logic will not be applied to this message.", + UserWarning, ) signed_rfc9421 = True else: @@ -131,10 +133,10 @@ def sign_request( if "draft-cavage" in sign_with and not signed_cavage: if "rfc9421" in sign_with: warnings.warn( - 'Draft and RFC9421 Signing is exclusive. ' - 'Legacy Draft mode is enabled to maintain compatibility. ' - 'The RFC 9421 (Structured Fields) signing logic will not be applied to this message.', - UserWarning + "Draft and RFC9421 Signing is exclusive. " + "Legacy Draft mode is enabled to maintain compatibility. " + "The RFC 9421 (Structured Fields) signing logic will not be applied to this message.", + UserWarning, ) signed_rfc9421 = True signer = draft.Signer( @@ -151,32 +153,20 @@ def sign_request( if "rsa2017" in sign_with and body and not signed_rsa2017: ld_signer = apsig.LDSignature() body = ld_signer.sign( - doc=( - body - if not isinstance(body, bytes) - else json.loads(body) - ), + doc=(body if not isinstance(body, bytes) else json.loads(body)), creator=signature.key_id, private_key=signature.private_key, ) signed_rsa2017 = True elif isinstance(signature.private_key, ed25519.Ed25519PrivateKey): - if "fep8b32" in sign_with and body and not signed_fep8b32: now = ( - datetime.datetime.now().isoformat( - sep="T", timespec="seconds" - ) - + "Z" - ) - fep_8b32_signer = apsig.ProofSigner( - private_key=signature.private_key + datetime.datetime.now().isoformat(sep="T", timespec="seconds") + "Z" ) + fep_8b32_signer = apsig.ProofSigner(private_key=signature.private_key) body = fep_8b32_signer.sign( unsecured_document=( - body - if not isinstance(body, bytes) - else json.loads(body) + body if not isinstance(body, bytes) else json.loads(body) ), options={ "type": "DataIntegrityProof", @@ -208,9 +198,7 @@ def validate_webfinger_result( ) -def _is_expected_content_type( - actual_ctype: str, expected_ctype_prefix: str -) -> bool: +def _is_expected_content_type(actual_ctype: str, expected_ctype_prefix: str) -> bool: mime_type = actual_ctype.split(";")[0].strip().lower() if mime_type == "application/json": diff --git a/src/apkit/client/asyncio/actor.py b/src/apkit/client/asyncio/actor.py index 8599c9f..0396110 100644 --- a/src/apkit/client/asyncio/actor.py +++ b/src/apkit/client/asyncio/actor.py @@ -19,9 +19,7 @@ async def resolve( headers: dict = {"Accept": "application/jrd+json"}, ) -> models.WebfingerResult: """Resolves an actor's profile from a remote server asynchronously.""" - headers = _common.reconstruct_headers( - headers, self.__client.user_agent - ) + headers = _common.reconstruct_headers(headers, self.__client.user_agent) resource = models.Resource(username=username, host=host) url = _common.build_webfinger_url(host=host, resource=resource) diff --git a/src/apkit/client/asyncio/client.py b/src/apkit/client/asyncio/client.py index 917dcb3..621126f 100644 --- a/src/apkit/client/asyncio/client.py +++ b/src/apkit/client/asyncio/client.py @@ -164,10 +164,7 @@ async def _request( "fep8b32", ], ) -> ActivityPubClientResponse: - headers = reconstruct_headers( - headers if headers else {}, self.user_agent, - json - ) + headers = reconstruct_headers(headers if headers else {}, self.user_agent, json) if signatures != [] and sign_with: j, headers = await asyncio.to_thread( sign_request, diff --git a/src/apkit/client/sync/actor.py b/src/apkit/client/sync/actor.py index 0167f24..c800f22 100644 --- a/src/apkit/client/sync/actor.py +++ b/src/apkit/client/sync/actor.py @@ -19,9 +19,7 @@ def resolve( headers: dict = {"Accept": "application/jrd+json"}, ) -> models.WebfingerResult: """Resolves an actor's profile from a remote server.""" - headers = _common.reconstruct_headers( - headers, self.__client.user_agent - ) + headers = _common.reconstruct_headers(headers, self.__client.user_agent) resource = models.Resource(username=username, host=host) url = _common.build_webfinger_url(host=host, resource=resource) @@ -37,9 +35,7 @@ def resolve( def fetch( self, url: str, headers: dict = {"Accept": "application/activity+json"} ) -> ActivityPubModel | dict | list | str | None: - headers = _common.reconstruct_headers( - headers, self.__client.user_agent - ) + headers = _common.reconstruct_headers(headers, self.__client.user_agent) resp = self.__client.get( url, headers=headers, diff --git a/src/apkit/helper/inbox.py b/src/apkit/helper/inbox.py index d2f09d8..b129abb 100644 --- a/src/apkit/helper/inbox.py +++ b/src/apkit/helper/inbox.py @@ -51,9 +51,7 @@ def __get_draft_signature_parts(self, signature: str) -> dict[Any, Any]: signature_parts[key.strip()] = value.strip().strip('"') return signature_parts - async def __get_signature_from_kv( - self, key_id: str - ) -> tuple[Optional[str], bool]: + async def __get_signature_from_kv(self, key_id: str) -> tuple[Optional[str], bool]: cache = False public_key = await self.config.kv.async_get(f"signature:{key_id}") if public_key: @@ -149,21 +147,15 @@ async def _verify_with_key( public_key_pem.encode("utf-8") ) except ValueError: - self.logger.warning( - f"Failed to load cached key {key_id}" - ) - if public_key_obj and await _verify_with_key( - key_id, public_key_obj, True - ): + self.logger.warning(f"Failed to load cached key {key_id}") + if public_key_obj and await _verify_with_key(key_id, public_key_obj, True): return True actor = await self.__fetch_actor(activity) if actor: key_from_actor = actor.get_key(key_id) if key_from_actor and key_from_actor.public_key: - if await _verify_with_key( - key_id, key_from_actor.public_key, False - ): + if await _verify_with_key(key_id, key_from_actor.public_key, False): return True return False @@ -232,9 +224,7 @@ async def __verify_draft( else: raise MissingSignature("this is not http signed activity.") - async def __verify_proof( - self, body: bytes, no_check_cache: bool = False - ) -> bool: + async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> bool: body_json = json.loads(body) proof_key = body_json.get("proof") @@ -252,9 +242,7 @@ async def __verify_proof( if not cache: public_keys = await self.__fetch_actor(activity) if public_keys: - public_key = public_keys.get_key( - verification_method - ) + public_key = public_keys.get_key(verification_method) if ( public_key and not isinstance(public_key, str) @@ -264,9 +252,7 @@ async def __verify_proof( try: proof.verify(body_json) if not cache: - if isinstance( - public_key, ed25519.Ed25519PublicKey - ): + if isinstance(public_key, ed25519.Ed25519PublicKey): ku = KeyUtil(public_key) await self.config.kv.async_set( f"signature:{verification_method}", @@ -289,9 +275,7 @@ async def __verify_proof( else: raise MissingSignature("this is not signed activity.") - async def __verify_ld( - self, body: bytes, no_check_cache: bool = False - ) -> bool: + async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: ld = LDSignature() body_json = json.loads(body) signature = body_json.get("signature") @@ -318,9 +302,7 @@ async def __verify_ld( if not isinstance(public_key, str) else public_key ) - if public_key and not isinstance( - public_key, Ed25519PublicKey - ): + if public_key and not isinstance(public_key, Ed25519PublicKey): ld.verify(body_json, public_key, raise_on_fail=True) if not cache: if isinstance(public_key, rsa.RSAPublicKey): @@ -360,9 +342,7 @@ async def verify(self, body: bytes, url, method, headers: dict) -> bool: if proof: return True except Exception as e: - self.logger.debug( - f"Object Integrity Proofs verification failed; {str(e)}" - ) + self.logger.debug(f"Object Integrity Proofs verification failed; {str(e)}") try: ld = await self.__verify_ld(body) if ld: @@ -374,7 +354,5 @@ async def verify(self, body: bytes, url, method, headers: dict) -> bool: if draft: return True except Exception as e: - self.logger.debug( - f"Draft HTTP signature verification failed; {str(e)}" - ) + self.logger.debug(f"Draft HTTP signature verification failed; {str(e)}") return False From 782a01a929fbcd18baa31f2c4241c19a6fd1dd51 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 05:44:28 +0900 Subject: [PATCH 3/9] fix: remove copied code from apsig --- rfc9421.py | 401 ----------------------------------------------------- 1 file changed, 401 deletions(-) delete mode 100644 rfc9421.py diff --git a/rfc9421.py b/rfc9421.py deleted file mode 100644 index 633ef8d..0000000 --- a/rfc9421.py +++ /dev/null @@ -1,401 +0,0 @@ -import base64 -import datetime as dt -import email.utils -import json -from typing import Any, List, Tuple, cast - -import pytz -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa -from http_sf import parse -from http_sf.types import ( - BareItemType, - InnerListType, - ItemType, - ParamsType, -) -from multiformats import multibase, multicodec - -from apsig.draft.tools import calculate_digest -from apsig.exceptions import MissingSignature, VerificationFailed - - -class RFC9421Signer: - def __init__(self, private_key: rsa.RSAPrivateKey, key_id: str): - self.private_key = private_key - self.key_id = key_id - self.sign_headers = [ - "date", - "@method", - "@path", - "@authority", - "content-type", - "content-length", - ] - - def __build_signature_base( - self, special_keys: dict[str, str], headers: dict[str, str] - ) -> bytes: - headers_new = [] - headers = headers.copy() - for h in self.sign_headers: - if h in ["@method", "@path", "@authority"]: - v = special_keys.get(h) - - if v: - headers_new.append(f'"{h}": {v}') - else: - raise ValueError(f"Missing Value: {h}") - elif h == "@signature-params": - v = special_keys.get(h) - - if v: - headers_new.append(f'"{h}": {self.__generate_sig_input()}') - else: - raise ValueError(f"Missing Value: {h}") - else: - v_raw = headers.get(h) - if v_raw is not None: - v = v_raw.strip() - headers_new.append(f'"{h}": {v}') - else: - raise ValueError(f"Missing Header Value: {h}") - headers_new.append( - f'"@signature-params": {self.__generate_sig_input()}' - ) - return ("\n".join(headers_new)).encode("utf-8") - - def generate_signature_header(self, signature: bytes) -> str: - return base64.b64encode(signature).decode("utf-8") - - def __generate_sig_input(self): - param = "(" - target_len = len(self.sign_headers) - timestamp = dt.datetime.now(dt.UTC) - for p in self.sign_headers: - param += f'"{p}"' - if p != self.sign_headers[target_len - 1]: - param += " " - param += ");" - param += f"created={int(timestamp.timestamp())};" - param += 'alg="rsa-v1_5-sha256";' - param += f'keyid="{self.key_id}"' - return param - - def sign( - self, - method: str, - path: str, - host: str, - headers: dict, - body: bytes | dict = b"", - ): - if isinstance(body, dict): - body = json.dumps(body).encode("utf-8") - - headers = {k.lower(): v for k, v in headers.items()} - if not headers.get("date"): - headers["date"] = email.utils.formatdate(usegmt=True) - if not headers.get("content-length"): - headers["content-length"] = str(len(body)) - - special_keys = { - "@method": method.upper(), - "@path": path, - "@authority": host, - } - - base = self.__build_signature_base(special_keys, headers) - signed = self.private_key.sign( - base, padding.PKCS1v15(), hashes.SHA256() - ) - headers_req = headers.copy() - headers_req["Signature"] = ( - f"sig1=:{self.generate_signature_header(signed)}:" - ) - headers_req["content-digest"] = f"sha-256=:{calculate_digest(body)}:" - headers_req["Signature-Input"] = f"sig1={self.__generate_sig_input()}" - return headers_req - - -class RFC9421Verifier: - def __init__( - self, - public_key: ed25519.Ed25519PublicKey - | rsa.RSAPublicKey - | ec.EllipticCurvePublicKey - | str, - method: str, - path: str, - host: str, - headers: dict[str, str], - body: bytes | dict | None = None, - clock_skew: int = 300, - ): - self.public_key: ( - ed25519.Ed25519PublicKey - | rsa.RSAPublicKey - | ec.EllipticCurvePublicKey - ) - - if isinstance(public_key, str): - codec, data = multicodec.unwrap(multibase.decode(public_key)) - match codec.name: - case "ed25519-pub": - self.public_key: ed25519.Ed25519PublicKey = ( - ed25519.Ed25519PublicKey.from_public_bytes(data) - ) - case "rsa-pub": - pubkey = serialization.load_pem_public_key(data) - if not isinstance(pubkey, rsa.RSAPublicKey): - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) - self.public_key = pubkey - case "p256-pub": - pubkey = serialization.load_pem_public_key(data) - if not isinstance(pubkey, ec.EllipticCurvePublicKey): - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) - self.public_key = pubkey - case "p384-pub": - pubkey = serialization.load_pem_public_key(data) - if not isinstance(pubkey, ec.EllipticCurvePublicKey): - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) - self.public_key = pubkey - case _: - raise TypeError( - "PublicKey must be ed25519 or RSA or ECDSA." - ) - else: - self.public_key: ed25519.Ed25519PublicKey | rsa.RSAPublicKey = ( - public_key - ) - self.clock_skew = clock_skew - self.method = method.upper() - self.path = path - self.host = host - self.headers = {key.lower(): value for key, value in headers.items()} - - def __expect_value_and_params_member( - self, - member: Any, - ) -> Tuple[ItemType | InnerListType, ParamsType]: - if not isinstance(member, tuple) or len(member) != 2: - raise ValueError("expected a (value, params) tuple") - value, params = member - if not isinstance(params, dict): - raise ValueError("expected params to be a dict") - return cast( - Tuple[ItemType | InnerListType, ParamsType], (value, params) - ) - - def __generate_sig_input( - self, headers: List[BareItemType], params: ParamsType - ) -> str: - created = params.get("created") - alg = params.get("alg") - keyid = params.get("keyid") - - if isinstance(created, dt.datetime): - created_timestamp = created - elif isinstance(created, int): - created_timestamp = dt.datetime.fromtimestamp(created) - elif isinstance(created, str): - created_timestamp = dt.datetime.fromtimestamp(int(created)) - else: - raise ValueError("Unknown created value") - request_time = created_timestamp.astimezone(pytz.utc) - current_time = dt.datetime.now(dt.UTC) - if abs((current_time - request_time).total_seconds()) > self.clock_skew: - raise VerificationFailed( - f"property created is too far from current time ({current_time}): {request_time}" - ) - - param = "(" - target_len = len(headers) - for p in headers: - param += f'"{p}"' - if p != headers[target_len - 1]: - param += " " - param += ");" - param += f"created={int(created_timestamp.timestamp())};" - param += f'alg="{alg}";' - param += f'keyid="{keyid}"' - return param - - def __rebuild_sigbase( - self, headers: List[BareItemType], params: ParamsType - ) -> bytes: - special_keys = { - "@method": self.method, - "@path": self.path, - "@authority": self.host, - } - base = [] - for h in cast(List[str], headers): - if h in ["@method", "@path", "@authority"]: - base.append(f'"{h}": {special_keys.get(h)}') - else: - v_raw = self.headers.get(h) - if v_raw is not None: - v = v_raw.strip() - base.append(f'"{h}": {v}') - else: - raise ValueError(f"Missing Header Value: {h}") - base.append( - f'"@signature-params": {self.__generate_sig_input(headers=headers, params=params)}' - ) - return ("\n".join(base)).encode("utf-8") - - def verify(self, raise_on_fail: bool = False) -> str | None: - signature = self.headers.get("signature") - if not signature: - if raise_on_fail: - raise MissingSignature("Signature header is missing") - return None - - signature_input = self.headers.get("signature-input") - if not signature_input: - if raise_on_fail: - raise MissingSignature("Signature-Input header is missing") - return None - - signature_input_parsed = parse( - signature_input.encode("utf-8"), tltype="dictionary" - ) - signature_parsed = parse(signature.encode("utf-8"), tltype="dictionary") - - if not isinstance(signature_input_parsed, dict): - raise VerificationFailed( - f"Unsupported Signature-Input type: {type(signature_input_parsed)}" - ) - - if not isinstance(signature_parsed, dict): - raise VerificationFailed( - f"Unsupported Signature type: {type(signature_parsed)}" - ) - - for k, v in signature_input_parsed.items(): - try: - value, params = self.__expect_value_and_params_member(v) - if isinstance(value, list): - headers: List[BareItemType] = [ - itm[0] if isinstance(itm, tuple) else itm - for itm in value - ] - else: - raise ValueError( - "expected the value to be an inner-list (list of items)" - ) - - created = params.get("created") - key_id = str(params.get("keyid")) - alg = params.get("alg") - - if not created: - raise VerificationFailed("created not found.") - if not key_id: - raise VerificationFailed("keyid not found.") - if not alg: - raise VerificationFailed("alg not found.") - if alg not in [ - "ed25519", - "rsa-v1_5-sha256", - "rsa-v1_5-sha512", - "rsa-pss-sha512", - ]: - raise VerificationFailed(f"Unsupported algorithm: {alg}") - - sigi = self.__rebuild_sigbase(headers, params) - signature_bytes = signature_parsed.get(k) - if not isinstance(signature_bytes, tuple): - raise VerificationFailed( - f"Unknown Signature: {type(signature_bytes)}" - ) - - sig_val = None - for sig in cast(InnerListType, signature_bytes): - if isinstance(sig, bytes): - sig_val = sig - break - if sig_val is None: - raise ValueError("No Signature found.") - try: - match alg: - case "ed25519": - if not isinstance( - self.public_key, ed25519.Ed25519PublicKey - ): - raise VerificationFailed("Algorithm missmatch.") - self.public_key.verify(sig_val, sigi) - case "rsa-v1_5-sha256": - if not isinstance( - self.public_key, rsa.RSAPublicKey - ): - raise VerificationFailed("Algorithm missmatch.") - self.public_key.verify( - sig_val, - sigi, - padding.PKCS1v15(), - hashes.SHA256(), - ) - case "rsa-v1_5-sha512": - if not isinstance( - self.public_key, rsa.RSAPublicKey - ): - raise VerificationFailed("Algorithm missmatch.") - self.public_key.verify( - sig_val, - sigi, - padding.PKCS1v15(), - hashes.SHA512(), - ) - case "rsa-pss-sha512": - if not isinstance( - self.public_key, rsa.RSAPublicKey - ): - raise VerificationFailed("Algorithm missmatch.") - self.public_key.verify( - sig_val, - sigi, - padding.PSS( - mgf=padding.MGF1(hashes.SHA512()), - salt_length=hashes.SHA512().digest_size, - ), - hashes.SHA512(), - ) - case "ecdsa-p256-sha256": - if not isinstance( - self.public_key, ec.EllipticCurvePublicKey - ): - raise VerificationFailed("Algorithm missmatch.") - self.public_key.verify( - sig_val, - sigi, - ec.ECDSA(hashes.SHA256()), - ) - case "ecdsa-p384-sha384": - if not isinstance( - self.public_key, ec.EllipticCurvePublicKey - ): - raise VerificationFailed("Algorithm missmatch.") - self.public_key.verify( - sig_val, - sigi, - ec.ECDSA(hashes.SHA384()), - ) - return key_id - except Exception as e: - if raise_on_fail: - raise VerificationFailed(str(e)) - return None - except ValueError: - continue - - if raise_on_fail: - raise VerificationFailed("RFC9421 Signature verification failed.") - return None From c5131b5a1637097f53b15e189ce424ad311f3a58 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 05:45:08 +0900 Subject: [PATCH 4/9] fix: remove warning --- src/apkit/client/_common.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/apkit/client/_common.py b/src/apkit/client/_common.py index 677edf5..5dc5a3b 100644 --- a/src/apkit/client/_common.py +++ b/src/apkit/client/_common.py @@ -1,7 +1,7 @@ -import warnings import datetime import json import urllib.parse +import warnings from collections.abc import Mapping from typing import ( Any, @@ -25,7 +25,9 @@ def reconstruct_headers( - headers: Any, user_agent: str, json: Optional[dict | ActivityPubModel | Any] = None + headers: Any, + user_agent: str, + json: Optional[dict | ActivityPubModel | Any] = None, ) -> Dict[str, str]: processed_headers: Dict[str, Any] = {} @@ -54,7 +56,9 @@ def reconstruct_headers( if json: if isinstance(json, ActivityPubModel): if "content-type" not in processed_headers: - processed_headers["content-type"] = "application/activity+json; charset=UTF-8" + processed_headers["content-type"] = ( + "application/activity+json; charset=UTF-8" + ) processed_headers["content-type_original_key"] = "Content-Type" elif isinstance(json, dict): if "content-type" not in processed_headers: @@ -102,20 +106,15 @@ def sign_request( if "rfc9421" in sign_with and not signed_rfc9421: if "draft-cavage" in sign_with: warnings.warn( - 'Draft and RFC9421 Signing is exclusive. ' - 'Legacy Draft mode is enabled to maintain compatibility. ' - 'The RFC 9421 (Structured Fields) signing logic will not be applied to this message.', - UserWarning + "Draft and RFC9421 Signing is exclusive. " + "Legacy Draft mode is enabled to maintain compatibility. " + "The RFC 9421 (Structured Fields) signing logic will not be applied to this message.", + UserWarning, ) signed_rfc9421 = True else: parsed_url = urllib.parse.urlparse(url) if parsed_url.hostname: - # warnings.warn( - # 'This signature spec "rfc9421" is not implemented yet.', - # category=NotImplementedWarning, - # stacklevel=2, - # ) rfc_signer = RFC9421Signer( signature.private_key, signature.key_id ) @@ -131,10 +130,10 @@ def sign_request( if "draft-cavage" in sign_with and not signed_cavage: if "rfc9421" in sign_with: warnings.warn( - 'Draft and RFC9421 Signing is exclusive. ' - 'Legacy Draft mode is enabled to maintain compatibility. ' - 'The RFC 9421 (Structured Fields) signing logic will not be applied to this message.', - UserWarning + "Draft and RFC9421 Signing is exclusive. " + "Legacy Draft mode is enabled to maintain compatibility. " + "The RFC 9421 (Structured Fields) signing logic will not be applied to this message.", + UserWarning, ) signed_rfc9421 = True signer = draft.Signer( @@ -161,7 +160,6 @@ def sign_request( ) signed_rsa2017 = True elif isinstance(signature.private_key, ed25519.Ed25519PrivateKey): - if "fep8b32" in sign_with and body and not signed_fep8b32: now = ( datetime.datetime.now().isoformat( From efd478c873eb9e830dfa1fffc2bd3cfe35e70b64 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:47:27 +0000 Subject: [PATCH 5/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/apkit/client/_common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apkit/client/_common.py b/src/apkit/client/_common.py index 88cd344..a3894d4 100644 --- a/src/apkit/client/_common.py +++ b/src/apkit/client/_common.py @@ -56,9 +56,9 @@ def reconstruct_headers( if json: if isinstance(json, ActivityPubModel): if "content-type" not in processed_headers: - processed_headers["content-type"] = ( - "application/activity+json; charset=UTF-8" - ) + processed_headers[ + "content-type" + ] = "application/activity+json; charset=UTF-8" processed_headers["content-type_original_key"] = "Content-Type" elif isinstance(json, dict): if "content-type" not in processed_headers: From 90fae655ff960ea9356d78167f56a4465b8d44d6 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 06:25:48 +0900 Subject: [PATCH 6/9] chore: update apsig to 0.6.0 --- pyproject.toml | 2 +- src/apkit/_version.py | 4 ++-- uv.lock | 46 +++++++++++++++++++++++++++++++------------ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1232a99..cdc3423 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.11" dependencies = [ "aiohttp>=3.12.15", "apmodel>=0.5.1", - "apsig>=0.5.4", + "apsig>=0.6.0", "charset-normalizer>=3.4.3", "httpcore[http2,socks]>=1.0.9", "httpx>=0.28.1", diff --git a/src/apkit/_version.py b/src/apkit/_version.py index 54c1fab..ac8011d 100644 --- a/src/apkit/_version.py +++ b/src/apkit/_version.py @@ -28,7 +28,7 @@ commit_id: COMMIT_ID __commit_id__: COMMIT_ID -__version__ = version = '0.3.3.post1.dev27+ge47c822fb.d20251228' -__version_tuple__ = version_tuple = (0, 3, 3, 'post1', 'dev27', 'ge47c822fb.d20251228') +__version__ = version = '0.3.3.post1.dev32+g5c3dfc179.d20251228' +__version_tuple__ = version_tuple = (0, 3, 3, 'post1', 'dev32', 'g5c3dfc179.d20251228') __commit_id__ = commit_id = None diff --git a/uv.lock b/uv.lock index 3bd8186..699ddf6 100644 --- a/uv.lock +++ b/uv.lock @@ -200,7 +200,7 @@ lint = [ requires-dist = [ { name = "aiohttp", specifier = ">=3.12.15" }, { name = "apmodel", git = "https://github.com/fedi-libs/apmodel.git?branch=develop" }, - { name = "apsig", git = "https://github.com/fedi-libs/apsig.git" }, + { name = "apsig", specifier = ">=0.6.0" }, { name = "charset-normalizer", specifier = ">=3.4.3" }, { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.116.1" }, { name = "httpcore", extras = ["http2", "socks"], specifier = ">=1.0.9" }, @@ -241,17 +241,22 @@ dependencies = [ [[package]] name = "apsig" -version = "0.5.4.post1.dev0+g978995e92.d20251223" -source = { git = "https://github.com/fedi-libs/apsig.git#978995e928630382fd165e813b992c70f9cc03b1" } +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "authlib" }, { name = "cryptography" }, + { name = "http-sf" }, { name = "jcs" }, { name = "multiformats" }, - { name = "pyfill" }, { name = "pyld", extra = ["aiohttp", "requests"] }, { name = "pytz" }, { name = "typing-extensions" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/9f/99/68ed305078b58b2f79539f9e4fce210b2b63318cf6ea56821490a259a7c0/apsig-0.6.0.tar.gz", hash = "sha256:9bf17c7978ff45f21313a40f0255dd1fae35cca61ef7e613fb70274ca64ec823", size = 151386, upload-time = "2025-12-28T21:23:52.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/6f/7f4293cdfc39a2869bd95402ae4d680ea42e3578c6064e32700c918e7647/apsig-0.6.0-py3-none-any.whl", hash = "sha256:1a4d742f13deb0b3a344ee69e3edbdc3c091a03d0b47a40ce70966b2e637ceb5", size = 20242, upload-time = "2025-12-28T21:23:51.285Z" }, +] [[package]] name = "async-timeout" @@ -271,6 +276,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "authlib" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, +] + [[package]] name = "babel" version = "2.17.0" @@ -817,6 +834,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, ] +[[package]] +name = "http-sf" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/3a/68604f0071830a0a0054a2f521063807a1bc23341df32926fb61607f95a6/http_sf-1.0.7.tar.gz", hash = "sha256:862b8cd7c386cbcdd382f499cdfbf66eebc48fc4f0e974bfea483fd001265f96", size = 18371, upload-time = "2025-11-26T04:05:18.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/b9/9d68c4fad07019060bff4518765d4ea56363f4f4d2d8aea120ca97d09d3c/http_sf-1.0.7-py3-none-any.whl", hash = "sha256:8eab6db3194fe3ecb2ed01766e53d85b75184f413af94ecedef00d28f8b82320", size = 23154, upload-time = "2025-11-26T04:05:17.186Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1573,15 +1602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] -[[package]] -name = "pyfill" -version = "0.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/31/594dcd00ec7a13e7a847d8b3c14a9be74d785feaf7812f3313bbe2a97854/pyfill-0.1.3.tar.gz", hash = "sha256:f8416ca329c5c61da9323527b3f8f263f9ac71139d00ac3f5d2fa1398afca274", size = 4186, upload-time = "2025-02-28T08:45:22.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/61/c76ae9a76a0951c2cef4169056433b6900d470575a71af08321f5e46be9f/pyfill-0.1.3-py3-none-any.whl", hash = "sha256:f6057ac9ab418855f278a21b02082a80f969b2decd15229a476c9f74a2f353d7", size = 5039, upload-time = "2025-02-28T08:45:21.687Z" }, -] - [[package]] name = "pygments" version = "2.19.2" From c65fc9498a24e26eb8d445f2e585566213d59a38 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 06:26:58 +0900 Subject: [PATCH 7/9] fix: remove tool.uv.sources --- pyproject.toml | 4 ---- uv.lock | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cdc3423..466ca5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,6 @@ server = [ [tool.uv] default-groups = "all" -[tool.uv.sources] -apmodel = { git = "https://github.com/fedi-libs/apmodel.git", branch = "develop" } -apsig = { git = "https://github.com/fedi-libs/apsig.git" } - [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index 699ddf6..b00e690 100644 --- a/uv.lock +++ b/uv.lock @@ -199,7 +199,7 @@ lint = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = ">=3.12.15" }, - { name = "apmodel", git = "https://github.com/fedi-libs/apmodel.git?branch=develop" }, + { name = "apmodel", specifier = ">=0.5.1" }, { name = "apsig", specifier = ">=0.6.0" }, { name = "charset-normalizer", specifier = ">=3.4.3" }, { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.116.1" }, @@ -230,7 +230,7 @@ lint = [ [[package]] name = "apmodel" version = "0.5.1" -source = { git = "https://github.com/fedi-libs/apmodel.git?branch=develop#9a9c00f8435d1466d4fa7299f9afbd283fa864f2" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "multiformats" }, @@ -238,6 +238,10 @@ dependencies = [ { name = "pyld", extra = ["aiohttp", "requests"] }, { name = "typing-extensions" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/9e/42/9519ea411e329137e333e704acfcb2c8f8ac1e2904dbad77afe28a124f4b/apmodel-0.5.1.tar.gz", hash = "sha256:0e6974a7d6cad6c82db5a43caad6d0aac6197463484b31470b9591c6fa3277a4", size = 221722, upload-time = "2025-12-23T07:47:01.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/16/20225cb4923bbec98cbebf004fb5de717dad5be0e0ffd15967399dd40e11/apmodel-0.5.1-py3-none-any.whl", hash = "sha256:6fbb8e7de0cdc917508c43c7de59fa062bfc6b5ee5b34c45107118699e21c5ea", size = 68257, upload-time = "2025-12-23T07:46:59.851Z" }, +] [[package]] name = "apsig" From 0fb21a6177f426dd6fea068736c59dfab86cb580 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 06:27:35 +0900 Subject: [PATCH 8/9] fix: update deps --- .pre-commit-config.yaml | 151 ++++++++++++++++++++-------------------- src/apkit/_version.py | 4 +- 2 files changed, 79 insertions(+), 76 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83b2ca4..848cb8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,75 +1,78 @@ repos: - - hooks: - - additional_dependencies: - - aiohttp>=3.12.15 - - apmodel>=0.5.0 - - apsig>=0.5.4 - - charset-normalizer>=3.4.3 - - coverage>=7.10.7 - - fastapi>=0.116.1 - - httpcore[http2,socks]>=1.0.9 - - httpx>=0.28.1 - - pytest>=8.4.1 - - redis>=5.0.4 - - requests>=2.32.5 - - types-requests>=2.32.4.20250913 - - uvicorn>=0.35.0 - args: - - --fix - description: Run 'ruff' for extremely fast Python linting - entry: ruff check --force-exclude - id: ruff - language: python - minimum_pre_commit_version: 2.9.2 - name: ruff - require_serial: true - types_or: - - python - - pyi - - additional_dependencies: - - aiohttp>=3.12.15 - - apmodel>=0.5.0 - - apsig>=0.5.4 - - charset-normalizer>=3.4.3 - - coverage>=7.10.7 - - fastapi>=0.116.1 - - httpcore[http2,socks]>=1.0.9 - - httpx>=0.28.1 - - pytest>=8.4.1 - - redis>=5.0.4 - - requests>=2.32.5 - - types-requests>=2.32.4.20250913 - - uvicorn>=0.35.0 - args: [] - description: Run 'ruff format' for extremely fast Python formatting - entry: ruff format --force-exclude - id: ruff-format - language: python - minimum_pre_commit_version: 2.9.2 - name: ruff-format - require_serial: true - types_or: - - python - - pyi - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 - - hooks: - - additional_dependencies: - - aiohttp>=3.12.15 - - apmodel>=0.5.0 - - apsig>=0.5.4 - - charset-normalizer>=3.4.3 - - coverage>=7.10.7 - - fastapi>=0.116.1 - - httpcore[http2,socks]>=1.0.9 - - httpx>=0.28.1 - - pytest>=8.4.1 - - redis>=5.0.4 - - requests>=2.32.5 - - types-requests>=2.32.4.20250913 - - uvicorn>=0.35.0 - id: pyrefly-check - name: Pyrefly (type checking) - pass_filenames: false - repo: https://github.com/facebook/pyrefly-pre-commit - rev: 0.46.0 +- hooks: + - additional_dependencies: + - aiohttp>=3.12.15 + - apmodel>=0.5.1 + - apsig>=0.6.0 + - charset-normalizer>=3.4.3 + - coverage>=7.10.7 + - fastapi>=0.116.1 + - httpcore[http2,socks]>=1.0.9 + - httpx>=0.28.1 + - pytest-cov>=7.0.0 + - pytest>=8.4.1 + - redis>=5.0.4 + - requests>=2.32.5 + - types-requests>=2.32.4.20250913 + - uvicorn>=0.35.0 + args: + - --fix + description: Run 'ruff' for extremely fast Python linting + entry: ruff check --force-exclude + id: ruff + language: python + minimum_pre_commit_version: 2.9.2 + name: ruff + require_serial: true + types_or: + - python + - pyi + - additional_dependencies: + - aiohttp>=3.12.15 + - apmodel>=0.5.1 + - apsig>=0.6.0 + - charset-normalizer>=3.4.3 + - coverage>=7.10.7 + - fastapi>=0.116.1 + - httpcore[http2,socks]>=1.0.9 + - httpx>=0.28.1 + - pytest-cov>=7.0.0 + - pytest>=8.4.1 + - redis>=5.0.4 + - requests>=2.32.5 + - types-requests>=2.32.4.20250913 + - uvicorn>=0.35.0 + args: [] + description: Run 'ruff format' for extremely fast Python formatting + entry: ruff format --force-exclude + id: ruff-format + language: python + minimum_pre_commit_version: 2.9.2 + name: ruff-format + require_serial: true + types_or: + - python + - pyi + repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 +- hooks: + - additional_dependencies: + - aiohttp>=3.12.15 + - apmodel>=0.5.1 + - apsig>=0.6.0 + - charset-normalizer>=3.4.3 + - coverage>=7.10.7 + - fastapi>=0.116.1 + - httpcore[http2,socks]>=1.0.9 + - httpx>=0.28.1 + - pytest-cov>=7.0.0 + - pytest>=8.4.1 + - redis>=5.0.4 + - requests>=2.32.5 + - types-requests>=2.32.4.20250913 + - uvicorn>=0.35.0 + id: pyrefly-check + name: Pyrefly (type checking) + pass_filenames: false + repo: https://github.com/facebook/pyrefly-pre-commit + rev: 0.46.0 diff --git a/src/apkit/_version.py b/src/apkit/_version.py index ac8011d..bea3035 100644 --- a/src/apkit/_version.py +++ b/src/apkit/_version.py @@ -28,7 +28,7 @@ commit_id: COMMIT_ID __commit_id__: COMMIT_ID -__version__ = version = '0.3.3.post1.dev32+g5c3dfc179.d20251228' -__version_tuple__ = version_tuple = (0, 3, 3, 'post1', 'dev32', 'g5c3dfc179.d20251228') +__version__ = version = '0.3.3.post1.dev36+gc65fc9498' +__version_tuple__ = version_tuple = (0, 3, 3, 'post1', 'dev36', 'gc65fc9498') __commit_id__ = commit_id = None From 39d841537d1e3a42a225269515cbe4a109e91bf9 Mon Sep 17 00:00:00 2001 From: AmaseCocoa Date: Mon, 29 Dec 2025 06:30:17 +0900 Subject: [PATCH 9/9] fix: fix imports --- src/apkit/helper/inbox.py | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/apkit/helper/inbox.py b/src/apkit/helper/inbox.py index b129abb..b45ef3a 100644 --- a/src/apkit/helper/inbox.py +++ b/src/apkit/helper/inbox.py @@ -10,9 +10,9 @@ from apsig import KeyUtil, LDSignature, ProofVerifier from apsig.draft.verify import Verifier from apsig.exceptions import ( - MissingSignature, - UnknownSignature, - VerificationFailed, + MissingSignatureError, + UnknownSignatureError, + VerificationFailedError, ) from apsig.rfc9421 import RFC9421Verifier from cryptography.hazmat.primitives import serialization @@ -74,13 +74,13 @@ async def __verify_rfc9421( signature_input_header = headers.get("signature-input") if not signature_input_header: - raise MissingSignature("signature-input header is missing") + raise MissingSignatureError("signature-input header is missing") signature_input_parsed = http_sf.parse( signature_input_header.encode("utf-8"), tltype="dictionary" ) if not isinstance(signature_input_parsed, dict): - raise VerificationFailed( + raise VerificationFailedError( f"Unsupported Signature-Input type: {type(signature_input_parsed)}" ) @@ -117,7 +117,7 @@ async def _verify_with_key( f"signature:{key_id}", ku.encode_multibase() ) return True - except VerificationFailed as e: + except VerificationFailedError as e: if is_cache: return False # Will be retried with fresh key raise e @@ -174,7 +174,7 @@ async def __verify_draft( signature_parts = self.__get_draft_signature_parts(signature_header) key_id = signature_parts.get("keyId") if not key_id: - raise MissingSignature("keyId does not exist.") + raise MissingSignatureError("keyId does not exist.") cache = False public_key = None if not no_check_cache: @@ -212,17 +212,17 @@ async def __verify_draft( return True except Exception as e: if not cache: - raise VerificationFailed(f"{str(e)}") + raise VerificationFailedError(f"{str(e)}") else: return await self.__verify_draft( body, url, method, headers, no_check_cache=True ) else: - raise VerificationFailed("publicKey does not exist.") + raise VerificationFailedError("publicKey does not exist.") else: raise ValueError("unsupported model type") else: - raise MissingSignature("this is not http signed activity.") + raise MissingSignatureError("this is not http signed activity.") async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> bool: body_json = json.loads(body) @@ -261,19 +261,19 @@ async def __verify_proof(self, body: bytes, no_check_cache: bool = False) -> boo return True except Exception as e: if not cache: - raise VerificationFailed(f"{str(e)}") + raise VerificationFailedError(f"{str(e)}") else: return await self.__verify_proof( body, no_check_cache=True ) else: - raise VerificationFailed("publicKey does not exist.") + raise VerificationFailedError("publicKey does not exist.") else: raise ValueError("unsupported model type") else: - raise MissingSignature("verificationMethod does not exist.") + raise MissingSignatureError("verificationMethod does not exist.") else: - raise MissingSignature("this is not signed activity.") + raise MissingSignatureError("this is not signed activity.") async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: ld = LDSignature() @@ -283,7 +283,7 @@ async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: if isinstance(signature, dict): creator: Optional[str] = signature.get("creator") if creator is None: - raise MissingSignature("creator does not exist.") + raise MissingSignatureError("creator does not exist.") cache = False public_key = None if not no_check_cache: @@ -314,18 +314,18 @@ async def __verify_ld(self, body: bytes, no_check_cache: bool = False) -> bool: ), ) return True - raise VerificationFailed("publicKey does not exist.") + raise VerificationFailedError("publicKey does not exist.") except ( - UnknownSignature, - MissingSignature, - VerificationFailed, + UnknownSignatureError, + MissingSignatureError, + VerificationFailedError, ) as e: if not cache: - raise VerificationFailed(f"{str(e)}") + raise VerificationFailedError(f"{str(e)}") else: return await self.__verify_ld(body, no_check_cache=True) else: - raise VerificationFailed("publicKey does not exist.") + raise VerificationFailedError("publicKey does not exist.") else: raise ValueError("unsupported model type")