penguin/utils

my env utils

commit 8fbcb5cdc6f8e890328c2753cc0c65e0d6dfffb5

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-28T02:16:13Z
subjectSign rbashrc
commit 8fbcb5cdc6f8e890328c2753cc0c65e0d6dfffb5
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-28T02:16:13Z

    Sign rbashrc
---
 bash/core/00_env.bash           |   2 +
 bash/core/50_upgrade.bash       | 204 ++++++++++++++++++++++++++++------------
 bash/dist/rbash-signing-key.asc |  11 +++
 bash/dist/rbashrc               | 180 ++++++++++++++++++++++++++---------
 bash/dist/rbashrc.asc           |   8 ++
 bash/tools/sign-rbashrc.sh      | 144 ++++++++++++++++++++++++++++
 6 files changed, 446 insertions(+), 103 deletions(-)

diff --git a/bash/core/00_env.bash b/bash/core/00_env.bash
index 0ab2e86..ef57cc6 100755
--- a/bash/core/00_env.bash
+++ b/bash/core/00_env.bash
@@ -9,4 +9,6 @@ if [ "$RHOSTNAME" = "<HOSTNAME>" ]; then
 	RCOLOR=$(( RANDOM * 6 / 32767 + 1 ))
 fi
 
+RBASH_SIGNING_FPR="7C1E948D841254C569EDFBDED5CF5FB1CFAD27C3"
+
 _RBASH_LOADED=
