baobab/tools/gen_auth.py
Maxime Van Hees 0ebda7c1aa Updates
2025-08-14 14:14:34 +02:00

109 lines
3.9 KiB
Python

#!/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())