From c6048c7d53df659947224a1de0c54f7dc0b3745d Mon Sep 17 00:00:00 2001 From: Thomas Roeblitz Date: Fri, 14 Mar 2025 22:05:52 +0100 Subject: [PATCH 1/7] use bot's app_name as namespace when signing files --- scripts/eessi-upload-to-staging | 11 +++++-- scripts/sign_verify_file_ssh.sh | 52 ++++++++++++++++++--------------- tasks/deploy.py | 3 ++ 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/scripts/eessi-upload-to-staging b/scripts/eessi-upload-to-staging index 25fd9675..46557338 100755 --- a/scripts/eessi-upload-to-staging +++ b/scripts/eessi-upload-to-staging @@ -75,6 +75,8 @@ function display_help echo " expansion will be applied; arg '-l'" >&2 echo " lists variables that are defined at" >&2 echo " the time of expansion" >&2 + echo " -b | --bot-instance NAME - name of the bot instance that uploads" >&2 + echo " files to S3" >&2 echo " -e | --endpoint-url URL - endpoint url (needed for non AWS S3)" >&2 echo " -h | --help - display this usage information" >&2 echo " -i | --pr-comment-id - identifier of a PR comment; may be" >&2 @@ -134,6 +136,7 @@ sign_key= sign_script= # provided via options in the bot's config file app.cfg and/or command line argument +bot_instance= metadata_prefix= artefact_prefix= @@ -147,6 +150,10 @@ while [[ $# -gt 0 ]]; do artefact_prefix="$2" shift 2 ;; + -b|--bot-instance) + bot_instance="$2" + shift 2 + ;; -e|--endpoint-url) endpoint_url="$2" shift 2 @@ -263,7 +270,7 @@ for file in "$*"; do # 1st sign artefact, and upload signature if [[ "${sign}" = "1" ]]; then # sign artefact - ${sign_script} sign ${sign_key} ${file} + ${sign_script} sign ${sign_key} ${file} ${bot_instance} # TODO check if signing worked (just check exit code == 0) sig_file=${file}.sig aws_sig_file=${aws_file}.sig @@ -314,7 +321,7 @@ for file in "$*"; do # 2nd sign metadata file, and upload signature if [[ "${sign}" = "1" ]]; then # sign metadata file - ${sign_script} sign ${sign_key} ${metadata_file} + ${sign_script} sign ${sign_key} ${metadata_file} ${bot_instance} # TODO check if signing worked (just check exit code == 0) sig_metadata_file=${metadata_file}.sig aws_sig_metadata_file=${aws_metadata_file}.sig diff --git a/scripts/sign_verify_file_ssh.sh b/scripts/sign_verify_file_ssh.sh index 679ea7d6..c79e94c0 100755 --- a/scripts/sign_verify_file_ssh.sh +++ b/scripts/sign_verify_file_ssh.sh @@ -25,13 +25,14 @@ usage() { cat < + $0 sign $0 verify [signature_file] Options: sign: - : Path to SSH private key (use KEY_PASSPHRASE env for passphrase) - : File to sign + - : Additional information to limit scope of the signature verify: - : Path to the allowed signers file @@ -39,7 +40,7 @@ Options: - [signature_file]: Optional, defaults to '.sig' Example allowed signers format: - identity_1 + identity_1 namespaces="namespace",valid-before="last-valid-day" EOF exit 9 } @@ -48,12 +49,14 @@ EOF FILE_PROBLEM=1 CONVERSION_FAILURE=2 VALIDATION_FAILED=3 +MISSING_NAMESPACE=4 # Ensure minimum arguments [ "$#" -lt 3 ] && usage MODE="$1" FILE_TO_SIGN="$3" +NAMESPACE="$4" # Ensure the target file exists if [ ! -f "$FILE_TO_SIGN" ]; then @@ -102,19 +105,24 @@ if [ "$MODE" == "sign" ]; then 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 < Date: Fri, 14 Mar 2025 22:20:17 +0100 Subject: [PATCH 2/7] adjust and extend CI for sign/verify script --- .github/workflows/tests_scripts.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 1f8d012b..9c76744a 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -24,16 +24,30 @@ jobs: echo "Very important stuff" > out.txt export FILE_TO_SIGN="out.txt" # Sign the file - ./scripts/sign_verify_file_ssh.sh sign id_rsa.pem "$FILE_TO_SIGN" + ./scripts/sign_verify_file_ssh.sh sign id_rsa.pem "$FILE_TO_SIGN" ci # Create an allowed_signers file based on the public key - echo -n "allowed_identity " > allowed_signers + valid_before=$(date --date='today+3days' +%Y%m%d) + echo -n 'allowed_identity namespaces="ci",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.pem.pub >> allowed_signers # Verify the signature ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" # 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 + echo -n 'disallowed_identity namespaces="ci",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.alt.pem.pub >> allowed_signers # Make sure signature checking fails in this case ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" && exit 1 || echo "Expected failure for unknown identity" + # Check if wrong namespace let verification fail + # Replace the allowed signers file + echo -n 'wrong_namespace_identity namespaces="CI",valid-before="'$valid_before'" ' > allowed_signers + cat id_rsa.pem.pub >> allowed_signers + # Make sure signature checking fails in this case + ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" && exit 2 || echo "Expected failure for wrong namespace" + # Check if expired key let verification fail + # Replace the allowed signers file + valid_expired=$(date --date='today-3days' +%Y%m%d) + echo -n 'expired_key_identity namespaces="ci",valid-before="'$valid_expired'" ' > allowed_signers + cat id_rsa.pem.pub >> allowed_signers + # Make sure signature checking fails in this case + ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" && exit 3 || echo "Expected failure for expired key" From 0f67582d6c4edf1f85d0fe7185cec653edc3e1a6 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 17 Mar 2025 11:28:36 +0100 Subject: [PATCH 3/7] Use named arguments and parsing in ssh signing script --- .github/workflows/tests_scripts.yml | 62 +++++++----- scripts/eessi-upload-to-staging | 4 +- scripts/sign_verify_file_ssh.sh | 143 ++++++++++++++++++++-------- 3 files changed, 146 insertions(+), 63 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 9c76744a..a4ca010f 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -13,41 +13,57 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - name: checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: test sign_verify_file_ssh.sh script - run: | - # Create a PEM format ssh identity + - name: Prepare SSH key pair, file and signature + run: | ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa.pem -N "" - # Create a file to sign echo "Very important stuff" > out.txt export FILE_TO_SIGN="out.txt" - # Sign the file - ./scripts/sign_verify_file_ssh.sh sign id_rsa.pem "$FILE_TO_SIGN" ci - # Create an allowed_signers file based on the public key + ./scripts/sign_verify_file_ssh.sh --sign --private-key id_rsa.pem --file "$FILE_TO_SIGN" --namespace ci + + - name: Create allowed signers file and verify + run: | valid_before=$(date --date='today+3days' +%Y%m%d) echo -n 'allowed_identity namespaces="ci",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.pem.pub >> allowed_signers - # Verify the signature - ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" - # Make a new signature that does not appear in the allowed signers file + ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt + + - name: Replace allowed signers with disallowed identity + run: | ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa.alt.pem -N "" - # Replace the allowed signers file echo -n 'disallowed_identity namespaces="ci",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.alt.pem.pub >> allowed_signers - # Make sure signature checking fails in this case - ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" && exit 1 || echo "Expected failure for unknown identity" - # Check if wrong namespace let verification fail - # Replace the allowed signers file + + - name: Ensure verification fails for unknown identity + run: | + ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt && exit 1 || echo "Expected failure for unknown identity" + + - name: Replace allowed signers with wrong namespace + run: | echo -n 'wrong_namespace_identity namespaces="CI",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.pem.pub >> allowed_signers - # Make sure signature checking fails in this case - ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" && exit 2 || echo "Expected failure for wrong namespace" - # Check if expired key let verification fail - # Replace the allowed signers file + + - name: Ensure verification fails for wrong namespace + run: | + ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt && exit 2 || echo "Expected failure for wrong namespace" + + - name: Replace allowed signers with expired key + run: | valid_expired=$(date --date='today-3days' +%Y%m%d) echo -n 'expired_key_identity namespaces="ci",valid-before="'$valid_expired'" ' > allowed_signers cat id_rsa.pem.pub >> allowed_signers - # Make sure signature checking fails in this case - ./scripts/sign_verify_file_ssh.sh verify allowed_signers "$FILE_TO_SIGN" && exit 3 || echo "Expected failure for expired key" + + - name: Ensure verification fails for expired key + run: | + ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt && exit 3 || echo "Expected failure for expired key" + + - name: Ensure verification when looping through allowed signers file + run: | + # Add the approved identity to the end + echo -n 'listed_identity namespaces="ci",valid-before="'$valid_before'" ' >> allowed_signers + cat id_rsa.pem.pub >> allowed_signers + ../scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt + # Make sure we get exactly what we want in terse mode + ../scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt --terse | grep -q '{\"identity\": \"listed_identity\", \"namespace\": \"ci\"}' diff --git a/scripts/eessi-upload-to-staging b/scripts/eessi-upload-to-staging index 46557338..fa3d7f57 100755 --- a/scripts/eessi-upload-to-staging +++ b/scripts/eessi-upload-to-staging @@ -270,7 +270,7 @@ for file in "$*"; do # 1st sign artefact, and upload signature if [[ "${sign}" = "1" ]]; then # sign artefact - ${sign_script} sign ${sign_key} ${file} ${bot_instance} + ${sign_script} --sign --private-key ${sign_key} --file ${file} --namespace ${bot_instance} # TODO check if signing worked (just check exit code == 0) sig_file=${file}.sig aws_sig_file=${aws_file}.sig @@ -321,7 +321,7 @@ for file in "$*"; do # 2nd sign metadata file, and upload signature if [[ "${sign}" = "1" ]]; then # sign metadata file - ${sign_script} sign ${sign_key} ${metadata_file} ${bot_instance} + ${sign_script} --sign --private-key ${sign_key} --file ${metadata_file} --namespace ${bot_instance} # TODO check if signing worked (just check exit code == 0) sig_metadata_file=${metadata_file}.sig aws_sig_metadata_file=${aws_metadata_file}.sig diff --git a/scripts/sign_verify_file_ssh.sh b/scripts/sign_verify_file_ssh.sh index c79e94c0..2c5717d2 100755 --- a/scripts/sign_verify_file_ssh.sh +++ b/scripts/sign_verify_file_ssh.sh @@ -7,6 +7,7 @@ # Generates a signature file named `.sig` in the same directory. # # Author: Alan O'Cais +# Author: Thomas Roeblitz # # 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 @@ -23,40 +24,107 @@ # Usage message usage() { + local exit_code=${1:-9} cat < - $0 verify [signature_file] + $0 --sign --private-key --file [--namespace ] + $0 --verify --allowed-signers-file --file [--signature-file ] [--terse] Options: - sign: - - : Path to SSH private key (use KEY_PASSPHRASE env for passphrase) - - : File to sign - - : Additional information to limit scope of the signature + --sign: + --private-key : Path to SSH private key (use KEY_PASSPHRASE env for passphrase) + --file : File to sign + --namespace : Optional, defaults to "file" if not specified - verify: - - : Path to the allowed signers file - - : File to verify - - [signature_file]: Optional, defaults to '.sig' + --verify: + --allowed-signers-file : Path to the allowed signers file + --file : File to verify + --signature-file : Optional, defaults to '.sig' + --terse: If set, output only matching identity and namespace for verification in JSON format Example allowed signers format: identity_1 namespaces="namespace",valid-before="last-valid-day" + +If the private key has a passphrase, this can be provided via a 'KEY_PASSPHRASE' environment variable. EOF - exit 9 + exit "$exit_code" } # Error codes FILE_PROBLEM=1 CONVERSION_FAILURE=2 VALIDATION_FAILED=3 -MISSING_NAMESPACE=4 # Ensure minimum arguments -[ "$#" -lt 3 ] && usage +if [ "$#" -lt 3 ]; then + echo "Error: Missing required arguments." + usage +fi + +# Parse options +TERSE_MODE=false +while [[ "$#" -gt 0 ]]; do + case "$1" in + --sign) + MODE="sign" + shift + ;; + --verify) + MODE="verify" + shift + ;; + --private-key) + PRIVATE_KEY="$2" + shift 2 + ;; + --file) + FILE_TO_SIGN="$2" + shift 2 + ;; + --namespace) + NAMESPACE="$2" + shift 2 + ;; + --allowed-signers-file) + ALLOWED_SIGNERS_FILE="$2" + shift 2 + ;; + --signature-file) + SIG_FILE="$2" + shift 2 + ;; + --terse) + TERSE_MODE=true + shift + ;; + *) + echo "Error: Invalid argument: $1" + usage + ;; + esac +done + +# Set default namespace if not provided +if [ -z "$NAMESPACE" ]; then + NAMESPACE="file" +fi -MODE="$1" -FILE_TO_SIGN="$3" -NAMESPACE="$4" +# Ensure mode is set +if [ -z "$MODE" ]; then + echo "Error: Missing operation mode (either --sign or --verify)" + usage +fi + +# Ensure required arguments +if [ "$MODE" == "sign" ]; then + [ -z "$PRIVATE_KEY" ] && { echo "Error: --private-key not specified."; usage $FILE_PROBLEM; } + [ -z "$FILE_TO_SIGN" ] && { echo "Error: --file not specified."; usage $FILE_PROBLEM; } + SIG_FILE="${FILE_TO_SIGN}.sig" +elif [ "$MODE" == "verify" ]; then + [ -z "$ALLOWED_SIGNERS_FILE" ] && { echo "Error: --allowed-signers-file not specified."; usage $FILE_PROBLEM; } + [ -z "$FILE_TO_SIGN" ] && { echo "Error: --file not specified."; usage $FILE_PROBLEM; } + SIG_FILE="${SIG_FILE:-${FILE_TO_SIGN}.sig}" +fi # Ensure the target file exists if [ ! -f "$FILE_TO_SIGN" ]; then @@ -64,8 +132,8 @@ 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 077 || { echo "Error: Failed to set 0177 umask."; exit $FILE_PROBLEM; } +# Use a very conservative umask throughout this script since we are dealing with sensitive things +umask 0077 || { echo "Error: Failed to set 0077 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; } @@ -94,9 +162,7 @@ convert_private_key() { # 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; } @@ -105,18 +171,13 @@ if [ "$MODE" == "sign" ]; then convert_private_key "$PRIVATE_KEY" "$TEMP_KEY" echo "Signing the file..." - if [[ -n "${NAMESPACE}" ]]; then - ssh-keygen -Y sign -f "$TEMP_KEY" -P "${KEY_PASSPHRASE:-}" -n "${NAMESPACE}" "$FILE_TO_SIGN" - cat < /dev/null 2>&1; then + # Output in JSON format + echo "{\"identity\": \"$principal\", \"namespace\": \"$namespaces\"}" + exit 0 + fi + else + if ssh-keygen -Y verify -f "$ALLOWED_SIGNERS_FILE" -n "$namespaces" -I "$principal" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then + echo "Signature is valid for principal: $principal and namespace: $namespaces" + exit 0 + else + echo + echo "Signature _not_ valid for principal: $principal and namespace: $namespaces" + fi fi done < "$ALLOWED_SIGNERS_FILE" echo "Error: No valid signature found." exit $VALIDATION_FAILED - else + echo "Error: Invalid operation mode. Use --sign or --verify." usage fi From 8da77b00bd5dfaccf1243b126e8b2e33cbfedbdf Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 17 Mar 2025 11:34:53 +0100 Subject: [PATCH 4/7] Wrong path to script in final check --- .github/workflows/tests_scripts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index a4ca010f..aab1d8e3 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -64,6 +64,6 @@ jobs: # Add the approved identity to the end echo -n 'listed_identity namespaces="ci",valid-before="'$valid_before'" ' >> allowed_signers cat id_rsa.pem.pub >> allowed_signers - ../scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt + ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt # Make sure we get exactly what we want in terse mode - ../scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt --terse | grep -q '{\"identity\": \"listed_identity\", \"namespace\": \"ci\"}' + ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt --terse | grep -q '{\"identity\": \"listed_identity\", \"namespace\": \"ci\"}' From 1813229ef3e574f02c96d926f6fa3a1e00f5577e Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 17 Mar 2025 11:41:37 +0100 Subject: [PATCH 5/7] Also run the tests when the testing changes --- .github/workflows/tests_scripts.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index aab1d8e3..873cd13f 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -4,9 +4,11 @@ on: push: paths: - scripts/sign_verify_file_ssh.sh + - .github/workflows/tests_scripts.yml pull_request: paths: - scripts/sign_verify_file_ssh.sh + - .github/workflows/tests_scripts.yml permissions: contents: read # to fetch code (actions/checkout) jobs: From eefaec125df8dbd947bb1230476f02794456d5be Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 17 Mar 2025 11:44:47 +0100 Subject: [PATCH 6/7] Fix test issue --- .github/workflows/tests_scripts.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 873cd13f..6dfd99b8 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -64,6 +64,7 @@ jobs: - name: Ensure verification when looping through allowed signers file run: | # Add the approved identity to the end + valid_before=$(date --date='today+3days' +%Y%m%d) echo -n 'listed_identity namespaces="ci",valid-before="'$valid_before'" ' >> allowed_signers cat id_rsa.pem.pub >> allowed_signers ./scripts/sign_verify_file_ssh.sh --verify --allowed-signers-file allowed_signers --file out.txt From 7a2b0e24c2d36b81f0eb5c856e3b9220a85863f6 Mon Sep 17 00:00:00 2001 From: Alan O'Cais Date: Mon, 17 Mar 2025 11:57:09 +0100 Subject: [PATCH 7/7] Be more careful with variables between steps --- .github/workflows/tests_scripts.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 6dfd99b8..88a473b6 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -34,6 +34,7 @@ jobs: - name: Replace allowed signers with disallowed identity run: | + valid_before=$(date --date='today+3days' +%Y%m%d) ssh-keygen -t rsa -b 4096 -m PEM -f id_rsa.alt.pem -N "" echo -n 'disallowed_identity namespaces="ci",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.alt.pem.pub >> allowed_signers @@ -44,6 +45,7 @@ jobs: - name: Replace allowed signers with wrong namespace run: | + valid_before=$(date --date='today+3days' +%Y%m%d) echo -n 'wrong_namespace_identity namespaces="CI",valid-before="'$valid_before'" ' > allowed_signers cat id_rsa.pem.pub >> allowed_signers