diff --git a/bash/core/50_upgrade.bash b/bash/core/50_upgrade.bash
index 5b7a52f..4401076 100755
--- a/bash/core/50_upgrade.bash
+++ b/bash/core/50_upgrade.bash
@@ -1,60 +1,148 @@
+rbash_verify_upgrade() {
+	local new_rbashrc="$1"
+	local sig_file="$2"
+	local key_file="$3"
+
+	local gnupg_home key_fpr rc
+
+	command -v gpg >/dev/null 2>&1 || {
+		echo "gpg is required for rbash upgrade verification"
+		return 1
+	}
+
+	[ -n "${RBASH_SIGNING_FPR:-}" ] || {
+		echo "RBASH_SIGNING_FPR is not set"
+		echo "Refusing to upgrade without a pinned signing key fingerprint"
+		return 1
+	}
+
+	[ -f "$new_rbashrc" ] || {
+		echo "missing rbashrc: $new_rbashrc"
+		return 1
+	}
+
+	[ -f "$sig_file" ] || {
+		echo "missing signature: $sig_file"
+		return 1
+	}
+
+	[ -f "$key_file" ] || {
+		echo "missing public key: $key_file"
+		return 1
+	}
+
+	gnupg_home="$(mktemp -d)"
+	chmod 700 "$gnupg_home"
+
+	GNUPGHOME="$gnupg_home" gpg --quiet --batch --import "$key_file" >/dev/null 2>&1 || {
+		rc=$?
+		rm -rf "$gnupg_home"
+		echo "failed to import rbash signing key"
+		return "$rc"
+	}
+
+	key_fpr="$(
+		GNUPGHOME="$gnupg_home" gpg --with-colons --fingerprint 2>/dev/null |
+			awk -F: '$1 == "fpr" { print $10; exit }'
+	)"
+
+	if [ "$key_fpr" != "$RBASH_SIGNING_FPR" ]; then
+		rm -rf "$gnupg_home"
+
+		echo "rbash signing key fingerprint mismatch"
+		echo "expected: $RBASH_SIGNING_FPR"
+		echo "got:      ${key_fpr:-<none>}"
+		return 1
+	fi
+
+	GNUPGHOME="$gnupg_home" gpg \
+		--quiet \
+		--batch \
+		--verify "$sig_file" "$new_rbashrc" >/dev/null 2>&1
+
+	rc=$?
+	rm -rf "$gnupg_home"
+
+	if [ "$rc" -ne 0 ]; then
+		echo "rbash signature verification failed"
+		return "$rc"
+	fi
+
+	return 0
+}
+
 rbash-upgrade() {
-  local target url tmp backup
-
-  target="$__RBASH_SCRIPT"
-  url="$(rbash_url "dist/rbashrc")"
-
-  echo "Updating $target"
-
-  tmp="$(mktemp "${TMPDIR:-/tmp}/rbashrc.download.XXXXXX")" || return 1
-
-  if ! rbash_download "$url" > "$tmp"; then
-    echo "rbash: upgrade download failed: $url" >&2
-    rm -f "$tmp"
-    return 1
-  fi
-
-  if [ ! -s "$tmp" ]; then
-    echo "rbash: downloaded rbashrc is empty; refusing to install" >&2
-    rm -f "$tmp"
-    return 1
-  fi
-
-  if ! grep -q 'RBASH_HOME' "$tmp"; then
-    echo "rbash: downloaded file does not look like rbashrc; refusing to install" >&2
-    rm -f "$tmp"
-    return 1
-  fi
-
-  if ! bash -n "$tmp"; then
-    echo "rbash: downloaded rbashrc has syntax errors; refusing to install" >&2
-    rm -f "$tmp"
-    return 1
-  fi
-
-  backup="$target.bak.$(date +%Y%m%d%H%M%S)"
-
-  if [ -f "$target" ]; then
-    cp "$target" "$backup" || {
-      echo "rbash: failed to create backup: $backup" >&2
-      rm -f "$tmp"
-      return 1
-    }
-    echo "Backup: $backup"
-  fi
-
-  chmod 700 "$tmp"
-
-  if ! mv "$tmp" "$target"; then
-    echo "rbash: failed to replace $target" >&2
-    echo "rbash: backup remains at $backup" >&2
-    rm -f "$tmp"
-    return 1
-  fi
-
-  rm -rf "$RBASH_CACHE_DIR"
-  mkdir -p "$RBASH_CACHE_DIR"
-
-  # shellcheck source=/dev/null
-  source "$target"
+	local target backup backup_suffix
+	local tmp_dir tmp_rbashrc tmp_sig tmp_key
+	local base_url rbashrc_url sig_url key_url
+	local rc
+
+	target="${__RBASH_SCRIPT:-$HOME/.bashrc}"
+
+	base_url="$(rbash_url "dist")"
+
+	rbashrc_url="$base_url/rbashrc"
+	sig_url="$base_url/rbashrc.asc"
+	key_url="$base_url/rbash-signing-key.asc"
+
+	command -v gpg >/dev/null 2>&1 || {
+		echo "gpg is required"
+		return 1
+	}
+
+	tmp_dir="$(mktemp -d)"
+	tmp_rbashrc="$tmp_dir/rbashrc"
+	tmp_sig="$tmp_dir/rbashrc.asc"
+	tmp_key="$tmp_dir/rbash-signing-key.asc"
+
+	__rbash_upgrade_cleanup() {
+		rm -rf "$tmp_dir"
+	}
+
+	trap __rbash_upgrade_cleanup RETURN
+
+	echo "Downloading rbashrc..."
+	rbash_download "$rbashrc_url" > "$tmp_rbashrc" || return 1
+
+	echo "Downloading rbashrc signature..."
+	rbash_download "$sig_url" > "$tmp_sig" || return 1
+
+	echo "Downloading rbash signing public key..."
+	rbash_download "$key_url" > "$tmp_key" || return 1
+
+	echo "Verifying rbashrc signature..."
+	rbash_verify_upgrade "$tmp_rbashrc" "$tmp_sig" "$tmp_key" || {
+		echo "rbash upgrade aborted"
+		return 1
+	}
+
+	backup_suffix="$(date +%Y%m%d%H%M%S)"
+	backup="$target.bak.$backup_suffix"
+
+	echo "Updating $target"
+	echo "Backup: $backup"
+
+	if [ -f "$target" ]; then
+		cp "$target" "$backup" || {
+			echo "failed to create backup: $backup"
+			return 1
+		}
+	fi
+
+	cp "$tmp_rbashrc" "$target" || {
+		rc=$?
+
+		echo "failed to install new rbashrc"
+
+		if [ -f "$backup" ]; then
+			echo "restoring backup"
+			cp "$backup" "$target" || true
+		fi
+
+		return "$rc"
+	}
+
+	chmod 600 "$target" 2>/dev/null || true
+
+	echo "rbash upgrade complete"
 }
