Harden secmonlib release evidence privacy
ober
f630151ff12d612ebbbb4f734a46692bf31e9243
--- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -74,7 +74,17 @@ "agent_no_secret_startup_status=pass", "target_platform_status=linux-or-freebsd-recorded" ] - } + }, + "proofMaterialPolicy": "reject-overlarge-private-sensitive", + "forbiddenEvidenceMaterial": [ + "host-private-paths", + "raw-ssh-remotes", + "private-keys", + "authorization-headers", + "credential-shaped-material", + "raw-production-agent-output" + ], + "sanitizeEvidence": "scripts/sanitize-evidence.sh" }, "ffi": { "requireCollectSafeBlockingCalls": true, --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ REPRO_DIR ?= dist/reproducibility SOAK_DIR ?= dist/soak BINARY_SMOKE_DIR ?= dist/binary-smoke -.PHONY: all help build binary keygen agent collector analyze test security audit verify sbom reproducibility-report soak-evidence binary-smoke release-evidence install clean ensure-jsqlite +.PHONY: all help build binary keygen agent collector analyze test security audit verify sbom reproducibility-report soak-evidence binary-smoke sanitize-evidence release-evidence install clean ensure-jsqlite .DEFAULT_GOAL := help all: binary @@ -46,6 +46,7 @@ help: @echo " make reproducibility-report" @echo " Compare two clean local binary builds" @echo " make soak-evidence Record load/soak evidence status" + @echo " make sanitize-evidence Redact/check release evidence" @echo " make release-evidence Write release evidence under dist/" # Standalone native binary via .jerbuild (entry bin/agent.ss -> secmon-agent). @@ -115,14 +116,18 @@ soak-evidence: binary-smoke: JSECMONLIB_BINARY_SMOKE_DIR="$(BINARY_SMOKE_DIR)" scripts/binary-smoke.sh +sanitize-evidence: + scripts/sanitize-evidence.sh "$(DIST_DIR)" + release-evidence: verify sbom reproducibility-report soak-evidence binary-smoke 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" + printf 'jerbuild_version=' >> "$(DIST_DIR)/build-environment.txt" $(JERBUILD) --version >> "$(DIST_DIR)/build-environment.txt" - $(JERBUILD) --jerboa-home >> "$(DIST_DIR)/build-environment.txt" + printf 'jerboa_home_status=present\n' >> "$(DIST_DIR)/build-environment.txt" cargo metadata --manifest-path patches/jerboa-native-rs/Cargo.toml --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-native.json" cd patches/jerboa-native-rs && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-native.txt" shasum -a 256 secmon-agent > "$(DIST_DIR)/secmon-agent-sha256.txt" @@ -135,6 +140,7 @@ release-evidence: verify sbom reproducibility-report soak-evidence binary-smoke docs/release-evidence.md \ docs/threat-model.md \ scripts/binary-smoke.sh \ + scripts/sanitize-evidence.sh \ scripts/reproducibility-report.sh \ scripts/sbom.sh \ scripts/security-check.sh \ @@ -149,6 +155,13 @@ release-evidence: verify sbom reproducibility-report soak-evidence binary-smoke cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility" cp -R "$(SOAK_DIR)" "$(DIST_DIR)/soak" cp -R "$(BINARY_SMOKE_DIR)" "$(DIST_DIR)/binary-smoke" + scripts/sanitize-evidence.sh "$(DIST_DIR)" + { \ + echo "repo=jerboa-secmonlib"; \ + echo "release_evidence_status=pass"; \ + echo "evidence_privacy_status=sanitized"; \ + echo "private_path_scan_status=pass"; \ + } > "$(DIST_DIR)/result.txt" install: binary mkdir -p $(BIN_DIR) --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -17,14 +17,15 @@ 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. +- `build-environment.txt` with host-neutral OS and Jerboa toolchain identity. - Cargo dependency metadata and RustSec audit output for the patched native Rust crate. - SHA-256 hash for `secmon-agent`. - Static-binary source input hashes, generated FFI symbol list, and dynamic linkage output from `otool` or `ldd`. - `sbom/` with a source manifest, source hashes, native Cargo metadata, and - release input hashes. + release input hashes. Host-private paths are not preserved in copied release + evidence. - `reproducibility/` with a recorded WPO prewarm build, two measured local binary builds, release-input hashes for policy/evidence scripts, and `result.txt`. @@ -32,6 +33,13 @@ artifacts under `dist/release-evidence/`: this records an explicit platform block instead of implying runtime success. - `soak/` with agent/collector load and sandbox status. Local default evidence records blocked statuses until a Linux or FreeBSD release-host soak is run. +- `result.txt` with the top-level `release_evidence_status=pass` and evidence + privacy scan status. + +Before finalizing the release evidence directory, `scripts/sanitize-evidence.sh` +redacts local paths, SSH clone references, authorization headers, and +credential-shaped fields from copied evidence and fails if private or sensitive +material remains. Reproducibility is claimed only when `reproducibility/result.txt` records `prewarm_status=present`, matching `binary_status`, `ffi_symbols_status`, @@ -70,6 +78,8 @@ proof is absent or incomplete. `target_platform_status=linux-or-freebsd-recorded`. When proof is valid, the scripts copy it into `dist/release-evidence/soak/` or -`dist/release-evidence/binary-smoke/` and write a `.sha256` sidecar. When proof -is required but missing or incomplete, the status is `blocked-target-proof` and -the evidence target fails. +`dist/release-evidence/binary-smoke/` and write a `.sha256` sidecar. Proof files +are rejected before copy if they are oversized or contain host-private paths, +SSH remotes, private keys, authorization headers, or credential-shaped fields. +When proof is required but missing, incomplete, private, or sensitive, the +status is `blocked-target-proof` and the evidence target fails. --- a/scripts/binary-smoke.sh +++ b/scripts/binary-smoke.sh @@ -8,12 +8,14 @@ OUT=${JSECMONLIB_BINARY_SMOKE_DIR:-dist/binary-smoke} REQUIRE=${JSECMONLIB_REQUIRE_BINARY_SMOKE:-0} TARGET_BINARY_SMOKE_PROOF=${JSECMONLIB_TARGET_BINARY_SMOKE_PROOF_FILE:-} REQUIRE_TARGET_BINARY_SMOKE_PROOF=${JSECMONLIB_REQUIRE_TARGET_BINARY_SMOKE_PROOF:-0} +MAX_PROOF_BYTES=${JSECMONLIB_MAX_PROOF_BYTES:-65536} PLATFORM=$(uname -s) rm -rf "$OUT" mkdir -p "$OUT" target_binary_smoke_proof_status=not-run +proof_rejection_reason= write_status() { local status=$1 @@ -58,6 +60,26 @@ record_proof() { shasum -a 256 "$destination" > "$destination.sha256" } +reject_proof_material() { + local proof=$1 + local bytes + + proof_rejection_reason= + bytes=$(wc -c < "$proof" | tr -d ' ') + if [ "$bytes" -gt "$MAX_PROOF_BYTES" ]; then + proof_rejection_reason="target binary smoke proof is larger than $MAX_PROOF_BYTES bytes" + return 1 + fi + if [ -n "${HOME:-}" ] && grep -F -q "$HOME" "$proof"; then + proof_rejection_reason="target binary smoke proof contains a host-private home path" + return 1 + fi + if LC_ALL=C grep -E -q '/Users/|/private/var/folders/|/var/folders/|git@|ssh://|BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY|Authorization:|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|GITHUB_TOKEN|(api[_-]?key|token|password|secret)[[:space:]]*[:=]' "$proof"; then + proof_rejection_reason="target binary smoke proof contains private paths, remotes, or credential-shaped material" + return 1 + fi +} + proof_requested=0 if [ -n "$TARGET_BINARY_SMOKE_PROOF" ] || [ "$REQUIRE_TARGET_BINARY_SMOKE_PROOF" = "1" ]; then proof_requested=1 @@ -73,6 +95,15 @@ if [ "$proof_requested" = "1" ]; then "set JSECMONLIB_TARGET_BINARY_SMOKE_PROOF_FILE to a non-empty target-host binary smoke proof" exit 1 fi + if ! reject_proof_material "$TARGET_BINARY_SMOKE_PROOF"; then + write_status \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "not-run" \ + "blocked-private-or-sensitive" \ + "$proof_rejection_reason" + exit 1 + fi if ! require_marker "$TARGET_BINARY_SMOKE_PROOF" "target_binary_smoke_status=target-evidence-recorded" || ! require_marker "$TARGET_BINARY_SMOKE_PROOF" "agent_no_secret_startup_status=pass" || ! require_marker "$TARGET_BINARY_SMOKE_PROOF" "target_platform_status=linux-or-freebsd-recorded"; then --- a/scripts/reproducibility-report.sh +++ b/scripts/reproducibility-report.sh @@ -25,6 +25,7 @@ hash_release_inputs() { docs/release-evidence.md \ docs/threat-model.md \ scripts/binary-smoke.sh \ + scripts/sanitize-evidence.sh \ scripts/reproducibility-report.sh \ scripts/sbom.sh \ scripts/security-check.sh \ new file mode 100755 --- /dev/null +++ b/scripts/sanitize-evidence.sh @@ -0,0 +1,61 @@ +#!/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#\$\((HOME)\)/mine#<local-path>#g; + s#git\@#git-at-#g; + s#ssh://[^[:space:]\x22\x27()]+#ssh://<redacted>#g; + s#(Authorization:[[:space:]]*)[^[:space:]\x0a]+#${1}<redacted>#gi; + s#((api[_-]?key|token|password|secret)[[: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)[[:space:]]*[:=][[:space:]]*[^<[:space:]]' \ + "$evidence_dir"; then + printf '[secmonlib-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 @@ -17,8 +17,11 @@ mkdir -p "$OUT" date -u '+%Y-%m-%dT%H:%M:%SZ' printf 'jerbuild_version=' "$JERBUILD" --version - printf 'jerboa_home=' - "$JERBUILD" --jerboa-home + if "$JERBUILD" --jerboa-home >/dev/null 2>&1; then + echo "jerboa_home_status=present" + else + echo "jerboa_home_status=unavailable" + fi echo "static_binary=secmon-agent" echo "native_crate=patches/jerboa-native-rs" echo "vendored_jsqlite=vendor/jsqlite" @@ -52,5 +55,6 @@ shasum -a 256 \ patches/jerboa-native-rs/Cargo.lock \ support/static-main.c \ support/gen-ffi-symbols.sh \ + scripts/sanitize-evidence.sh \ Makefile \ .jerbuild > "$OUT/release-inputs-sha256.txt" --- a/scripts/security-check.sh +++ b/scripts/security-check.sh @@ -53,6 +53,7 @@ require_file "LICENSE" require_file "SECURITY.md" require_file ".gitignore" require_file ".jerboa/security.json" +require_file "scripts/sanitize-evidence.sh" find . -maxdepth 1 -iname "README*" -type f | grep -q . || { printf '[secmonlib-security] missing README\n' >&2 fail=1 @@ -67,12 +68,17 @@ require_text "scripts/soak-evidence.sh" "blocked-target-proof" require_text "scripts/binary-smoke.sh" "JSECMONLIB_TARGET_BINARY_SMOKE_PROOF_FILE" require_text "scripts/binary-smoke.sh" "JSECMONLIB_REQUIRE_TARGET_BINARY_SMOKE_PROOF" require_text "scripts/binary-smoke.sh" "blocked-target-proof" +require_text "scripts/soak-evidence.sh" "reject_proof_material" +require_text "scripts/binary-smoke.sh" "reject_proof_material" +require_text "scripts/sanitize-evidence.sh" "private or sensitive evidence material remains" +require_text "Makefile" "scripts/sanitize-evidence.sh" require_text "docs/threat-model.md" "target_load_proof_status: documented" require_text "docs/threat-model.md" "target_sandbox_proof_status: documented" require_text "docs/threat-model.md" "target_binary_smoke_proof_status: documented" require_text ".jerboa/security.json" "\"targetLoadProof\"" require_text ".jerboa/security.json" "\"targetSandboxProof\"" require_text ".jerboa/security.json" "\"targetBinarySmokeProof\"" +require_text ".jerboa/security.json" "\"proofMaterialPolicy\"" if command -v rg >/dev/null 2>&1; then say "running high-confidence secret scan" --- a/scripts/soak-evidence.sh +++ b/scripts/soak-evidence.sh @@ -11,6 +11,7 @@ TARGET_LOAD_PROOF=${JSECMONLIB_TARGET_LOAD_PROOF_FILE:-} REQUIRE_TARGET_LOAD_PROOF=${JSECMONLIB_REQUIRE_TARGET_LOAD_PROOF:-0} TARGET_SANDBOX_PROOF=${JSECMONLIB_TARGET_SANDBOX_PROOF_FILE:-} REQUIRE_TARGET_SANDBOX_PROOF=${JSECMONLIB_REQUIRE_TARGET_SANDBOX_PROOF:-0} +MAX_PROOF_BYTES=${JSECMONLIB_MAX_PROOF_BYTES:-65536} PLATFORM=$(uname -s) rm -rf "$OUT" @@ -18,6 +19,7 @@ mkdir -p "$OUT" target_load_proof_status=not-run target_sandbox_proof_status=not-run +proof_rejection_reason= write_status() { local status=$1 @@ -71,6 +73,27 @@ record_proof() { shasum -a 256 "$destination" > "$destination.sha256" } +reject_proof_material() { + local proof=$1 + local label=$2 + local bytes + + proof_rejection_reason= + bytes=$(wc -c < "$proof" | tr -d ' ') + if [ "$bytes" -gt "$MAX_PROOF_BYTES" ]; then + proof_rejection_reason="$label proof is larger than $MAX_PROOF_BYTES bytes" + return 1 + fi + if [ -n "${HOME:-}" ] && grep -F -q "$HOME" "$proof"; then + proof_rejection_reason="$label proof contains a host-private home path" + return 1 + fi + if LC_ALL=C grep -E -q '/Users/|/private/var/folders/|/var/folders/|git@|ssh://|BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY|Authorization:|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|GITHUB_TOKEN|(api[_-]?key|token|password|secret)[[:space:]]*[:=]' "$proof"; then + proof_rejection_reason="$label proof contains private paths, remotes, or credential-shaped material" + return 1 + fi +} + load_proof_requested=0 if [ -n "$TARGET_LOAD_PROOF" ] || [ "$REQUIRE_TARGET_LOAD_PROOF" = "1" ]; then load_proof_requested=1 @@ -89,6 +112,18 @@ if [ "$load_proof_requested" = "1" ]; then "set JSECMONLIB_TARGET_LOAD_PROOF_FILE to a non-empty target-host load proof" exit 1 fi + if ! reject_proof_material "$TARGET_LOAD_PROOF" "target load"; then + write_status \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-private-or-sensitive" \ + "$target_sandbox_proof_status" \ + "$proof_rejection_reason" + exit 1 + fi if ! require_marker "$TARGET_LOAD_PROOF" "production_load_status=current-run-recorded" "target load" || ! require_marker "$TARGET_LOAD_PROOF" "agent_runtime_status=release-host-runtime-recorded" "target load" || ! require_marker "$TARGET_LOAD_PROOF" "collector_load_status=release-host-sustained-recorded" "target load" || @@ -126,6 +161,18 @@ if [ "$sandbox_proof_requested" = "1" ]; then "set JSECMONLIB_TARGET_SANDBOX_PROOF_FILE to a non-empty target-host sandbox proof" exit 1 fi + if ! reject_proof_material "$TARGET_SANDBOX_PROOF" "target sandbox"; then + write_status \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "blocked-target-proof" \ + "$target_load_proof_status" \ + "blocked-private-or-sensitive" \ + "$proof_rejection_reason" + exit 1 + fi if ! require_marker "$TARGET_SANDBOX_PROOF" "sandbox_status=target-evidence-recorded" "target sandbox" || ! require_marker "$TARGET_SANDBOX_PROOF" "privilege_drop_status=target-evidence-recorded" "target sandbox" || ! require_marker "$TARGET_SANDBOX_PROOF" "negative_access_test_status=target-denied-recorded" "target sandbox"; then