diff --git a/.github/workflows/tests_scripts.yml b/.github/workflows/tests_scripts.yml index 1f8d012b..88a473b6 100644 --- a/.github/workflows/tests_scripts.yml +++ b/.github/workflows/tests_scripts.yml @@ -4,36 +4,71 @@ 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: 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" - # Create an allowed_signers file based on the public key - echo -n "allowed_identity " > allowed_signers + ./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: | + valid_before=$(date --date='today+3days' +%Y%m%d) 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" + + - 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: | + 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 + + - 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 + + - 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 + 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 + # 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 25fd9675..fa3d7f57 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 --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 @@ -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 --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 679ea7d6..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,25 +24,30 @@ # 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 + --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 + 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 @@ -50,10 +56,75 @@ CONVERSION_FAILURE=2 VALIDATION_FAILED=3 # Ensure minimum arguments -[ "$#" -lt 3 ] && usage +if [ "$#" -lt 3 ]; then + echo "Error: Missing required arguments." + usage +fi -MODE="$1" -FILE_TO_SIGN="$3" +# 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 + +# 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 @@ -61,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; } @@ -91,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; } @@ -102,55 +171,57 @@ 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" + 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 - done + 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 diff --git a/tasks/deploy.py b/tasks/deploy.py index 0baa8ee4..37ad3adf 100644 --- a/tasks/deploy.py +++ b/tasks/deploy.py @@ -266,6 +266,8 @@ def upload_artefact(job_dir, payload, timestamp, repo_name, pr_number, pr_commen metadata_prefix = deploycfg.get(config.DEPLOYCFG_SETTING_METADATA_PREFIX) artefact_prefix = deploycfg.get(config.DEPLOYCFG_SETTING_ARTEFACT_PREFIX) signing_str = deploycfg.get(config.DEPLOYCFG_SETTING_SIGNING) or '' + github = cfg[config.SECTION_GITHUB] + app_name = github.get(config.GITHUB_SETTING_APP_NAME) try: signing = json.loads(signing_str) except json.decoder.JSONDecodeError: @@ -375,6 +377,7 @@ def upload_artefact(job_dir, payload, timestamp, repo_name, pr_number, pr_commen cmd_args.extend(['--pr-comment-id', str(pr_comment_id)]) cmd_args.extend(['--pull-request-number', str(pr_number)]) cmd_args.extend(['--repository', repo_name]) + cmd_args.extend(['--bot-instance', app_name]) cmd_args.extend(sign_args) cmd_args.append(abs_path)