109 lines
3.9 KiB
Python
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()) |