#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR"
cd "$ROOT"

REPO_URL="https://github.com/curl/curl.git"
FIXED_COMMIT="7be1e70cb6bcd83e130ecfe8cb91b6a7dcdeff42"
EXPECTED_VULN="7ea37abc6ac0120ba5f6d94be8d196f7cf1506bb"
EMPTY_CA_NAME="cve-2026-80231-empty-ca.pem"
HARNESS_SOURCE="$REPRO_DIR/native_ca_reuse_harness.c"

# The ticket's prepared cache is the deterministic primary location.
CACHE_DIR=""
if [ -r "$ROOT/project_cache_context.json" ]; then
  CACHE_DIR="$(python3 - "$ROOT/project_cache_context.json" <<'PY'
import json, sys
try:
    c=json.load(open(sys.argv[1], encoding='utf-8'))
    p=c.get('project_cache_dir')
    if c.get('prepared') is True and isinstance(p, str) and p:
        print(p)
except Exception:
    pass
PY
)"
fi
if [ -z "$CACHE_DIR" ]; then
  CACHE_DIR="$ROOT/artifacts/curl-cache"
fi
REPO="$CACHE_DIR/repo"
WORKTREES="$CACHE_DIR/worktrees"
BUILDS="$CACHE_DIR/build"
EMPTY_CA="$CACHE_DIR/$EMPTY_CA_NAME"
mkdir -p "$CACHE_DIR" "$WORKTREES" "$BUILDS"

# Keep the shell-wide diagnostic log, but do not bind its hash in the manifest
# because tee is still writing to it when the manifest is finalized.
exec > >(tee "$LOGS/reproduction_steps.log") 2>&1

FINALIZED=0
SERVER_PID=""
TRUST_CERT="/usr/local/share/ca-certificates/pruva-cve-2026-80231.crt"
cleanup() {
  if [ -n "$SERVER_PID" ]; then
    kill "$SERVER_PID" 2>/dev/null || true
    wait "$SERVER_PID" 2>/dev/null || true
  fi
  sudo rm -f "$TRUST_CERT" || true
  sudo update-ca-certificates --fresh >/dev/null 2>&1 || true
  if [ "$FINALIZED" -ne 1 ]; then
    printf '%s\n' 'runtime attempt incomplete; see logs/reproduction_steps.log' > "$REPRO_DIR/attempt-status.txt"
    local d
    d="$(sha256sum "$REPRO_DIR/attempt-status.txt" | awk '{print $1}')"
    python3 - "$REPRO_DIR/runtime_manifest.json" "$d" <<'PY'
import json, sys
m={
 "entrypoint_kind":"function_call",
 "entrypoint_detail":"libcurl HTTPS connection reuse with differing effective CURLSSLOPT_NATIVE_CA policy",
 "service_started":False,
 "healthcheck_passed":False,
 "target_path_reached":False,
 "runtime_stack":[],
 "proof_artifacts":["repro/attempt-status.txt"],
 "artifact_sha256":{"repro/attempt-status.txt":sys.argv[2]},
 "notes":"Attempt did not complete; inspect logs/reproduction_steps.log"
}
with open(sys.argv[1], 'w', encoding='utf-8') as f:
    json.dump(m, f, indent=2); f.write('\n')
PY
  fi
}
trap cleanup EXIT

if [ ! -r "$HARNESS_SOURCE" ]; then
  echo "Missing required harness source: $HARNESS_SOURCE" >&2
  exit 2
fi
if [ ! -d "$REPO/.git" ]; then
  git clone "$REPO_URL" "$REPO"
fi
git -C "$REPO" fetch origin "$FIXED_COMMIT" --quiet || true
FIXED_RESOLVED="$(git -C "$REPO" rev-parse "$FIXED_COMMIT^{commit}")"
VULN_COMMIT="$(git -C "$REPO" rev-parse "$FIXED_COMMIT^1")"
if [ "$FIXED_RESOLVED" != "$FIXED_COMMIT" ] || [ "$VULN_COMMIT" != "$EXPECTED_VULN" ]; then
  echo "Unexpected source identities" >&2
  exit 2
fi

# Verify the vulnerable parent lacks the matching field and fixed commit has it.
if git -C "$REPO" show "$VULN_COMMIT:lib/vtls/vtls_config.c" |
     grep -q 'c1->native_ca_store == c2->native_ca_store'; then
  echo "Vulnerable checkout unexpectedly contains the fix hunk" >&2
  exit 2
fi
git -C "$REPO" show "$FIXED_RESOLVED:lib/vtls/vtls_config.c" |
  grep -q 'c1->native_ca_store == c2->native_ca_store'

# The build default and explicit CURLOPT_CAINFO use the exact same path. The
# file contains no trust anchors. CURL_CA_NATIVE supplies the only trust path
# for the priming transfer.
: > "$EMPTY_CA"

