Harden ProtonMail release evidence privacy

ober

dccdb9a2dbf72d20635a58dcb24d99a6d0afa876

diff --git a/.jerboa/security.json b/.jerboa/security.json
index 26035c7..6a6d78d 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -29,6 +29,23 @@
     },
     "targetEvidence": {
       "bridgeProof": "fail-closed",
+      "proofMaterialPolicy": {
+        "maxBytes": 65536,
+        "rejectBeforeCopy": true,
+        "forbiddenMaterial": [
+          "local-user-paths",
+          "ssh-urls",
+          "authorization-headers",
+          "proton-account-passwords",
+          "bridge-passwords",
+          "raw-imap-login-commands",
+          "eml-or-mbox-mail-artifacts",
+          "decrypted-message-bodies",
+          "attachments",
+          "mailbox-transcripts",
+          "private-keys"
+        ]
+      },
       "requiredMarkers": [
         "bridge_doctor_status=target-evidence-recorded",
         "bridge_loopback_status=confirmed",
@@ -42,6 +59,26 @@
         "raw_mail_output_status=not-preserved",
         "sensitive_artifact_policy=no-proton-account-passwords-bridge-passwords-eml-mbox-mail-bodies-attachments-or-imap-transcripts-preserved"
       ]
+    },
+    "releaseEvidence": {
+      "privacy": "sanitized-host-neutral",
+      "sanitizer": "scripts/sanitize-evidence.sh",
+      "requiredResultMarkers": [
+        "release_evidence_status=pass",
+        "evidence_privacy_status=sanitized",
+        "private_path_scan_status=pass"
+      ],
+      "forbiddenMaterial": [
+        "local-user-paths",
+        "ssh-urls",
+        "authorization-headers",
+        "proton-or-bridge-secret-values",
+        "raw-imap-login-commands",
+        "mailbox-transcripts",
+        "plaintext-mail",
+        "attachments",
+        "private-keys"
+      ]
     }
   },
   "suppressions": []
diff --git a/Makefile b/Makefile
index cbe2ac5..72384b5 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ else
 endif
 LD_ENV = $(LD_VAR)="$(JERBOA_SSL_DIR)"
 
-.PHONY: help ensure-jerboa-tools ensure-deps deps build run test integration-test doctor verify security dependency-evidence sbom reproducibility-report target-evidence target-evidence-selftest release-evidence clean
+.PHONY: help ensure-jerboa-tools ensure-deps deps build run test integration-test doctor verify security sanitize-evidence dependency-evidence sbom reproducibility-report target-evidence target-evidence-selftest release-evidence clean
 .DEFAULT_GOAL := help
 
 help:
@@ -95,7 +95,7 @@ integration-test:
 		bin/protonmail-read doctor; \
 	fi
 
-verify: test
+verify: security test
 
 security:
 	@set -eu; \
@@ -106,8 +106,16 @@ security:
 	  echo "High-confidence secret pattern found."; \
 	  exit 1; \
 	fi; \
+	test -x scripts/sanitize-evidence.sh; \
+	grep -q 'evidence_privacy_status=sanitized' Makefile; \
+	grep -q 'private_path_scan_status=pass' Makefile; \
+	grep -q 'target-bridge-proof-contains-sensitive-material' scripts/target-evidence.sh; \
+	grep -q 'proofMaterialPolicy' .jerboa/security.json; \
 	echo "secret_scan_status=pass"
 
+sanitize-evidence:
+	@sh scripts/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)"
+
 dependency-evidence: deps
 	$(MAKE) -C "$(JERBOA_SSL_DIR)" JERBUILD="$(JERBUILD)" release-evidence
 	$(MAKE) -C "$(JERBOA_MAIL_DIR)" JERBUILD="$(JERBUILD)" release-evidence
@@ -142,6 +150,13 @@ release-evidence:
 	@echo "==> Collecting git status"
 	@git status --short > "$(RELEASE_EVIDENCE_DIR)/git-status.txt"
 	@git rev-parse HEAD > "$(RELEASE_EVIDENCE_DIR)/git-head.txt" 2>/dev/null || true
