diff --git a/bin/keycutter b/bin/keycutter index 47b2837..ee0d26f 100755 --- a/bin/keycutter +++ b/bin/keycutter @@ -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 diff --git a/lib/functions b/lib/functions index 34db1c1..50be0bd 100644 --- a/lib/functions +++ b/lib/functions @@ -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" diff --git a/lib/git b/lib/git new file mode 100644 index 0000000..7293c8f --- /dev/null +++ b/lib/git @@ -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 \ No newline at end of file diff --git a/ssh_config/scripts/git-commit-sign b/ssh_config/scripts/git-commit-sign new file mode 100755 index 0000000..fcfa345 --- /dev/null +++ b/ssh_config/scripts/git-commit-sign @@ -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 \ No newline at end of file