This commit is contained in:
David Tomaschik
2026-02-19 13:20:21 -08:00
parent 9ab1f9c298
commit f50edc1fa6
4 changed files with 262 additions and 9 deletions

164
bin/ssh-sign Executable file
View File

@@ -0,0 +1,164 @@
#!/bin/bash
# A robust wrapper for ssh-keygen to sign and verify files.
# --- Color Codes for Output ---
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'
COLOR_NONE='\033[0m' # No Color
# --- Default values ---
DEFAULT_SIGNING_KEY="$HOME/.ssh/id_signing"
DEFAULT_ALLOWED_SIGNERS="$HOME/.ssh/allowed_signers"
DEFAULT_IDENTITY="david@systemoverlord.com"
DEFAULT_NAMESPACE="file"
# --- Usage Message ---
usage() {
cat << EOF
Usage: $(basename "$0") <sign|verify> [OPTIONS] [FILE]
A wrapper for 'ssh-keygen -Y' to simplify file signing and verification.
COMMANDS:
sign Sign a file. The path to the file to be signed is provided as a positional argument.
verify Verify a signature. The original file content is read from stdin.
OPTIONS:
-f <file> For 'sign': Path to the private key for signing.
Defaults to '$DEFAULT_SIGNING_KEY' if it exists.
For 'verify': Path to the allowed_signers file.
Defaults to '$DEFAULT_ALLOWED_SIGNERS'.
-n <namespace> Signature namespace.
Defaults to '$DEFAULT_NAMESPACE'.
-I <identity> For 'verify': The identity to check the signature against.
Defaults to '$DEFAULT_IDENTITY'.
-s <sig_file> For 'verify': Path to the signature file to verify (e.g., file.sig). REQUIRED for verify.
-h, --help Show this help message.
EXAMPLE USAGE:
# Sign a file using the default key
$(basename "$0") sign release.tar.gz
# Sign a file with a specific key
$(basename "$0") sign -f ~/.ssh/id_ed25519_my_signing_key release.tar.gz
# Verify a signature using default allowed_signers and identity
cat release.tar.gz | $(basename "$0") verify -s release.tar.gz.sig
# Verify a signature with a specific allowed_signers file and identity
cat release.tar.gz | $(basename "$0") verify -f ./my_signers -I other@example.com -s release.tar.gz.sig
EOF
exit 1
}
# --- Helper for error messages ---
error() {
echo -e "${COLOR_RED}Error: $1${COLOR_NONE}" >&2
exit 1
}
# --- Main Script Logic ---
if [[ "$1" != "sign" && "$1" != "verify" ]]; then
usage
fi
SUBCOMMAND=$1
shift # Consume the subcommand
# --- Argument Parsing and Validation ---
# Separate arguments from the file to be signed
declare -a remaining_args
file_to_sign=""
while [[ "$#" -gt 0 ]]; do
# If we see a non-flag argument, assume it's the file to sign.
# This works because the file to sign is the only positional argument.
if [[ "$1" != -* ]] && [[ -z "$file_to_sign" ]]; then
file_to_sign="$1"
else
remaining_args+=("$1")
fi
shift
done
# --- Build command based on subcommand ---
declare -a CMD_ARGS
CMD_ARGS=("ssh-keygen" "-Y" "$SUBCOMMAND")
# Append all the flag-based arguments (-f, -n, -I, -s)
CMD_ARGS+=("${remaining_args[@]}")
# Scan for provided flags to handle defaults correctly
f_provided=false
n_provided=false
I_provided=false
s_provided=false
for arg in "${remaining_args[@]}"; do
[[ "$arg" == "-f" ]] && f_provided=true
[[ "$arg" == "-n" ]] && n_provided=true
[[ "$arg" == "-I" ]] && I_provided=true
[[ "$arg" == "-s" ]] && s_provided=true
done
if [[ "$SUBCOMMAND" == "sign" ]]; then
if [[ -z "$file_to_sign" ]]; then
error "Path to file to be signed is required for 'sign' command."
fi
# Set default signing key if -f was not provided
if ! $f_provided; then
if [[ ! -f "$DEFAULT_SIGNING_KEY" ]]; then
error "Default signing key not found at '$DEFAULT_SIGNING_KEY'. Specify one with -f."
fi
CMD_ARGS+=("-f" "$DEFAULT_SIGNING_KEY")
fi
# Set default namespace if -n was not provided
if ! $n_provided; then
CMD_ARGS+=("-n" "$DEFAULT_NAMESPACE")
fi
# The file to sign MUST be the last argument for ssh-keygen
CMD_ARGS+=("$file_to_sign")
elif [[ "$SUBCOMMAND" == "verify" ]]; then
if [[ -n "$file_to_sign" ]]; then
error "The 'verify' command reads from stdin and does not accept a positional file argument. Found '$file_to_sign'."
fi
if ! $s_provided; then
error "Signature file must be provided with -s for 'verify' command."
fi
# Set default allowed_signers if -f was not provided
if ! $f_provided; then
if [[ ! -f "$DEFAULT_ALLOWED_SIGNERS" ]]; then
error "Default allowed signers file not found at '$DEFAULT_ALLOWED_SIGNERS'. Specify one with -f."
fi
CMD_ARGS+=("-f" "$DEFAULT_ALLOWED_SIGNERS")
fi
# Set default identity if -I was not provided
if ! $I_provided; then
CMD_ARGS+=("-I" "$DEFAULT_IDENTITY")
fi
# Set default namespace if -n was not provided
if ! $n_provided; then
CMD_ARGS+=("-n" "$DEFAULT_NAMESPACE")
fi
fi
# --- Execute and Report ---
# We capture the output and stderr to show it to the user
if output=$("${CMD_ARGS[@]}" 2>&1); then
echo -e "${COLOR_GREEN}Success:${COLOR_NONE}"
echo "$output"
exit 0
else
echo -e "${COLOR_RED}Command Failed:${COLOR_NONE}"
echo "$output" >&2
exit 1
fi