+	@{ \
+	  echo "repo=jerboa-protonmail"; \
+	  echo "generated_at_utc=$$(date -u '+%Y-%m-%dT%H:%M:%SZ')"; \
+	  echo "git_commit=$$(git rev-parse HEAD 2>/dev/null || true)"; \
+	  echo "platform=$$(uname -srm)"; \
+	  echo "make_status=present"; \
+	} > "$(RELEASE_EVIDENCE_DIR)/build-environment.txt"
 	@echo "==> Running high-confidence secret scan"
 	@$(MAKE) security > "$(RELEASE_EVIDENCE_DIR)/security.log" 2>&1
 	@echo "==> Running tests"
@@ -165,6 +180,29 @@ release-evidence:
 	@rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom"
 	@cp -R "$(DIST_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/sbom"
 	@find bin protonmail test docs support scripts .github .jerboa/security.json -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256"
+	@if ! grep -qx 'status=match' "$(RELEASE_EVIDENCE_DIR)/reproducibility/report.txt"; then \
+	  echo "reproducibility_match_status=fail" > "$(RELEASE_EVIDENCE_DIR)/reproducibility-match.status"; \
+	  { echo "release_evidence_status=blocked"; echo "blocked_reason=reproducibility-mismatch"; echo "evidence_privacy_status=not-run"; echo "private_path_scan_status=not-run"; } > "$(RELEASE_EVIDENCE_DIR)/result.txt"; \
+	  exit 1; \
+	else \
+	  echo "reproducibility_match_status=pass" > "$(RELEASE_EVIDENCE_DIR)/reproducibility-match.status"; \
+	fi
+	@set -eu; \
+	  code=0; \
+	  sh scripts/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)" >/dev/null 2>&1 || code=$$?; \
+	  { \
+	    echo "sanitize_evidence_exit_status=$$code"; \
+	    echo "sanitize_evidence_log_status=not-retained"; \
+	  } > "$(RELEASE_EVIDENCE_DIR)/sanitize-evidence.status"; \
+	  if [ "$$code" -ne 0 ]; then \
+	    { echo "release_evidence_status=blocked"; echo "blocked_reason=evidence-privacy-scan-failed"; echo "evidence_privacy_status=failed"; echo "private_path_scan_status=fail"; } > "$(RELEASE_EVIDENCE_DIR)/result.txt"; \
+	    exit "$$code"; \
+	  fi
+	@{ \
+	  echo "release_evidence_status=pass"; \
+	  echo "evidence_privacy_status=sanitized"; \
+	  echo "private_path_scan_status=pass"; \
+	} > "$(RELEASE_EVIDENCE_DIR)/result.txt"
 	@echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)"
 
 clean:
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index bd31eb2..5897f76 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -17,6 +17,15 @@ The evidence bundle contains:
 - a reproducibility note that this repo ships a launcher/script path rather than
   a standalone native binary.
 
+Before the final result is written, the evidence tree is sanitized for
+host-neutral release review. Passing evidence must include:
+
+```text
+release_evidence_status=pass
+evidence_privacy_status=sanitized
+private_path_scan_status=pass
+```
+
 This local evidence does not replace live Proton Bridge release blockers:
 marker-complete least-privilege Bridge credential proof, loopback-only Bridge
 exposure review, plaintext export review, and external review of the read-only
