Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Git commit sign #9

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
19 changes: 19 additions & 0 deletions bin/keycutter
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,25 @@ keycutter-list() {
github-ssh-keys
}

keycutter-git-signing-config() {
git-signing-config "$@"
}
keycutter-git-signing-setup() {
git-signing-setup "$@"
}

keycutter-git-commit-signing-enable() {
enable-git-commit-signing "$@"
}

keycutter-git-commit-signing-disable() {
disable-git-commit-signing "$@"
}

keycutter-git-signing-help() {
git-signing-help
}

keycutter-update() {
keycutter-update-git
check_requirements
Expand Down
1 change: 1 addition & 0 deletions lib/functions
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ KEYCUTTER_ROOT="$(readlink -f "$(dirname -- "${BASH_SOURCE[0]:-${0:A}}")/../")"
[[ -z $SSH_CONNECTION ]] && : ${KEYCUTTER_ORIGIN:="$(hostname -s)"}

source "${KEYCUTTER_ROOT}/lib/github"
source "${KEYCUTTER_ROOT}/lib/git"
source "${KEYCUTTER_ROOT}/lib/ssh"
source "${KEYCUTTER_ROOT}/lib/utils"
284 changes: 284 additions & 0 deletions lib/git
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
# Define colors globally
color_reset=$(tput sgr0)
color_local=$(tput setaf 2) # Green for local
color_global=$(tput setaf 4) # Blue for global

# Function to format the scope with appropriate color
format_scope() {
local scope="$1"
if [[ "$scope" == "local" ]]; then
echo "${color_local}${scope}${color_reset}"
elif [[ "$scope" == "global" ]]; then
echo "${color_global}${scope}${color_reset}"
else
echo "$scope" # Default to no color if not local or global
fi
}

# Git functions

# Function to set Git config
git-config-set() {
local scope="$1"
local key="$2"
local value="$3"
local config_flag=""
[[ "$scope" == "global" ]] && config_flag="--global"

git config $config_flag "$key" "$value"
log "Set $key to $value ($(format_scope $scope) config)"
}

# Function to get Git config
git-config-get() {
local scope="$1"
local key="$2"
local config_flag=""
[[ "$scope" == "global" ]] && config_flag="--global"

local value
value=$(git config $config_flag --get "$key")
if [ -n "$value" ]; then
echo "$value"
else
echo "Not set ($(format_scope $scope) config)"
fi
}

# Function to check if a command exists
command-exists() {
command -v "$1" >/dev/null 2>&1
}

# Check Git version
git-version-check() {
local git_version
git_version=$(git --version | awk '{print $3}')
if [ "$(printf '%s\n' "2.34" "$git_version" | sort -V | head -n1)" != "2.34" ]; then
echo "Error: Git version 2.34 or higher is required for SSH signing."
return 1
fi
return 0
}

# Function to set up Git SSH signing config
git-signing-setup() {
local scope=""
local ssh_key=""

# Function to display help
show_help() {
echo "Usage: git-ssh-signing-setup [OPTIONS] [SSH_KEY_PATH]"
echo
echo "Set up Git SSH signing configuration."
echo
echo "Options:"
echo " --global Set the configuration globally"
echo " --local Set the configuration locally (default if not specified)"
echo " help Display this help message"
echo
echo "If SSH_KEY_PATH is not provided, you'll be prompted to select a key from \$KEYCUTTER_SSH_KEY_DIR"
}

# Function to select SSH key
select_ssh_key() {
if command -v fzf &> /dev/null; then
ssh_key=$(find "$KEYCUTTER_SSH_KEY_DIR" -type f -not -name '*.pub' | fzf --prompt="Select SSH key: " --preview="ssh-keygen -lf {}")
else
echo "Select an SSH key:"
local keys=()
while IFS= read -r -d $'\0' file; do
keys+=("$file")
done < <(find "$KEYCUTTER_SSH_KEY_DIR" -type f -not -name '*.pub' -print0)

select key in "${keys[@]}"; do
if [ -n "$key" ]; then
ssh_key="$key"
break
else
echo "Invalid selection. Please try again."
fi
done
fi

if [ -z "$ssh_key" ]; then
log "No SSH key selected. Exiting."
return 1
fi
}

# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--global)
scope="global"
shift
;;
--local)
scope="local"
shift
;;
help)
show_help
return 0
;;
*)
# Assume it's the SSH key path
ssh_key="$1"
shift
;;
esac
done

# If no scope was explicitly set, default to local
if [ -z "$scope" ]; then
log "No scope specified. Defaulting to 'local' config."
scope="local"
fi

# If no SSH key was provided, use select_ssh_key function to choose one
if [ -z "$ssh_key" ]; then
select_ssh_key || return 1
fi

# Display proposed changes and ask for confirmation
log "Proposed Git SSH signing configuration:"
log " Scope: $(format_scope $scope)"
log " SSH Key: $ssh_key"
log " gpg.ssh.program: ~/.ssh/keycutter/scripts/git-commit-sign"
log " gpg.format: ssh"

