From f50edc1fa61be8ff00b4cf73e59781d011f1d409 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Thu, 19 Feb 2026 13:20:21 -0800 Subject: [PATCH] Work --- bin/ssh-sign | 164 ++++++++++++++++++++++++++++++++ dotfiles/ssh/config | 8 +- install.sh | 34 +++++-- skeltools/merge_authorized_keys | 65 +++++++++++++ 4 files changed, 262 insertions(+), 9 deletions(-) create mode 100755 bin/ssh-sign create mode 100755 skeltools/merge_authorized_keys diff --git a/bin/ssh-sign b/bin/ssh-sign new file mode 100755 index 0000000..8a550d4 --- /dev/null +++ b/bin/ssh-sign @@ -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") [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 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 Signature namespace. + Defaults to '$DEFAULT_NAMESPACE'. + -I For 'verify': The identity to check the signature against. + Defaults to '$DEFAULT_IDENTITY'. + -s 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 diff --git a/dotfiles/ssh/config b/dotfiles/ssh/config index 93f9c08..1424984 100644 --- a/dotfiles/ssh/config +++ b/dotfiles/ssh/config @@ -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 diff --git a/install.sh b/install.sh index daf1116..1398dca 100755 --- a/install.sh +++ b/install.sh @@ -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 + # 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 - cp "${BASEDIR}"/keys/known_hosts "${HOME}"/.ssh/known_hosts + # 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() { diff --git a/skeltools/merge_authorized_keys b/skeltools/merge_authorized_keys new file mode 100755 index 0000000..0c0b395 --- /dev/null +++ b/skeltools/merge_authorized_keys @@ -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") " + 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"