Harden PGP release evidence privacy

ober

e7b2f9bafddfc30200230966affdeeadb6923ff1

diff --git a/.jerboa/security.json b/.jerboa/security.json
index 82cd2d3..ee63536 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -19,6 +19,12 @@
     "crypto": { "zeroizeSecrets": true, "strictKeyFilePermissions": true },
     "targetEvidence": {
       "keyProof": "fail-closed",
+      "proofMaterialPolicy": {
+        "maxBytes": 65536,
+        "rejectBinary": true,
+        "rejectPrivatePaths": true,
+        "rejectSecretPatterns": true
+      },
       "requiredMarkers": [
         "age_encrypt_decrypt_status=target-evidence-recorded",
         "signature_roundtrip_status=target-evidence-recorded",
@@ -30,6 +36,22 @@
         "production_crypto_review_status=current-run-recorded",
         "sensitive_artifact_policy=no-secret-identities-passphrases-plaintext-or-decrypted-payloads-preserved"
       ]
+    },
+    "releaseEvidence": {
+      "sanitizeEvidence": "scripts/sanitize-evidence.sh",
+      "evidencePrivacyStatus": "sanitized",
+      "privatePathScanStatus": "pass",
+      "binaryMode": "compile-whole-program standalone default",
+      "forbiddenEvidenceMaterial": [
+        "host-private absolute paths",
+        "SSH clone URLs",
+        "authorization headers",
+        "API tokens",
+        "passwords or passphrases",
+        "private keys",
+        "age secret keys",
+        "plaintext or decrypted payloads"
+      ]
     }
   },
   "suppressions": []
diff --git a/Makefile b/Makefile
index cd7a439..9545fd1 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,7 @@ endif
 
 XC_LIBDIRS := $(CURDIR):$(JERBOA_SOURCE_HOME)/lib:$(JERBOA_SOURCE_HOME)/.chez-cross-ta6le/lib:$(JERBOA_SOURCE_HOME)/.chez-cross-ta6fb/lib
 
-.PHONY: help run test test-interop build-native binary binary-version-smoke install install-script clean linux-amd64 freebsd-amd64 linux freebsd jpg-linux-amd64 jpg-freebsd-amd64 ensure-jerboa-tools audit verify security sbom reproducibility-report target-evidence release-evidence
+.PHONY: help run test test-interop build-native binary binary-version-smoke install install-script clean linux-amd64 freebsd-amd64 linux freebsd jpg-linux-amd64 jpg-freebsd-amd64 ensure-jerboa-tools audit verify security sbom reproducibility-report target-evidence sanitize-evidence release-evidence
 .DEFAULT_GOAL := help
 
 help:
@@ -36,6 +36,7 @@ help:
 	@echo "  make test                    Run smoke tests"
 	@echo "  make test-interop            Report disabled OpenPGP interop status"
 	@echo "  make release-evidence        Write test/audit/SBOM/reproducibility evidence"
+	@echo "  make sanitize-evidence       Redact/check local release evidence"
 	@echo ""
 	@echo "Distribution:"
 	@echo "  make binary                  Build native binary (requires Chez+Jerboa)"
@@ -91,6 +92,12 @@ security:
 	  exit 1; \
 	fi; \
 	echo "secret_scan_status=pass"
+	@test -x scripts/sanitize-evidence.sh || { echo "scripts/sanitize-evidence.sh is required and must be executable."; exit 1; }
+	@grep -q 'private or sensitive evidence material remains' scripts/sanitize-evidence.sh || { echo "sanitize-evidence must fail when private/sensitive material remains."; exit 1; }
+	@grep -q 'scripts/sanitize-evidence.sh' scripts/release-evidence.sh || { echo "release-evidence must invoke scripts/sanitize-evidence.sh."; exit 1; }
+	@grep -q 'evidence_privacy_status=sanitized' scripts/release-evidence.sh || { echo "release-evidence must report sanitized evidence privacy status."; exit 1; }
+	@grep -q 'proofMaterialPolicy' .jerboa/security.json || { echo ".jerboa/security.json must document proof material policy."; exit 1; }
+	@grep -q 'target-key-proof-contains-sensitive-material' scripts/target-evidence.sh || { echo "target proof files must reject sensitive material."; exit 1; }
 
 BINARY_OUTPUT ?= jpg-bin
 
