Harden AWS release evidence privacy

ober

c54ff2f96fdaf0f62f954266626ca051f8e96eb9

diff --git a/.jerboa/security.json b/.jerboa/security.json
index 08b032c..bf61c80 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -24,6 +24,7 @@
     },
     "targetEvidence": {
       "awsProof": "fail-closed",
+      "proofMaterialPolicy": "reject-overlarge-private-sensitive",
       "requiredMarkers": [
         "sts_get_caller_identity_status=target-evidence-recorded",
         "least_privilege_iam_status=confirmed",
@@ -36,7 +37,9 @@
         "raw_service_output_status=not-preserved",
         "sensitive_artifact_policy=no-aws-access-keys-session-tokens-signed-requests-account-exports-or-service-payloads-preserved"
       ]
-    }
+    },
+    "localVerification": ["make verify", "make release-evidence"],
+    "evidencePrivacy": "sanitized-host-neutral-hash-only-reproducibility"
   },
   "suppressions": []
 }
diff --git a/Makefile b/Makefile
index d055da6..6742f38 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ DIST_DIR ?= $(CURDIR)/dist
 RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence
 TARGET_EVIDENCE_DIR ?= $(DIST_DIR)/target-evidence
 
-.PHONY: all build binary run test native-runtime security sbom reproducibility-report target-evidence target-evidence-selftest release-evidence install clean ensure-jerboa-tools
+.PHONY: all build binary run test native-runtime security verify sbom reproducibility-report target-evidence target-evidence-selftest release-evidence install clean ensure-jerboa-tools
 
 all: binary
 
@@ -101,6 +101,8 @@ target-evidence-selftest:
 	DIST_DIR="$(DIST_DIR)" \
 	sh scripts/test-target-evidence.sh
 
+verify: security test target-evidence-selftest target-evidence reproducibility-report sbom
+
 release-evidence:
 	@rm -rf "$(RELEASE_EVIDENCE_DIR)" "$(TARGET_EVIDENCE_DIR)" "$(DIST_DIR)/reproducibility" "$(DIST_DIR)/sbom"
 	@mkdir -p "$(RELEASE_EVIDENCE_DIR)"
@@ -130,6 +132,7 @@ release-evidence:
 	@rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom"
 	@cp -R "$(DIST_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/sbom"
 	@find aws.ss pssm.ss build*.ss lib test support docs scripts .github .jerboa/security.json -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256"
+	@sh scripts/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)"
 	@echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)"
 
 install: binary
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index 9f9157f..0e6431b 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -15,6 +15,13 @@ The evidence bundle contains:
 - target AWS proof status and proof-gate self-test output;
 - repeated binary and generated FFI-symbol reproducibility checks.
 
+Release evidence is sanitized before it is kept. SBOM and build-environment
+records use host-neutral status fields instead of local checkout paths, and the
+reproducibility report archives hashes/status only. Raw build logs, binary help
+output, and scratch build directories are not preserved; the report must record
+`artifact_archive_status=hashes-only` and
+`scratch_build_logs_status=not-archived`.
+
 SigV4 signing uses `(std crypto native-rust)` through Jerboa's bundled
 `jerboa-native-rs` with `tls,crypto` features. The prior PATH-resolved
 `openssl dgst` subprocess is no longer part of signing.
