Updates
This commit is contained in:
109
tools/gen_auth.py
Normal file
109
tools/gen_auth.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/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())
|
Reference in New Issue
Block a user