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