#!/bin/bash # Enable strict mode set -eo pipefail SCRIPT=$(basename "$0") ARCHITECTURES=("i386" "amd64" "arm64" "armhf") # Check if DEBUG is set to true or 1, enable debug print statements if so if [[ "$DEBUG" == "true" || "$DEBUG" == "1" ]]; then set -x fi # Default artifacts directory ARTIFACTS_DIR="$(pwd)/deb-repo-test" bold() { echo -e "\033[1m$1\033[0m" } header() { echo echo "================================================================================" bold "$1" echo } # Function to prompt user to continue to the next step with explanations prompt_continue() { echo read -p "Press Enter to continue..." } # Function to start up Docker container for environment docker_setup() { # If the gpg-playground container already exists, prompt the user whether to tear it down if [ "$(docker ps -a -q -f name=gpg-playground)" ]; then read -p "Do you want to teardown the existing container and recreate it? (y/n): " choice if [[ "$choice" == "y" || "$choice" == "Y" ]]; then docker_teardown else echo "Skip setup and using existing container." docker exec -it gpg-playground bash exit 0 fi fi echo "Setting up a new container." docker run -it -d --name gpg-playground -v "$(pwd)/script/debian-devel:/root/debian-devel" -w "/root" ubuntu:22.04 # Set up MOTD to display on shell entry docker exec gpg-playground bash -c "cat >> /root/.bashrc << EOF # Display MOTD cat << "MOTD" ================================================================================ Self-bootstrapping Debian Repository Development Environment ================================================================================ Welcome to the gpg-playground container! Available commands: $SCRIPT setup - Create initial Debian repository and packages $SCRIPT newkey - Generate new signing key $SCRIPT deprecate - Deprecate old signing key $SCRIPT teardown - Clean up repository and configuration Run $SCRIPT without arguments for full help. ================================================================================ MOTD EOF" docker exec -it gpg-playground bash } # Function to handle the setup process setup() { header "Step 1: Installing prerequisites" if ! command -v sudo &> /dev/null; then # This is to make sure the script runs on both containers and VMs. apt-get update apt-get install -y sudo fi sudo apt-get update sudo apt-get install --no-upgrade -y busybox gpg reprepro tree if [ ! -d "$ARTIFACTS_DIR" ]; then echo "Directory does not exist. Creating $ARTIFACTS_DIR..." mkdir -p "$ARTIFACTS_DIR" echo "Directory $ARTIFACTS_DIR created." else echo "Directory $ARTIFACTS_DIR already exists." fi prompt_continue header "Step 2: Creating a PGP key/cert that expires in 1 minute" cd "$ARTIFACTS_DIR" echo "%echo Generating an example PGP key Key-Type: RSA Key-Length: 4096 Name-Real: example-key-1 Name-Email: example@example.com Expire-Date: seconds=$((1 * 60)) %no-ask-passphrase %no-protection %commit" > example-pgp-key-1.batch mkdir -m 700 -p "$ARTIFACTS_DIR/temp-gpg-home" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --no-tty --batch --gen-key example-pgp-key-1.batch GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --armor --export-secret-keys > pgp-key.private # ASCII version GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --armor --export > pgp-key.public # ASCII version GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --export > pgp-key.gpg # Binary version KEY1_FINGERPRINT="$(GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys --list-options show-only-fpr-mbox | cut -f1 -d' ' | head -n 1)" echo -n "$KEY1_FINGERPRINT" > "$ARTIFACTS_DIR/key1-fingerprint" echo "Generated PGP key with fingerprint: $KEY1_FINGERPRINT" prompt_continue header "Step 3: Creating the archive-keyring deb file" mkdir -p "$ARTIFACTS_DIR/example-archive-keyring_0.0.1-1_all" cd "$ARTIFACTS_DIR/example-archive-keyring_0.0.1-1_all" mkdir -p usr/share/keyrings GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --export > usr/share/keyrings/example-archive-keyring.gpg mkdir -p etc/apt/sources.list.d echo -n "# Created by example-archive-keyring package deb [arch=$(echo "${ARCHITECTURES[@]}" | sed 's/ /,/g') allow-insecure=yes signed-by=/usr/share/keyrings/example-archive-keyring.gpg] http://127.0.0.1:8000/apt-repo stable main " > etc/apt/sources.list.d/example.list # Add the preferences file to enable automatic upgrades for the archive keyring package. # The `Pin-Priority: 100` ensures that the package is considered for upgrades. # # See: # - https://wiki.debian.org/DebianRepository/UseThirdParty#Standard_pinning # - https://wiki.debian.org/DebianRepository/UseThirdParty#Certificate_rollover_and_updates mkdir -p etc/apt/preferences.d echo -n "# Created by example-archive-keyring package Package: example-archive-keyring Pin: origin example.com Pin-Priority: 100 " > etc/apt/preferences.d/example.pref mkdir -p DEBIAN # The "Section:" and "Priority:" values are to make the deb files similar to what # we build in our CI. Also, omitting them from here will result in reprepro failure. echo -n "Package: example-archive-keyring Version: 0.0.1 Maintainer: example Architecture: all Section: Priority: optional Homepage: http://example.com Description: Example archive keyring " > DEBIAN/control echo "Control file for example-archive-keyring created at $ARTIFACTS_DIR/example-archive-keyring_0.0.1-1_all/DEBIAN/control" cd "$ARTIFACTS_DIR" dpkg --build example-archive-keyring_0.0.1-1_all echo "Deb package created: example-archive-keyring_0.0.1-1_all.deb" # To inspect the .deb package: # dpkg-deb --info example-archive-keyring_0.0.1-1_all.deb # dpkg-deb --contents example-archive-keyring_0.0.1-1_all.deb prompt_continue header "Step 4: Creating the hello-world deb files for supported architectures" for arch in "${ARCHITECTURES[@]}"; do mkdir -p "$ARTIFACTS_DIR/hello-world_0.0.1-1_$arch" cd "$ARTIFACTS_DIR/hello-world_0.0.1-1_$arch" mkdir -p usr/bin cat << EOF > usr/bin/hello-world #!/bin/bash echo "hello world (packaged for $arch)" EOF chmod +x usr/bin/hello-world # Create DEBIAN control file mkdir -p DEBIAN # The "Section:" and "Priority:" values are to make the deb files similar to what # we build in our CI. Also, omitting them from here will result in reprepro failure. echo "Package: hello-world Version: 0.0.1 Maintainer: example Architecture: $arch Section: Priority: optional Homepage: http://example.com Description: A statically compiled Go program that prints hello" > DEBIAN/control # Build the deb package cd "$ARTIFACTS_DIR" dpkg --build hello-world_0.0.1-1_$arch echo "Deb package created: hello-world_0.0.1-1_$arch.deb" # Inspect the deb package # dpkg-deb --info hello-world_0.0.1-1_$arch.deb # dpkg-deb --contents hello-world_0.0.1-1_$arch.deb done prompt_continue header "Step 5: Creating apt repository using reprepro" mkdir -p "$ARTIFACTS_DIR/apt-repo" # make reprepro config template file cat < "$ARTIFACTS_DIR/distributions.template" Origin: Example Repository Label: Example Codename: stable Architectures: ${ARCHITECTURES[@]} Components: main Description: An example software repository SignWith: EOF # Create a temp directory before calling reprepro. This is to mimic the stateless # implementation we have in our CI workflow. Further reprepro calls will have a # separate temp directory. mkdir -p "$ARTIFACTS_DIR/temp-reprepro-1/conf" cd "$ARTIFACTS_DIR/temp-reprepro-1/conf" cp "$ARTIFACTS_DIR/distributions.template" distributions sed -i "s//$KEY1_FINGERPRINT/g" distributions cd "$ARTIFACTS_DIR/temp-reprepro-1" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" reprepro includedeb stable "$ARTIFACTS_DIR/example-archive-keyring_0.0.1-1_all.deb" for arch in "${ARCHITECTURES[@]}"; do GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" reprepro includedeb stable "$ARTIFACTS_DIR/hello-world_0.0.1-1_$arch.deb" done cp -a dists/ pool/ "$ARTIFACTS_DIR/apt-repo/" echo "This is the content of the Packages file (for amd64):" cat dists/stable/main/binary-amd64/Packages echo "! Note that there should be two entries above; hello-world and the archive-keyring package." prompt_continue header "Step 6: Hosting repository with a simple HTTP server" cd "$ARTIFACTS_DIR" busybox httpd -p 8000 # runs in background by default echo "HTTP server started at http://127.0.0.1:8000" prompt_continue header "Step 7: Adding apt repo entry to /etc/apt/sources.list.d" # Install the trust anchor (public key) on the host system for later verification. # # Note: This trust anchor installation is the manual step that should be done when the # package is installed for the first time. That is we should still include this step # in our installation docs as we already have it. Future automatic upgrades of the # archive-keyring package will update the installed keyring, so the user would not # need to do this step ever again. sudo mkdir -p -m 755 /usr/share/keyrings sudo cp "$ARTIFACTS_DIR/pgp-key.gpg" /usr/share/keyrings/example-archive-keyring.gpg sudo chmod go+r /usr/share/keyrings/example-archive-keyring.gpg echo "deb [arch=$(echo "${ARCHITECTURES[@]}" | sed 's/ /,/g') allow-insecure=yes signed-by=/usr/share/keyrings/example-archive-keyring.gpg] http://127.0.0.1:8000/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/example.list echo echo "! Note that with this config, apt allows plain HTTP comms but still verifies package signatures." echo "! If you wait here until the generated key expires, you'll see the next step fails due to the expired key." prompt_continue header "Step 8: Updating apt and installing the packages" sudo apt-get update # We don't need --allow-insecure-repositories sudo apt-get install -y example-archive-keyring hello-world echo bold "✅ Hello World and the archive keyring packages installed successfully." echo echo "Run 'newkey' target next to rotate the signing key." echo echo "! Note that there should be no warnings about unverified packages in the logs above." } # Function to handle the signing key rotation process newkey() { # Verify that the current key is not yet expired header "Step 1: Verifying the current key is not yet expired" KEY1_FINGERPRINT="$(cat "$ARTIFACTS_DIR/key1-fingerprint")" expiry_date="$(GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys --with-colons --fingerprint $KEY1_FINGERPRINT | grep -E "^pub:" | cut -f7 -d:)" current_date="$(date +%s)" if [ "$expiry_date" -lt "$current_date" ]; then echo bold "⚠️ The key is already expired. Please 'teardown' and start over." exit 1 else echo "Confirmed that the current signing key is still valid." fi # Generate a new key and add it to the keyring prompt_continue header "Step 2: Generating a new PGP key (expires in 15 mins) and add it to the keyring" # Get the list of existing keys existing_keys_list="$(GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys --with-colons)" cd "$ARTIFACTS_DIR" echo "%echo Generating an example PGP key Key-Type: RSA Key-Length: 4096 Name-Real: example-key-2 Name-Email: example@example.com Expire-Date: seconds=$((15 * 60)) %no-ask-passphrase %no-protection %commit" > example-pgp-key-2.batch mkdir -m 700 -p "$ARTIFACTS_DIR/temp-gpg-home" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --no-tty --batch --gen-key example-pgp-key-2.batch GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --armor --export-secret-keys > pgp-key.private # ASCII version GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --armor --export > pgp-key.public # ASCII version GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --export > pgp-key.gpg # Binary version new_keys_list="$(GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys --with-colons)" added_key="$(comm -1 -3 <(echo "$existing_keys_list" | sort) <(echo "$new_keys_list" | sort))" KEY2_FINGERPRINT="$(echo "$added_key" | grep -E "^fpr:" | cut -f10 -d: | head -n1)" echo -n "$KEY2_FINGERPRINT" > "$ARTIFACTS_DIR/key2-fingerprint" echo "Generated PGP key with fingerprint: $KEY2_FINGERPRINT" prompt_continue header "Step 3: Generating the new archive-keyring package and updating the apt repository." # create a copy of the files of the previous version of the archive keyring package cp -r "$ARTIFACTS_DIR/example-archive-keyring_0.0.1-1_all" "$ARTIFACTS_DIR/example-archive-keyring_0.0.2-1_all" # update the keyring file with the new keyring (including both keys) cd "$ARTIFACTS_DIR/example-archive-keyring_0.0.2-1_all" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --export > usr/share/keyrings/example-archive-keyring.gpg # bump the version in the debian control file sed -i 's/Version: 0\.0\.1/Version: 0.0.2/' DEBIAN/control cd "$ARTIFACTS_DIR" dpkg --build example-archive-keyring_0.0.2-1_all echo "Deb package created: example-archive-keyring_0.0.2-1_all.deb" # To inspect the .deb package: # dpkg-deb --info example-archive-keyring_0.0.2-1_all.deb # dpkg-deb --contents example-archive-keyring_0.0.2-1_all.deb # Create a new temp directory before calling reprepro. mkdir -p "$ARTIFACTS_DIR/temp-reprepro-2/conf" cd "$ARTIFACTS_DIR/temp-reprepro-2/conf" cp "$ARTIFACTS_DIR/distributions.template" distributions sed -i "s//$KEY1_FINGERPRINT $KEY2_FINGERPRINT/g" distributions cd "$ARTIFACTS_DIR/temp-reprepro-2" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" reprepro includedeb stable "$ARTIFACTS_DIR/example-archive-keyring_0.0.2-1_all.deb" for arch in "${ARCHITECTURES[@]}"; do GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" reprepro includedeb stable "$ARTIFACTS_DIR/hello-world_0.0.1-1_$arch.deb" done cp -a dists/ pool/ "$ARTIFACTS_DIR/apt-repo/" cd "$ARTIFACTS_DIR/apt-repo" echo "New packages (for amd64):" cat dists/stable/main/binary-amd64/Packages echo bold "✅ apt repository has been updated with the new archive keyring package." echo echo "! Note that there should be two entries above, with the archive keyring version bumped to 0.0.2." prompt_continue header "Step 4: Upgrading the archive keyring package on the host system." sudo apt-get update sudo apt-get install -y --only-upgrade example-archive-keyring echo bold "✅ archive-keyring package upgraded successfully." echo echo "Run 'deprecate' target next to deprecate the old key." echo echo "! Note that in the above logs the archive-keyring is now bumped to 0.0.2." } deprecate() { header "Step 1: Verifying the old/first key is expired" KEY1_FINGERPRINT="$(cat "$ARTIFACTS_DIR/key1-fingerprint")" KEY2_FINGERPRINT="$(cat "$ARTIFACTS_DIR/key2-fingerprint")" expiry_date="$(GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --list-keys --with-colons --fingerprint $KEY1_FINGERPRINT | grep -E "^pub:" | cut -f7 -d:)" current_date="$(date +%s)" if [ "$expiry_date" -gt "$current_date" ]; then SLEEP=$(( $expiry_date - $current_date + 2 )) bold "Sleeping $SLEEP seconds to allow the old/first key to expire." sleep $SLEEP fi prompt_continue header "Step 2: Generating the new archive-keyring package with the old key removed." # create a copy of the files of the previous version of the archive keyring package cp -r "$ARTIFACTS_DIR/example-archive-keyring_0.0.2-1_all" "$ARTIFACTS_DIR/example-archive-keyring_0.0.3-1_all" # update the keyring file with the new keyring (now with only the latest key) cd "$ARTIFACTS_DIR/example-archive-keyring_0.0.3-1_all" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" gpg --export $KEY2_FINGERPRINT > usr/share/keyrings/example-archive-keyring.gpg # bump the version in the debian control file sed -i 's/Version: 0\.0\.2/Version: 0.0.3/' DEBIAN/control cd "$ARTIFACTS_DIR" dpkg --build example-archive-keyring_0.0.3-1_all echo "Deb package created: example-archive-keyring_0.0.3-1_all.deb" # To inspect the .deb package: # dpkg-deb --info example-archive-keyring_0.0.3-1_all.deb # dpkg-deb --contents example-archive-keyring_0.0.3-1_all.deb # Create a new temp directory before calling reprepro. mkdir -p "$ARTIFACTS_DIR/temp-reprepro-3/conf" cd "$ARTIFACTS_DIR/temp-reprepro-3/conf" cp "$ARTIFACTS_DIR/distributions.template" distributions sed -i "s//$KEY2_FINGERPRINT/g" distributions cd "$ARTIFACTS_DIR/temp-reprepro-3" GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" reprepro includedeb stable "$ARTIFACTS_DIR/example-archive-keyring_0.0.3-1_all.deb" for arch in "${ARCHITECTURES[@]}"; do GNUPGHOME="$ARTIFACTS_DIR/temp-gpg-home" reprepro includedeb stable "$ARTIFACTS_DIR/hello-world_0.0.1-1_$arch.deb" done cp -a dists/ pool/ "$ARTIFACTS_DIR/apt-repo/" cd "$ARTIFACTS_DIR/apt-repo" echo "New packages (for amd64):" cat dists/stable/main/binary-amd64/Packages echo bold "✅ apt repository has been updated with the new archive keyring package." echo echo "! Note that there should be two entries above, with the archive keyring version bumped to 0.0.3." prompt_continue header "Step 3: Upgrading the archive keyring package on the host system." sudo apt-get update sudo apt-get install -y --only-upgrade example-archive-keyring echo bold "✅ archive-keyring package upgraded successfully." echo echo "! Note that in the above logs the archive-keyring is now bumped to 0.0.3." } # Function to handle the teardown process teardown() { # Kill the HTTP server if it's running if pgrep -f "busybox httpd" > /dev/null; then header "Stopping the HTTP server..." pkill -f "busybox httpd" echo "Server stopped." fi # Check if the hello-world package is installed, and remove it safely if dpkg -l | grep -q "^ii hello-world"; then header "Removing the hello-world package..." sudo apt-get remove --purge -y hello-world echo "Package hello-world removed." else echo "Package hello-world is not installed, skipping removal." fi # Remove the PGP keyrings if [ -f /etc/apt/keyrings/example-archive-keyring.gpg ]; then header "Removing the PGP keyring from /etc/apt/keyrings..." sudo rm /etc/apt/keyrings/example-archive-keyring.gpg echo "PGP keyring removed from /etc/apt/keyrings." fi if [ -f /usr/share/keyrings/example-archive-keyring.gpg ]; then header "Removing the PGP keyring from /usr/share/keyrings..." sudo rm /usr/share/keyrings/example-archive-keyring.gpg echo "PGP keyring removed from /usr/share/keyrings." fi # Kill gpg-agent if pgrep "^gpg-agent" > /dev/null; then header "Stopping the GPG agent..." pkill "^gpg-agent" echo "GPG agent stopped." fi # Remove the artifacts directory if [ -d "$ARTIFACTS_DIR" ]; then header "Removing the artifacts directory: $ARTIFACTS_DIR" rm -rf "$ARTIFACTS_DIR" echo "Artifacts directory removed." fi # Remove installed test packages if [ -f /etc/apt/sources.list.d/example.list ]; then sudo apt-get remove example-archive-keyring hello-world -y || echo "Test packages not installed, skipping removal." fi # Remove apt source remains (if not removed by the above command) if [ -f /etc/apt/sources.list.d/example.list ]; then header "Removing the repository from /etc/apt/sources.list.d..." sudo rm /etc/apt/sources.list.d/example.list echo "Repository removed." fi # Remove the preferences from preferences.d if [ -f /etc/apt/preferences.d/example.pref ]; then header "Removing the preferences from /etc/apt/preferences.d..." sudo rm /etc/apt/preferences.d/example.pref echo "Preferences removed." fi # Clean apt cache header "Cleaning apt cache..." sudo apt-get clean sudo apt-get update echo "Cleanup complete." } # Function to start up Docker container for environment docker_teardown() { docker stop gpg-playground && docker rm gpg-playground } # Check for the setup or teardown argument if [ "$1" == "docker_setup" ]; then docker_setup elif [ "$1" == "setup" ]; then setup elif [ "$1" == "newkey" ]; then newkey elif [ "$1" == "deprecate" ]; then deprecate elif [ "$1" == "teardown" ]; then teardown elif [ "$1" == "docker_teardown" ]; then docker_teardown else echo " $(bold "Self-bootstrapping Debian repository development tool") Usage: $SCRIPT TARGET The following targets are typical order used to setup and exercise the GitHub CLI Debian packaging process: - $(bold docker_setup): create Docker container for testing environment - $(bold setup): create initial Debian repository, create and sign sample application and keyring archive packages - $(bold newkey): generate new signing key, update keyring archive package - $(bold deprecate): deprecate old signing key, update keyring archive package, sign new sample application package - $(bold teardown): clean up Debian repository and local configuration - $(bold docker_teardown): stop and remove Docker container " exit 1 fi