diff --git a/bash/dist/rbash-signing-key.asc b/bash/dist/rbash-signing-key.asc
new file mode 100644
index 0000000..a1ee3f9
--- /dev/null
+++ b/bash/dist/rbash-signing-key.asc
@@ -0,0 +1,11 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEakCA9RYJKwYBBAHaRw8BAQdABA3i7/ksB6wkBBNv8aDG77uCMctElvLffD2+
+mah0zeK0THJiYXNoIHJlbGVhc2Ugc2lnbmluZyAocmJhc2ggcmVsZWFzZSBzaWdu
+aW5nIGtleSkgPHBlbmd1aW5AYXN0cm9wZW5ndWluLm5ldD6ItQQTFgoAXRYhBHwe
+lI2EElTFae373tXPX7HPrSfDBQJqQID1GxSAAAAAAAQADm1hbnUyLDIuNSsxLjEy
+LDAsMwIbAwUJA8JnAAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRDVz1+x
+z60nw63rAP9IlxolGkJT8No57cWoVOtyRR3qFaGV0/6JBE+e8DvxBgEA2ejTyqYt
+Jejb/OZ3jZCN7vLPMQKfG6XMCNncjCezLAw=
+=arV7
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/bash/dist/rbashrc b/bash/dist/rbashrc
index aa40cc6..613f121 100755
--- a/bash/dist/rbashrc
+++ b/bash/dist/rbashrc
@@ -34,6 +34,8 @@ if [ "$RHOSTNAME" = "<HOSTNAME>" ]; then
 	RCOLOR=$(( RANDOM * 6 / 32767 + 1 ))
 fi
 
+RBASH_SIGNING_FPR="7C1E948D841254C569EDFBDED5CF5FB1CFAD27C3"
+
 _RBASH_LOADED=
 
 