View File

@@ -1,6 +1,12 @@
# Universal Settings
Protocol 2
Host *
# Add the post-quantum (PQ) KEX algorithms to the front of the default list.
# The client will try them in this order before falling back to standard ones.
# The (+) syntax requires OpenSSH 7.8 or newer.
KexAlgorithms +mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com
# Permit Local Overrides
Include ~/.ssh/config.d/*
@@ -33,4 +39,4 @@ Match final
ServerAliveCountMax 3
UpdateHostKeys yes
User david
VerifyHostKeyDNS yes
VerifyHostKeyDNS ask

View File

@@ -88,17 +88,35 @@ install_gpg_keys() {
install_known_hosts() {
verbose 'Installing known hosts...' >&2
if [[ ! -f "${BASEDIR}/keys/known_hosts" ]] ; then
local skel_hosts="${BASEDIR}/keys/known_hosts"
local user_hosts="${HOME}/.ssh/known_hosts"
local merge_script="${BASEDIR}/skeltools/merge_authorized_keys"
if [[ ! -f "${skel_hosts}" ]]; then
return 0
fi
mkdir -p "${HOME}/.ssh"
if [[ -f "${HOME}/.ssh/known_hosts" ]] ; then
local tmpf="$(mktemp)"
cat "${BASEDIR}"/keys/known_hosts "${HOME}"/.ssh/known_hosts \
| sort -u > "$tmpf"
mv "$tmpf" "${HOME}"/.ssh/known_hosts
if [[ -f "${user_hosts}" ]]; then
# User has an existing known_hosts file, merge is required.
local tmpf
tmpf="$(mktemp)"
if [[ -x "${merge_script}" ]]; then
# Use the robust awk script for merging.
verbose "Merging known_hosts with authoritative script..."
"${merge_script}" "${skel_hosts}" "${user_hosts}" > "$tmpf"
else
cp "${BASEDIR}"/keys/known_hosts "${HOME}"/.ssh/known_hosts
# Fallback to the old, less robust method if the script is missing.
verbose "Warning: ${merge_script} not found or not executable. Using simple sort."
cat "${skel_hosts}" "${user_hosts}" | sort -u > "$tmpf"
fi
# Safely replace the original file.
cat "$tmpf" >| "${user_hosts}"
rm "$tmpf"
else
# User does not have a known_hosts file, just copy the new one.
cp "${skel_hosts}" "${user_hosts}"
fi
}
@@ -153,7 +171,7 @@ save_prefs() {
echo "INSTALL_KEYS=\"${INSTALL_KEYS}\""
echo "TRUST_ALL_KEYS=\"${TRUST_ALL_KEYS}\""
echo "VERBOSE=\"${VERBOSE}\""
} > "$pref_file"
} >| "$pref_file"
}
cleanup() {

65
skeltools/merge_authorized_keys Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Check for the correct number of arguments and display usage if incorrect.
if [ "$#" -ne 2 ]; then
echo "Usage: $(basename "$0") <authoritative_hosts_file> <target_known_hosts_file>"
echo "Merges two known_hosts files, with entries from the authoritative file"
echo "replacing matching entries (by hostname and key type) in the target file."
echo "The final merged result is printed to standard output."
exit 1
fi
AUTHORITATIVE_FILE="$1"
TARGET_FILE="$2"
awk '
# Part 1: Process the authoritative source file first (NR==FNR).
# NR==FNR is an awk pattern that is only true while reading the first file
# listed on the command line.
NR==FNR {
# The key type is always the 2nd field (e.g., "ssh-ed25519").
key_type = $2;
# Split all hostnames/IPs from the 1st field (e.g., "host.com,192.0.2.1").
split($1, hosts, ",");
for (i in hosts) {
# Build an associative array using a compound key: (hostname, key_type).
# The value stored is the entire line from the authoritative file.
auth_map[hosts[i], key_type] = $0;
}
next; # Skip to the next line in the authoritative file.
}
# Part 2: Process the main/target known_hosts file.
# This block only runs for the second file onwards (NR != FNR).
{
is_managed = 0;
# The key type is always the 2nd field.
key_type = $2;
split($1, hosts, ",");
for (i in hosts) {
# Check if this specific (hostname, key_type) pair is managed
# by our authoritative source.
if ((hosts[i], key_type) in auth_map) {
is_managed = 1; # Mark this key as one that will be replaced.
break;
}
}
# If this specific key is NOT managed, keep the original line by printing it.
if (!is_managed) {
print $0;
}
}
# Part 3: After all files are read, print the authoritative entries.
END {
# Use a "printed" array to ensure we only print each unique line once,
# even if it was stored multiple times in auth_map (e.g., for "host,ip").
for (entry in auth_map) {
line = auth_map[entry];
if (!(line in printed)) {
print line;
printed[line] = 1;
}
}
}
' "$AUTHORITATIVE_FILE" "$TARGET_FILE"