diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index de227dfc25..f4e9c0f496 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -12,6 +12,7 @@ on: - run_in_compat_layer_env.sh - scripts/utils.sh - update_lmod_cache.sh + - sign_verify_file_ssh.sh pull_request: paths: @@ -24,6 +25,7 @@ on: - run_in_compat_layer_env.sh - scripts/utils.sh - update_lmod_cache.sh + - sign_verify_file_ssh.sh permissions: contents: read # to fetch code (actions/checkout) jobs: @@ -99,6 +101,27 @@ jobs: # check if tarballs have been produced ls -l *.tar.gz + - name: test sign_verify_file_ssh.sh script + run: | + # Create a PEM format ssh identity + ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa.pem -N "" + # Find a file to sign + export TARBALL="$(ls *.tar.gz | tail -1)" + # Sign the file + ./sign_verify_file_ssh.sh sign id_rsa.pem "$TARBALL" + # Create an allowed_signers file based on the public key + echo -n "allowed_identity " > allowed_signers + cat id_rsa.pem.pub >> allowed_signers + # Verify the signature + ./sign_verify_file_ssh.sh verify allowed_signers "$TARBALL" + # Make a new signature that does not appear in the allowed signers file + ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa.alt.pem -N "" + # Replace the allowed signers file + echo -n "disallowed_identity " > allowed_signers + cat id_rsa.alt.pem.pub >> allowed_signers + # Make sure signature checking fails in this case + ./sign_verify_file_ssh.sh verify allowed_signers "$TARBALL" && exit 1 || echo "Expected failure for unknown identity" + - name: test create_lmodsitepackage.py script run: | # bind current directory into container as /software-layer diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh new file mode 100755 index 0000000000..679ea7d649 --- /dev/null +++ b/sign_verify_file_ssh.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# +# SSH Signature Signing and Verification Script +# - Sign a file using an SSH private key. +# - Verify a signed file using an allowed signers file. +# +# Generates a signature file named `.sig` in the same directory. +# +# Author: Alan O'Cais +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Usage message +usage() { + cat < + $0 verify [signature_file] + +Options: + sign: + - : Path to SSH private key (use KEY_PASSPHRASE env for passphrase) + - : File to sign + + verify: + - : Path to the allowed signers file + - : File to verify + - [signature_file]: Optional, defaults to '.sig' + +Example allowed signers format: + identity_1 +EOF + exit 9 +} + +# Error codes +FILE_PROBLEM=1 +CONVERSION_FAILURE=2 +VALIDATION_FAILED=3 + +# Ensure minimum arguments +[ "$#" -lt 3 ] && usage + +MODE="$1" +FILE_TO_SIGN="$3" + +# Ensure the target file exists +if [ ! -f "$FILE_TO_SIGN" ]; then + echo "Error: File '$FILE_TO_SIGN' not found." + exit $FILE_PROBLEM +fi + +# Use a very conservatuve umask throughout this script since we are dealing with sensitive things +umask 077 || { echo "Error: Failed to set 0177 umask."; exit $FILE_PROBLEM; } + +# Create a restricted temporary directory and ensure cleanup on exit +TEMP_DIR=$(mktemp -d) || { echo "Error: Failed to create temporary directory."; exit $FILE_PROBLEM; } +trap 'rm -rf "$TEMP_DIR"' EXIT + +# Converts the SSH private key to OpenSSH format and generates a public key +convert_private_key() { + local input_key="$1" + local output_key="$2" + + echo "Converting SSH key to OpenSSH format..." + cp "$input_key" "$output_key" || { echo "Error: Failed to copy $input_key to $output_key"; exit $FILE_PROBLEM; } + + # This saves the key in the default OpenSSH format (which is required for signing) + ssh-keygen -p -f "$output_key" -P "${KEY_PASSPHRASE:-}" -N "${KEY_PASSPHRASE:-}" || { + echo "Error: Failed to convert key to OpenSSH format." + exit $CONVERSION_FAILURE + } + + # Extract the public key from the private key + ssh-keygen -y -f "$input_key" -P "${KEY_PASSPHRASE:-}" > "${output_key}.pub" || { + echo "Error: Failed to extract public key." + exit $CONVERSION_FAILURE + } +} + +# Sign mode +if [ "$MODE" == "sign" ]; then + PRIVATE_KEY="$2" + TEMP_KEY="$TEMP_DIR/converted_key" + SIG_FILE="${FILE_TO_SIGN}.sig" + + # Check for key and existing signature + [ ! -f "$PRIVATE_KEY" ] && { echo "Error: Private key not found."; exit $FILE_PROBLEM; } + [ -f "$SIG_FILE" ] && { echo "Error: Signature already exists. Remove to re-sign."; exit $FILE_PROBLEM; } + + convert_private_key "$PRIVATE_KEY" "$TEMP_KEY" + + echo "Signing the file..." + ssh-keygen -Y sign -f "$TEMP_KEY" -P "${KEY_PASSPHRASE:-}" -n file "$FILE_TO_SIGN" + + [ ! -f "$SIG_FILE" ] && { echo "Error: Signing failed."; exit $FILE_PROBLEM; } + echo "Signature created: $SIG_FILE" + + cat <