@@ -403,65 +405,153 @@ r2dissource() {
 
 # ---- rbash: bash/core/50_upgrade.bash ----
 
+rbash_verify_upgrade() {
+	local new_rbashrc="$1"
+	local sig_file="$2"
+	local key_file="$3"
+
+	local gnupg_home key_fpr rc
+
+	command -v gpg >/dev/null 2>&1 || {
+		echo "gpg is required for rbash upgrade verification"
+		return 1
+	}
+
+	[ -n "${RBASH_SIGNING_FPR:-}" ] || {
+		echo "RBASH_SIGNING_FPR is not set"
+		echo "Refusing to upgrade without a pinned signing key fingerprint"
+		return 1
+	}
+
+	[ -f "$new_rbashrc" ] || {
+		echo "missing rbashrc: $new_rbashrc"
+		return 1
+	}
+
+	[ -f "$sig_file" ] || {
+		echo "missing signature: $sig_file"
+		return 1
+	}
+
+	[ -f "$key_file" ] || {
+		echo "missing public key: $key_file"
+		return 1
+	}
+
+	gnupg_home="$(mktemp -d)"
+	chmod 700 "$gnupg_home"
+
+	GNUPGHOME="$gnupg_home" gpg --quiet --batch --import "$key_file" >/dev/null 2>&1 || {
+		rc=$?
+		rm -rf "$gnupg_home"
+		echo "failed to import rbash signing key"
+		return "$rc"
+	}
+
+	key_fpr="$(
+		GNUPGHOME="$gnupg_home" gpg --with-colons --fingerprint 2>/dev/null |
+			awk -F: '$1 == "fpr" { print $10; exit }'
+	)"
+
+	if [ "$key_fpr" != "$RBASH_SIGNING_FPR" ]; then
+		rm -rf "$gnupg_home"
+
+		echo "rbash signing key fingerprint mismatch"
+		echo "expected: $RBASH_SIGNING_FPR"
+		echo "got:      ${key_fpr:-<none>}"
+		return 1
+	fi
+
+	GNUPGHOME="$gnupg_home" gpg \
+		--quiet \
+		--batch \
+		--verify "$sig_file" "$new_rbashrc" >/dev/null 2>&1
+
+	rc=$?
+	rm -rf "$gnupg_home"
+
+	if [ "$rc" -ne 0 ]; then
+		echo "rbash signature verification failed"
+		return "$rc"
+	fi
+
+	return 0
+}
+
 rbash-upgrade() {
-  local target url tmp backup
+	local target backup backup_suffix
+	local tmp_dir tmp_rbashrc tmp_sig tmp_key
+	local base_url rbashrc_url sig_url key_url
+	local rc
 
-  target="$__RBASH_SCRIPT"
-  url="$(rbash_url "dist/rbashrc")"
+	target="${__RBASH_SCRIPT:-$HOME/.bashrc}"
 
-  echo "Updating $target"
+	base_url="$(rbash_url "dist")"
 
-  tmp="$(mktemp "${TMPDIR:-/tmp}/rbashrc.download.XXXXXX")" || return 1
+	rbashrc_url="$base_url/rbashrc"
+	sig_url="$base_url/rbashrc.asc"
+	key_url="$base_url/rbash-signing-key.asc"
 
-  if ! rbash_download "$url" > "$tmp"; then
-    echo "rbash: upgrade download failed: $url" >&2
-    rm -f "$tmp"
-    return 1
-  fi
+	command -v gpg >/dev/null 2>&1 || {
+		echo "gpg is required"
+		return 1
+	}
 
-  if [ ! -s "$tmp" ]; then
-    echo "rbash: downloaded rbashrc is empty; refusing to install" >&2
-    rm -f "$tmp"
-    return 1
-  fi
+	tmp_dir="$(mktemp -d)"
+	tmp_rbashrc="$tmp_dir/rbashrc"
+	tmp_sig="$tmp_dir/rbashrc.asc"
+	tmp_key="$tmp_dir/rbash-signing-key.asc"
 
-  if ! grep -q 'RBASH_HOME' "$tmp"; then
-    echo "rbash: downloaded file does not look like rbashrc; refusing to install" >&2
-    rm -f "$tmp"
-    return 1
-  fi
+	__rbash_upgrade_cleanup() {
+		rm -rf "$tmp_dir"
+	}
 
-  if ! bash -n "$tmp"; then
-    echo "rbash: downloaded rbashrc has syntax errors; refusing to install" >&2
-    rm -f "$tmp"
-    return 1
-  fi
+	trap __rbash_upgrade_cleanup RETURN
 
-  backup="$target.bak.$(date +%Y%m%d%H%M%S)"
+	echo "Downloading rbashrc..."
+	rbash_download "$rbashrc_url" > "$tmp_rbashrc" || return 1
 
-  if [ -f "$target" ]; then
-    cp "$target" "$backup" || {
-      echo "rbash: failed to create backup: $backup" >&2
-      rm -f "$tmp"
-      return 1
-    }
-    echo "Backup: $backup"
-  fi
+	echo "Downloading rbashrc signature..."
+	rbash_download "$sig_url" > "$tmp_sig" || return 1
 
-  chmod 700 "$tmp"
+	echo "Downloading rbash signing public key..."
+	rbash_download "$key_url" > "$tmp_key" || return 1
 
-  if ! mv "$tmp" "$target"; then
-    echo "rbash: failed to replace $target" >&2
-    echo "rbash: backup remains at $backup" >&2
-    rm -f "$tmp"
-    return 1
-  fi
+	echo "Verifying rbashrc signature..."
+	rbash_verify_upgrade "$tmp_rbashrc" "$tmp_sig" "$tmp_key" || {
+		echo "rbash upgrade aborted"
+		return 1
+	}
 
-  rm -rf "$RBASH_CACHE_DIR"
-  mkdir -p "$RBASH_CACHE_DIR"
+	backup_suffix="$(date +%Y%m%d%H%M%S)"
+	backup="$target.bak.$backup_suffix"
 
-  # shellcheck source=/dev/null
-  source "$target"
+	echo "Updating $target"
+	echo "Backup: $backup"
+
+	if [ -f "$target" ]; then
+		cp "$target" "$backup" || {
+			echo "failed to create backup: $backup"
+			return 1
+		}
+	fi
+
+	cp "$tmp_rbashrc" "$target" || {
+		rc=$?
+
+		echo "failed to install new rbashrc"
+
+		if [ -f "$backup" ]; then
+			echo "restoring backup"
+			cp "$backup" "$target" || true
+		fi
+
+		return "$rc"
+	}
+
+	chmod 600 "$target" 2>/dev/null || true
+
+	echo "rbash upgrade complete"
 }
 
 
