Harden mail release evidence privacy

ober

5d6382371decb5ce93016b46a1710d65255401b0

diff --git a/.jerboa/security.json b/.jerboa/security.json
index b537e0a..4a5061c 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -15,6 +15,18 @@
     "attachment_filenames": "caller-validated-before-use",
     "logging": "body-redaction-required"
   },
+  "release_evidence": {
+    "sanitize_evidence": "scripts/sanitize-evidence.sh",
+    "evidence_privacy_status": "sanitized",
+    "forbidden_evidence_material": [
+      "host-private-paths",
+      "raw-ssh-remotes",
+      "private-keys",
+      "authorization-headers",
+      "credential-shaped-material",
+      "raw-mail-bodies"
+    ]
+  },
   "required_release_gates": [
     "make security",
     "make test",
diff --git a/Makefile b/Makefile
index 2f13280..96a568f 100644
--- a/Makefile
+++ b/Makefile
@@ -13,7 +13,7 @@ DIST_DIR ?= dist/release-evidence
 SBOM_DIR ?= dist/sbom
 REPRO_DIR ?= dist/reproducibility
 
-.PHONY: help ensure-jerboa-tools test corpus-smoke fuzz-evidence security audit verify sbom reproducibility-report release-evidence clean
+.PHONY: help ensure-jerboa-tools test corpus-smoke fuzz-evidence security audit verify sbom reproducibility-report sanitize-evidence release-evidence clean
 .DEFAULT_GOAL := help
 
 help:
@@ -29,6 +29,7 @@ help:
 	@echo "  make sbom                 Write SBOM/provenance evidence"
 	@echo "  make reproducibility-report"
 	@echo "                            Compare repeated local parser evidence"
+	@echo "  make sanitize-evidence    Redact/check release evidence"
 	@echo "  make release-evidence     Write release evidence under dist/"
 	@echo "  make clean                Remove local generated files"
 	@echo ""
@@ -89,18 +90,22 @@ sbom:
 reproducibility-report:
 	JERBUILD="$(JERBUILD)" JERBOA_HOME="$(JERBOA_HOME)" JERBOA_MAIL_REPRO_DIR="$(REPRO_DIR)" scripts/reproducibility-report.sh
 
+sanitize-evidence:
+	scripts/sanitize-evidence.sh "$(DIST_DIR)"
+
 release-evidence: verify reproducibility-report sbom
 	rm -rf "$(DIST_DIR)"
 	mkdir -p "$(DIST_DIR)"
 	git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt"
 	git status --short > "$(DIST_DIR)/git-status.txt"
-	uname -a > "$(DIST_DIR)/build-environment.txt"
+	uname -srm > "$(DIST_DIR)/build-environment.txt"
 	@if [ -x "$(JERBUILD)" ]; then \
+		printf 'jerbuild_version=' >> "$(DIST_DIR)/build-environment.txt"; \
 		"$(JERBUILD)" --version >> "$(DIST_DIR)/build-environment.txt" 2>/dev/null || true; \
-		"$(JERBUILD)" --jerboa-home >> "$(DIST_DIR)/build-environment.txt" 2>/dev/null || true; \
+		if "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then echo "jerboa_home_status=present" >> "$(DIST_DIR)/build-environment.txt"; else echo "jerboa_home_status=missing" >> "$(DIST_DIR)/build-environment.txt"; fi; \
 	fi
 	@printf '%s\n' "jerboa-mail has no native/Rust/external package dependencies." > "$(DIST_DIR)/dependency-audit.txt"
-	shasum -a 256 Makefile .jerboa/security.json SECURITY.md docs/threat-model.md docs/parser-hardening.md docs/rendering-policy.md docs/release-evidence.md scripts/security-check.sh support/corpus-smoke.ss support/fuzz-evidence.ss > "$(DIST_DIR)/release-inputs-sha256.txt"
+	shasum -a 256 Makefile .jerboa/security.json SECURITY.md docs/threat-model.md docs/parser-hardening.md docs/rendering-policy.md docs/release-evidence.md scripts/security-check.sh scripts/sanitize-evidence.sh support/corpus-smoke.ss support/fuzz-evidence.ss > "$(DIST_DIR)/release-inputs-sha256.txt"
 	find jerboa-mail test -type f -name '*.ss' -print | sort | xargs shasum -a 256 > "$(DIST_DIR)/source-sha256.txt"
 	$(MAKE) corpus-smoke > "$(DIST_DIR)/corpus-smoke.txt" 2>&1
 	$(MAKE) fuzz-evidence > "$(DIST_DIR)/fuzz-evidence.txt" 2>&1
@@ -113,6 +118,14 @@ release-evidence: verify reproducibility-report sbom
 	grep -q 'sanitizer_status: caller-required-before-html-rendering' "$(DIST_DIR)/rendering-policy.txt"
 	cp -R "$(SBOM_DIR)" "$(DIST_DIR)/sbom"
 	cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility"
+	grep -q '^status=match$$' "$(DIST_DIR)/reproducibility/result.txt"
+	scripts/sanitize-evidence.sh "$(DIST_DIR)"
+	@{ \
+		echo "repo=jerboa-mail"; \
+		echo "release_evidence_status=pass"; \
+		echo "evidence_privacy_status=sanitized"; \
+		echo "private_path_scan_status=pass"; \
+	} > "$(DIST_DIR)/result.txt"
 
 clean:
 	rm -rf tmp dist
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index e556b41..eda670b 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -14,7 +14,8 @@ The `release-evidence` target runs `make verify` first, then writes untracked
 artifacts under `dist/release-evidence/`:
 
 - `git-commit.txt` and `git-status.txt`.
-- `build-environment.txt` with OS and Jerboa toolchain identity when available.
+- `build-environment.txt` with host-neutral OS and Jerboa toolchain identity
+  when available.
 - `dependency-audit.txt` documenting the absence of native, Rust, or external
   package dependencies.
 - `release-inputs-sha256.txt` for the release gate, security profile, policy,
@@ -27,6 +28,18 @@ artifacts under `dist/release-evidence/`:
   provenance.
 - `reproducibility/` with two repeated local parser evidence passes and
   `result.txt`.
+- `result.txt` with top-level `release_evidence_status=pass` after copied
+  evidence has been sanitized.
+
+Evidence privacy markers:
+
+- `evidence_privacy_status: sanitized`
+- `private_path_scan_status: pass`
+
+`scripts/sanitize-evidence.sh` redacts host-private paths, SSH clone
+references, authorization headers, mail-body markers, and credential-shaped
+fields from copied evidence, then fails if private or sensitive material
+remains.
 
 For a release candidate, archive those files with the exact Jerboa toolchain
 used to run the tests. Reproducibility is claimed only when
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
index 2c62f7c..0b83793 100755
--- a/scripts/reproducibility-report.sh
+++ b/scripts/reproducibility-report.sh
@@ -31,6 +31,7 @@ run_pass() {
         docs/rendering-policy.md \
         docs/release-evidence.md \
         scripts/security-check.sh \
+        scripts/sanitize-evidence.sh \
         scripts/sbom.sh \
         scripts/reproducibility-report.sh \
         support/corpus-smoke.ss \
diff --git a/scripts/sanitize-evidence.sh b/scripts/sanitize-evidence.sh
new file mode 100755
index 0000000..db0ad88
--- /dev/null
+++ b/scripts/sanitize-evidence.sh
@@ -0,0 +1,60 @@
+#!/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|mail[_-]?body)[[: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 (RSA |DSA |EC |OPENSSH )?PRIVATE KEY' \
+            -e 'Authorization:[[:space:]]*[^<[:space:]]' \
+            -e '(api[_-]?key|token|password|secret|mail[_-]?body)[[:space:]]*[:=][[:space:]]*[^<[:space:]]' \
+            "$evidence_dir"; then
+            printf '[mail-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 07f700f..6094d2f 100755
--- a/scripts/sbom.sh
+++ b/scripts/sbom.sh
@@ -18,8 +18,11 @@ mkdir -p "$OUT"
     if command -v "$JERBUILD" >/dev/null 2>&1 || [ -x "$JERBUILD" ]; then
         printf 'jerbuild_version='
         "$JERBUILD" --version 2>/dev/null || true
-        printf 'jerboa_home='
-        "$JERBUILD" --jerboa-home 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
     fi
     echo "native_dependencies=none"
     echo "rust_dependencies=none"
@@ -47,6 +50,7 @@ shasum -a 256 \
     docs/rendering-policy.md \
     docs/release-evidence.md \
     scripts/security-check.sh \
+    scripts/sanitize-evidence.sh \
     scripts/sbom.sh \
     scripts/reproducibility-report.sh \
     support/corpus-smoke.ss \
diff --git a/scripts/security-check.sh b/scripts/security-check.sh
index e7d24d2..dd709d0 100755
--- a/scripts/security-check.sh
+++ b/scripts/security-check.sh
@@ -21,6 +21,7 @@ require_file docs/threat-model.md
 require_file docs/parser-hardening.md
 require_file docs/rendering-policy.md
 require_file docs/release-evidence.md
+require_file scripts/sanitize-evidence.sh
 require_file support/corpus-smoke.ss
 require_file support/fuzz-evidence.ss
 require_file corpus/mail/accept/simple.eml
@@ -58,6 +59,23 @@ do
 done
 
 for marker in \
+    'evidence_privacy_status: sanitized' \
+    'private_path_scan_status: pass'
+do
+    if ! grep -q "$marker" docs/release-evidence.md; then
+        note_fail "missing evidence privacy marker: $marker"
+    fi
+done
+
+if ! grep -q 'private or sensitive evidence material remains' scripts/sanitize-evidence.sh; then
+    note_fail "sanitize-evidence script must fail when private/sensitive material remains"
+fi
+
+if ! grep -q 'scripts/sanitize-evidence.sh' Makefile; then
+    note_fail "release-evidence must run scripts/sanitize-evidence.sh"
+fi
+
+for marker in \
     'fuzz_property_status: documented' \
     'fuzz_cases_minimum: 2048' \
     'fuzz_scope_status: headers-transfer-multipart-limits'