@@ -135,6 +142,9 @@ target-evidence:
 	TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" \
 	sh scripts/target-evidence.sh
 
+sanitize-evidence:
+	@scripts/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)"
+
 release-evidence:
 	@REPO_ROOT="$(CURDIR)" \
 	DIST_DIR="$(DIST_DIR)" \
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index 478b172..d44dc6e 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -19,6 +19,16 @@ checks fail. OpenPGP interop is intentionally disabled so the production native
 dependency graph can pass strict RustSec audit. See
 `docs/dependency-provenance.md` for the current dependency decision record.
 
+Evidence is sanitized before the top-level result is written. Host-private
+paths, SSH clone URLs, authorization headers, tokens, passphrases, private keys,
+and plaintext/decrypted payload labels are redacted or rejected, and
+`result.txt` records:
+
+```text
+evidence_privacy_status=sanitized
+private_path_scan_status=pass
+```
+
 ## Target Key Proof
 
 Default local evidence records live key/passphrase release proof as
@@ -46,5 +56,9 @@ sensitive_artifact_policy=no-secret-identities-passphrases-plaintext-or-decrypte
 ```
 
 Missing, empty, or marker-incomplete required proof files fail closed with
-`status=blocked-target-proof`. Accepted proof files are copied into release
-evidence with a SHA-256 sidecar and still require review before production.
+`status=blocked-target-proof`. Proof files must also be short text evidence,
+not copied secrets: binary files, oversized files, host-private paths, SSH
+URLs, authorization headers, API tokens, private keys, age secret keys,
+passphrases, plaintext, or decrypted payload material fail closed before the
+proof is copied. Accepted proof files are copied into release evidence with a
+SHA-256 sidecar and still require review before production.
diff --git a/scripts/release-evidence.sh b/scripts/release-evidence.sh
index ea49b05..3153fa1 100755
--- a/scripts/release-evidence.sh
+++ b/scripts/release-evidence.sh
@@ -5,6 +5,7 @@ repo_root=${REPO_ROOT:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
 dist_dir=${DIST_DIR:-"$repo_root/dist"}
 evidence_dir=${RELEASE_EVIDENCE_DIR:-"$dist_dir/release-evidence"}
 make_cmd=${MAKE:-make}
+sanitizer="$repo_root/scripts/sanitize-evidence.sh"
 mkdir -p "$dist_dir"
 rm -rf "$evidence_dir" "$dist_dir/sbom" "$dist_dir/reproducibility" "$dist_dir/target-evidence"
 mkdir -p "$evidence_dir"
@@ -45,6 +46,16 @@ sha256_file() {
 
 git status --short > "$evidence_dir/git-status.txt"
 git rev-parse HEAD > "$evidence_dir/git-head.txt" 2>/dev/null || true
+{
+  echo "repo=jerboa-pgp"
+  echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+  echo "platform=$(uname -srm)"
+  if "$make_cmd" --version >/dev/null 2>&1; then
+    echo "make_status=present"
+  else
+    echo "make_status=unavailable"
+  fi
+} > "$evidence_dir/build-environment.txt"
 
 run_step security "$make_cmd" --no-print-directory security
 run_step test "$make_cmd" --no-print-directory test
@@ -67,9 +78,31 @@ find .jerboa/security.json Makefile README.md SECURITY.md LICENSE PLAN.md docs p
     sha256_file "$file"
   done > "$evidence_dir/source-hashes.sha256"
 
+if [ ! -f "$evidence_dir/reproducibility/report.txt" ] || ! grep -qx 'status=match' "$evidence_dir/reproducibility/report.txt"; then
+  fail=1
+fi
+
+privacy_fail=0
+if [ -x "$sanitizer" ]; then
+  if ! "$sanitizer" "$evidence_dir"; then
+    fail=1
+    privacy_fail=1
+  fi
+else
+  fail=1
+  privacy_fail=1
+fi
+
 if [ "$fail" -ne 0 ]; then
   {
     echo "release_evidence_status=blocked"
+    if [ "$privacy_fail" -eq 0 ]; then
+      echo "evidence_privacy_status=sanitized"
+      echo "private_path_scan_status=pass"
+    else
+      echo "evidence_privacy_status=blocked"
+      echo "private_path_scan_status=fail"
+    fi
     echo "blocked_reason=one-or-more-production-gates-failed"
   } > "$evidence_dir/result.txt"
   echo "Release evidence written to $evidence_dir"
@@ -77,5 +110,9 @@ if [ "$fail" -ne 0 ]; then
   exit 1
 fi
 
-echo "release_evidence_status=pass" > "$evidence_dir/result.txt"
+{
+  echo "release_evidence_status=pass"
+  echo "evidence_privacy_status=sanitized"
+  echo "private_path_scan_status=pass"
+} > "$evidence_dir/result.txt"
 echo "Release evidence written to $evidence_dir"
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
index 9285c14..f8f1fee 100755
--- a/scripts/reproducibility-report.sh
+++ b/scripts/reproducibility-report.sh
@@ -27,7 +27,6 @@ hash_source_manifest() {
   find .jerboa/security.json Makefile README.md SECURITY.md LICENSE PLAN.md docs pgp pgp-native support test .github scripts \
     -type f \
     ! -path '*/dist/*' \
-    ! -path '*/.jerboa/*' \
     ! -path '*/target/*' \
     ! -name '*.so' \
     ! -name '*.dylib' \
@@ -114,15 +113,24 @@ stabilize_once() {
 cd "$repo_root"
 
 {
-  echo "repo=$repo_root"
+  echo "repo=jerboa-pgp"
   echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
-  echo "jerbuild=$jerbuild"
-  "$jerbuild" --version 2>/dev/null || true
+  if "$jerbuild" --version >/dev/null 2>&1; then
+    echo "jerbuild_status=present"
+    printf 'jerbuild_version='
+    "$jerbuild" --version 2>/dev/null || true
+  else
+    echo "jerbuild_status=unavailable"
+  fi
   rustc --version 2>/dev/null || true
   cargo --version 2>/dev/null || true
   echo "bin=$bin_name"
-  echo "native_staticlib=$native_staticlib"
-  echo "native_lib=$native_lib"
+  echo "native_staticlib_path_status=configured"
+  if [ -n "$native_lib" ]; then
+    echo "native_dylib_path_status=configured"
+  else
+    echo "native_dylib_path_status=unconfigured"
+  fi
 } > "$out_dir/build-env.txt"
 
 clean_scheme_artifacts
diff --git a/scripts/sanitize-evidence.sh b/scripts/sanitize-evidence.sh
new file mode 100755
index 0000000..c052857
--- /dev/null
+++ b/scripts/sanitize-evidence.sh
@@ -0,0 +1,61 @@
+#!/bin/sh
+set -eu
+
+if [ "$#" -eq 0 ]; then
+  set -- dist/release-evidence
+fi
+
+sanitize_file() {
+  file=$1
+
+  if ! LC_ALL=C grep -Iq . "$file" 2>/dev/null; then
+    return 0
+  fi
+
+  perl -0pi -e '
+    s#/Users/[^[:space:]\x22\x27()]+#<local-path>#g;
+    s#/private/var/folders/[^[:space:]\x22\x27()]+#<local-temp-path>#g;
+    s#/var/folders/[^[:space:]\x22\x27()]+#<local-temp-path>#g;
+    if (defined $ENV{HOME} && length $ENV{HOME}) {
+      my $home = quotemeta($ENV{HOME});
+      s#$home#<home>#g;
+    }
+    s#~/mine#<local-path>#g;
+    s#git\@#git-at-#g;
+    s#ssh://[^[:space:]\x22\x27()]+#ssh://<redacted>#g;
+    s#Authorization:[^\x0a]*#Authorization: <redacted>#gi;
+    s#((api[_-]?key|token|password|secret|passphrase|private[_-]?key|identity|plaintext|decrypted[_-]?payload)[[:space:]]*[:=][[:space:]]*)[^[:space:]\x0a]+#${1}<redacted>#gi;
+  ' "$file"
+}
+
+scan_for_private_material() {
+  evidence_dir=$1
+
+  if command -v rg >/dev/null 2>&1; then
+    if rg -n -S --hidden \
+      -e '/Users/' \
+      -e '/private/var/folders/' \
+      -e '/var/folders/' \
+      -e 'git@' \
+      -e 'ssh://[^[:space:]]+' \
+      -e '-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----' \
+      -e 'AGE-SECRET-KEY-1[A-Za-z0-9_-]+' \
+      -e 'Authorization:[[:space:]]*[^<[:space:]]' \
+      -e '(api[_-]?key|token|password|secret|passphrase|private[_-]?key|identity|plaintext|decrypted[_-]?payload)[[:space:]]*[:=][[:space:]]*[^<[:space:]]' \
+      "$evidence_dir"; then
+      printf '[pgp-sanitize] private or sensitive evidence material remains in %s\n' "$evidence_dir" >&2
+      return 1
+    fi
+  fi
+}
+
+for evidence_dir in "$@"; do
+  if [ ! -d "$evidence_dir" ]; then
+    continue
+  fi
+
+  find "$evidence_dir" -type f -print | while IFS= read -r file; do
+    sanitize_file "$file"
+  done
+  scan_for_private_material "$evidence_dir"
+done
diff --git a/scripts/sbom.sh b/scripts/sbom.sh
index 45c9fd1..8300176 100755
--- a/scripts/sbom.sh
+++ b/scripts/sbom.sh
@@ -39,16 +39,25 @@ hash_manifest() {
 }
 
 cd "$repo_root"
-jerboa_home=$("$jerbuild" --jerboa-home 2>/dev/null || true)
 
 {
-  echo "repo=$repo_root"
+  echo "repo=jerboa-pgp"
   echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
   echo "git_head=$(git rev-parse HEAD 2>/dev/null || true)"
-  echo "uname=$(uname -a)"
-  echo "jerbuild=$jerbuild"
-  "$jerbuild" --version 2>/dev/null || true
-  echo "jerboa_home=$jerboa_home"
+  echo "platform=$(uname -srm)"
+  if "$jerbuild" --version >/dev/null 2>&1; then
+    echo "jerbuild_status=present"
+    printf 'jerbuild_version='
+    "$jerbuild" --version 2>/dev/null || true
+    if "$jerbuild" --jerboa-home >/dev/null 2>&1; then
+      echo "jerboa_home_status=present"
+    else
+      echo "jerboa_home_status=unavailable"
+    fi
+  else
+    echo "jerbuild_status=unavailable"
+    echo "jerboa_home_status=unavailable"
+  fi
   rustc --version 2>/dev/null || true
   cargo --version 2>/dev/null || true
   cargo audit --version 2>/dev/null || true
@@ -91,8 +100,17 @@ hash_manifest "$repo_root/pgp-native" "$out_dir/native-source.sha256"
 hash_manifest "$repo_root/test" "$out_dir/test-source.sha256"
 hash_manifest "$repo_root/support" "$out_dir/support.sha256"
 hash_manifest "$repo_root/docs" "$out_dir/docs.sha256"
+hash_manifest "$repo_root/scripts" "$out_dir/scripts.sha256"
 hash_manifest "$repo_root/.github" "$out_dir/github-workflows.sha256"
 
+{
+  for file in Makefile .jerboa/security.json SECURITY.md README.md PLAN.md docs/release-evidence.md scripts/release-evidence.sh scripts/reproducibility-report.sh scripts/sanitize-evidence.sh scripts/sbom.sh scripts/target-evidence.sh; do
+    if [ -f "$file" ]; then
+      sha256_file "$file"
+    fi
+  done
+} > "$out_dir/release-inputs.sha256"
+
 find "$repo_root" -maxdepth 1 -type f \
   \( -name '*.ss' -o -name '*.chez' -o -name 'Makefile' -o -name 'README.md' -o -name 'SECURITY.md' -o -name 'LICENSE' -o -name '*.md' \) \
   -print | LC_ALL=C sort | while IFS= read -r file; do
diff --git a/scripts/target-evidence.sh b/scripts/target-evidence.sh
index 6cdd0e2..20a79e0 100644
--- a/scripts/target-evidence.sh
+++ b/scripts/target-evidence.sh
@@ -6,6 +6,7 @@ dist_dir=${DIST_DIR:-"$repo_root/dist"}
 out_dir=${TARGET_EVIDENCE_DIR:-"$dist_dir/target-evidence"}
 proof_file=${JPGP_TARGET_KEY_PROOF_FILE:-}
 require_proof=${JPGP_REQUIRE_TARGET_KEY_PROOF:-0}
+max_proof_bytes=${JPGP_MAX_PROOF_BYTES:-65536}
 
 rm -rf "$out_dir"
 mkdir -p "$out_dir"
@@ -54,6 +55,40 @@ require_marker() {
   fi
 }
 
+reject_sensitive_proof_material() {
+  if ! LC_ALL=C grep -Iq . "$proof_file" 2>/dev/null; then
+    write_blocked_status "target-key-proof-must-be-text"
+    exit 1
+  fi
+
+  proof_bytes=$(wc -c < "$proof_file" | tr -d ' ')
+  case "$proof_bytes" in
+    ''|*[!0-9]*)
+      write_blocked_status "target-key-proof-size-unavailable"
+      exit 1
+      ;;
+  esac
+  if [ "$proof_bytes" -gt "$max_proof_bytes" ]; then
+    write_blocked_status "target-key-proof-too-large"
+    exit 1
+  fi
+
+  if LC_ALL=C grep -I -E -q \
+    -e '/Users/' \
+    -e '/private/var/folders/' \
+    -e '/var/folders/' \
+    -e 'git@' \
+    -e 'ssh://[^[:space:]]+' \
+    -e '-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----' \
+    -e 'AGE-SECRET-KEY-1[A-Za-z0-9_-]+' \
+    -e 'Authorization:[[:space:]]*[^<[:space:]]' \
+    -e '(api[_-]?key|token|password|secret|passphrase|private[_-]?key|identity|plaintext|decrypted[_-]?payload)[[:space:]]*[:=][[:space:]]*[^<[:space:]]' \
+    "$proof_file"; then
+    write_blocked_status "target-key-proof-contains-sensitive-material"
+    exit 1
+  fi
+}
+
 if [ -z "$proof_file" ]; then
   if [ "$require_proof" = "1" ]; then
     write_blocked_status "target-key-proof-required-but-not-provided"
@@ -68,6 +103,8 @@ if [ ! -s "$proof_file" ]; then
   exit 1
 fi
 
+reject_sensitive_proof_material
+
 : > "$out_dir/missing-markers.txt"
 require_marker "age_encrypt_decrypt_status=target-evidence-recorded"
 require_marker "signature_roundtrip_status=target-evidence-recorded"