build_role() {
  local role="$1" commit="$2"
  local wt="$WORKTREES/cve-2026-80231-$role"
  local build="$BUILDS/cve-2026-80231-$role"
  local head=""
  if [ -e "$wt/.git" ]; then
    head="$(git -C "$wt" rev-parse HEAD 2>/dev/null || true)"
  fi
  if [ "$head" != "$commit" ]; then
    if [ -e "$wt/.git" ]; then
      git -C "$REPO" worktree remove --force "$wt" || rm -rf "$wt"
    else
      rm -rf "$wt"
    fi
    git -C "$REPO" worktree add --detach "$wt" "$commit"
  fi
  if [ -r "$build/CMakeCache.txt" ] && ! grep -Fqx "CMAKE_HOME_DIRECTORY:INTERNAL=$wt" "$build/CMakeCache.txt"; then
    rm -rf "$build"
  fi
  cmake -S "$wt" -B "$build" -G 'Unix Makefiles' \
    -DCMAKE_BUILD_TYPE=RelWithDebInfo \
    -DBUILD_SHARED_LIBS=ON -DBUILD_CURL_EXE=ON \
    -DBUILD_EXAMPLES=OFF -DBUILD_LIBCURL_DOCS=OFF \
    -DBUILD_MISC_DOCS=OFF -DBUILD_TESTING=OFF \
    -DCURL_USE_GNUTLS=ON -DCURL_USE_OPENSSL=OFF \
    -DCURL_CA_NATIVE=ON -DCURL_CA_BUNDLE="$EMPTY_CA" \
    -DCURL_CA_PATH=none > "$LOGS/build-$role-configure.log" 2>&1
  cmake --build "$build" --parallel 4 > "$LOGS/build-$role.log" 2>&1
  cc -std=c99 -Wall -Wextra -I"$wt/include" "$HARNESS_SOURCE" \
    -L"$build/lib" -Wl,-rpath,"$build/lib" -lcurl \
    -o "$build/native_ca_reuse_harness"
  {
    printf 'role=%s\ncommit=%s\n' "$role" "$commit"
    "$build/src/curl" --version | head -2
    ldd "$build/native_ca_reuse_harness"
    sha256sum "$build/lib/libcurl.so" "$build/native_ca_reuse_harness"
  } > "$REPRO_DIR/build-identity-$role.txt"
}

build_role vulnerable "$VULN_COMMIT"
build_role fixed "$FIXED_RESOLVED"

echo "Built vulnerable and fixed libcurl with GnuTLS and NativeCA."

# Create a short-lived localhost certificate and add only that certificate to
# the system trust used by gnutls_certificate_set_x509_system_trust().
CERT="$REPRO_DIR/server-cert.pem"
KEY="$REPRO_DIR/server-key.pem"
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
  -subj '/CN=localhost' \
  -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' \
  -keyout "$KEY" -out "$CERT" > "$LOGS/certificate-generation.log" 2>&1
sudo cp "$CERT" "$TRUST_CERT"
sudo update-ca-certificates --fresh > "$LOGS/native-trust-update.log" 2>&1

PORT="$(python3 - <<'PY'
import socket
s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close()
PY
)"
SERVER="$REPRO_DIR/https_test_server.py"
cat > "$SERVER" <<'PY'
import http.server, ssl, sys
class H(http.server.BaseHTTPRequestHandler):
    protocol_version = 'HTTP/1.1'
    def do_GET(self):
        body=b'PRUVA_TLS_OK\n'
        self.send_response(200)
        self.send_header('Content-Type','text/plain')
        self.send_header('Content-Length',str(len(body)))
        self.send_header('Connection','keep-alive')
        self.end_headers(); self.wfile.write(body); self.wfile.flush()
    def log_message(self, fmt, *args):
        print('REQUEST ' + (fmt % args), flush=True)
class S(http.server.ThreadingHTTPServer):
    daemon_threads=True
    def handle_error(self, request, client_address):
        pass
ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(sys.argv[2], sys.argv[3])
s=S(('127.0.0.1', int(sys.argv[1])), H)
s.socket=ctx.wrap_socket(s.socket, server_side=True)
print('READY port=' + sys.argv[1], flush=True)
s.serve_forever()
PY
python3 "$SERVER" "$PORT" "$CERT" "$KEY" > "$REPRO_DIR/server.log" 2>&1 &
SERVER_PID=$!
for _ in 1 2 3 4 5 6 7 8 9 10; do
  grep -q '^READY ' "$REPRO_DIR/server.log" && break
  sleep 0.2
done
grep -q '^READY ' "$REPRO_DIR/server.log"

run_attempt() {
  local role="$1" attempt="$2" expected_rc="$3"
  local build="$BUILDS/cve-2026-80231-$role"
  local out="$REPRO_DIR/$role-attempt-$attempt.log"
  set +e
  LD_LIBRARY_PATH="$build/lib" timeout 20 \
    "$build/native_ca_reuse_harness" "https://localhost:$PORT/" "$EMPTY_CA" \
    > "$out" 2>&1
  local rc=$?
  set -e
  echo "$role attempt $attempt exit=$rc expected=$expected_rc"
  if [ "$rc" -ne "$expected_rc" ]; then
    cat "$out"
    return 1
  fi
}