@@ -54,8 +63,10 @@ raw_mail_output_status=not-preserved
 sensitive_artifact_policy=no-proton-account-passwords-bridge-passwords-eml-mbox-mail-bodies-attachments-or-imap-transcripts-preserved
 ```
 
-The proof gate rejects missing, empty, marker-incomplete, or high-confidence
-sensitive proof files. The proof must not preserve Proton account passwords,
-Bridge passwords, raw IMAP `LOGIN` commands, `.eml`/`.mbox` files, decrypted
-message bodies, attachments, mailbox transcripts, private hostnames, or raw
-Bridge output.
+The proof gate rejects missing, empty, oversized, binary,
+marker-incomplete, or high-confidence sensitive proof files before any proof is
+copied into `dist/target-evidence/`. The proof must not preserve Proton account
+passwords, Bridge passwords, raw IMAP `LOGIN` commands, `.eml`/`.mbox` files,
+decrypted message bodies, attachments, mailbox transcripts, private hostnames,
+authorization headers, SSH URLs, local host paths, private keys, or raw Bridge
+output.
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
index 84275d7..96ad436 100755
--- a/scripts/reproducibility-report.sh
+++ b/scripts/reproducibility-report.sh
@@ -4,6 +4,7 @@ set -eu
 repo_root=${REPO_ROOT:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
 dist_dir=${DIST_DIR:-"$repo_root/dist"}
 out_dir=${REPRO_DIR:-"$dist_dir/reproducibility"}
+sanitizer="$repo_root/scripts/sanitize-evidence.sh"
 rm -rf "$out_dir"
 mkdir -p "$out_dir"
 
@@ -17,6 +18,13 @@ sha256_file_value() {
 
 cd "$repo_root"
 
+{
+  echo "repo=jerboa-protonmail"
+  echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+  echo "platform=$(uname -srm)"
+  echo "standalone_binary_status=not-applicable"
+} > "$out_dir/build-env.txt"
+
 launcher_first=$(sha256_file_value bin/protonmail-read)
 source_first=$(find protonmail test bin docs support scripts .github .jerboa/security.json -type f -print | LC_ALL=C sort | while IFS= read -r file; do
   if command -v sha256sum >/dev/null 2>&1; then
@@ -61,3 +69,7 @@ esac
   echo "source_manifest_status=$source_manifest_status"
   echo "status=$status"
 } | tee "$out_dir/report.txt"
+
+if [ -x "$sanitizer" ]; then
+  sh "$sanitizer" "$out_dir"
+fi
diff --git a/scripts/sanitize-evidence.sh b/scripts/sanitize-evidence.sh
new file mode 100755
index 0000000..06167a0
--- /dev/null
+++ b/scripts/sanitize-evidence.sh
@@ -0,0 +1,62 @@
+#!/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#(^|[[:space:]])(LOGIN[[:space:]]+)[^[:space:]\x0a]+[[:space:]]+[^[:space:]\x0a]+#${1}${2}<redacted> <redacted>#gi;
+    s#((proton[_-]?(account[_-]?)?password|proton[_-]?bridge[_-]?password|bridge[_-]?password|mailbox[_-]?password|imap[_-]?password|api[_-]?key|token|password|secret|passphrase|private[_-]?key|raw[_-]?imap[_-]?transcript|mailbox[_-]?transcript|plaintext[_-]?mail|message[_-]?body|attachment)[[: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 'Authorization:[[:space:]]*[^<[:space:]]' \
+      -e '(?i)(^|[[:space:]])LOGIN[[:space:]]+[^<[:space:]][^[:space:]]*[[:space:]]+[^<[:space:]][^[:space:]]*' \
+      -e '(?i)(proton[_-]?(account[_-]?)?password|proton[_-]?bridge[_-]?password|bridge[_-]?password|mailbox[_-]?password|imap[_-]?password|api[_-]?key|token|password|secret|passphrase|private[_-]?key|raw[_-]?imap[_-]?transcript|mailbox[_-]?transcript|plaintext[_-]?mail|message[_-]?body|attachment)[[:space:]]*[:=][[:space:]]*[^<[:space:]]' \
+      "$evidence_dir"; then
+      printf '[protonmail-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 3c319b7..d5579e9 100755
--- a/scripts/sbom.sh
+++ b/scripts/sbom.sh
@@ -14,18 +14,19 @@ hash_manifest() {
   dir=$1
   out=$2
   if [ -d "$dir" ]; then
-    find "$dir" -type f \
-      ! -path '*/.git/*' \
-      ! -path '*/dist/*' \
-      ! -path '*/.jerboa/*' \
-      ! -path '*/.deps/*' \
+    (cd "$dir" && find . -type f \
+      ! -path './.git/*' \
+      ! -path './dist/*' \
+      ! -path './.jerboa/*' \
+      ! -path './.deps/*' \
+      ! -path './target/*' \
       -print | LC_ALL=C sort | while IFS= read -r file; do
         if command -v sha256sum >/dev/null 2>&1; then
           sha256sum "$file"
         else
           shasum -a 256 "$file"
         fi
-      done > "$out"
+      done) > "$out"
   else
     : > "$out"
   fi
@@ -43,13 +44,14 @@ git_state() {
   label=$1
   dir=$2
   {
-    echo "${label}_dir=$dir"
     if [ -d "$dir/.git" ]; then
+      echo "${label}_dir_status=present"
       echo "${label}_git_head=$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)"
       echo "${label}_git_status_begin"
       git -C "$dir" status --short 2>/dev/null || true
       echo "${label}_git_status_end"
     else
+      echo "${label}_dir_status=missing"
       echo "${label}_git_head=blocked"
     fi
   }
@@ -67,16 +69,22 @@ copy_if_present() {
 }
 
 cd "$repo_root"
-jerboa_home=$("$jerbuild" --jerboa-home 2>/dev/null || true)
+if "$jerbuild" --jerboa-home >/dev/null 2>&1; then
+  jerbuild_status=present
+  jerboa_home_status=present
+else
+  jerbuild_status=unavailable
+  jerboa_home_status=unavailable
+fi
 
 {
-  echo "repo=$repo_root"
+  echo "repo=jerboa-protonmail"
   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"
+  echo "platform=$(uname -srm)"
+  echo "jerbuild_status=$jerbuild_status"
   "$jerbuild" --version 2>/dev/null || true
-  echo "jerboa_home=$jerboa_home"
+  echo "jerboa_home_status=$jerboa_home_status"
   git_state "jerboa_ssl" "$ssl_dir"
   git_state "jerboa_mail" "$mail_dir"
 } > "$out_dir/build-environment.txt"
@@ -85,6 +93,7 @@ jerboa_home=$("$jerbuild" --jerboa-home 2>/dev/null || true)
   echo "bridge_transport=proton-mail-bridge-imap"
   echo "bridge_host_default=127.0.0.1"
   echo "bridge_credentials=environment-only"
+  echo "evidence_privacy=sanitized-host-neutral"
   echo "mailbox_policy=read-only"
   echo "imap_fetch_policy=BODY.PEEK"
   echo "tls_dependency=jerboa-ssl"
@@ -116,17 +125,33 @@ hash_manifest "$repo_root/support" "$out_dir/support.sha256"
 hash_manifest "$repo_root/scripts" "$out_dir/scripts.sha256"
 hash_manifest "$repo_root/.github" "$out_dir/github-workflows.sha256"
 if [ -f "$repo_root/.jerboa/security.json" ]; then
-  sha256_file "$repo_root/.jerboa/security.json" > "$out_dir/jerboa-security.sha256"
+  sha256_file ".jerboa/security.json" > "$out_dir/jerboa-security.sha256"
 else
   : > "$out_dir/jerboa-security.sha256"
 fi
 hash_manifest "$ssl_dir/lib" "$out_dir/jerboa-ssl-lib.sha256"
 hash_manifest "$mail_dir" "$out_dir/jerboa-mail-source.sha256"
 
-find "$repo_root" -maxdepth 1 -type f \
+find . -maxdepth 1 -type f \
   \( -name '*.ss' -o -name 'Makefile' -o -name 'README.md' -o -name 'SECURITY.md' -o -name 'LICENSE' \) \
   -print | LC_ALL=C sort | while IFS= read -r file; do
     sha256_file "$file"
   done > "$out_dir/top-level.sha256"
 
+{
+  for file in \
+    Makefile \
+    .jerboa/security.json \
+    docs/release-evidence.md \
+    scripts/reproducibility-report.sh \
+    scripts/sbom.sh \
+    scripts/sanitize-evidence.sh \
+    scripts/target-evidence.sh \
+    scripts/test-target-evidence.sh; do
+    if [ -f "$file" ]; then
+      sha256_file "$file"
+    fi
+  done
+} > "$out_dir/release-inputs.sha256"
+
 echo "status=generated"
diff --git a/scripts/target-evidence.sh b/scripts/target-evidence.sh
index c58a179..034aa71 100755
--- 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=${JPROTONMAIL_TARGET_PROOF_FILE:-}
 require_proof=${JPROTONMAIL_REQUIRE_TARGET_PROOF:-0}
+max_proof_bytes=${JPROTONMAIL_MAX_PROOF_BYTES:-65536}
 
 rm -rf "$out_dir"
 mkdir -p "$out_dir"
@@ -57,8 +58,21 @@ require_marker() {
 }
 
 reject_sensitive_proof_material() {
-  if grep -E -i '(proton_bridge_password[[:space:]]*=|login[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+|BEGIN[[:space:]]+(RSA|OPENSSH|EC|DSA|PRIVATE)[[:space:]]+KEY|raw[._-]?imap[._-]?transcript[[:space:]]*=|\\.eml|\\.mbox)' "$proof_file" >/dev/null 2>&1; then
-    write_blocked_status "target-bridge-proof-contains-sensitive-material-pattern"
+  proof_size=$(wc -c < "$proof_file" | tr -d '[:space:]')
+
+  if [ "$proof_size" -gt "$max_proof_bytes" ]; then
+    write_blocked_status "target-bridge-proof-contains-sensitive-material"
+    exit 1
+  fi
+
+  if ! LC_ALL=C grep -Iq . "$proof_file" 2>/dev/null; then
+    write_blocked_status "target-bridge-proof-contains-sensitive-material"
+    exit 1
+  fi
+
+  sensitive_pattern='(/Users/|/private/var/folders/|/var/folders/|git@|ssh://|-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----|Authorization:[[:space:]]*[^[:space:]]|(^|[[:space:]])LOGIN[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+|proton[_-]?(account[_-]?)?password[[:space:]]*[:=]|proton[_-]?bridge[_-]?password[[:space:]]*[:=]|bridge[_-]?password[[:space:]]*[:=]|mailbox[_-]?password[[:space:]]*[:=]|imap[_-]?password[[:space:]]*[:=]|(api[_-]?key|token|password|secret|passphrase|private[_-]?key|raw[._-]?imap[._-]?transcript|mailbox[_-]?transcript|plaintext[_-]?mail|message[_-]?body|attachment)[[:space:]]*[:=]|\\.eml|\\.mbox)'
+  if grep -I -E -i -q "$sensitive_pattern" "$proof_file"; then
+    write_blocked_status "target-bridge-proof-contains-sensitive-material"
     exit 1
   fi
 }
diff --git a/scripts/test-target-evidence.sh b/scripts/test-target-evidence.sh
index 466b9ff..17c1ff9 100755
--- a/scripts/test-target-evidence.sh
+++ b/scripts/test-target-evidence.sh
@@ -7,6 +7,7 @@ selftest_dir="$dist_dir/target-evidence-selftest"
 proof_file="$selftest_dir/valid-proof.txt"
 incomplete_file="$selftest_dir/incomplete-proof.txt"
 sensitive_file="$selftest_dir/sensitive-proof.txt"
+private_file="$selftest_dir/private-proof.txt"
 out_dir="$selftest_dir/out"
 
 rm -rf "$selftest_dir"
@@ -78,7 +79,19 @@ expect_failure env \
   TARGET_EVIDENCE_DIR="$out_dir/sensitive" \
   JPROTONMAIL_TARGET_PROOF_FILE="$sensitive_file" \
   sh scripts/target-evidence.sh
-require_line "$out_dir/sensitive/status.txt" "blocked_reason=target-bridge-proof-contains-sensitive-material-pattern"
+require_line "$out_dir/sensitive/status.txt" "blocked_reason=target-bridge-proof-contains-sensitive-material"
+
+{
+  cat "$proof_file"
+  printf '%s\n' "diagnostic_path=/Users/user/private-mailbox"
+} > "$private_file"
+
+expect_failure env \
+  TARGET_EVIDENCE_DIR="$out_dir/private" \
+  JPROTONMAIL_TARGET_PROOF_FILE="$private_file" \
+  sh scripts/target-evidence.sh
+require_line "$out_dir/private/status.txt" "blocked_reason=target-bridge-proof-contains-sensitive-material"
+test ! -f "$out_dir/private/target-bridge-proof.txt"
 
 rm -rf "$selftest_dir"
 echo "target_evidence_selftest_status=pass"