From cbbd94ca89f7874d639d86411c6dd0bf8e2a767f Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Thu, 27 Feb 2025 13:18:26 +0100 Subject: [PATCH 01/13] Add a script that can sign a file based on an ssh key --- sign_verify_file_ssh.sh | 128 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100755 sign_verify_file_ssh.sh diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh new file mode 100755 index 0000000000..26dc79efe3 --- /dev/null +++ b/sign_verify_file_ssh.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +# Usage message +usage() { + echo "Usage: $0 sign | verify [signature_file]" + exit 1 +} + +# Ensure at least two arguments are provided +if [ "$#" -lt 3 ]; then + usage +fi + +MODE="$1" +FILE_TO_SIGN="$2" + +# Ensure the tarball exists +if [ ! -f "$FILE_TO_SIGN" ]; then + echo "Error: File '$FILE_TO_SIGN' not found." + exit 1 +fi + +# Function to securely convert PEM to OpenSSH if needed +convert_pem_to_ssh() { + local key_file="$1" + local output_file="$2" + + if grep -q "BEGIN RSA PRIVATE KEY" "$key_file" || grep -q "BEGIN OPENSSH PRIVATE KEY" "$key_file"; then + # Ensure cleanup on exit + trap 'rm -f "$output_file" "$output_file.pub"' EXIT + + # Convert PEM key to OpenSSH format + cp "$key_file" "$output_file" && ssh-keygen -c -C "Converted from PEM" -f "$output_file" || { + echo "Error: Failed to convert PEM key to OpenSSH." + exit 1 + } + + # Generate the public key + ssh-keygen -y -f "$key_file" > "$output_file.pub" || { + echo "Error: Failed to generate public key from PEM key." + exit 1 + } + else + echo "$key_file doesn't look like a PEM format key!" + exit 1 + fi +} + +# Sign mode +if [ "$MODE" == "sign" ]; then + PRIVATE_KEY_PEM="$3" + PRIVATE_KEY="conversion_id" + SIG_FILE="${FILE_TO_SIGN}.sig" + PUB_KEY="${PRIVATE_KEY}.pub" + + if [ ! -f "$PRIVATE_KEY_PEM" ]; then + echo "Error: Private key '$PRIVATE_KEY_PEM' not found." + exit 1 + fi + + # Convert PEM key to OpenSSH if needed + echo "Converting SSH key to OpenSSH format..." + convert_pem_to_ssh "$PRIVATE_KEY_PEM" "$PRIVATE_KEY" + + # Sign the tarball + echo "Signing the tarball..." + ssh-keygen -Y sign -f "$PRIVATE_KEY" -n file "$FILE_TO_SIGN" + + if [ ! -f "$SIG_FILE" ]; then + echo "Error: Signing failed." + rm -f "$PRIVATE_KEY" "$PUB_KEY" + exit 1 + fi + + echo "Signature created: $SIG_FILE" + echo -e "\nAn allowed signatures file for this key would have contents like:\n" + echo -e "some_name $(cat "$PUB_KEY")\n" + + # Verify the signature + echo -e "Validating the signature of the file..." + if ssh-keygen -Y check-novalidate -n file -f "$PUB_KEY" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then + echo "- Signature validation successful." + else + echo "- Signature validation failed." + rm -f "$PRIVATE_KEY" "$PUB_KEY" + exit 1 + fi + + rm -f "$PRIVATE_KEY" "$PUB_KEY" + +# Verify mode +elif [ "$MODE" == "verify" ]; then + ALLOWED_SIGNERS_FILE="$3" + SIG_FILE="${4:-${FILE_TO_SIGN}.sig}" + + if [ ! -f "$ALLOWED_SIGNERS_FILE" ]; then + echo "Error: Allowed signers file '$ALLOWED_SIGNERS_FILE' not found." + exit 1 + fi + + if [ ! -f "$SIG_FILE" ]; then + echo "Error: Signature file '$SIG_FILE' not found." + exit 1 + fi + + # Loop through each line of the allowed_signers file + while read -r identity pub_key; do + # Check if identity and public key are not empty + if [[ -n "$identity" && -n "$pub_key" ]]; then + echo "Verifying signature for identity: $identity" + + # Use ssh-keygen to verify the signature (example with RSA) + if ssh-keygen -Y verify -f "$ALLOWED_SIGNERS_FILE" -n file -I "$identity" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then + echo "Signature is valid for identity: $identity" + exit 0 # Exit once we find a valid signature + else + echo "Invalid signature for identity: $identity" + fi + fi + done < "$ALLOWED_SIGNERS_FILE" + + echo "No valid signature found in allowed signers. The allowed signers file should contain entries in the format: ." + exit 1 + +else + usage +fi + From fe2959770f105fb0cee79be2f41a9a923c16d124 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Thu, 27 Feb 2025 13:52:16 +0100 Subject: [PATCH 02/13] Add test for script --- .github/workflows/tests_scripts.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index de227dfc25..94c2dc119c 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: @@ -89,6 +91,20 @@ jobs: sed -i "s@./EESSI-install-software.sh@./EESSI-install-software.sh --skip-cuda-install@g" install_software_layer.sh ./eessi_container.sh --mode run --verbose /software-layer/install_software_layer.sh + - 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 "" + # Create a file to sign + echo "I will sign this" > check_ssh_signing.txt + # Sign the file + ./sign_verify_file_ssh.sh sign check_ssh_signing.txt id_rsa.pem + # Create an allowed_signers file based on the public key + echo -n "allowed_signer " > allowed_signers + cat id_rsa.pem.pub >> allowed_signers + # Verify the signature + ./sign_verify_file_ssh.sh verify check_ssh_signing.txt allowed_signers + - name: test create_directory_tarballs.sh script run: | # bind current directory into container as /software-layer From d3441d185ccca6d96d08aa294c8a073773774c51 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Thu, 27 Feb 2025 14:17:32 +0100 Subject: [PATCH 03/13] Test the signing aspect of the script in the container --- .github/workflows/tests_scripts.yml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 94c2dc119c..3a263a954c 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -91,30 +91,32 @@ jobs: sed -i "s@./EESSI-install-software.sh@./EESSI-install-software.sh --skip-cuda-install@g" install_software_layer.sh ./eessi_container.sh --mode run --verbose /software-layer/install_software_layer.sh + - name: test create_directory_tarballs.sh script + run: | + # bind current directory into container as /software-layer + export SINGULARITY_BIND="${PWD}:/software-layer" + # scripts need to be copied to /tmp, + # since create_directory_tarballs.sh must be accessible from within build container + ./eessi_container.sh --mode run --verbose /software-layer/create_directory_tarballs.sh 2023.06 + # 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 "" # Create a file to sign echo "I will sign this" > check_ssh_signing.txt - # Sign the file - ./sign_verify_file_ssh.sh sign check_ssh_signing.txt id_rsa.pem + # bind current directory into container as /software-layer + export SINGULARITY_BIND="${PWD}:/software-layer" + # Sign the file in the container + ./eessi_container.sh --mode run --verbose /software-layer/sign_verify_file_ssh.sh sign /software-layer/check_ssh_signing.txt /software-layer/id_rsa.pem # Create an allowed_signers file based on the public key echo -n "allowed_signer " > allowed_signers cat id_rsa.pem.pub >> allowed_signers # Verify the signature ./sign_verify_file_ssh.sh verify check_ssh_signing.txt allowed_signers - - name: test create_directory_tarballs.sh script - run: | - # bind current directory into container as /software-layer - export SINGULARITY_BIND="${PWD}:/software-layer" - # scripts need to be copied to /tmp, - # since create_directory_tarballs.sh must be accessible from within build container - ./eessi_container.sh --mode run --verbose /software-layer/create_directory_tarballs.sh 2023.06 - # check if tarballs have been produced - ls -l *.tar.gz - - name: test create_lmodsitepackage.py script run: | # bind current directory into container as /software-layer From 2a88400b259c8dab1f2fad4433bba22e4f51b623 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Thu, 27 Feb 2025 14:34:42 +0100 Subject: [PATCH 04/13] More accurately represent expected workflow --- .github/workflows/tests_scripts.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 3a263a954c..8af547e9ac 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -105,12 +105,10 @@ jobs: run: | # Create a PEM format ssh identity ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa.pem -N "" - # Create a file to sign - echo "I will sign this" > check_ssh_signing.txt - # bind current directory into container as /software-layer - export SINGULARITY_BIND="${PWD}:/software-layer" - # Sign the file in the container - ./eessi_container.sh --mode run --verbose /software-layer/sign_verify_file_ssh.sh sign /software-layer/check_ssh_signing.txt /software-layer/id_rsa.pem + # Find a file to sign + export TARBALL="$(ls *.tar.gz | tail -1)" + # Sign the file + ./sign_verify_file_ssh.sh sign "$TARBALL" id_rsa.pem # Create an allowed_signers file based on the public key echo -n "allowed_signer " > allowed_signers cat id_rsa.pem.pub >> allowed_signers From a4f66c2e40aac93fae7861f90e111d02b06bf3b6 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Thu, 27 Feb 2025 14:41:34 +0100 Subject: [PATCH 05/13] Use $TARBALL everywhere --- .github/workflows/tests_scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 8af547e9ac..c27ef24384 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -113,7 +113,7 @@ jobs: echo -n "allowed_signer " > allowed_signers cat id_rsa.pem.pub >> allowed_signers # Verify the signature - ./sign_verify_file_ssh.sh verify check_ssh_signing.txt allowed_signers + ./sign_verify_file_ssh.sh verify "$TARBALL" allowed_signers - name: test create_lmodsitepackage.py script run: | From ea15255a24e67ed3819fd5c9024df2be8736e077 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Thu, 27 Feb 2025 14:43:29 +0100 Subject: [PATCH 06/13] Remove references to tarball in script, can be any file --- sign_verify_file_ssh.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh index 26dc79efe3..422f0415f6 100755 --- a/sign_verify_file_ssh.sh +++ b/sign_verify_file_ssh.sh @@ -14,7 +14,7 @@ fi MODE="$1" FILE_TO_SIGN="$2" -# Ensure the tarball exists +# Ensure the file exists if [ ! -f "$FILE_TO_SIGN" ]; then echo "Error: File '$FILE_TO_SIGN' not found." exit 1 @@ -62,8 +62,8 @@ if [ "$MODE" == "sign" ]; then echo "Converting SSH key to OpenSSH format..." convert_pem_to_ssh "$PRIVATE_KEY_PEM" "$PRIVATE_KEY" - # Sign the tarball - echo "Signing the tarball..." + # Sign the file + echo "Signing the file..." ssh-keygen -Y sign -f "$PRIVATE_KEY" -n file "$FILE_TO_SIGN" if [ ! -f "$SIG_FILE" ]; then From 524760c10262598d8ed8ef64e550075abd17db59 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Tue, 4 Mar 2025 17:37:18 +0100 Subject: [PATCH 07/13] Address review and update the script --- .github/workflows/tests_scripts.yml | 13 ++- sign_verify_file_ssh.sh | 157 +++++++++++++++++----------- 2 files changed, 107 insertions(+), 63 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index c27ef24384..c4247eb53f 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -108,12 +108,19 @@ jobs: # Find a file to sign export TARBALL="$(ls *.tar.gz | tail -1)" # Sign the file - ./sign_verify_file_ssh.sh sign "$TARBALL" id_rsa.pem + ./sign_verify_file_ssh.sh sign id_rsa.pem "$TARBALL" # Create an allowed_signers file based on the public key - echo -n "allowed_signer " > allowed_signers + echo -n "allowed_identity " > allowed_signers cat id_rsa.pem.pub >> allowed_signers # Verify the signature - ./sign_verify_file_ssh.sh verify "$TARBALL" allowed_signers + ./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 "" + # Sign the file again + rm "$TARBALL".sig + ./sign_verify_file_ssh.sh sign id_rsa.alt.pem "$TARBALL" + # Make sure signature checking fails + ./sign_verify_file_ssh.sh verify allowed_signers "$TARBALL" && exit 1 || echo "Expected failure for unknown identity" - name: test create_lmodsitepackage.py script run: | diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh index 422f0415f6..552ce81635 100755 --- a/sign_verify_file_ssh.sh +++ b/sign_verify_file_ssh.sh @@ -1,18 +1,39 @@ #!/bin/bash +# SSH Signature Signing and Verification Script +# - Generate a digital signature for a file using an SSH private key. +# - Verify the signature of a signed file using an allowed signers file. +# +# The script generates a signature file named `.sig` in the same directory. # Usage message usage() { - echo "Usage: $0 sign | verify [signature_file]" + echo "This script allows you to securely sign files using an SSH private key and verify signatures using an allowed signers file." + echo "Usage:" + echo " $0 sign " + echo " $0 verify [signature_file]" + echo "where" + echo "- : Path to the SSH private key (if a KEY_passphrase exists it can be" + echo " provided via the KEY_PASSPHRASE environment variable)" + echo "- : Path to the file to be signed/verified" + echo "- : Path to the allowed signers file" + echo "- [signature_file]: (optional) Path to the signature file" + echo " (defaults to '.sig' if not provided)." + echo exit 1 } -# Ensure at least two arguments are provided +# Error codes +FILE_PROBLEM=1 +CONVERSION_FAILURE=2 +VALIDATION_FAILED=3 + +# Ensure at least three arguments are provided if [ "$#" -lt 3 ]; then usage fi MODE="$1" -FILE_TO_SIGN="$2" +FILE_TO_SIGN="$3" # Ensure the file exists if [ ! -f "$FILE_TO_SIGN" ]; then @@ -20,77 +41,79 @@ if [ ! -f "$FILE_TO_SIGN" ]; then exit 1 fi -# Function to securely convert PEM to OpenSSH if needed -convert_pem_to_ssh() { +# Function to securely convert the private key to OpenSSH format +# (which is required for signing) +convert_private_key_to_openssh_format() { local key_file="$1" local output_file="$2" - if grep -q "BEGIN RSA PRIVATE KEY" "$key_file" || grep -q "BEGIN OPENSSH PRIVATE KEY" "$key_file"; then - # Ensure cleanup on exit - trap 'rm -f "$output_file" "$output_file.pub"' EXIT - - # Convert PEM key to OpenSSH format - cp "$key_file" "$output_file" && ssh-keygen -c -C "Converted from PEM" -f "$output_file" || { - echo "Error: Failed to convert PEM key to OpenSSH." - exit 1 - } - - # Generate the public key - ssh-keygen -y -f "$key_file" > "$output_file.pub" || { - echo "Error: Failed to generate public key from PEM key." - exit 1 - } - else - echo "$key_file doesn't look like a PEM format key!" - exit 1 - fi + # Convert the key to OpenSSH format (the default format hence no '-m ') for any input format + # (first copy the file as it will be overwritten during the conversion) + echo "Copying $key_file to $output_file and performing format conversion" + cp "$key_file" "$output_file" || { + echo "Copy failed" + exit $FILE_PROBLEM + } + ssh-keygen -p -f "$output_file" -P "$KEY_PASSPHRASE" -N "$KEY_PASSPHRASE" || { + echo "Error: Failed to convert key $key_file to OpenSSH format" + echo "(set the environment variable KEY_PASSPHRASE to use a passphrase for the key)." + exit $CONVERSION_FAILURE + } + + # Generate the public key from the private key + ssh-keygen -y -f "$key_file" -P "$KEY_PASSPHRASE"> "$output_file.pub" || { + echo "Error: Failed to generate public key from PEM key." + exit $CONVERSION_FAILURE + } } # Sign mode if [ "$MODE" == "sign" ]; then - PRIVATE_KEY_PEM="$3" + PRIVATE_KEY_ORIG="$2" PRIVATE_KEY="conversion_id" SIG_FILE="${FILE_TO_SIGN}.sig" PUB_KEY="${PRIVATE_KEY}.pub" - - if [ ! -f "$PRIVATE_KEY_PEM" ]; then - echo "Error: Private key '$PRIVATE_KEY_PEM' not found." - exit 1 + + # Ensure cleanup on exit of our temporary key + trap 'rm -f "$PRIVATE_KEY" "$PUB_KEY"' EXIT + + if [ ! -f "$PRIVATE_KEY_ORIG" ]; then + echo "Error: Private key '$PRIVATE_KEY_ORIG' not found." + exit $FILE_PROBLEM fi - - # Convert PEM key to OpenSSH if needed + if [ -f "$SIG_FILE" ]; then + echo "Error: Signature file '$SIG_FILE' already exists. Please remove to re-sign!" + exit $FILE_PROBLEM + fi + # Convert key to OpenSSH format echo "Converting SSH key to OpenSSH format..." - convert_pem_to_ssh "$PRIVATE_KEY_PEM" "$PRIVATE_KEY" + convert_private_key_to_openssh_format "$PRIVATE_KEY_ORIG" "$PRIVATE_KEY" # Sign the file echo "Signing the file..." - ssh-keygen -Y sign -f "$PRIVATE_KEY" -n file "$FILE_TO_SIGN" + ssh-keygen -Y sign -f "$PRIVATE_KEY" -P "$KEY_PASSPHRASE" -n file "$FILE_TO_SIGN" if [ ! -f "$SIG_FILE" ]; then - echo "Error: Signing failed." - rm -f "$PRIVATE_KEY" "$PUB_KEY" - exit 1 + echo "Error: Signing failed, no file $SIG_FILE found." + exit $FILE_PROBLEM fi echo "Signature created: $SIG_FILE" - echo -e "\nAn allowed signatures file for this key would have contents like:\n" - echo -e "some_name $(cat "$PUB_KEY")\n" + echo -e "\nAn allowed signatures file has the format:" + echo -e "\n \n" + echo -e "and so for the provided key could have contents like:\n" + echo -e "identity_1 $(cat "$PUB_KEY")\n" # Verify the signature echo -e "Validating the signature of the file..." - if ssh-keygen -Y check-novalidate -n file -f "$PUB_KEY" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then - echo "- Signature validation successful." - else + ssh-keygen -Y check-novalidate -n file -f "$PUB_KEY" -s "$SIG_FILE" < "$FILE_TO_SIGN" || { echo "- Signature validation failed." - rm -f "$PRIVATE_KEY" "$PUB_KEY" - exit 1 - fi - - rm -f "$PRIVATE_KEY" "$PUB_KEY" + exit $VALIDATION_FAILED + } # Verify mode elif [ "$MODE" == "verify" ]; then - ALLOWED_SIGNERS_FILE="$3" + ALLOWED_SIGNERS_FILE="$2" SIG_FILE="${4:-${FILE_TO_SIGN}.sig}" if [ ! -f "$ALLOWED_SIGNERS_FILE" ]; then @@ -104,23 +127,37 @@ elif [ "$MODE" == "verify" ]; then fi # Loop through each line of the allowed_signers file - while read -r identity pub_key; do - # Check if identity and public key are not empty - if [[ -n "$identity" && -n "$pub_key" ]]; then - echo "Verifying signature for identity: $identity" - - # Use ssh-keygen to verify the signature (example with RSA) - if ssh-keygen -Y verify -f "$ALLOWED_SIGNERS_FILE" -n file -I "$identity" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then - echo "Signature is valid for identity: $identity" - exit 0 # Exit once we find a valid signature + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip empty lines or comments + [[ -z "$line" || "$line" == \#* ]] && continue + + # Extract principals (field 1) + principals=$(echo "$line" | cut -d' ' -f1) + echo "$principals" + + # Iterate over each principal (comma-separated) + OLD_IFS=$IFS + IFS=',' read -ra principal_list <<< "$principals" + IFS=$OLD_IFS + + for principal in "${principal_list[@]}"; do + echo "Processing Principal: $principal" + + # Use ssh-keygen to verify the signature + if ssh-keygen -Y verify -f "$ALLOWED_SIGNERS_FILE" -n file -I "$principal" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then + echo -e "\nSignature is valid for principal: $principal" + exit 0 # Exit on first valid signature else - echo "Invalid signature for identity: $identity" + echo "Invalid signature for principal: $principal" fi - fi + done done < "$ALLOWED_SIGNERS_FILE" - echo "No valid signature found in allowed signers. The allowed signers file should contain entries in the format: ." - exit 1 + echo + echo "No valid signature found in allowed signers." + echo "The allowed signers file should contain entries in the format:" + echo " " + exit $VALIDATION_FAILED else usage From 0c8b7cb0fba20aaa687f9efc07b7d0e336430393 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Tue, 4 Mar 2025 20:09:38 +0100 Subject: [PATCH 08/13] Another round of tidying up --- sign_verify_file_ssh.sh | 195 +++++++++++++++++++--------------------- 1 file changed, 92 insertions(+), 103 deletions(-) diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh index 552ce81635..f5f9b4d8e0 100755 --- a/sign_verify_file_ssh.sh +++ b/sign_verify_file_ssh.sh @@ -1,25 +1,47 @@ #!/bin/bash +# # SSH Signature Signing and Verification Script -# - Generate a digital signature for a file using an SSH private key. -# - Verify the signature of a signed file using an allowed signers file. +# - 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 # -# The script generates a signature file named `.sig` in the same directory. +# 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() { - echo "This script allows you to securely sign files using an SSH private key and verify signatures using an allowed signers file." - echo "Usage:" - echo " $0 sign " - echo " $0 verify [signature_file]" - echo "where" - echo "- : Path to the SSH private key (if a KEY_passphrase exists it can be" - echo " provided via the KEY_PASSPHRASE environment variable)" - echo "- : Path to the file to be signed/verified" - echo "- : Path to the allowed signers file" - echo "- [signature_file]: (optional) Path to the signature file" - echo " (defaults to '.sig' if not provided)." - echo - exit 1 + 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 @@ -27,87 +49,71 @@ FILE_PROBLEM=1 CONVERSION_FAILURE=2 VALIDATION_FAILED=3 -# Ensure at least three arguments are provided -if [ "$#" -lt 3 ]; then - usage -fi +# Ensure minimum arguments +[ "$#" -lt 3 ] && usage MODE="$1" FILE_TO_SIGN="$3" -# Ensure the file exists +# Ensure the target file exists if [ ! -f "$FILE_TO_SIGN" ]; then echo "Error: File '$FILE_TO_SIGN' not found." - exit 1 + exit $FILE_PROBLEM fi -# Function to securely convert the private key to OpenSSH format -# (which is required for signing) -convert_private_key_to_openssh_format() { - local key_file="$1" - local output_file="$2" - - # Convert the key to OpenSSH format (the default format hence no '-m ') for any input format - # (first copy the file as it will be overwritten during the conversion) - echo "Copying $key_file to $output_file and performing format conversion" - cp "$key_file" "$output_file" || { - echo "Copy failed" - exit $FILE_PROBLEM - } - ssh-keygen -p -f "$output_file" -P "$KEY_PASSPHRASE" -N "$KEY_PASSPHRASE" || { - echo "Error: Failed to convert key $key_file to OpenSSH format" - echo "(set the environment variable KEY_PASSPHRASE to use a passphrase for the key)." +# Create a restricted temporary directory and ensure cleanup on exit +TEMP_DIR=$(mktemp -d) || { echo "Error: Failed to create temporary directory."; exit $FILE_PROBLEM; } +chmod 700 "$TEMP_DIR" +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 } - # Generate the public key from the private key - ssh-keygen -y -f "$key_file" -P "$KEY_PASSPHRASE"> "$output_file.pub" || { - echo "Error: Failed to generate public key from PEM key." + # 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_ORIG="$2" - PRIVATE_KEY="conversion_id" + PRIVATE_KEY="$2" + TEMP_KEY="$TEMP_DIR/converted_key" SIG_FILE="${FILE_TO_SIGN}.sig" - PUB_KEY="${PRIVATE_KEY}.pub" - - # Ensure cleanup on exit of our temporary key - trap 'rm -f "$PRIVATE_KEY" "$PUB_KEY"' EXIT - - if [ ! -f "$PRIVATE_KEY_ORIG" ]; then - echo "Error: Private key '$PRIVATE_KEY_ORIG' not found." - exit $FILE_PROBLEM - fi - if [ -f "$SIG_FILE" ]; then - echo "Error: Signature file '$SIG_FILE' already exists. Please remove to re-sign!" - exit $FILE_PROBLEM - fi - # Convert key to OpenSSH format - echo "Converting SSH key to OpenSSH format..." - convert_private_key_to_openssh_format "$PRIVATE_KEY_ORIG" "$PRIVATE_KEY" - # Sign the file - echo "Signing the file..." - ssh-keygen -Y sign -f "$PRIVATE_KEY" -P "$KEY_PASSPHRASE" -n file "$FILE_TO_SIGN" + # 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; } - if [ ! -f "$SIG_FILE" ]; then - echo "Error: Signing failed, no file $SIG_FILE found." - exit $FILE_PROBLEM - fi + 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" - echo -e "\nAn allowed signatures file has the format:" - echo -e "\n \n" - echo -e "and so for the provided key could have contents like:\n" - echo -e "identity_1 $(cat "$PUB_KEY")\n" - - # Verify the signature - echo -e "Validating the signature of the file..." - ssh-keygen -Y check-novalidate -n file -f "$PUB_KEY" -s "$SIG_FILE" < "$FILE_TO_SIGN" || { - echo "- Signature validation failed." + + cat < " + echo "Error: No valid signature found." exit $VALIDATION_FAILED else usage fi - From 9d90c12ba9537b336850e6713caf1d52fdaf599f Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Wed, 5 Mar 2025 09:02:32 +0100 Subject: [PATCH 09/13] Keep the signed file the same in tests, change the allowed signers instead --- .github/workflows/tests_scripts.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index c4247eb53f..d33dd2ccb6 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -116,10 +116,10 @@ jobs: ./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 "" - # Sign the file again - rm "$TARBALL".sig - ./sign_verify_file_ssh.sh sign id_rsa.alt.pem "$TARBALL" - # Make sure signature checking fails + # Replace the allowed signers file + echo -n "disallowed_identity " > allowed_signers + cat id_rsa.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 From 4896ac2f47486ee9b146da7f6628fa6a28267ed5 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Wed, 5 Mar 2025 12:06:13 +0100 Subject: [PATCH 10/13] Use correct key for second case --- .github/workflows/tests_scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index d33dd2ccb6..44cf6b262e 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -118,7 +118,7 @@ jobs: 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.pem.pub >> allowed_signers + cat id_rsa.pem.alt.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" From 739ca78f2b4fcbb27c3771f8c4c4deb2576a9d88 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Wed, 5 Mar 2025 12:50:51 +0100 Subject: [PATCH 11/13] Use correct key for second case --- .github/workflows/tests_scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 44cf6b262e..f4e9c0f496 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -118,7 +118,7 @@ jobs: 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.pem.alt.pub >> 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" From 2be31133d3f7e3d4caf622c8b8e2c14795d3d303 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 10 Mar 2025 12:04:37 +0100 Subject: [PATCH 12/13] Use conservative umask for entire script --- sign_verify_file_ssh.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh index f5f9b4d8e0..81c719f16e 100755 --- a/sign_verify_file_ssh.sh +++ b/sign_verify_file_ssh.sh @@ -61,9 +61,11 @@ if [ ! -f "$FILE_TO_SIGN" ]; then exit $FILE_PROBLEM fi +# Use a very conservatuve umask throughout this script since we are dealing with sensitive things +umask 0177 || { 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; } -chmod 700 "$TEMP_DIR" trap 'rm -rf "$TEMP_DIR"' EXIT # Converts the SSH private key to OpenSSH format and generates a public key From d7bc52ae291af8111e1cda1b63dec176c6327b64 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 10 Mar 2025 12:29:07 +0100 Subject: [PATCH 13/13] umask too restrictive --- sign_verify_file_ssh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sign_verify_file_ssh.sh b/sign_verify_file_ssh.sh index 81c719f16e..679ea7d649 100755 --- a/sign_verify_file_ssh.sh +++ b/sign_verify_file_ssh.sh @@ -62,7 +62,7 @@ if [ ! -f "$FILE_TO_SIGN" ]; then fi # Use a very conservatuve umask throughout this script since we are dealing with sensitive things -umask 0177 || { echo "Error: Failed to set 0177 umask."; exit $FILE_PROBLEM; } +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; }