Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions .github/workflows/tests_scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}'
11 changes: 9 additions & 2 deletions scripts/eessi-upload-to-staging
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
155 changes: 113 additions & 42 deletions scripts/sign_verify_file_ssh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# Generates a signature file named `<file>.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
Expand All @@ -23,25 +24,30 @@

# Usage message
usage() {
local exit_code=${1:-9}
cat <<EOF
Usage:
$0 sign <private_key> <file>
$0 verify <allowed_signers_file> <file> [signature_file]
$0 --sign --private-key <private_key> --file <file> [--namespace <namespace>]
$0 --verify --allowed-signers-file <allowed_signers_file> --file <file> [--signature-file <signature_file>] [--terse]

Options:
sign:
- <private_key>: Path to SSH private key (use KEY_PASSPHRASE env for passphrase)
- <file>: File to sign
--sign:
--private-key <private_key>: Path to SSH private key (use KEY_PASSPHRASE env for passphrase)
--file <file>: File to sign
--namespace <namespace>: Optional, defaults to "file" if not specified

verify:
- <allowed_signers_file>: Path to the allowed signers file
- <file>: File to verify
- [signature_file]: Optional, defaults to '<file>.sig'
--verify:
--allowed-signers-file <allowed_signers_file>: Path to the allowed signers file
--file <file>: File to verify
--signature-file <signature_file>: Optional, defaults to '<file>.sig'
--terse: If set, output only matching identity and namespace for verification in JSON format

Example allowed signers format:
identity_1 <public-key>
identity_1 namespaces="namespace",valid-before="last-valid-day" <public-key>

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
Expand All @@ -50,19 +56,84 @@ 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
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; }
# 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; }
Expand Down Expand Up @@ -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; }
Expand All @@ -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 <<EOF

For verification, your allowed signers file could contain:
identity_1 $(cat "${TEMP_KEY}.pub")
identity_1 namespaces="${NAMESPACE}",valid-before="LAST_VALID_DAY" $(cat "${TEMP_KEY}.pub")
EOF

[ ! -f "$SIG_FILE" ] && { echo "Error: Signing failed."; exit $FILE_PROBLEM; }
echo "Signature created: $SIG_FILE"

echo "Validating the signature..."
ssh-keygen -Y check-novalidate -n file -f "${TEMP_KEY}.pub" -s "$SIG_FILE" < "$FILE_TO_SIGN" || {
ssh-keygen -Y check-novalidate -n "${NAMESPACE}" -f "${TEMP_KEY}.pub" -s "$SIG_FILE" < "$FILE_TO_SIGN" || {
echo "Error: Signature validation failed."
exit $VALIDATION_FAILED
}

# Verify mode
elif [ "$MODE" == "verify" ]; then
ALLOWED_SIGNERS_FILE="$2"
SIG_FILE="${4:-${FILE_TO_SIGN}.sig}"

# Ensure required files exist
for file in "$ALLOWED_SIGNERS_FILE" "$SIG_FILE"; do
[ ! -f "$file" ] && { echo "Error: File '$file' not found."; exit $FILE_PROBLEM; }
done

echo "Verifying the signature against allowed signers..."

# Iterate through each principal in the allowed signers file
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue

# Extract and process each principal
principals=$(echo "$line" | cut -d' ' -f1)
IFS=',' read -ra principal_list <<< "$principals"

for principal in "${principal_list[@]}"; do
echo "Checking principal: $principal"
if ssh-keygen -Y verify -f "$ALLOWED_SIGNERS_FILE" -n file -I "$principal" -s "$SIG_FILE" < "$FILE_TO_SIGN"; then
echo "Signature is valid for principal: $principal"
while read -r principal options key
Comment thread
ocaisa marked this conversation as resolved.
do
[[ -z "$principal" || "$principal" == \#* ]] && continue

namespaces=$(echo "$options" | grep -oP "namespaces=\"\K[^\"]+")
Comment thread
ocaisa marked this conversation as resolved.

if [ "$TERSE_MODE" = true ]; then
if ssh-keygen -Y verify -f "$ALLOWED_SIGNERS_FILE" -n "$namespaces" -I "$principal" -s "$SIG_FILE" < "$FILE_TO_SIGN" > /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
3 changes: 3 additions & 0 deletions tasks/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down