Harden mail release evidence privacy
ober
5d6382371decb5ce93016b46a1710d65255401b0
--- 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", --- 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 --- 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 --- 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 \ new file mode 100755 --- /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 --- 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 \ --- 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'