#!/bin/sh # ############################################################################### # # edh-keygen.sh # # Diffie-Hellman Key Generation and Service Management Script # # This script generates Diffie-Hellman parameter files for various key sizes, # manages their permissions and can synchronize keys to custom locations # with specified ownership and permissions. It supports service restarts # for both root and non-root systemd users and is designed for integration # with automated cron jobs. # # Configuration is read from a .conf or .local file, supporting per-service # customization including: # - Service name and owner # - DH key size # - Sync path for DH key # - User.group for destination # - File permissions for the key # # Authors: Ivo Noack aka Insonic # Stephan Düsterhaupt # # Copyright (c) 2016-2026 CB-601 - the open tec Elevator # License: MIT # # Project Home: https://dev.town-square.de/cb601/edh-keygen # ############################################################################### # MIT License # # Copyright (c) 2025 CB-601 - the open tec Elevator # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense and/or sell # copies of the Software and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Default values (in case not set in config) tls_tmp_path='/etc/pki/tls/tmp' tls_private_path='/etc/pki/tls/private/' # Determine config file: prefer edh-keygen.local, else use edh-keygen.conf script_dir=$(dirname "$0") if [ -f "$script_dir/edh-keygen.local" ]; then my_service_conf="$script_dir/edh-keygen.local" else my_service_conf="$script_dir/edh-keygen.conf" fi # Check config file if [ ! -f "$my_service_conf" ]; then echo "Service config file $my_service_conf not found!" 1>&2 exit 1 fi # Read global settings from config file while IFS= read -r line || [ -n "$line" ]; do case "$line" in ''|\#*) continue ;; # Skip empty lines and comments tls_tmp_path=*) tls_tmp_path="${line#tls_tmp_path=}" ;; tls_private_path=*) tls_private_path="${line#tls_private_path=}" ;; key_sizes=*) key_sizes="${line#key_sizes=}" ;; *) break ;; # Stop at first non-global line (service lines start here) esac done < "$my_service_conf" # Create path for 'tmp' in '/etc/pki/tls' if [ ! -d "$tls_tmp_path" ]; then if ! mkdir -p "$tls_tmp_path"; then echo "Failed to create temporary directory $tls_tmp_path." >&2 exit 1 fi fi umask 022 # If key_sizes is set in the config, use it; otherwise, extract from service lines or set a default if [ -z "$key_sizes" ]; then key_sizes=$(awk -F: 'NF >= 3 && $3 ~ /^[0-9]+$/ { print $3 }' "$my_service_conf" | sort -nu) [ -z "$key_sizes" ] && key_sizes="2048 4096" fi # Generate DH params for bits in $key_sizes; do echo "Generating DH parameters for $bits bits..." if ! openssl dhparam -out "$tls_tmp_path/dh_${bits}.pem" "$bits"; then echo "Failed to generate ${bits}-bit DH parameters." >&2 rm -rf "$tls_tmp_path" exit 1 fi done # Set permissions if ! find "$tls_tmp_path" -type f -name '*.pem' -exec chmod 644 {} \;; then echo "Failed to set permissions on generated DH parameters." >&2 rm -rf "$tls_tmp_path" exit 1 fi # Synchronize DH parameters from the temporary to the private directory. if [ ! -d "$tls_private_path" ]; then if ! mkdir -p "$tls_private_path"; then echo "Failed to create private key directory $tls_private_path." >&2 rm -rf "$tls_tmp_path" exit 1 fi fi if ! chown root:root "$tls_private_path"; then echo "Failed to set ownership on private key directory $tls_private_path." >&2 rm -rf "$tls_tmp_path" exit 1 fi if ! chmod 750 "$tls_private_path"; then echo "Failed to set permissions on private key directory $tls_private_path." >&2 rm -rf "$tls_tmp_path" exit 1 fi for dh_file in "$tls_tmp_path"/dh_*.pem; do if [ ! -f "$dh_file" ]; then echo "No generated DH parameter files found in $tls_tmp_path." >&2 rm -rf "$tls_tmp_path" exit 1 fi if ! rsync -a "$dh_file" "$tls_private_path"; then echo "Failed to synchronize $dh_file to $tls_private_path." >&2 rm -rf "$tls_tmp_path" exit 1 fi done # Delete the temporary files if ! rm -rf "$tls_tmp_path"; then echo "Failed to remove temporary directory $tls_tmp_path." >&2 exit 1 fi # Query systemd unit properties for system-wide and per-user service units. # # get_service_property # # Prints the value of the requested systemd property to standard output. # Root-owned services are queried through the system manager. Services owned # by another user are queried through that user's systemd --user manager. # The caller must provide the numeric UID for per-user units so that the # required XDG_RUNTIME_DIR can be set explicitly. # # get_service_state # # Resolves and validates the systemd state required for service handling. # On success, it sets the following global variables for the current service: # # load_state Unit load state, e.g. loaded or not-found. # active_state High-level unit state, e.g. active, inactive, or failed. # sub_state Unit type-specific detailed state, e.g. running, dead, # exited, or failed. # service_type Service type, e.g. simple, notify, forking, or oneshot. # service_uid Numeric UID for per-user units; empty for root units. # # A LoadState of "not-found" is returned as a successful lookup so the caller # can report a missing configured unit explicitly. Other query failures return # a non-zero status. get_service_property() { edh_property_name=$1 edh_service_name=$2 edh_service_owner=$3 edh_service_uid=$4 if [ "$edh_service_owner" = "root" ]; then /usr/bin/systemctl show \ --property="$edh_property_name" \ --value \ "$edh_service_name.service" 2>/dev/null else sudo -u "$edh_service_owner" \ XDG_RUNTIME_DIR="/run/user/$edh_service_uid" \ /usr/bin/systemctl --user show \ --property="$edh_property_name" \ --value \ "$edh_service_name.service" 2>/dev/null fi } # Return 0 for a valid unit state or for LoadState=not-found. # Return non-zero if the owner does not exist or systemd cannot be queried. # Result values are exported through the global variables documented above. get_service_state() { edh_state_service=$1 edh_state_owner=$2 load_state= active_state= sub_state= service_type= service_uid= if [ "$edh_state_owner" != "root" ]; then service_uid=$(id -u "$edh_state_owner" 2>/dev/null) if [ -z "$service_uid" ]; then return 1 fi fi load_state=$(get_service_property \ LoadState \ "$edh_state_service" \ "$edh_state_owner" \ "$service_uid") if [ -z "$load_state" ]; then return 1 fi if [ "$load_state" = "not-found" ]; then return 0 fi active_state=$(get_service_property \ ActiveState \ "$edh_state_service" \ "$edh_state_owner" \ "$service_uid") || return 1 sub_state=$(get_service_property \ SubState \ "$edh_state_service" \ "$edh_state_owner" \ "$service_uid") || return 1 service_type=$(get_service_property \ Type \ "$edh_state_service" \ "$edh_state_owner" \ "$service_uid") || return 1 [ -n "$active_state" ] && [ -n "$sub_state" ] && [ -n "$service_type" ] } # Read and process service list while IFS= read -r line || [ -n "$line" ]; do # Skip empty lines, comments and global settings (lines with '=') case "$line" in ''|\#*|*=*) continue ;; esac # Extract service, owner and sync parameters service=$(printf '%s\n' "$line" | awk -F: '{print $1}') owner=$(printf '%s\n' "$line" | awk -F: '{print $2}') key_size=$(printf '%s\n' "$line" | awk -F: '{print $3}') sync_path=$(printf '%s\n' "$line" | awk -F: '{print $4}') user_group=$(printf '%s\n' "$line" | awk -F: '{print $5}') permissions=$(printf '%s\n' "$line" | awk -F: '{print $6}') # Validate required base fields. if [ -z "$service" ] || [ -z "$owner" ]; then echo "Invalid service definition: $line" >&2 continue fi echo "$service.service (owner: $owner)..." if ! get_service_state "$service" "$owner"; then if [ "$owner" != "root" ] && [ -z "$service_uid" ]; then echo "User $owner not found. Skipping $service." >&2 else echo "Could not query the systemd state of $service.service. Skipping restart and DH key sync." >&2 fi continue fi if [ "$load_state" = "not-found" ]; then if [ "$owner" = "root" ]; then echo "$service.service does not exist. Skipping restart and DH key sync." >&2 else echo "$service.service does not exist for user $owner. Skipping restart and DH key sync." >&2 fi continue fi # Decide which action to apply after a successful optional DH key sync. service_action=none case "$active_state" in active) service_action=restart ;; inactive) if [ "$service_type" = "oneshot" ]; then service_action=start echo "$service.service is inactive (SubState: $sub_state, Type: oneshot). Syncing DH key and starting unit." else echo "$service.service is inactive (SubState: $sub_state). DH parameters are updated; skipping service action." fi ;; failed) if [ "$service_type" = "oneshot" ]; then service_action=start echo "$service.service is failed (SubState: $sub_state, Type: oneshot). Syncing DH key and starting unit." >&2 else echo "$service.service is failed (SubState: $sub_state). Syncing DH key without restart." >&2 fi ;; *) echo "$service.service is $active_state (SubState: $sub_state). Skipping restart and DH key sync." >&2 continue ;; esac # Synchronize DH parameters when a complete extended configuration is present. if [ -z "$key_size" ] && [ -z "$sync_path" ] && [ -z "$user_group" ] && [ -z "$permissions" ]; then : elif [ -z "$key_size" ] || [ -z "$sync_path" ] || [ -z "$user_group" ] || [ -z "$permissions" ]; then echo "Incomplete DH key sync configuration for $service. Skipping sync and service action." >&2 continue else # The configuration format uses user.group. Convert it to user:group for chown. case "$user_group" in *.*) ug_user=${user_group%%.*} ug_group=${user_group#*.} ;; *) echo "Invalid owner '$user_group' for $service; expected user.group. Skipping sync." >&2 continue ;; esac if [ -z "$ug_user" ] || [ -z "$ug_group" ]; then echo "Invalid owner '$user_group' for $service; expected user.group. Skipping sync." >&2 continue fi dh_file="${tls_private_path%/}/dh_${key_size}.pem" target_file="${sync_path%/}/dh_${key_size}.pem" if [ ! -f "$dh_file" ]; then echo "DH key $dh_file not found. Skipping sync for $service." >&2 continue fi # Create the destination directory if it does not exist. if [ ! -d "$sync_path" ]; then if ! mkdir -p "$sync_path"; then echo "Failed to create $sync_path for $service." >&2 continue fi if ! chown "$ug_user:$ug_group" "$sync_path"; then echo "Failed to set ownership on $sync_path for $service." >&2 continue fi if ! chmod 750 "$sync_path"; then echo "Failed to set permissions on $sync_path for $service." >&2 continue fi echo "Created directory $sync_path for $service." fi if ! cp "$dh_file" "$target_file"; then echo "Failed to copy DH key to $target_file for $service." >&2 continue fi if ! chown "$ug_user:$ug_group" "$target_file"; then echo "Failed to set ownership on $target_file for $service." >&2 continue fi if ! chmod "$permissions" "$target_file"; then echo "Failed to set permissions on $target_file for $service." >&2 continue fi echo "Synced DH key (${key_size}-bit) to $sync_path for $service." fi # Apply the selected action after the optional DH key synchronization. case "$service_action" in restart) echo "$service.service is active (SubState: $sub_state), restarting as $owner..." if [ "$owner" = "root" ]; then if ! /usr/bin/systemctl restart "$service.service"; then echo "Failed to restart $service.service." >&2 continue fi else if ! sudo -u "$owner" \ XDG_RUNTIME_DIR="/run/user/$service_uid" \ /usr/bin/systemctl --user restart "$service.service"; then echo "Failed to restart $service.service as $owner." >&2 continue fi fi echo "$service.service restarted." ;; start) echo "$service.service is $active_state oneshot, starting as $owner..." if [ "$owner" = "root" ]; then if ! /usr/bin/systemctl start "$service.service"; then echo "Failed to start $service.service." >&2 continue fi else if ! sudo -u "$owner" \ XDG_RUNTIME_DIR="/run/user/$service_uid" \ /usr/bin/systemctl --user start "$service.service"; then echo "Failed to start $service.service as $owner." >&2 continue fi fi echo "$service.service started." ;; none) : ;; *) echo "Internal error: unsupported action '$service_action' for $service." >&2 continue ;; esac done < "$my_service_conf" exit 0