diff --git a/bash/dist/rbashrc.asc b/bash/dist/rbashrc.asc
new file mode 100644
index 0000000..0a1295a
--- /dev/null
+++ b/bash/dist/rbashrc.asc
@@ -0,0 +1,8 @@
+-----BEGIN PGP SIGNATURE-----
+
+iJEEABYKADkWIQR8HpSNhBJUxWnt+97Vz1+xz60nwwUCakCD2BsUgAAAAAAEAA5t
+YW51MiwyLjUrMS4xMiwwLDMACgkQ1c9fsc+tJ8MfWwEAx539cnhecaZdpc4pj0gQ
+ekUURDkVLf4654Ot6il0TbQA/iWaIOPgSOgDYwB8+83cjqlNrXRiq/Uj0Nu5vWZm
+V/AO
+=LRnk
+-----END PGP SIGNATURE-----
diff --git a/bash/tools/sign-rbashrc.sh b/bash/tools/sign-rbashrc.sh
new file mode 100755
index 0000000..76c6412
--- /dev/null
+++ b/bash/tools/sign-rbashrc.sh
@@ -0,0 +1,144 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "${BASH_SOURCE[0]}")/../.."
+
+DIST="bash/dist"
+TARGET="$DIST/rbashrc"
+SIG="$DIST/rbashrc.asc"
+PUBKEY="$DIST/rbash-signing-key.asc"
+
+KEY_NAME_DEFAULT="rbash release signing"
+KEY_EMAIL_DEFAULT="rbash@astropenguin.net"
+KEY_COMMENT_DEFAULT="rbash release signing key"
+
+die() {
+	echo "error: $*" >&2
+	exit 1
+}
+
+need() {
+	command -v "$1" >/dev/null 2>&1 || die "$1 is required"
+}
+
+prompt() {
+	local var_name="$1"
+	local label="$2"
+	local default_value="${3:-}"
+	local value
+
+	if [ -n "$default_value" ]; then
+		read -r -p "$label [$default_value]: " value
+		value="${value:-$default_value}"
+	else
+		read -r -p "$label: " value
+	fi
+
+	printf -v "$var_name" '%s' "$value"
+}
+
+gpg_key_exists() {
+	local email="$1"
+
+	gpg --list-secret-keys --with-colons "$email" 2>/dev/null |
+		awk -F: '$1 == "sec" { found = 1 } END { exit !found }'
+}
+
+gpg_key_fpr() {
+	local email="$1"
+
+	gpg --list-secret-keys --with-colons --fingerprint "$email" |
+		awk -F: '$1 == "fpr" { print $10; exit }'
+}
+
+create_key() {
+	local name email comment expire batch_file
+
+	echo "No rbash signing key found."
+	echo
+
+	prompt name "Name" "$KEY_NAME_DEFAULT"
+	prompt email "Email" "$KEY_EMAIL_DEFAULT"
+	prompt comment "Comment" "$KEY_COMMENT_DEFAULT"
+	prompt expire "Expire-Date, e.g. 2y, 5y, 0 for never" "5y"
+
+	[ -n "$name" ] || die "name is required"
+	[ -n "$email" ] || die "email is required"
+
+	batch_file="$(mktemp)"
+	trap 'rm -f "$batch_file"' RETURN
+
+	cat > "$batch_file" <<EOF
+%ask-passphrase
+Key-Type: eddsa
+Key-Curve: ed25519
+Key-Usage: sign
+Name-Real: $name
+Name-Email: $email
+Name-Comment: $comment
+Expire-Date: $expire
+%commit
+EOF
+
+	echo
+	echo "Creating GPG signing key..."
+	gpg --batch --generate-key "$batch_file"
+
+	echo "$email"
+}
+
+pick_or_create_key() {
+	local configured_email
+
+	configured_email="${RBASH_SIGNING_KEY_EMAIL:-$KEY_EMAIL_DEFAULT}"
+
+	if gpg_key_exists "$configured_email"; then
+		echo "$configured_email"
+		return 0
+	fi
+
+	create_key
+}
+
+main() {
+	local key_email fpr
+
+	need gpg
+	[ -f "$TARGET" ] || die "$TARGET does not exist"
+
+	key_email="$(pick_or_create_key)"
+	fpr="$(gpg_key_fpr "$key_email")"
+
+	[ -n "$fpr" ] || die "failed to resolve signing key fingerprint"
+
+	echo "Signing key:"
+	echo "  email:       $key_email"
+	echo "  fingerprint: $fpr"
+	echo
+
+	echo "Signing $TARGET -> $SIG"
+	gpg \
+		--batch \
+		--yes \
+		--armor \
+		--detach-sign \
+		--local-user "$fpr" \
+		--output "$SIG" \
+		"$TARGET"
+
+	echo "Exporting public key -> $PUBKEY"
+	gpg \
+		--batch \
+		--yes \
+		--armor \
+		--export "$fpr" > "$PUBKEY"
+
+	echo
+	echo "Done."
+	echo
+	echo "Add/pin this fingerprint in rbashrc:"
+	echo
+	echo "RBASH_SIGNING_FPR=\"$fpr\""
+}
+
+main "$@"