Harden ProtonMail release evidence privacy
ober
dccdb9a2dbf72d20635a58dcb24d99a6d0afa876
--- 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": [] --- 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: --- 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. --- 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 new file mode 100755 --- /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 --- 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" --- 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 } --- 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"