import base64 import hashlib import http.client import re import secrets import time from urllib.parse import urlsplit from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey def load_key(pem): pem = pem.strip() if not re.fullmatch(rb"-----BEGIN PRIVATE KEY-----[A-Za-z0-9+/=\r\n]+-----END PRIVATE KEY-----", pem): raise ValueError("Expected one unencrypted PKCS#8 PEM private key") key = serialization.load_pem_private_key(pem, password=None) if not isinstance(key, Ed25519PrivateKey): raise ValueError("Private key must be Ed25519") return key def sign(key, key_id, method, target, body, timestamp, nonce): digest = "SHA-256=" + base64.b64encode(hashlib.sha256(body).digest()).decode("ascii") payload = "\n".join([ "INFINI-OPENAPI-V2", key_id, method, target, timestamp, nonce, digest ]) + "\n" signature = base64.b64encode(key.sign(payload.encode("utf-8"))).decode("ascii") return { "X-API-Key-ID": key_id, "X-API-Timestamp": timestamp, "X-API-Nonce": nonce, "Digest": digest, "Authorization": f'Ed25519 signature="{signature}"', "Content-Type": "application/json", }, payload class InfiniClient: def __init__(self, origin, key_id, private_key_pem): base = urlsplit(origin) if (base.scheme != "https" or not base.hostname or base.username or base.password or base.path not in ("", "/") or base.query or base.fragment): raise ValueError("Expected an HTTPS API origin") if not re.fullmatch(r"[1-9A-HJ-NP-Za-km-z]{32}", key_id): raise ValueError("Expected a 32-character Base58 Key ID") self.host, self.port = base.hostname, base.port self.key_id, self.key = key_id, load_key(private_key_pem) def request(self, method, target, body=b""): """Return (HTTP status, raw response bytes). Caller checks business code.""" method = method.upper() if not re.fullmatch(r"[A-Z]+", method): raise ValueError("Invalid HTTP method") if (not target.startswith("/v2/") or "#" in target or any(ord(c) <= 32 or ord(c) >= 127 for c in target)): raise ValueError("Expected an escaped /v2/ path and raw query") nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).rstrip(b"=").decode("ascii") headers, _ = sign(self.key, self.key_id, method, target, body, str(int(time.time())), nonce) connection = http.client.HTTPSConnection(self.host, self.port, timeout=15) try: connection.request(method, target, body=body, headers=headers) response = connection.getresponse() return response.status, response.read() finally: connection.close()