Secure GitHub GPG signing bootstrap using an Ed25519 certification key, dedicated signing subkey, key expiration, revocation protection, automatic Git commit/tag signing, and local verification.

GitHub GPG Signing Bootstrap

A small, auditable Bash script for setting up GPG signing for Git and GitHub.

The script creates a dedicated OpenPGP identity with:

  • Ed25519 primary key for certification
  • Ed25519 signing subkey for daily Git signing
  • Configurable key expiration
  • Revocation certificate
  • Automatic Git commit signing
  • Automatic Git tag signing
  • Local cryptographic signing verification

The script is intentionally interactive and contains no personal identity information.

Why this setup?

Git commit signing is a useful way to establish cryptographic provenance for your commits.

This setup separates the OpenPGP identity key from the key used for everyday signing:

Primary key
└── Ed25519
    └── Certification only

Signing subkey
└── Ed25519
    └── Signing only
#!/usr/bin/env bash
# ============================================================================
# GitHub GPG Signing Bootstrap
# ============================================================================
#
# Creates a dedicated OpenPGP identity for GitHub with:
#
# Primary key: Ed25519 certification-only
# Subkey: Ed25519 signing-only
#
# Configures Git to automatically sign:
#
# - commits
# - tags
#
# Generates:
#
# - public key for GitHub
# - revocation certificate
#
# This script:
#
# - does NOT contain personal identity information
# - does NOT upload anything to GitHub
# - does NOT export private keys
# - does NOT delete existing keys
# - does NOT overwrite existing backup files
# - does NOT store your passphrase
#
# Requirements:
#
# - Bash
# - GnuPG 2.x
# - Git
#
# ============================================================================
set -Eeuo pipefail
# ============================================================================
# Constants
# ============================================================================
SCRIPT_NAME="$(basename "$0")"
# ============================================================================
# Colors
# ============================================================================
#
# Disable colors automatically when stdout is not a terminal.
#
# ============================================================================
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
BOLD=''
RESET=''
fi
# ============================================================================
# Helper functions
# ============================================================================
die() {
printf '%bERROR:%b %s\n' "$RED" "$RESET" "$*" >&2
exit 1
}
info() {
printf '\n%b==>%b %s\n' "$BLUE" "$RESET" "$*"
}
success() {
printf '%bOK:%b %s\n' "$GREEN" "$RESET" "$*"
}
warning() {
printf '%bWARNING:%b %s\n' "$YELLOW" "$RESET" "$*"
}
cleanup() {
if [[ -n "${TEST_FILE:-}" && -e "${TEST_FILE:-}" ]]; then
rm -f "$TEST_FILE"
fi
if [[ -n "${TEST_SIGNATURE:-}" && -e "${TEST_SIGNATURE:-}" ]]; then
rm -f "$TEST_SIGNATURE"
fi
}
trap cleanup EXIT
on_error() {
local exit_code=$?
printf '\n%bERROR:%b Setup failed at line %s.\n' \
"$RED" \
"$RESET" \
"${BASH_LINENO[0]:-unknown}"
printf '%s\n' \
'No existing GPG keys were intentionally deleted or modified.'
exit "$exit_code"
}
trap on_error ERR
# ============================================================================
# Banner
# ============================================================================
printf '\n'
printf '%b%s%b\n' "$BOLD" 'GitHub GPG Signing Bootstrap' "$RESET"
printf '%s\n' '============================='
printf '\n'
printf '%s\n' \
'This script creates a dedicated GPG identity for signing Git commits'
printf '%s\n' \
'and tags on GitHub.'
printf '\n'
printf '%s\n' 'Key design:'
printf '%s\n' ' Primary key → Ed25519 certification only'
printf '%s\n' ' Signing key → Ed25519 signing only'
printf '%s\n' ' Expiration → configurable'
printf '\n'
printf '%s\n' \
'Nothing is uploaded to GitHub automatically.'
printf '%s\n' \
'You will manually add the public key to your GitHub account.'
printf '\n'
# ============================================================================
# Requirements
# ============================================================================
info "Checking required software."
command -v gpg >/dev/null 2>&1 || \
die "GnuPG is not installed."
command -v git >/dev/null 2>&1 || \
die "Git is not installed."
GPG_VERSION="$(
gpg --version |
awk 'NR == 1 { print $3 }'
)"
[[ -n "$GPG_VERSION" ]] || \
die "Could not determine the installed GnuPG version."
case "$GPG_VERSION" in
2.*)
;;
*)
die "GnuPG 2.x is required. Found: ${GPG_VERSION}"
;;
esac
success "GnuPG ${GPG_VERSION} found."
success "Git found."
# ============================================================================
# Collect identity information
# ============================================================================
printf '\n'
printf '%b%s%b\n' "$BOLD" 'Identity Configuration' "$RESET"
printf '%s\n' '----------------------'
printf '\n'
printf '%s\n' \
'Use the name you want associated with your Git commits.'
printf '%s\n' \
'For GitHub, the email should be an email address verified on your account.'
printf '\n'
while true; do
read -r -p "Full name: " FULL_NAME
if [[ -n "$FULL_NAME" ]]; then
break
fi
warning "Name cannot be empty."
done
while true; do
read -r -p "GitHub-verified email: " EMAIL
if [[ -z "$EMAIL" ]]; then
warning "Email cannot be empty."
continue
fi
if [[ "$EMAIL" != *@*.* ]]; then
warning "That does not look like a valid email address."
continue
fi
break
done
# ============================================================================
# Key expiration
# ============================================================================
printf '\n'
printf '%s\n' \
'Choose how long the OpenPGP keys should remain valid.'
printf '%s\n' \
'Two years is a reasonable default for a developer signing identity.'
printf '\n'
while true; do
read -r -p "Key expiration [2y]: " KEY_EXPIRATION
KEY_EXPIRATION="${KEY_EXPIRATION:-2y}"
case "$KEY_EXPIRATION" in
0|never|Never|NEVER)
warning "A non-expiring key is not recommended for this setup."
read -r -p "Use a non-expiring key anyway? [y/N] " CONFIRM_NEVER
case "${CONFIRM_NEVER,,}" in
y|yes)
break
;;
*)
continue
;;
esac
;;
*)
break
;;
esac
done
# ============================================================================
# Backup directory
# ============================================================================
DEFAULT_BACKUP_DIR="${HOME}/gpg-backup"
printf '\n'
printf '%s\n' \
'The script will save your public key and revocation certificate here.'
printf '%s\n' \
'The revocation certificate is sensitive and should be stored securely.'
printf '\n'
read -r -p "Backup directory [${DEFAULT_BACKUP_DIR}]: " BACKUP_DIR
BACKUP_DIR="${BACKUP_DIR:-$DEFAULT_BACKUP_DIR}"
if [[ "$BACKUP_DIR" != /* ]]; then
BACKUP_DIR="${PWD}/${BACKUP_DIR}"
fi
BACKUP_DIR="${BACKUP_DIR%/}"
# ============================================================================
# Display configuration
# ============================================================================
printf '\n'
printf '%b%s%b\n' "$BOLD" 'Configuration' "$RESET"
printf '%s\n' '-------------'
printf '\n'
printf 'Name: %s\n' "$FULL_NAME"
printf 'Email: %s\n' "$EMAIL"
printf 'Expiration: %s\n' "$KEY_EXPIRATION"
printf 'Backup: %s\n' "$BACKUP_DIR"
printf '\n'
printf '%s\n' 'The following keys will be created:'
printf '%s\n' ' Primary: Ed25519 certification-only'
printf '%s\n' ' Subkey: Ed25519 signing-only'
printf '\n'
printf '%b%s%b\n' "$BOLD" 'Important' "$RESET"
printf '%s\n' \
'GnuPG will ask you to create/provide a passphrase.'
printf '%s\n' \
'Use a strong, unique passphrase.'
printf '%s\n' \
'This script never records that passphrase.'
printf '\n'
read -r -p "Continue? [y/N] " CONFIRM
case "${CONFIRM,,}" in
y|yes)
;;
*)
printf 'Cancelled.\n'
exit 0
;;
esac
# ============================================================================
# Check for an existing secret key
# ============================================================================
info "Checking for an existing secret key."
EXISTING_KEY="$(
gpg \
--list-secret-keys \
--with-colons \
"$EMAIL" 2>/dev/null |
awk -F: '$1 == "sec" { print $5; exit }'
)" || true
if [[ -n "$EXISTING_KEY" ]]; then
printf '\n'
warning "An existing secret key was found for ${EMAIL}."
printf '\n'
gpg \
--list-secret-keys \
--keyid-format=long \
"$EMAIL"
printf '\n'
die \
"Refusing to create another key for this email. Review the existing key first."
fi
success "No existing secret key found for ${EMAIL}."
# ============================================================================
# Prepare backup directory
# ============================================================================
info "Preparing backup directory."
if [[ -e "$BACKUP_DIR" && ! -d "$BACKUP_DIR" ]]; then
die "Backup path exists but is not a directory: ${BACKUP_DIR}"
fi
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
PUBLIC_KEY_FILE="${BACKUP_DIR}/gpg-public-key.asc"
REVOCATION_FILE="${BACKUP_DIR}/gpg-revocation.asc"
if [[ -e "$PUBLIC_KEY_FILE" ]]; then
die "Refusing to overwrite existing file: ${PUBLIC_KEY_FILE}"
fi
if [[ -e "$REVOCATION_FILE" ]]; then
die "Refusing to overwrite existing file: ${REVOCATION_FILE}"
fi
success "Backup directory ready."
# ============================================================================
# Generate primary key
# ============================================================================
#
# The primary key is deliberately certification-only.
#
# It establishes ownership of the OpenPGP identity.
#
# Daily signing is delegated to a separate signing subkey.
#
# ============================================================================
info "Generating the Ed25519 certification-only primary key."
printf '\n'
printf '%s\n' \
'GnuPG will now prompt for your key passphrase.'
printf '\n'
gpg \
--quick-generate-key \
"${FULL_NAME} <${EMAIL}>" \
ed25519 \
cert \
"$KEY_EXPIRATION"
success "Primary certification key created."
# ============================================================================
# Find primary fingerprint
# ============================================================================
info "Reading primary key fingerprint."
PRIMARY_FINGERPRINT="$(
gpg \
--list-secret-keys \
--with-colons \
"$EMAIL" |
awk -F: '
$1 == "sec" {
found = 1
next
}
found && $1 == "fpr" {
print $10
exit
}
'
)" || true
[[ -n "$PRIMARY_FINGERPRINT" ]] || \
die "Could not determine the primary key fingerprint."
printf '\n'
printf 'Primary fingerprint:\n'
printf '%s\n' "$PRIMARY_FINGERPRINT"
# ============================================================================
# Generate signing subkey
# ============================================================================
#
# This key is used for:
#
# git commit -S
# git tag -s
#
# ============================================================================
info "Generating the dedicated Ed25519 signing subkey."
printf '\n'
printf '%s\n' \
'GnuPG may ask for your primary key passphrase.'
printf '\n'
gpg \
--quick-add-key \
"$PRIMARY_FINGERPRINT" \
ed25519 \
sign \
"$KEY_EXPIRATION"
success "Signing subkey created."
# ============================================================================
# Find signing subkey fingerprint
# ============================================================================
info "Reading signing subkey fingerprint."
SIGNING_FINGERPRINT="$(
gpg \
--list-secret-keys \
--with-subkey-fingerprint \
--with-colons \
"$PRIMARY_FINGERPRINT" |
awk -F: '
$1 == "ssb" && $12 ~ /s/ {
find_next_fingerprint = 1
next
}
find_next_fingerprint && $1 == "fpr" {
print $10
exit
}
'
)" || true
[[ -n "$SIGNING_FINGERPRINT" ]] || \
die "Could not determine the signing subkey fingerprint."
printf '\n'
printf 'Signing fingerprint:\n'
printf '%s\n' "$SIGNING_FINGERPRINT"
# ============================================================================
# Generate revocation certificate
# ============================================================================
#
# A revocation certificate allows the key owner to formally invalidate the
# OpenPGP identity if the private key is compromised or otherwise needs to
# be retired.
#
# Keep this file private.
#
# ============================================================================
info "Generating revocation certificate."
printf '\n'
printf '%s\n' \
'GnuPG will ask you to confirm the revocation certificate details.'
printf '%s\n' \
'Do not publish the resulting revocation certificate.'
printf '\n'
gpg \
--output "$REVOCATION_FILE" \
--armor \
--generate-revocation \
"$PRIMARY_FINGERPRINT"
chmod 600 "$REVOCATION_FILE"
success "Revocation certificate created."
# ============================================================================
# Export public key
# ============================================================================
#
# This file contains public information only.
#
# It is safe to provide to GitHub.
#
# ============================================================================
info "Exporting public key."
gpg \
--armor \
--export \
"$PRIMARY_FINGERPRINT" \
> "$PUBLIC_KEY_FILE"
chmod 644 "$PUBLIC_KEY_FILE"
success "Public key exported."
# ============================================================================
# Configure Git
# ============================================================================
info "Configuring Git."
git config --global user.name "$FULL_NAME"
git config --global user.email "$EMAIL"
git config --global gpg.format openpgp
git config --global user.signingkey "$SIGNING_FINGERPRINT"
git config --global commit.gpgsign true
git config --global tag.gpgSign true
success "Git configured for automatic commit and tag signing."
# ============================================================================
# Start GPG agent
# ============================================================================
info "Starting GPG agent."
gpgconf --launch gpg-agent
gpg-connect-agent reloadagent /bye >/dev/null 2>&1 || true
success "GPG agent started."
# ============================================================================
# Test cryptographic signing
# ============================================================================
info "Testing cryptographic signing."
TEST_FILE="$(mktemp)"
TEST_SIGNATURE="${TEST_FILE}.asc"
printf '%s\n' \
'GitHub GPG signing verification test.' \
> "$TEST_FILE"
gpg \
--local-user "$SIGNING_FINGERPRINT" \
--armor \
--detach-sign \
"$TEST_FILE"
gpg \
--verify \
"$TEST_SIGNATURE" \
"$TEST_FILE"
success "Cryptographic signing test passed."
# ============================================================================
# Display key hierarchy
# ============================================================================
printf '\n'
printf '%b%s%b\n' "$BOLD" 'GPG KEY HIERARCHY' "$RESET"
printf '%s\n' '================='
printf '\n'
gpg \
--list-secret-keys \
--with-subkey-fingerprint \
--keyid-format=long \
"$PRIMARY_FINGERPRINT"
# ============================================================================
# Display fingerprints
# ============================================================================
printf '\n'
printf '%b%s%b\n' "$BOLD" 'FINGERPRINTS' "$RESET"
printf '%s\n' '============'
printf '\n'
printf 'Primary key:\n'
printf '%s\n' "$PRIMARY_FINGERPRINT"
printf '\n'
printf 'Signing subkey:\n'
printf '%s\n' "$SIGNING_FINGERPRINT"
# ============================================================================
# Final instructions
# ============================================================================
printf '\n'
printf '%b%s%b\n' "$BOLD" 'SETUP COMPLETE' "$RESET"
printf '%s\n' '=============='
printf '\n'
printf '%b%s%b\n' "$GREEN" \
'Git is now configured to automatically sign commits and tags.' \
"$RESET"
printf '\n'
printf '%b%s%b\n' "$BOLD" 'PUBLIC KEY FOR GITHUB' "$RESET"
printf '%s\n' '====================='
printf '\n'
cat "$PUBLIC_KEY_FILE"
printf '\n'
printf '%b%s%b\n' "$BOLD" 'GitHub' "$RESET"
printf '%s\n' \
'1. Open GitHub Settings.'
printf '%s\n' \
'2. Open SSH and GPG keys.'
printf '%s\n' \
'3. Select New GPG key.'
printf '%s\n' \
'4. Give the key a descriptive title.'
printf '%s\n' \
'5. Paste the public key above.'
printf '%s\n' \
'6. Save the key.'
printf '\n'
printf '%b%s%b\n' "$BOLD" 'FILES CREATED' "$RESET"
printf '%s\n' '--------------'
printf '\n'
printf 'Public key:\n'
printf '%s\n' "$PUBLIC_KEY_FILE"
printf '\n'
printf 'Revocation certificate:\n'
printf '%s\n' "$REVOCATION_FILE"
printf '\n'
warning \
'Keep the revocation certificate private and store a backup somewhere secure.'
printf '\n'
printf '%b%s%b\n' "$BOLD" 'LOCAL VERIFICATION' "$RESET"
printf '%s\n' '------------------'
printf '\n'
printf '%s\n' \
'After making a signed commit:'
printf '%s\n' \
' git log --show-signature -1'
printf '\n'
printf '%s\n' \
'To verify the current commit:'
printf '%s\n' \
' git verify-commit HEAD'
printf '\n'
printf '%b%s%b\n' "$BOLD" 'GITHUB VERIFICATION' "$RESET"
printf '%s\n' '-------------------'
printf '\n'
printf '%s\n' \
'After pushing a signed commit to GitHub, the commit should display:'
printf '%s\n' \
' Verified'
printf '\n'
printf '%s\n' \
'If GitHub does not show Verified, check that the GitHub account email'
printf '%s\n' \
'matches the email used by Git and that the public key was added correctly.'
printf '\n'
printf '%b%s%b\n' "$GREEN" 'Done.' "$RESET"
printf '\n'
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论