#!/usr/bin/env python3 """ Generate secp256k1 keypair and sign a nonce in the exact format the server expects. Install dependencies once: python3 -m pip install -r tools/requirements.txt Usage examples: # Generate a new keypair and sign a nonce (prints PRIVATE_HEX, PUBLIC_HEX, SIGNATURE_HEX) python tools/gen_auth.py --nonce "PASTE_NONCE_FROM_fetch_nonce" # Sign with an existing private key (64 hex chars) python tools/gen_auth.py --nonce "PASTE_NONCE" --priv "YOUR_PRIVATE_KEY_HEX" # Output JSON instead of key=value lines python tools/gen_auth.py --nonce "PASTE_NONCE" --json Notes: - Public key is compressed (33 bytes) hex, starting with 02/03 (66 hex chars total). - Signature is compact ECDSA (r||s) 64 bytes (128 hex chars). - The nonce should be the exact ASCII string returned by fetch_nonce(). - The message signed is sha256(nonce_ascii) to match client/server behavior: - [rust.AuthHelper::sign_message()](interfaces/openrpc/client/src/auth.rs:55) - [rust.AuthManager::verify_signature()](interfaces/openrpc/server/src/auth.rs:85) """ import argparse import hashlib import json import sys from typing import Dict, Tuple, Optional try: from ecdsa import SigningKey, VerifyingKey, SECP256k1, util except Exception as e: print("Missing dependency 'ecdsa'. Install with:", file=sys.stderr) print(" python3 -m pip install -r tools/requirements.txt", file=sys.stderr) raise def sha256_ascii(s: str) -> bytes: return hashlib.sha256(s.encode()).digest() def to_compact_signature_hex(sk: SigningKey, nonce_ascii: str) -> str: digest = sha256_ascii(nonce_ascii) sig = sk.sign_digest(digest, sigencode=util.sigencode_string) # 64 bytes r||s return sig.hex() def compressed_pubkey_hex(vk: VerifyingKey) -> str: # Prefer compressed output if library supports it directly (ecdsa>=0.18) try: return vk.to_string("compressed").hex() except TypeError: # Manual compression (02/03 + X) p = vk.pubkey.point x = p.x() y = p.y() prefix = b"\x02" if (y % 2 == 0) else b"\x03" return (prefix + x.to_bytes(32, "big")).hex() def generate_or_load_sk(priv_hex: Optional[str]) -> Tuple[SigningKey, bool]: if priv_hex: if len(priv_hex) != 64: raise ValueError("Provided --priv must be 64 hex chars (32 bytes).") return SigningKey.from_string(bytes.fromhex(priv_hex), curve=SECP256k1), False return SigningKey.generate(curve=SECP256k1), True def run(nonce: str, priv_hex: Optional[str], as_json: bool) -> int: sk, generated = generate_or_load_sk(priv_hex) vk = sk.get_verifying_key() out: Dict[str, str] = { "PUBLIC_HEX": compressed_pubkey_hex(vk), "NONCE": nonce, "SIGNATURE_HEX": to_compact_signature_hex(sk, nonce), } # Always print the private key for convenience (either generated or provided) out["PRIVATE_HEX"] = sk.to_string().hex() if as_json: print(json.dumps(out, separators=(",", ":"))) else: # key=value form for easy copy/paste print(f"PRIVATE_HEX={out['PRIVATE_HEX']}") print(f"PUBLIC_HEX={out['PUBLIC_HEX']}") print(f"NONCE={out['NONCE']}") print(f"SIGNATURE_HEX={out['SIGNATURE_HEX']}") return 0 def main() -> int: parser = argparse.ArgumentParser(description="Generate secp256k1 auth material and signature for a nonce.") parser.add_argument("--nonce", required=True, help="Nonce string returned by fetch_nonce (paste as-is)") parser.add_argument("--priv", help="Existing private key hex (64 hex chars). If omitted, a new keypair is generated.") parser.add_argument("--json", action="store_true", help="Output JSON instead of key=value lines.") args = parser.parse_args() try: return run(args.nonce, args.priv, args.json) except Exception as e: print(f"Error: {e}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())