#!/usr/bin/env bash set -euo pipefail usage() { cat <<'USAGE' Usage: gen_auth.sh --nonce "" [--priv ] [--json] Options: --nonce The nonce string returned by fetch_nonce (paste as-is). --priv Optional private key hex (64 hex chars). If omitted, a new key is generated. --json Output JSON instead of plain KEY=VALUE lines. Outputs: PRIVATE_HEX Private key hex (only when generated, or echoed back if provided) PUBLIC_HEX Compressed secp256k1 public key hex (33 bytes, 66 hex chars) NONCE The nonce string you passed in SIGNATURE_HEX Compact ECDSA signature hex (64 bytes, 128 hex chars) Notes: - The signature is produced by signing sha256(nonce_ascii) and encoded as compact r||s (64 bytes), which matches the server/client behavior ([interfaces/openrpc/client/src/auth.rs](interfaces/openrpc/client/src/auth.rs:55), [interfaces/openrpc/server/src/auth.rs](interfaces/openrpc/server/src/auth.rs:85)). USAGE } NONCE="" PRIV_HEX="" OUT_JSON=0 while [[ $# -gt 0 ]]; do case "$1" in --nonce) NONCE="${2:-}"; shift 2 ;; --priv) PRIV_HEX="${2:-}"; shift 2 ;; --json) OUT_JSON=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $1" >&2; usage; exit 1 ;; esac done if [[ -z "$NONCE" ]]; then echo "Error: --nonce is required" >&2 usage exit 1 fi if ! command -v python3 >/dev/null 2>&1; then echo "Error: python3 not found. Install Python 3 (e.g., sudo pacman -S python) and retry." >&2 exit 1 fi # Ensure 'ecdsa' module is available; install to user site if missing. if ! python3 - <<'PY' >/dev/null 2>&1 import importlib; importlib.import_module("ecdsa") PY then echo "Installing Python 'ecdsa' package in user site..." >&2 if ! python3 -m pip install --user --quiet ecdsa; then echo "Error: failed to install 'ecdsa'. Install manually: python3 -m pip install --user ecdsa" >&2 exit 1 fi fi # Now run Python to generate/derive keys and sign the nonce (ASCII) with compact ECDSA. python3 - "$NONCE" "$PRIV_HEX" "$OUT_JSON" <<'PY' import sys, json, hashlib from ecdsa import SigningKey, VerifyingKey, SECP256k1, util NONCE = sys.argv[1] PRIV_HEX = sys.argv[2] OUT_JSON = int(sys.argv[3]) == 1 def to_compact_signature(sk: SigningKey, msg_ascii: str) -> bytes: digest = hashlib.sha256(msg_ascii.encode()).digest() return sk.sign_digest(digest, sigencode=util.sigencode_string) # 64 bytes r||s def compressed_pubkey(vk: VerifyingKey) -> bytes: try: return vk.to_string("compressed") except TypeError: p = vk.pubkey.point x = p.x() y = vk.pubkey.point.y() prefix = b'\x02' if (y % 2 == 0) else b'\x03' return prefix + x.to_bytes(32, "big") generated = False if PRIV_HEX: if len(PRIV_HEX) != 64: print("ERROR: Provided --priv must be 64 hex chars", file=sys.stderr) sys.exit(1) sk = SigningKey.from_string(bytes.fromhex(PRIV_HEX), curve=SECP256k1) else: sk = SigningKey.generate(curve=SECP256k1) generated = True vk = sk.get_verifying_key() pub_hex = compressed_pubkey(vk).hex() sig_hex = to_compact_signature(sk, NONCE).hex() priv_hex = sk.to_string().hex() out = { "PUBLIC_HEX": pub_hex, "NONCE": NONCE, "SIGNATURE_HEX": sig_hex, } if generated or PRIV_HEX: out["PRIVATE_HEX"] = priv_hex if OUT_JSON: print(json.dumps(out, separators=(",", ":"))) else: if "PRIVATE_HEX" in out: 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']}") PY # End