From 7badd037e45be3bc2c61de727bbd220f7eed2c54 Mon Sep 17 00:00:00 2001 From: Z3RO-O Date: Fri, 24 Oct 2025 12:37:41 +0530 Subject: [PATCH] feat: Support GPG signing key configuration --- README.md | 132 +++++++++++++++++++++++++++++++++++++++ bin/gh-switch-standalone | 114 ++++++++++++++++++++++++++++++++- lib/cmd_add.sh | 28 ++++++++- lib/cmd_current.sh | 20 ++++++ lib/cmd_list.sh | 11 ++++ lib/cmd_use.sh | 1 + lib/git_config.sh | 28 +++++++++ lib/gpg_utils.sh | 117 ++++++++++++++++++++++++++++++++++ lib/profile.sh | 11 +++- 9 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 lib/gpg_utils.sh diff --git a/README.md b/README.md index 41fe8a5..6322427 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A fast, reliable command-line tool for managing multiple GitHub accounts. Switch - **Instant Switching**: Change GitHub accounts with a single command - **SSH Key Management**: Automatically configures SSH hosts for each profile - **Git Config Integration**: Updates user.name and user.email per repository +- **GPG Signing Support**: Configure GPG signing keys per profile for verified commits - **Profile Storage**: Securely stores multiple account profiles - **Auto-Detection**: Automatically detects and switches profiles based on repository - **Remote URL Updates**: Automatically updates git remote URLs when switching @@ -85,6 +86,9 @@ gh-switch add personal # - Git name: John Doe # - Git email: john@personal.com # - GitHub username: johndoe +# - Configure GPG signing? (optional) +# - GPG key ID: (if GPG is configured) +# - Enable automatic GPG signing? (if GPG is configured) ``` #### Listing Profiles @@ -99,6 +103,8 @@ gh-switch list # * personal (active) # User: John Doe # Email: john@personal.com +# GPG Key: ABCD1234EFGH5678 +# GPG Signing: Enabled # # work # User: John Smith @@ -221,6 +227,8 @@ Each profile stores: - Git email address - GitHub username - SSH host alias +- GPG signing key ID (optional) +- GPG auto-signing preference (optional) ## ๐ŸŽจ Command Reference @@ -269,6 +277,7 @@ ghclone() { - **OpenSSH**: Standard SSH client - **Bash**: Version 4.0+ (macOS/Linux) - **GitHub Account**: With SSH keys configured +- **GPG** (optional): For commit signing (gpg or gpg2) ### Setting Up SSH Keys @@ -288,6 +297,129 @@ ssh-add ~/.ssh/id_ed25519_work Then add each public key to the corresponding GitHub account's settings. +### Setting Up GPG Keys for Signed Commits + +GitHub supports GPG-signed commits to verify your identity. gh-switch can manage GPG keys per profile. + +#### Generating GPG Keys + +```bash +# Generate a GPG key for personal account +gpg --full-generate-key +# Select: (1) RSA and RSA or (9) ECC and ECC +# Key size: 4096 (for RSA) or use default (for ECC) +# Expiration: Choose based on your security policy +# Real name: Your Name +# Email: personal@email.com + +# Generate a GPG key for work account +gpg --full-generate-key +# Use your work email: work@company.com +``` + +#### List Your GPG Keys + +```bash +# List all GPG keys with their IDs +gpg --list-secret-keys --keyid-format=long + +# Output example: +# sec rsa4096/ABCD1234EFGH5678 2024-01-01 [SC] +# uid [ultimate] Your Name +``` + +The key ID is the part after the `/` (e.g., `ABCD1234EFGH5678`). + +#### Add GPG Key to GitHub + +```bash +# Export your public GPG key +gpg --armor --export ABCD1234EFGH5678 + +# Copy the output (including BEGIN and END lines) +# Then add it to GitHub: +# 1. Go to GitHub Settings โ†’ SSH and GPG keys +# 2. Click "New GPG key" +# 3. Paste your public key +``` + +#### Configure GPG in gh-switch + +When adding a new profile, gh-switch will prompt you to configure GPG signing: + +```bash +gh-switch add personal + +# You'll be prompted: +# - SSH key path +# - Git name +# - Git email +# - GitHub username +# - Configure GPG signing? (y/N) +# - GPG key ID: ABCD1234EFGH5678 +# - Enable GPG signing for all commits? (Y/n) +``` + +For existing profiles, you can delete and re-add them with GPG support: + +```bash +gh-switch delete personal +gh-switch add personal +``` + +#### Verify GPG Signing is Working + +```bash +# Switch to your profile +gh-switch use personal + +# Check current configuration +gh-switch current +# Should show: +# GPG key: ABCD1234EFGH5678 +# GPG signing: Enabled + +# Make a commit +git commit -m "Test signed commit" + +# Verify the commit is signed +git log --show-signature -1 +# Should show "Good signature from ..." +``` + +#### Manual GPG Configuration + +If you prefer to manually sign specific commits: + +1. When configuring GPG in gh-switch, answer "n" to "Enable GPG signing for all commits?" +2. This sets up the signing key but doesn't auto-sign +3. Sign individual commits with: `git commit -S -m "Your message"` + +#### GPG Troubleshooting + +**Issue: "gpg: signing failed: No secret key"** +```bash +# Ensure the key is in your keyring +gpg --list-secret-keys + +# If missing, import it +gpg --import your-private-key.asc +``` + +**Issue: "gpg: signing failed: Inappropriate ioctl for device"** +```bash +# Add to ~/.bashrc or ~/.zshrc +export GPG_TTY=$(tty) + +# Or use gpg-agent +echo 'use-agent' >> ~/.gnupg/gpg.conf +``` + +**Issue: Commits not showing as verified on GitHub** +- Ensure the GPG key email matches the commit email +- Check that the public key is added to your GitHub account +- The key must not be expired or revoked + ## ๐Ÿงช Typical Workflows ### Daily Development diff --git a/bin/gh-switch-standalone b/bin/gh-switch-standalone index 9c1087a..d44c839 100755 --- a/bin/gh-switch-standalone +++ b/bin/gh-switch-standalone @@ -30,12 +30,15 @@ ensure_config_dir() { # === Profile Functions === create_profile() { local name="$1" ssh_key="$2" git_name="$3" git_email="$4" github_user="$5" + local gpg_key_id="${6:-}" gpg_sign_commits="${7:-false}" cat > "${PROFILES_DIR}/${name}" </dev/null; then + git config --${scope} --unset user.signingkey 2>/dev/null || true + git config --${scope} --unset commit.gpgsign 2>/dev/null || true + elif [[ "$scope" == "global" ]]; then + git config --${scope} --unset user.signingkey 2>/dev/null || true + git config --${scope} --unset commit.gpgsign 2>/dev/null || true + fi + fi +} + update_remote_url() { local profile="$1" local current_url=$(git remote get-url origin 2>/dev/null) @@ -113,6 +144,51 @@ update_remote_url() { fi } +# === GPG Functions === +check_gpg_installed() { + command -v gpg &>/dev/null || command -v gpg2 &>/dev/null +} + +get_gpg_cmd() { + command -v gpg2 &>/dev/null && echo "gpg2" || command -v gpg &>/dev/null && echo "gpg" || echo "" +} + +list_gpg_keys() { + local gpg_cmd=$(get_gpg_cmd) + [[ -z "$gpg_cmd" ]] && log_error "GPG is not installed" && return 1 + echo "Available GPG keys:" + echo "===================" + $gpg_cmd --list-secret-keys --keyid-format LONG 2>/dev/null | grep -E "^(sec|uid)" | sed 's/^/ /' + echo "" + echo "Hint: Use the key ID after 'sec' (e.g., 'rsa4096/ABCD1234EFGH5678')" + echo " Extract just the ID part: ABCD1234EFGH5678" +} + +validate_gpg_key() { + local key_id="$1" gpg_cmd=$(get_gpg_cmd) + [[ -z "$gpg_cmd" ]] && log_error "GPG is not installed" && return 1 + [[ -z "$key_id" ]] && return 0 + $gpg_cmd --list-secret-keys "$key_id" &>/dev/null || (log_error "GPG key '$key_id' not found in keyring" && return 1) +} + +select_gpg_key_interactive() { + local gpg_cmd=$(get_gpg_cmd) + [[ -z "$gpg_cmd" ]] && log_info "GPG is not installed. Skipping GPG key setup." && echo "" && return 0 + ! $gpg_cmd --list-secret-keys &>/dev/null && log_info "No GPG keys found in keyring. Skipping GPG key setup." && echo "" && return 0 + echo "" + list_gpg_keys + local gpg_key="" + read -p "GPG key ID (optional, press Enter to skip): " gpg_key + if [[ -n "$gpg_key" ]]; then + validate_gpg_key "$gpg_key" && echo "$gpg_key" && return 0 + log_error "Invalid GPG key. Profile will be created without GPG signing." + echo "" + return 1 + fi + echo "" + return 0 +} + # === Shell Detection Functions === detect_shell() { # Check parent process @@ -320,10 +396,26 @@ cmd_add() { read -p "Git email: " git_email read -p "GitHub username: " github_user - create_profile "$name" "$ssh_key" "$git_name" "$git_email" "$github_user" + local gpg_key="" gpg_sign="false" + if check_gpg_installed; then + echo "" + log_info "GPG Signing Configuration (Optional)" + echo "Would you like to configure GPG commit signing for this profile?" + read -p "Configure GPG signing? (y/N): " configure_gpg + if [[ "$configure_gpg" =~ ^[Yy]$ ]]; then + gpg_key=$(select_gpg_key_interactive) + if [[ -n "$gpg_key" ]]; then + read -p "Enable GPG signing for all commits? (Y/n): " enable_signing + [[ ! "$enable_signing" =~ ^[Nn]$ ]] && gpg_sign="true" + fi + fi + fi + + create_profile "$name" "$ssh_key" "$git_name" "$git_email" "$github_user" "$gpg_key" "$gpg_sign" add_ssh_host "$name" "$ssh_key" log_success "Profile ${name} added successfully!" + [[ -n "$gpg_key" ]] && log_info "GPG signing configured with key: ${gpg_key}" echo "Use 'gh-switch use ${name}' to activate" } @@ -335,6 +427,7 @@ cmd_use() { load_profile "$profile_name" || (log_error "Profile '${profile_name}' not found" && exit 1) apply_git_profile "${GIT_NAME}" "${GIT_EMAIL}" "${scope}" + apply_git_gpg_config "${GPG_KEY_ID}" "${GPG_SIGN_COMMITS}" "${scope}" echo "${profile_name}" > "${GH_SWITCH_DIR}/current" git rev-parse --git-dir &>/dev/null && update_remote_url "${profile_name}" @@ -351,11 +444,20 @@ cmd_current() { echo " Git user: ${GIT_NAME}" echo " Git email: ${GIT_EMAIL}" echo " GitHub user: ${GITHUB_USER}" + + if [[ -n "${GPG_KEY_ID:-}" ]]; then + echo " GPG key: ${GPG_KEY_ID}" + [[ "${GPG_SIGN_COMMITS:-false}" == "true" ]] && echo " GPG signing: Enabled" || echo " GPG signing: Manual" + fi if git rev-parse --git-dir &>/dev/null; then echo -e "\nRepository config:" echo " user.name: $(get_git_config user.name)" echo " user.email: $(get_git_config user.email)" + local signing_key=$(get_git_config user.signingkey) + local gpg_sign=$(get_git_config commit.gpgsign) + [[ -n "$signing_key" ]] && echo " user.signingkey: ${signing_key}" + [[ -n "$gpg_sign" ]] && echo " commit.gpgsign: ${gpg_sign}" fi else log_error "Current profile '${profile_name}' not found" @@ -374,7 +476,13 @@ cmd_list() { source "$profile_file" [[ "$profile_name" == "$current_profile" ]] && echo -e "${GREEN}* ${profile_name}${NC} (active)" || echo " ${profile_name}" - echo -e " User: ${GIT_NAME}\n Email: ${GIT_EMAIL}\n" + echo " User: ${GIT_NAME}" + echo " Email: ${GIT_EMAIL}" + if [[ -n "${GPG_KEY_ID:-}" ]]; then + echo " GPG Key: ${GPG_KEY_ID}" + [[ "${GPG_SIGN_COMMITS:-false}" == "true" ]] && echo " GPG Signing: Enabled" || echo " GPG Signing: Manual" + fi + echo "" fi done } diff --git a/lib/cmd_add.sh b/lib/cmd_add.sh index f99eaaa..4c6c8a2 100644 --- a/lib/cmd_add.sh +++ b/lib/cmd_add.sh @@ -3,6 +3,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" source "$(dirname "${BASH_SOURCE[0]}")/profile.sh" source "$(dirname "${BASH_SOURCE[0]}")/ssh_writer.sh" +source "$(dirname "${BASH_SOURCE[0]}")/gpg_utils.sh" validate_ssh_key() { local key_path="$1" @@ -41,9 +42,34 @@ cmd_add() { read -p "Git email: " git_email read -p "GitHub username: " github_user - create_profile "$name" "$ssh_key" "$git_name" "$git_email" "$github_user" + # GPG key setup (optional) + local gpg_key="" + local gpg_sign="false" + + if check_gpg_installed; then + echo "" + log_info "GPG Signing Configuration (Optional)" + echo "Would you like to configure GPG commit signing for this profile?" + read -p "Configure GPG signing? (y/N): " configure_gpg + + if [[ "$configure_gpg" =~ ^[Yy]$ ]]; then + gpg_key=$(select_gpg_key_interactive) + + if [[ -n "$gpg_key" ]]; then + read -p "Enable GPG signing for all commits? (Y/n): " enable_signing + if [[ ! "$enable_signing" =~ ^[Nn]$ ]]; then + gpg_sign="true" + fi + fi + fi + fi + + create_profile "$name" "$ssh_key" "$git_name" "$git_email" "$github_user" "$gpg_key" "$gpg_sign" add_ssh_host "$name" "$ssh_key" log_success "Profile ${name} added successfully!" + if [[ -n "$gpg_key" ]]; then + log_info "GPG signing configured with key: ${gpg_key}" + fi echo "Use 'gh-switch use ${name}' to activate" } \ No newline at end of file diff --git a/lib/cmd_current.sh b/lib/cmd_current.sh index 790c350..cd5209b 100644 --- a/lib/cmd_current.sh +++ b/lib/cmd_current.sh @@ -18,12 +18,32 @@ cmd_current() { echo " Git user: ${GIT_NAME}" echo " Git email: ${GIT_EMAIL}" echo " GitHub user: ${GITHUB_USER}" + + # Display GPG info if available + if [[ -n "${GPG_KEY_ID:-}" ]]; then + echo " GPG key: ${GPG_KEY_ID}" + if [[ "${GPG_SIGN_COMMITS:-false}" == "true" ]]; then + echo " GPG signing: Enabled" + else + echo " GPG signing: Manual" + fi + fi if git rev-parse --git-dir &>/dev/null; then echo "" echo "Repository config:" echo " user.name: $(get_git_config user.name)" echo " user.email: $(get_git_config user.email)" + + # Display GPG config if set + local signing_key=$(get_git_config user.signingkey) + local gpg_sign=$(get_git_config commit.gpgsign) + if [[ -n "$signing_key" ]]; then + echo " user.signingkey: ${signing_key}" + fi + if [[ -n "$gpg_sign" ]]; then + echo " commit.gpgsign: ${gpg_sign}" + fi fi else log_error "Current profile '${profile_name}' not found" diff --git a/lib/cmd_list.sh b/lib/cmd_list.sh index 0f2d30c..5042ab0 100644 --- a/lib/cmd_list.sh +++ b/lib/cmd_list.sh @@ -26,6 +26,17 @@ cmd_list() { fi echo " User: ${GIT_NAME}" echo " Email: ${GIT_EMAIL}" + + # Display GPG info if available + if [[ -n "${GPG_KEY_ID:-}" ]]; then + echo " GPG Key: ${GPG_KEY_ID}" + if [[ "${GPG_SIGN_COMMITS:-false}" == "true" ]]; then + echo " GPG Signing: Enabled" + else + echo " GPG Signing: Manual" + fi + fi + echo "" fi done diff --git a/lib/cmd_use.sh b/lib/cmd_use.sh index e80e1b4..c558a38 100644 --- a/lib/cmd_use.sh +++ b/lib/cmd_use.sh @@ -23,6 +23,7 @@ cmd_use() { fi apply_git_profile "${GIT_NAME}" "${GIT_EMAIL}" "${scope}" + apply_git_gpg_config "${GPG_KEY_ID}" "${GPG_SIGN_COMMITS}" "${scope}" echo "${profile_name}" > "${GH_SWITCH_DIR}/current" diff --git a/lib/git_config.sh b/lib/git_config.sh index bef61f1..fdd36e4 100644 --- a/lib/git_config.sh +++ b/lib/git_config.sh @@ -36,4 +36,32 @@ apply_git_profile() { set_git_config "user.name" "${name}" "${scope}" set_git_config "user.email" "${email}" "${scope}" +} + +# Apply GPG configuration to git +apply_git_gpg_config() { + local gpg_key_id="$1" + local gpg_sign_commits="$2" + local scope="${3:-local}" + + if [[ -n "$gpg_key_id" ]]; then + set_git_config "user.signingkey" "${gpg_key_id}" "${scope}" + log_info "Set GPG signing key: ${gpg_key_id}" + + if [[ "$gpg_sign_commits" == "true" ]]; then + set_git_config "commit.gpgsign" "true" "${scope}" + log_info "Enabled automatic GPG commit signing" + else + set_git_config "commit.gpgsign" "false" "${scope}" + fi + else + # Unset GPG config if no key is provided (for profiles without GPG) + if [[ "$scope" == "local" ]] && git rev-parse --git-dir &>/dev/null; then + git config --${scope} --unset user.signingkey 2>/dev/null || true + git config --${scope} --unset commit.gpgsign 2>/dev/null || true + elif [[ "$scope" == "global" ]]; then + git config --${scope} --unset user.signingkey 2>/dev/null || true + git config --${scope} --unset commit.gpgsign 2>/dev/null || true + fi + fi } \ No newline at end of file diff --git a/lib/gpg_utils.sh b/lib/gpg_utils.sh new file mode 100644 index 0000000..e523102 --- /dev/null +++ b/lib/gpg_utils.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +# Check if GPG is installed +check_gpg_installed() { + if ! command -v gpg &>/dev/null && ! command -v gpg2 &>/dev/null; then + return 1 + fi + return 0 +} + +# Get the GPG command (gpg or gpg2) +get_gpg_cmd() { + if command -v gpg2 &>/dev/null; then + echo "gpg2" + elif command -v gpg &>/dev/null; then + echo "gpg" + else + echo "" + fi +} + +# List all available GPG keys +list_gpg_keys() { + local gpg_cmd=$(get_gpg_cmd) + + if [[ -z "$gpg_cmd" ]]; then + log_error "GPG is not installed" + return 1 + fi + + echo "Available GPG keys:" + echo "===================" + $gpg_cmd --list-secret-keys --keyid-format LONG 2>/dev/null | \ + grep -E "^(sec|uid)" | \ + sed 's/^/ /' + echo "" + echo "Hint: Use the key ID after 'sec' (e.g., 'rsa4096/ABCD1234EFGH5678')" + echo " Extract just the ID part: ABCD1234EFGH5678" +} + +# Validate if a GPG key exists +validate_gpg_key() { + local key_id="$1" + local gpg_cmd=$(get_gpg_cmd) + + if [[ -z "$gpg_cmd" ]]; then + log_error "GPG is not installed" + return 1 + fi + + if [[ -z "$key_id" ]]; then + return 0 # Empty key is valid (optional) + fi + + # Check if the key exists in the keyring + if $gpg_cmd --list-secret-keys "$key_id" &>/dev/null; then + return 0 + else + log_error "GPG key '$key_id' not found in keyring" + return 1 + fi +} + +# Get the email associated with a GPG key +get_gpg_key_email() { + local key_id="$1" + local gpg_cmd=$(get_gpg_cmd) + + if [[ -z "$gpg_cmd" ]] || [[ -z "$key_id" ]]; then + return 1 + fi + + $gpg_cmd --list-keys "$key_id" 2>/dev/null | \ + grep -oP '(?<=<)[^>]+(?=>)' | \ + head -n 1 +} + +# Interactive GPG key selection +select_gpg_key_interactive() { + local gpg_cmd=$(get_gpg_cmd) + + if [[ -z "$gpg_cmd" ]]; then + log_info "GPG is not installed. Skipping GPG key setup." + echo "" + return 0 + fi + + # Check if there are any secret keys + if ! $gpg_cmd --list-secret-keys &>/dev/null; then + log_info "No GPG keys found in keyring. Skipping GPG key setup." + echo "" + return 0 + fi + + echo "" + list_gpg_keys + + local gpg_key="" + read -p "GPG key ID (optional, press Enter to skip): " gpg_key + + if [[ -n "$gpg_key" ]]; then + if validate_gpg_key "$gpg_key"; then + echo "$gpg_key" + return 0 + else + log_error "Invalid GPG key. Profile will be created without GPG signing." + echo "" + return 1 + fi + fi + + echo "" + return 0 +} + diff --git a/lib/profile.sh b/lib/profile.sh index e3a2dcc..372b59b 100644 --- a/lib/profile.sh +++ b/lib/profile.sh @@ -3,7 +3,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" # Profile structure: -# name|ssh_key|git_name|git_email|github_user +# name|ssh_key|git_name|git_email|github_user|gpg_key_id|gpg_sign_commits # Create a new profile file create_profile() { @@ -12,6 +12,8 @@ create_profile() { local git_name="$3" local git_email="$4" local github_user="$5" + local gpg_key_id="${6:-}" + local gpg_sign_commits="${7:-false}" local profile_file="${PROFILES_DIR}/${name}" @@ -21,6 +23,8 @@ GIT_NAME="${git_name}" GIT_EMAIL="${git_email}" GITHUB_USER="${github_user}" HOST_ALIAS="github.com-${name}" +GPG_KEY_ID="${gpg_key_id}" +GPG_SIGN_COMMITS="${gpg_sign_commits}" EOF log_success "Profile '${name}' created" @@ -33,6 +37,11 @@ load_profile() { if [[ -f "${profile_file}" ]]; then source "${profile_file}" + + # Set defaults for backward compatibility + GPG_KEY_ID="${GPG_KEY_ID:-}" + GPG_SIGN_COMMITS="${GPG_SIGN_COMMITS:-false}" + return 0 fi return 1