Harden drive release evidence privacy
ober
6a362858e941fac107f7e39254998bc4150acd24
--- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -29,6 +29,7 @@ }, "targetEvidence": { "storageProof": "fail-closed", + "forbidPrivateProofMaterial": true, "requiredMarkers": [ "proton_live_drive_status=target-evidence-recorded", "s3_encrypted_roundtrip_status=target-evidence-recorded", --- a/Makefile +++ b/Makefile @@ -66,6 +66,19 @@ TARGET_EVIDENCE_DIR ?= $(DIST_DIR)/target-evidence .PHONY: help ensure-jerboa-tools ensure-deps deps run test integration s3-integration doctor binary binary-doctor-smoke audit verify security dependency-evidence sbom reproducibility-report target-evidence release-evidence bundle install linux linux-amd64 linux-arm64 freebsd freebsd-amd64 clean .DEFAULT_GOAL := help +lib/libjerboa_native$(SO_EXT): ensure-jerboa-tools + @mkdir -p lib + @src="$(JERBOA_NATIVE_DIR)/libjerboa_native$(SO_EXT)"; \ + sibling="$(CURDIR)/../jerboa/lib/libjerboa_native$(SO_EXT)"; \ + if [ -f "$$src" ]; then \ + ln -sf "$$src" "$@"; \ + elif [ -f "$$sibling" ]; then \ + ln -sf "$$sibling" "$@"; \ + else \ + echo "ERROR: libjerboa_native$(SO_EXT) not found in $(JERBOA_NATIVE_DIR) or sibling jerboa/lib; run make -C ../jerboa native" >&2; \ + exit 1; \ + fi + help: @echo "jerboa-drive" @echo "" @@ -129,7 +142,7 @@ ensure-deps: @test -d "$(JERBOA_HTTPS_DIR)" || { echo "ERROR: JERBOA_HTTPS_DIR missing: $(JERBOA_HTTPS_DIR)" >&2; exit 1; } @test -d "$(JERBOA_SSL_DIR)" || { echo "ERROR: JERBOA_SSL_DIR missing: $(JERBOA_SSL_DIR)" >&2; exit 1; } -deps: ensure-jerboa-tools ensure-deps +deps: ensure-jerboa-tools ensure-deps lib/libjerboa_native$(SO_EXT) @if [ ! -f "$(JERBOA_SSL_DIR)/lib/jerboa-ssl.sls" ] || [ ! -f "$(JERBOA_SSL_DIR)/jerboa_ssl_shim$(SO_EXT)" ]; then \ $(MAKE) -C "$(JERBOA_SSL_DIR)" JERBUILD="$(JERBUILD)" build; \ fi @@ -174,9 +187,12 @@ binary-doctor-smoke: binary audit: deps $(MAKE) -C "$(JERBOA_PROTON_BRIDGE_DIR)" JERBUILD="$(JERBUILD)" audit -verify: security test doctor audit +verify: security test doctor audit reproducibility-report sbom target-evidence security: + @test -f scripts/sanitize-evidence.sh || { echo "missing scripts/sanitize-evidence.sh" >&2; exit 1; } + @grep -q 'sanitize-evidence' docs/release-evidence.md || { echo "missing release evidence sanitizer docs" >&2; exit 1; } + @grep -q 'host-private' docs/release-evidence.md || { echo "missing host-private proof docs" >&2; exit 1; } @set -eu; \ pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gldt-[A-Za-z0-9_-]{20,}|glrt-[A-Za-z0-9_-]{20,}|Authorization['\\\"]?[[:space:]]*[:=][[:space:]]*['\\\"]?Bearer[[:space:]]+[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}|PROTONDRIVE_(USERNAME|PASSWORD|VAULT_PASSWORD|ACCESS_TOKEN|REFRESH_TOKEN)=['\\\"]?[A-Za-z0-9_./+=:-]{24,}|JDRIVE_(S3_TEST_SECRET|S3_TEST_ACCESS|VAULT_PASSWORD)=['\\\"]?[A-Za-z0-9_./+=:-]{24,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16})"; \ matches="$$(git grep -n -I -E "$$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)"; \ @@ -313,6 +329,7 @@ freebsd-amd64: clean: rm -rf cache tmp mount dist protonstorage-bin protonstorage-linux-amd64 protonstorage-linux-arm64 protonstorage-freebsd-amd64 + rm -f lib/libjerboa_native$(SO_EXT) rm -f protonstorage-bin-main.c protonstorage-bin.wp.so rm -f protonstorage-linux-amd64-main.c protonstorage-linux-amd64.wp.so rm -f protonstorage-linux-arm64-main.c protonstorage-linux-arm64.wp.so --- a/SECURITY.md +++ b/SECURITY.md @@ -18,6 +18,8 @@ No public production-support commitment exists yet. Security-sensitive releases - Dependency and native-code audits are clean, or accepted risks are documented. - `make release-evidence` writes current test, audit, dependency, SBOM, and reproducibility evidence. +- Release evidence is sanitized and contains no private paths, SSH remotes, or + host fingerprints from `uname -a`. - Target Drive/S3/FUSE release proof must be marker-validated. Raw Proton or S3 account output, tokens, credentials, vault passwords, PIV PINs, plaintext file bodies, and plaintext remote object manifests must not be preserved in --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -17,6 +17,11 @@ The gate records: - SBOM/source/dependency hashes, and - post-warm two-build binary reproducibility output. +Release evidence is sanitized with `scripts/sanitize-evidence.sh`. The +sanitizer redacts local user and temporary paths and fails closed if private +paths, SSH remotes, or `uname -a` host fingerprints remain in the evidence +bundle. + The target is allowed to fail after writing evidence. A failed target means the repo is not production-releasable yet, but the evidence directory should explain which gate blocked publication. @@ -63,7 +68,9 @@ make release-evidence The proof file must contain these exact marker lines and must not contain raw Proton/S3 account output, tokens, credentials, vault passwords, PIV PINs, plaintext file bodies, plaintext remote manifests, or plaintext object keys -derived from local filenames: +derived from local filenames. Oversized or host-private proofs, including +private local paths, local temporary paths, SSH remotes, cookies, authorization +headers, or high-confidence credential material, fail closed before copy: ```text proton_live_drive_status=target-evidence-recorded @@ -79,6 +86,6 @@ raw_remote_output_status=not-preserved sensitive_artifact_policy=no-proton-passwords-tokens-s3-keys-vault-passwords-piv-pins-plaintext-files-or-raw-account-output-preserved ``` -Missing, empty, or marker-incomplete proof files fail closed as -`status=blocked-target-proof`. Accepted proof files are copied to -`dist/target-evidence/target-storage-proof.txt` and hashed. +Missing, empty, oversized, host-private, sensitive, or marker-incomplete proof +files fail closed as `status=blocked-target-proof`. Accepted proof files are +copied to `dist/target-evidence/target-storage-proof.txt` and hashed. --- a/scripts/release-evidence.sh +++ b/scripts/release-evidence.sh @@ -43,6 +43,10 @@ sha256_file() { fi } +sanitize_evidence() { + sh scripts/sanitize-evidence.sh "$evidence_dir" "$dist_dir/sbom" "$dist_dir/reproducibility" "$dist_dir/target-evidence" +} + git status --short > "$evidence_dir/git-status.txt" git rev-parse HEAD > "$evidence_dir/git-head.txt" 2>/dev/null || true @@ -74,10 +78,12 @@ if [ "$fail" -ne 0 ]; then echo "release_evidence_status=blocked" echo "blocked_reason=one-or-more-production-gates-failed" } > "$evidence_dir/result.txt" + sanitize_evidence echo "Release evidence written to $evidence_dir" echo "Production gates are blocked; see $evidence_dir/*.status and logs." exit 1 fi echo "release_evidence_status=pass" > "$evidence_dir/result.txt" +sanitize_evidence echo "Release evidence written to $evidence_dir" --- a/scripts/reproducibility-report.sh +++ b/scripts/reproducibility-report.sh @@ -115,9 +115,9 @@ stabilize_once() { cd "$repo_root" { - echo "repo=$repo_root" + echo "repo=jerboa-drive" echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo "jerbuild=$jerbuild" + echo "jerbuild_status=present" "$jerbuild" --version 2>/dev/null || true echo "binary=$binary_output" } > "$out_dir/build-env.txt" @@ -168,10 +168,10 @@ esac echo "generated_input_status=$generated_input_status" echo "generated_input_policy=diagnostic-scratch-not-release-gate" echo "source_manifest_status=$source_manifest_status" - echo "jerbuild_binary_obj_dir=$obj_dir" + echo "jerbuild_binary_obj_dir_status=fixed-scratch" echo "jerbuild_binary_keep_obj_dir=1" echo "warmup_build_used=1" - echo "warmup_jerbuild_binary_obj_dir=$warmup_obj_dir" + echo "warmup_jerbuild_binary_obj_dir_status=fixed-scratch" echo "stabilization_build_used=1" echo "measurement_mode=post-warm" echo "status=$status" new file mode 100755 --- /dev/null +++ b/scripts/sanitize-evidence.sh @@ -0,0 +1,45 @@ +#!/bin/sh +set -eu + +fail() { + printf 'sanitize-evidence: %s\n' "$*" >&2 + exit 1 +} + +redact_file() { + file=$1 + grep -Iq . "$file" || return 0 + tmp=$file.sanitize.$$ + sed -E \ + -e 's#/Users/[^[:space:]"]+#<redacted-path>#g' \ + -e 's#/home/[^/[:space:]"]+[^[:space:]"]*#<redacted-path>#g' \ + -e 's#/var/folders/[^[:space:]"]+#<redacted-path>#g' \ + -e 's#~/mine(/[^[:space:]"]*)?#<redacted-path>#g' \ + -e 's#git@[^[:space:]"]+#<redacted-ssh-remote>#g' \ + -e 's#uname -a#uname -s -r -m#g' \ + -e 's#repo=/[^[:space:]]+#repo=<redacted-path>#g' \ + -e 's#jerbuild=[^[:space:]]+#jerbuild=<redacted-path>#g' \ + -e 's#jerboa_home=[^[:space:]]+#jerboa_home=<redacted-path>#g' \ + "$file" > "$tmp" + mv "$tmp" "$file" +} + +[ "$#" -gt 0 ] || exit 0 + +for path in "$@"; do + [ -e "$path" ] || continue + find "$path" -type f -print | while IFS= read -r file; do + redact_file "$file" + done +done + +pattern='/Users/|/home/[^/[:space:]]+|/var/folders/|~/mine|git@|uname -a|repo=/|jerbuild=/Users|jerboa_home=/Users' +if command -v rg >/dev/null 2>&1; then + if rg -n -H "$pattern" "$@"; then + fail "private path, SSH remote, or uname -a marker found in release evidence" + fi +else + if grep -R -n -E "$pattern" "$@"; then + fail "private path, SSH remote, or uname -a marker found in release evidence" + fi +fi --- a/scripts/sbom.sh +++ b/scripts/sbom.sh @@ -56,13 +56,17 @@ cd "$repo_root" jerboa_home=$("$jerbuild" --jerboa-home 2>/dev/null || true) { - echo "repo=$repo_root" + echo "repo=jerboa-drive" 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" + printf 'platform=%s\n' "$(uname -srm 2>/dev/null || printf unknown)" + echo "jerbuild_status=present" "$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 } > "$out_dir/build-environment.txt" { --- 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=${JDRIVE_TARGET_STORAGE_PROOF_FILE:-} require_proof=${JDRIVE_REQUIRE_TARGET_STORAGE_PROOF:-0} +proof_max_bytes=${JDRIVE_TARGET_STORAGE_PROOF_MAX_BYTES:-1048576} rm -rf "$out_dir" mkdir -p "$out_dir" @@ -19,6 +20,22 @@ sha256_file() { fi } +valid_uint() { + case "$1" in + ''|*[!0-9]*) return 1 ;; + *) return 0 ;; + esac +} + +proof_size() { + wc -c < "$1" | tr -d '[:space:]' +} + +proof_has_forbidden_material() { + proof=$1 + grep -n -I -E '/Users/|/home/[^/[:space:]]+|/var/folders/|~/mine|git@|BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gldt-[A-Za-z0-9_-]{20,}|glrt-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|Authorization:[[:space:]]*(Bearer|Basic)|Cookie:|Set-Cookie:|PROTONDRIVE_(USERNAME|PASSWORD|VAULT_PASSWORD|ACCESS_TOKEN|REFRESH_TOKEN)=|JDRIVE_(S3_TEST_SECRET|S3_TEST_ACCESS|VAULT_PASSWORD)=' "$proof" >/dev/null 2>&1 +} + write_default_status() { { echo "repo=jerboa-drive" @@ -70,6 +87,16 @@ if [ ! -s "$proof_file" ]; then exit 1 fi +if ! valid_uint "$proof_max_bytes" || [ "$(proof_size "$proof_file")" -gt "$proof_max_bytes" ]; then + write_blocked_status "target-storage-proof-file-too-large" + exit 1 +fi + +if proof_has_forbidden_material "$proof_file"; then + write_blocked_status "target-storage-proof-contains-private-or-sensitive-material" + exit 1 +fi + : > "$out_dir/missing-markers.txt" require_marker "proton_live_drive_status=target-evidence-recorded" require_marker "s3_encrypted_roundtrip_status=target-evidence-recorded"