@@ -55,8 +62,9 @@ raw_service_output_status=not-preserved
 sensitive_artifact_policy=no-aws-access-keys-session-tokens-signed-requests-account-exports-or-service-payloads-preserved
 ```
 
-The proof gate rejects missing, empty, marker-incomplete, or high-confidence
-sensitive proof files. The proof file must not preserve access keys, session
-tokens, `Authorization` headers, signed canonical requests, account exports, raw
-service responses, S3 payloads, CloudWatch logs, SSM values, or private resource
-names.
+The proof gate rejects missing, empty, marker-incomplete, oversized,
+host-private, or high-confidence sensitive proof files. `JERBOA_AWS_TARGET_PROOF_MAX_BYTES`
+defaults to 65536. The proof file must not preserve local paths, SSH remote
+URLs, access keys, session tokens, `Authorization` headers, signed canonical
+requests, account exports, raw service responses, S3 payloads, CloudWatch logs,
+SSM values, or private resource names.
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
index 82f5522..161494e 100755
--- a/scripts/reproducibility-report.sh
+++ b/scripts/reproducibility-report.sh
@@ -6,8 +6,20 @@ dist_dir=${DIST_DIR:-"$repo_root/dist"}
 out_dir=${REPRO_DIR:-"$dist_dir/reproducibility"}
 make_cmd=${MAKE:-make}
 bin_name=${BIN:-jerboa-aws}
+scratch_parent=${JERBOA_AWS_REPRO_TMPDIR:-${TMPDIR:-/tmp}}
+scratch_dir=$scratch_parent/jerboa-aws-repro-$$
+sanitizer=${JERBOA_AWS_EVIDENCE_SANITIZER:-scripts/sanitize-evidence.sh}
 rm -rf "$out_dir"
 mkdir -p "$out_dir"
+rm -rf "$scratch_dir"
+mkdir -p "$scratch_dir"
+
+cleanup() {
+  status=$?
+  rm -rf "$scratch_dir"
+  exit "$status"
+}
+trap cleanup EXIT INT TERM
 
 sha256_file_value() {
   if command -v sha256sum >/dev/null 2>&1; then
@@ -20,16 +32,16 @@ sha256_file_value() {
 cd "$repo_root"
 
 rm -f "$bin_name"
-"$make_cmd" --no-print-directory binary > "$out_dir/binary-first.log" 2>&1
+"$make_cmd" --no-print-directory binary > "$scratch_dir/binary-first.log" 2>&1
 first_binary=$(sha256_file_value "$bin_name")
 first_symbols=$(sha256_file_value support/ffi-symbols.gen)
-./"$bin_name" --help > "$out_dir/binary-first-help.log" 2>&1
+./"$bin_name" --help > "$scratch_dir/binary-first-help.log" 2>&1
 
 rm -f "$bin_name"
-"$make_cmd" --no-print-directory binary > "$out_dir/binary-second.log" 2>&1
+"$make_cmd" --no-print-directory binary > "$scratch_dir/binary-second.log" 2>&1
 second_binary=$(sha256_file_value "$bin_name")
 second_symbols=$(sha256_file_value support/ffi-symbols.gen)
-./"$bin_name" --help > "$out_dir/binary-second-help.log" 2>&1
+./"$bin_name" --help > "$scratch_dir/binary-second-help.log" 2>&1
 
 echo "$first_binary  $bin_name" > "$out_dir/binary-first.sha256"
 echo "$second_binary  $bin_name" > "$out_dir/binary-second.sha256"
@@ -56,5 +68,11 @@ esac
 {
   echo "binary_status=$binary_status"
   echo "ffi_symbols_status=$ffi_symbols_status"
+  echo "artifact_archive_status=hashes-only"
+  echo "scratch_build_logs_status=not-archived"
   echo "status=$status"
 } | tee "$out_dir/report.txt"
+
+if [ -x "$sanitizer" ]; then
+  "$sanitizer" "$out_dir"
+fi
diff --git a/scripts/sanitize-evidence.sh b/scripts/sanitize-evidence.sh
new file mode 100755
index 0000000..f8cd980
--- /dev/null
+++ b/scripts/sanitize-evidence.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+set -eu
+
+if [ "$#" -eq 0 ]; then
+    set -- dist/release-evidence
+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
+        perl -0pi -e '
+          s#/Users/[^[:space:]\x22\x27()]+#<local-path>#g;
+          if (defined $ENV{HOME} && length $ENV{HOME}) {
+            my $home = quotemeta($ENV{HOME});
+            s#$home#<home>#g;
+          }
+          s#~/mine#<local-path>#g;
+          s#\$\((HOME)\)/mine#<local-path>#g;
+          s#users-MacBook-Pro#<host>#g;
+          s#git\@#git-at-#g;
+        ' "$file"
+    done
+done
diff --git a/scripts/sbom.sh b/scripts/sbom.sh
index cd5e46a..1e5b995 100755
--- a/scripts/sbom.sh
+++ b/scripts/sbom.sh
@@ -6,6 +6,7 @@ dist_dir=${DIST_DIR:-"$repo_root/dist"}
 out_dir=${SBOM_DIR:-"$dist_dir/sbom"}
 jerbuild=${JERBUILD:-jerbuild}
 bin_name=${BIN:-jerboa-aws}
+sanitizer=${JERBOA_AWS_EVIDENCE_SANITIZER:-scripts/sanitize-evidence.sh}
 rm -rf "$out_dir"
 mkdir -p "$out_dir"
 
@@ -13,17 +14,20 @@ hash_manifest() {
   dir=$1
   out=$2
   if [ -d "$dir" ]; then
-    find "$dir" -type f \
-      ! -path '*/.git/*' \
-      ! -path '*/dist/*' \
-      ! -path '*/.jerboa/*' \
-      -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"
+    (
+      cd "$dir"
+      find . -type f \
+        ! -path './.git/*' \
+        ! -path './dist/*' \
+        ! -path './.jerboa/*' \
+        -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"
   else
     : > "$out"
   fi
@@ -38,6 +42,9 @@ sha256_file() {
 }
 
 cd "$repo_root"
+if [ -x "$sanitizer" ]; then
+  trap '"$sanitizer" "$out_dir"' EXIT
+fi
 jerboa_home=$("$jerbuild" --jerboa-home 2>/dev/null || true)
 native_manifest=
 if [ -n "$jerboa_home" ] && [ -f "$jerboa_home/jerboa-native-rs/Cargo.toml" ]; then
@@ -45,13 +52,17 @@ if [ -n "$jerboa_home" ] && [ -f "$jerboa_home/jerboa-native-rs/Cargo.toml" ]; t
 fi
 
 {
-  echo "repo=$repo_root"
+  echo "repo=jerboa-aws"
   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=selected"
   "$jerbuild" --version 2>/dev/null || true
-  echo "jerboa_home=$jerboa_home"
+  if [ -n "$jerboa_home" ]; then
+    echo "jerboa_home_status=present"
+  else
+    echo "jerboa_home_status=missing"
+  fi
   echo "cc=$(${CC:-cc} --version 2>/dev/null | sed -n '1p' || true)"
   echo "cargo=$(cargo --version 2>/dev/null || true)"
   echo "rustc=$(rustc --version 2>/dev/null || true)"
@@ -105,14 +116,14 @@ hash_manifest "$repo_root/test" "$out_dir/test.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"
-if [ -f "$repo_root/.jerboa/security.json" ]; then
-  sha256_file "$repo_root/.jerboa/security.json" > "$out_dir/jerboa-security.sha256"
+if [ -f ".jerboa/security.json" ]; then
+  sha256_file ".jerboa/security.json" > "$out_dir/jerboa-security.sha256"
 else
   : > "$out_dir/jerboa-security.sha256"
 fi
 hash_manifest "$repo_root/.github" "$out_dir/github-workflows.sha256"
 
-find "$repo_root" -maxdepth 1 -type f \
+find . -maxdepth 1 -type f \
   \( -name '*.ss' -o -name '*.c' -o -name 'Makefile' -o -name 'README.md' -o -name 'SECURITY.md' -o -name '.jerbuild' -o -name 'LICENSE' \) \
   -print | LC_ALL=C sort | while IFS= read -r file; do
     sha256_file "$file"
diff --git a/scripts/target-evidence.sh b/scripts/target-evidence.sh
index 33edc84..5ed09ac 100755
--- a/scripts/target-evidence.sh
+++ b/scripts/target-evidence.sh
@@ -6,10 +6,26 @@ dist_dir=${DIST_DIR:-"$repo_root/dist"}
 out_dir=${TARGET_EVIDENCE_DIR:-"$dist_dir/target-evidence"}
 proof_file=${JERBOA_AWS_TARGET_PROOF_FILE:-}
 require_proof=${JERBOA_AWS_REQUIRE_TARGET_PROOF:-0}
+max_proof_bytes=${JERBOA_AWS_TARGET_PROOF_MAX_BYTES:-65536}
+sanitizer=${JERBOA_AWS_EVIDENCE_SANITIZER:-scripts/sanitize-evidence.sh}
 
 rm -rf "$out_dir"
 mkdir -p "$out_dir"
 cd "$repo_root"
+if [ -x "$sanitizer" ]; then
+  trap '"$sanitizer" "$out_dir"' EXIT
+fi
+
+case "$max_proof_bytes" in
+  ''|*[!0-9]*)
+    echo "JERBOA_AWS_TARGET_PROOF_MAX_BYTES must be a positive integer" >&2
+    exit 1
+    ;;
+esac
+if [ "$max_proof_bytes" -lt 1 ]; then
+  echo "JERBOA_AWS_TARGET_PROOF_MAX_BYTES must be a positive integer" >&2
+  exit 1
+fi
 
 sha256_file() {
   if command -v sha256sum >/dev/null 2>&1; then
@@ -20,11 +36,14 @@ sha256_file() {
 }
 
 write_default_status() {
+  proof_status=${1:-not-run}
+  final_status=${2:-local-status-recorded}
+  reason=${3:-}
   {
     echo "repo=jerboa-aws"
     echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
     echo "git_commit=$(git rev-parse HEAD 2>/dev/null || true)"
-    echo "target_aws_proof_status=not-run"
+    echo "target_aws_proof_status=$proof_status"
     echo "sts_get_caller_identity_status=blocked-not-run"
     echo "least_privilege_iam_status=blocked-not-run"
     echo "sso_or_sts_short_lived_status=blocked-not-run"
@@ -35,17 +54,17 @@ write_default_status() {
     echo "production_credential_review_status=blocked-not-run"
     echo "raw_service_output_status=blocked-not-run"
     echo "sensitive_artifact_policy=blocked-not-run"
-    echo "status=local-status-recorded"
+    if [ -n "$reason" ]; then
+      echo "blocked_reason=$reason"
+    fi
+    echo "status=$final_status"
   } > "$out_dir/status.txt"
 }
 
 write_blocked_status() {
   reason=$1
-  write_default_status
-  {
-    echo "blocked_reason=$reason"
-    echo "status=blocked-target-proof"
-  } >> "$out_dir/status.txt"
+  proof_status=${2:-blocked}
+  write_default_status "$proof_status" blocked-target-proof "$reason"
 }
 
 require_marker() {
@@ -55,28 +74,51 @@ require_marker() {
   fi
 }
 
-reject_sensitive_proof_material() {
-  if grep -E -i '(AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|aws_secret_access_key|aws_session_token|x-amz-security-token|authorization:[[:space:]]*AWS4-HMAC-SHA256|secretaccesskey|sessiontoken)' "$proof_file" >/dev/null 2>&1; then
-    write_blocked_status "target-aws-proof-contains-sensitive-material-pattern"
+proof_contains() {
+  pattern=$1
+  if command -v rg >/dev/null 2>&1; then
+    rg -q -I -e "$pattern" "$proof_file" 2>/dev/null
+  else
+    grep -E -q "$pattern" "$proof_file" 2>/dev/null
+  fi
+}
+
+reject_unsafe_proof_material() {
+  proof_size=$(wc -c < "$proof_file" | tr -d ' ')
+  if [ "$proof_size" -gt "$max_proof_bytes" ]; then
+    write_blocked_status "target-aws-proof-larger-than-JERBOA_AWS_TARGET_PROOF_MAX_BYTES" too-large
+    exit 1
+  fi
+  if proof_contains '(AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|[Aa][Ww][Ss]_[Ss][Ee][Cc][Rr][Ee][Tt]_[Aa][Cc][Cc][Ee][Ss][Ss]_[Kk][Ee][Yy]|[Aa][Ww][Ss]_[Ss][Ee][Ss][Ss][Ii][Oo][Nn]_[Tt][Oo][Kk][Ee][Nn]|[Xx]-[Aa][Mm][Zz]-[Ss][Ee][Cc][Uu][Rr][Ii][Tt][Yy]-[Tt][Oo][Kk][Ee][Nn]|[Aa][Uu][Tt][Hh][Oo][Rr][Ii][Zz][Aa][Tt][Ii][Oo][Nn]:[[:space:]]*AWS4-HMAC-SHA256|[Ss][Ee][Cc][Rr][Ee][Tt][Aa][Cc][Cc][Ee][Ss][Ss][Kk][Ee][Yy]|[Ss][Ee][Ss][Ss][Ii][Oo][Nn][Tt][Oo][Kk][Ee][Nn])'; then
+    write_blocked_status "target-aws-proof-contains-sensitive-material-pattern" sensitive-material
+    exit 1
+  fi
+  if proof_contains '(/Users/|~/mine|\$\(HOME\)/mine|git@|users-MacBook-Pro|uname[[:space:]]+-a)'; then
+    write_blocked_status "target-aws-proof-contains-private-material-pattern" private-material
     exit 1
   fi
 }
 
 if [ -z "$proof_file" ]; then
   if [ "$require_proof" = "1" ]; then
-    write_blocked_status "target-aws-proof-required-but-not-provided"
+    write_blocked_status "target-aws-proof-required-but-not-provided" missing
     exit 1
   fi
   write_default_status
   exit 0
 fi
 
+if [ ! -f "$proof_file" ]; then
+  write_blocked_status "target-aws-proof-file-missing" missing
+  exit 1
+fi
+
 if [ ! -s "$proof_file" ]; then
-  write_blocked_status "target-aws-proof-file-missing-or-empty"
+  write_blocked_status "target-aws-proof-file-empty" incomplete
   exit 1
 fi
 
-reject_sensitive_proof_material
+reject_unsafe_proof_material
 
 : > "$out_dir/missing-markers.txt"
 require_marker "sts_get_caller_identity_status=target-evidence-recorded"
@@ -91,7 +133,7 @@ require_marker "raw_service_output_status=not-preserved"
 require_marker "sensitive_artifact_policy=no-aws-access-keys-session-tokens-signed-requests-account-exports-or-service-payloads-preserved"
 
 if [ -s "$out_dir/missing-markers.txt" ]; then
-  write_blocked_status "target-aws-proof-missing-required-markers"
+  write_blocked_status "target-aws-proof-missing-required-markers" incomplete
   exit 1
 fi
 
diff --git a/scripts/test-target-evidence.sh b/scripts/test-target-evidence.sh
index d97de3f..8269909 100755
--- a/scripts/test-target-evidence.sh
+++ b/scripts/test-target-evidence.sh
@@ -7,6 +7,8 @@ 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"
+large_file="$selftest_dir/large-proof.txt"
 out_dir="$selftest_dir/out"
 
 rm -rf "$selftest_dir"
@@ -38,6 +40,7 @@ expect_failure env \
   JERBOA_AWS_REQUIRE_TARGET_PROOF=1 \
   sh scripts/target-evidence.sh
 require_line "$out_dir/missing-required/status.txt" "blocked_reason=target-aws-proof-required-but-not-provided"
+require_line "$out_dir/missing-required/status.txt" "target_aws_proof_status=missing"
 require_line "$out_dir/missing-required/status.txt" "status=blocked-target-proof"
 
 echo "sts_get_caller_identity_status=target-evidence-recorded" > "$incomplete_file"
@@ -46,6 +49,7 @@ expect_failure env \
   JERBOA_AWS_TARGET_PROOF_FILE="$incomplete_file" \
   sh scripts/target-evidence.sh
 require_line "$out_dir/incomplete/status.txt" "blocked_reason=target-aws-proof-missing-required-markers"
+require_line "$out_dir/incomplete/status.txt" "target_aws_proof_status=incomplete"
 require_line "$out_dir/incomplete/missing-markers.txt" "least_privilege_iam_status=confirmed"
 
 {
@@ -79,5 +83,26 @@ expect_failure env \
   sh scripts/target-evidence.sh
 require_line "$out_dir/sensitive/status.txt" "blocked_reason=target-aws-proof-contains-sensitive-material-pattern"
 
+{
+  cat "$proof_file"
+  echo "review_path=/Users/user/mine/aws-review"
+} > "$private_file"
+
+expect_failure env \
+  TARGET_EVIDENCE_DIR="$out_dir/private" \
+  JERBOA_AWS_TARGET_PROOF_FILE="$private_file" \
+  sh scripts/target-evidence.sh
+require_line "$out_dir/private/status.txt" "target_aws_proof_status=private-material"
+require_line "$out_dir/private/status.txt" "blocked_reason=target-aws-proof-contains-private-material-pattern"
+
+cat "$proof_file" > "$large_file"
+printf '%070000d\n' 0 >> "$large_file"
+expect_failure env \
+  TARGET_EVIDENCE_DIR="$out_dir/large" \
+  JERBOA_AWS_TARGET_PROOF_FILE="$large_file" \
+  sh scripts/target-evidence.sh
+require_line "$out_dir/large/status.txt" "target_aws_proof_status=too-large"
+require_line "$out_dir/large/status.txt" "blocked_reason=target-aws-proof-larger-than-JERBOA_AWS_TARGET_PROOF_MAX_BYTES"
+
 rm -rf "$selftest_dir"
 echo "target_evidence_selftest_status=pass"