echo
prompt "Do you want to apply these changes? (y/N) "
read -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
log "Operation cancelled."
return 1
fi

log "Setting up Git SSH signing ($(format_scope $scope) config)..."

# Apply the configurations
git-config-set "$scope" "gpg.format" "ssh"
local script_path="$HOME/.ssh/keycutter/scripts/git-commit-sign"
git-config-set "$scope" "gpg.ssh.program" "$script_path"

if [ ! -f "$ssh_key" ]; then
log "Error: SSH key not found at $ssh_key"
return 1
fi

git-config-set "$scope" "user.signingkey" "$ssh_key"
log "Git SSH signing basic setup complete for $(format_scope $scope) config!"
return 0
}

# Function to enable commit signing
enable-git-commit-signing() {
local scope=""
while [[ $# -gt 0 ]]; do
case $1 in
--global)
scope="global"
shift
;;
--local)
scope="local"
shift
;;
*)
log "Error: Unknown option $1"
return 1
;;
esac
done

if [ -z "$scope" ]; then
log "No scope specified. Defaulting to $(format_scope local) config."
scope="local"
fi

local config_flag=""
[[ "$scope" == "global" ]] && config_flag="--global"
git config $config_flag "commit.gpgsign" "true"
log "Enabled commit signing for $(format_scope $scope) config"
}

# Function to disable commit signing
disable-git-commit-signing() {
local scope=""
while [[ $# -gt 0 ]]; do
case $1 in
--global)
scope="global"
shift
;;
--local)
scope="local"
shift
;;
*)
log "Error: Unknown option $1"
return 1
;;
esac
done

if [ -z "$scope" ]; then
log "No scope specified. Defaulting to $(format_scope local) config."
scope="local"
fi

local config_flag=""
[[ "$scope" == "global" ]] && config_flag="--global"
git config $config_flag --unset "commit.gpgsign"
log "Disabled commit signing for $(format_scope $scope) config"
}

# Function to check Git SSH signing configuration
git-signing-config() {
local scope="$1"
local git_dir=""

# Get the current Git directory, if available
if git rev-parse --show-toplevel &>/dev/null; then
git_dir=$(git rev-parse --show-toplevel)
else
git_dir="Not a Git repository"
fi

if [[ "$scope" != "global" && "$scope" != "local" && -n "$scope" ]]; then
log "Invalid scope provided. Scope must be either 'global' or 'local'."
return 1
fi

if [ -z "$scope" ]; then
scope="local"
fi

log "Checking $(format_scope $scope) Git SSH signing configuration for directory: $git_dir"
log "gpg.format: $(git-config-get "$scope" gpg.format)"
log "gpg.ssh.program: $(git-config-get "$scope" gpg.ssh.program)"
log "user.signingkey: $(git-config-get "$scope" user.signingkey)"
log "commit.gpgsign: $(git-config-get "$scope" commit.gpgsign)"
}


# Function to print setup instructions
git-signing-help() {
local ssh_key="${1:-$KEYCUTTER_SSH_KEY_DIR}"
log "Setup instructions:"
log "1. Create a key via keycutter:"
log " \`keycutter create github.com_me@pc\`"
log "2. Make sure the Git SSH signing script is in place:"
log " \`keycutter update-ssh-config\`"
log "3. Set up Git SSH signing:"
log " \`keycutter git-signing-setup\`"
}

# If this script is run directly, print example usage
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
example_usage
fi
45 changes: 45 additions & 0 deletions ssh_config/scripts/git-commit-sign
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash

set -eo pipefail

# Usage:
#
# git config [--global] gpg.format ssh
# git config [--global] gpg.ssh.program "~/.ssh/keycutter/keys/scripts/git-commit-sign"
# git config [--global] user.signingkey "~/.ssh/keycutter/keys/{service}_user@{host}"

# Function to get Git config value, trying local then global
get_git_config() {
local value
value=$(git config --get "$1")
if [ -z "$value" ]; then
value=$(git config --global --get "$1")
fi
echo "$value"
}
# Get the signing key
signing_key=$(get_git_config user.signingkey)
if [ -z "$signing_key" ]; then
echo "Error: No signing key found in Git config (local or global)."
exit 1
fi

# Check if the signing key file exists
if [ ! -f "$signing_key" ]; then
log_error "Signing key file '$signing_key' does not exist."
exit 1
fi

# Get the key fingerprint
key_info=$(ssh-keygen -lf "$signing_key" | awk '{gsub(/[()]/, "", $4); print $4 " " $2}')
if [ -z "$key_info" ]; then
echo "Error: Could not retrieve key information."
exit 1
fi
echo "Confirm user presence for key $key_info" >&2

# Sign the commit
if ! ssh-keygen -Y sign -n git -f "$signing_key" "$@"; then
log_error "Signing failed."
exit 1
fi