# Two independent vulnerable and fixed attempts in one script invocation.
run_attempt vulnerable 1 0
run_attempt vulnerable 2 0
run_attempt fixed 1 1
run_attempt fixed 2 1

for n in 1 2; do
  grep -q '^ORACLE=VULNERABLE_WRONG_TRUST_REUSE$' "$REPRO_DIR/vulnerable-attempt-$n.log"
  grep -q '^RESULT label=native-default-prime code=0 .* new_connects=1 http=200$' "$REPRO_DIR/vulnerable-attempt-$n.log"
  grep -q '^RESULT label=custom-same-path-shared-cache code=0 .* new_connects=0 http=200$' "$REPRO_DIR/vulnerable-attempt-$n.log"
  grep -q '^RESULT label=custom-same-path-fresh-control code=60 .* new_connects=1 http=0$' "$REPRO_DIR/vulnerable-attempt-$n.log"
  grep -q '^ORACLE=FIXED_POLICY_ISOLATION$' "$REPRO_DIR/fixed-attempt-$n.log"
  grep -q '^RESULT label=custom-same-path-shared-cache code=60 .* new_connects=1 http=0$' "$REPRO_DIR/fixed-attempt-$n.log"
done

# Stop server before hashing immutable evidence.
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
SERVER_PID=""
# Remove the private key and generated server source: neither is proof and both
# are created by this script on every run.
rm -f "$KEY" "$SERVER"

printf 'repository=%s\nfixed=%s\nvulnerable=%s\n' \
  "$REPO_URL" "$FIXED_RESOLVED" "$VULN_COMMIT" > "$REPRO_DIR/source-identity.txt"

TARGET_DIGEST="$(printf 'git:%s@%s' "$REPO_URL" "$VULN_COMMIT" | sha256sum | awk '{print $1}')"
ARCH="$(uname -m)"
case "$ARCH" in x86_64|amd64) ARCH=x86_64;; aarch64|arm64) ARCH=aarch64;; esac
PROOFS=(
  "repro/source-identity.txt"
  "repro/build-identity-vulnerable.txt"
  "repro/build-identity-fixed.txt"
  "repro/server-cert.pem"
  "repro/server.log"
  "repro/vulnerable-attempt-1.log"
  "repro/vulnerable-attempt-2.log"
  "repro/fixed-attempt-1.log"
  "repro/fixed-attempt-2.log"
)
python3 - "$ROOT" "$REPRO_DIR/runtime_manifest.json" "$REPO_URL" \
  "$VULN_COMMIT" "$TARGET_DIGEST" "$ARCH" "${PROOFS[@]}" <<'PY'
import hashlib, json, os, sys
root, out, repo, commit, target_digest, arch, *proofs = sys.argv[1:]
digests={}
for rel in proofs:
    with open(os.path.join(root, rel), 'rb') as f:
        digests[rel]=hashlib.sha256(f.read()).hexdigest()
m={
 "entrypoint_kind":"function_call",
 "entrypoint_detail":"libcurl multi API HTTPS transfers with equal primary CAfile/ssl_options but differing effective native CA policy",
 "service_started":True,
 "healthcheck_passed":True,
 "target_path_reached":True,
 "runtime_stack":["libcurl","GnuTLS","Python HTTPS origin"],
 "target_identity":{
   "repository_url":repo,
   "commit_sha":commit,
   "target_digest":target_digest,
   "platform":"linux",
   "architecture":arch
 },
 "proof_artifacts":proofs,
 "artifact_sha256":digests,
 "notes":"Vulnerable parent reused native-trusted TLS for an explicit custom-CA policy that fails on a fresh connection; fixed commit rejected reuse. Linux GnuTLS provides equivalent native-store semantics although the advisory states production exposure is Windows/macOS."
}
with open(out, 'w', encoding='utf-8') as f:
    json.dump(m, f, indent=2); f.write('\n')
PY

# Update the prepared cache manifest, preserving the reserved repo-mirrors
# parent entry and listing only disjoint reusable paths.
if [ -r "$ROOT/project_cache_context.json" ]; then
  python3 - "$ROOT/project_cache_context.json" <<'PY'
import json, os, sys
c=json.load(open(sys.argv[1], encoding='utf-8'))
p=c.get('cache_manifest_path')
if c.get('prepared') is True and isinstance(p, str) and p:
    m={"schema_version":c.get("cache_manifest_schema_version",1),"entries":[
      {"path":"repo-mirrors","reuse_class":"repo"},
      {"path":"repo","reuse_class":"repo"},
      {"path":"worktrees","reuse_class":"repo"},
      {"path":"build","reuse_class":"build"},
      {"path":"cve-2026-80231-empty-ca.pem","reuse_class":"infrastructure"}
    ]}
    os.makedirs(os.path.dirname(p), exist_ok=True)
    with open(p,'w',encoding='utf-8') as f: json.dump(m,f,indent=2); f.write('\n')
PY
fi

FINALIZED=1
echo "CONFIRMED: vulnerable libcurl reused a TLS connection authenticated under the wrong native CA policy; fixed commit refused reuse."
exit 0
