diff --git a/script/debian-devel b/script/debian-devel new file mode 100755 index 000000000..606aafd2e --- /dev/null +++ b/script/debian-devel @@ -0,0 +1,561 @@ +#!/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