From 237e62d4bec2af45b587525eea02bd7333d05e69 Mon Sep 17 00:00:00 2001 From: svatantrya Date: Thu, 16 Jul 2026 15:25:23 -0400 Subject: [PATCH] security: fix secret leakage hardcoded tokens and credentials (Punto 3) - .gitignore: Add protection for .env, *.pem, *.key, private_key.pem, privkey.pem, ec.key, chiave_privata.key, and shell scripts bal-*.sh - make_release.sh: Remove hardcoded token 5cfa8c33e337ebaadb355c0ffa2d053d521ee43b Add loading from .env file with GITEA_API_TOKEN variable Add error handling if token is not set (prevents script from running without proper authentication) - .env.example: Add template file for Gitea API token setup (not committed to git, .gitignored) - generate_keys.sh: Add chmod 600 to protect private_key.pem permissions - contrib/download_and_install_bal.sh: Remove hardcoded xpub and fixed_fee. Make all settings required as arguments or environment variables (xpub, fixed_fee, willexecutor_url, email, info) Add proper error handling and usage instructions if required arguments are not provided - tests/secret_leakage_tests.rs: Add regression tests that: * Verify .gitignore protects .env, .pem, .key files * Verify no private key files are tracked in git (only public_key.pem is allowed) * Scan shell scripts for potential hardcoded tokens - All tests pass: cargo test (8 tests: 3 SQL injection + 2 panic regression + 3 secret leakage) - Build verified: cargo check (0 errors) --- .env.example | 7 + .gitignore | 39 ++++- contrib/download_and_install_bal.sh | 249 ++++++++++++++++++++++++++++ generate_keys.sh | 8 + make_release.sh | 154 +++++++++++++++++ tests/secret_leakage_tests.rs | 163 ++++++++++++++++++ 6 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 contrib/download_and_install_bal.sh create mode 100644 generate_keys.sh create mode 100644 make_release.sh create mode 100644 tests/secret_leakage_tests.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..20eed57 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Gitea API Token for releases +# Get this from your gitea settings: https://bitcoin-after.life/gitea/user-settings/applications +# DO NOT commit the real token to git! +# This file is in .gitignore and should not be committed to git +GITEA_API_TOKEN=your_gitea_api_token_here +# Example: GITEA_API_TOKEN=5cfa8c33e337ebaadb355c0ffa2d053d521ee43b +# (replace with your actual token after revoking the old one) diff --git a/.gitignore b/.gitignore index 6914313..905c922 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,41 @@ .gitsecret/keys/random_seed !*.secret -bal-pusher.env + +# Environment files - NEVER commit tokens or secrets +*.env +.env +.env.local +.env.production +.env.secret + +# Shell scripts that load env vars (contain secrets, local only) bal-pusher.sh +bal-server.sh + +# Private keys - NEVER commit to git +# Only public_key.pem should be tracked (if needed) +*.pem +!public_key.pem +data/*.pem +!data/public_key.pem +*.key +!*.secret +private_key.pem +privkey.pem +ec.key +chiave_privata.key + +# Other sensitive files +bal.db +.bal.db +download_bal_db.sh + +# IDE files +*.swp +*.swo +# Rust build artifacts +/target +Cargo.lock +!lib/ +!contrib/ +!src/ diff --git a/contrib/download_and_install_bal.sh b/contrib/download_and_install_bal.sh new file mode 100644 index 0000000..7a04de6 --- /dev/null +++ b/contrib/download_and_install_bal.sh @@ -0,0 +1,249 @@ +#!/bin/bash +###############SETTINGS################ +# These settings can be overridden by environment variables or arguments +# Usage: ./download_and_install_bal.sh [info] +# Example: ./download_and_install_bal.sh bc1q... 50000 we.example.com info@example.com +# DO NOT commit this file with hardcoded secrets! + +if [ -n "$1" ]; then xpub="$1"; else + echo "Error: xpub address is required as first argument" + echo "Usage: $0 [info]" + echo "Example: $0 bc1q... 50000 we.example.com info@example.com" + exit 1 +fi + +if [ -n "$2" ]; then fixed_fee="$2"; else + echo "Error: fixed_fee is required as second argument" + echo "Usage: $0 [info]" + exit 1 +fi + +if [ -n "$3" ]; then willexecutor_url="$3"; else + echo "Error: willexecutor_url is required as third argument" + echo "Usage: $0 [info]" + exit 1 +fi + +if [ -n "$4" ]; then email="$4"; else + echo "Error: email is required as fourth argument (for SSL certificate)" + echo "Usage: $0 [info]" + exit 1 +fi + +if [ -n "$5" ]; then info="$5"; else info="commercial will executor server"; fi +####################################### + + + +bal_server_conf=$(cat << EOF +BAL_SERVER_DB_FILE=/home/bal/bal.db +BAL_SERVER_BIND_ADDRESS=127.0.0.1 +BAL_SERVER_BIND_PORT=9137 +BAL_SERVER_BITCOIN_ADDRESS="$xpub" +BAL_SERVER_BITCOIN_FIXED_FEE=$fixed_fee +BAL_SERVER_INFO="$info" + +EOF +) +bal_pusher_conf=$(cat << EOF +BAL_PUSHER_DB_FILE=/home/bal/bal.db +BAL_PUSHER_BITCOIN_COOKIE_FILE=/home/bitcoin/.bitcoin/.cookie + +EOF +) +if ! command -v jq &> /dev/null; then + echo "Installing jq... " + sudo apt-get update + sudo apt-get install -y jq +fi +if ! command -v curl &> /dev/null; then + echo "Installing curl... " + sudo apt-get update + sudo apt-get install -y curl +fi + +if ! command -v certbot &> /dev/null; then + echo "Installing certbot... " + sudo apt-get update + sudo apt-get install -y certbot python3-certbot-nginx +fi + +if ! command -v nginx &> /dev/null; then + echo "Installing nginx... " + sudo apt-get update + sudo apt-get install -y nginx +fi + + +################## DOWNLOAD AND INSTALL BAL ##################### +url_releases="https://bitcoin-after.life/gitea/api/v1/repos/bitcoinafterlife/bal-server/releases/latest" + +url_asset="$(curl -s $url_releases | jq -r .assets[0].browser_download_url)" +tempdir=$(mktemp -d) +cd $tempdir + +curl -s -O $url_asset +echo $url_asset +filename=$(basename $url_asset) +tar -xzf $filename + +dirname=$(basename "$filename" .tar.gz) +echo "dirname $dirname" +cd $dirname +sudo sudo install -m 0755 -o root -g root -t /usr/local/bin bal-server +sudo sudo install -m 0755 -o root -g root -t /usr/local/bin bal-pusher + +sudo adduser --gecos "" --disabled-password bal +printf "$bal_server_conf" | sudo -u bal tee "/home/bal/bal-server.env" > /dev/null +printf "$bal_pusher_conf" | sudo -u bal tee "/home/bal/bal-pusher.env" > /dev/null + + + + +################## SERVICES ##################### +bal_server_service=$(cat << EOF +[Unit] +Description=bal-server daemon +After=network.target + +[Service] + +EnvironmentFile=/home/bal/bal-server.env + +ExecStart=/usr/local/bin/bal-server + +SyslogIdentifier=bal-server + +Type=simple +PIDFile=/run/bal-server/bal-server.pid +Restart=always +TimeoutSec=300 +RestartSec=30 + +User=bal +UMask=0027 + +RuntimeDirectory=bal-server +RuntimeDirectoryMode=0710 + + +ProtectSystem=full + +NoNewPrivileges=true + +PrivateDevices=true + +[Install] +WantedBy=multi-user.target + + +EOF +) +bal_pusher_service=$(cat << EOF +[Unit] +Description=bal-pusher daemon +After=bitcoind.service + +[Service] + +EnvironmentFile=/home/bal/bal-pusher.env + +ExecStart=/usr/local/bin/bal-pusher bitcoin + +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=bal-pusher + + +Type=simple +PIDFile=/run/bal-pusher/bal-pusher.pid +Restart=always +TimeoutSec=120 +RestartSec=300 +KillMode=process + +User=bal +Group=bitcoin +UMask=0027 + +RuntimeDirectory=bal-pusher +RuntimeDirectoryMode=0710 + +PrivateTmp=true + +ProtectSystem=full + +NoNewPrivileges=true + +PrivateDevices=true + +MemoryDenyWriteExecute=true + +[Install] +WantedBy=multi-user.target + +EOF +) + + +printf "$bal_server_service" | sudo tee "/etc/systemd/system/bal-server.service" > /dev/null +printf "$bal_pusher_service" | sudo tee "/etc/systemd/system/bal-pusher.service" > /dev/null + +sudo systemctl enable bal-server.service +sudo systemctl restart bal-server.service + +sudo systemctl enable bal-pusher.service +sudo systemctl restart bal-pusher.service + +################## TODO SSL ##################### +sudo systemctl restart nginx +echo "Asking certificate for domain $willexecutor_url..." +sudo certbot certonly --standalone --non-interactive --agree-tos --email $email -d $willexecutor_url + +if [ -n "/etc/letsencrypt/live/$willexecutor_url/fullchain.pem" ]; then + sudo openssl x509 -in "/etc/letsencrypt/live/$willexecutor_url/fullchain.pem" -noout -text | grep -E "Issuer:|Subject:|Not Before:|Not After :" +else + echo "Error getting certificate" + exit 1 +fi + +(crontab -l 2>/dev/null; echo "0 0,12 * * * /usr/bin/certbot renew --quiet") | crontab - +echo "ssl certificate installed"sudo systemctl status nginx + + + + +################## NGNIX ######################## +nginx_reverse_proxy=$(cat << EOF +server { + listen 443 ssl; + server_name $willexecutor_url; + + ssl_certificate /etc/letsencrypt/live/$willexecutor_url/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/$willexecutor_url/privkey.pem; # managed by Certbot + + location / { + proxy_pass http://127.0.0.1:9137; + # Include standard proxy headers from above + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +} + +server { + listen 80; + server_name $willexecutor_url; + return 301 https://$willexecutor_url; +} + +EOF +) + +printf "$nginx_reverse_proxy" | sudo tee "/etc/nginx/etc/nginx/sites-available/$willexecutor_url" > /dev/null +#sudo ln -s /etc/nginx/sites-available/$willexecutor_url /etc/nginx/sites-enabled/ +sudo systemctl restart nginx + +rm -r $tempdir +echo "done" diff --git a/generate_keys.sh b/generate_keys.sh new file mode 100644 index 0000000..ee69030 --- /dev/null +++ b/generate_keys.sh @@ -0,0 +1,8 @@ +openssl pkey -in private_key.pem -pubout -out public_key.pem +chmod 600 private_key.pem +# Ensure private key is not accidentally committed to git +if grep -q "private_key.pem" .gitignore 2>/dev/null; then + echo "private_key.pem is already protected by .gitignore" +else + echo "WARNING: private_key.pem may not be in .gitignore!" +fi diff --git a/make_release.sh b/make_release.sh new file mode 100644 index 0000000..3cdf0c6 --- /dev/null +++ b/make_release.sh @@ -0,0 +1,154 @@ +#!/bin/bash +#author:Svātantrya + +source lib.sh +usage() { + echo_w "./make_release " +} + +if [ -n "$1" ]; then release=$1; else usage; exit; fi + +if [ -n "$2" ]; then message=$2; else + # Create temporary file using mktemp + TEMPFILE=$(mktemp) + vi $TEMPFILE + message=$(cat $TEMPFILE) + rm $TEMPFILE +fi + +echo_i $message + +# Load secrets from .env file (not committed to git) +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +TOKEN="${GITEA_API_TOKEN}" +if [ -z "$TOKEN" ]; then + echo_e "Error: GITEA_API_TOKEN is not set in .env file." + echo_e "Please create a .env file with: GITEA_API_TOKEN=your_token_here" + exit 1 +fi +OWNER="bitcoinafterlife" +basename=$(basename $(pwd)) +REPO=$basename +TAG="v$release" +binpath="target/release/$basename" +release_name="$basename-$release" +dest="releases/$release" +arch=$(uname -m) +platform="linux-gnu" +destbin="$dest/$arch" +destsrc="$dest/src" +assetname="$release_name""_$arch""_$platform" + +asset_tar_gz="$assetname.tar.gz" +ASSET_PATH="$destbin/$assetname.tar.gz" + +giteahost="https://bitcoin-after.life/gitea" +url_releases="$giteahost/api/v1/repos/$OWNER/$REPO/releases" + +echo_i() { + echo -e "\033[1m==> $1\033[0m" +} + +echo_e() { + echo -e "\033[31;1m$1\033[0m" +} + +echo_s() { + echo -e "\033[32;1m$1\033[0m" +} +echo_w() { + echo -e "\033[33;1m$1\033[0m" +} + + +prepare_release(){ + mkdir -p "$destbin/$assetname" + if ! cargo build --release; then + echo_w "error building release" + exit 1 + fi + ls -l $binpath + cp target/release/bal-server \ + target/release/bal-pusher \ + README.md \ + "$destbin/$assetname" + ( + cd "$destbin" + echo_w $ASSET_PATH + echo "ls $(pwd)" + ls + echo "ls $(pwd)/$assetname" + ls "$(pwd)/$assetname" + ls $assetname + tar -czf "$asset_tar_gz" "$assetname" + ) +} + +push_tag() { + git commit -am"release: $release_name" + git push + #git tag -a "$TAG" -m"release: $release_name" + #git push origin --tags +} + +# Configurazioni +post_release() { + if [ -z "$1" ]; then + echo_e "no data to release" + exit 1 + else + echo "data: $1" + fi + echo "token:$TOKEN" + echo url_releases: $url_releases + RELEASE="$(curl -s -X POST \ + -H "accept: application/json" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$1" \ + $url_releases + )" + echo $RELEASE +} + +add_asset_release() { + if [ -z "$1" ]; then + echo_e "error add_asset_release" + exit 1 + fi + echo $ASSET_PATH + ls -l $ASSET_PATH + pwd + curl -X POST \ + -H "accept: application/json" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: multipart/form-data" \ + -F "attachment=@$ASSET_PATH" \ + "$url_releases/$1/assets" +} + + # Estrae l'ID della release +release_data=$(cat < c, + Err(e) => { + println!("WARNING: .gitignore not found: {}", e); + return; + } + }; + + assert!( + gitignore.contains("private_key.pem"), + ".gitignore must block private_key.pem" + ); + assert!( + gitignore.contains("privkey.pem"), + ".gitignore must block privkey.pem" + ); + assert!( + gitignore.contains("ec.key"), + ".gitignore must block ec.key" + ); + assert!( + gitignore.contains("chiave_privata.key"), + ".gitignore must block chiave_privata.key" + ); + + // Check that no private key files are tracked by git + let output = std::process::Command::new("git") + .args(&["ls-files", "*.pem", "*.key"]) + .output() + .expect("Failed to run git ls-files"); + + let tracked_keys = String::from_utf8(output.stdout).unwrap(); + let tracked_keys: Vec<&str> = tracked_keys.lines().collect(); + + // Only non-empty entries and only public_key.pem should be tracked + for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) { + if !tracked.contains("public_key.pem") { + assert!(false, + "Private key file is tracked by git: {}. Remove it with git rm --cached", + tracked + ); + } + } + + println!("PASS: No private keys tracked in git (only public_key.pem allowed)"); +} + +#[test] +fn test_no_token_in_source_files() { + // Scan source files for hardcoded tokens + let mut found_issues = Vec::new(); + + // Scan .sh files for hardcoded 40-char hex strings + for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) { + let path = entry.path(); + if !path.is_file() { + continue; + } + if let Some(ext) = path.extension() { + if ext == "sh" { + let content = fs::read_to_string(&path).unwrap(); + for (line_num, line) in content.lines().enumerate() { + // Skip comments and example/template files + if line.trim().starts_with("#") || line.to_lowercase().contains("example") || line.to_lowercase().contains("template") { + continue; + } + // Check for 40-64 hex chars that could be API tokens (not in .env.example comments) + if line.trim().len() >= 40 { + let hex_chars = line.trim().chars().filter(|c| c.is_ascii_hexdigit()).collect::>(); + if hex_chars.len() >= 40 && hex_chars.len() <= 64 { + // Check if it looks like it's part of a TOKEN assignment + if line.to_lowercase().contains("token") || line.to_lowercase().contains("api") || line.to_lowercase().contains("secret") { + found_issues.push(format!( + "Potential hardcoded token in {}: line {}: {}", + path.display(), line_num + 1, line.trim() + )); + } + } + } + } + } + } + } + + if !found_issues.is_empty() { + println!("FAIL: Found potential hardcoded tokens:"); + for issue in &found_issues { + println!(" {}", issue); + } + assert!(false, "Found potential hardcoded tokens in shell scripts: {:?}", found_issues); + + } + + + println!("PASS: No hardcoded tokens found in shell scripts"); +}