From fa8ada0ecce351b79cfff07c9bb36fb0b42f09fb Mon Sep 17 00:00:00 2001 From: Ning Shang Date: Thu, 8 Aug 2024 14:56:15 -0700 Subject: [PATCH 1/4] Support remote signing key management Add support of remote signing with Cloud KMS managed OTA signing keys. This feature is supported in trh v1.2.1. --- entrypoint.sh | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index d10044a..adb157e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -28,15 +28,29 @@ SCRIPT_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" readonly SCRIPT_DIR TRH_BINARY_PATH="${SCRIPT_DIR}/trh" readonly TRH_BINARY_PATH -TRH_DOWNLOAD_URL="https://downloads.thistle.tech/embedded-client/1.1.0/trh-1.1.0-x86_64-unknown-linux-musl.gz" +TRH_DOWNLOAD_URL="https://downloads.thistle.tech/embedded-client/1.2.1/trh-1.2.1-x86_64-unknown-linux-musl.gz" readonly TRH_DOWNLOAD_URL # Set environment variables -export THISTLE_KEY="${HOME}/.minisign/minisign.key" -export THISTLE_KEY_PASS="${INPUT_SIGNING_KEY_PASSWORD}" export THISTLE_TOKEN="${INPUT_PROJECT_ACCESS_TOKEN}" + +[ -z "${INPUT_SIGNING_KEY_MANAGEMENT}" ] && err "No signing key management provided" +[ "${INPUT_SIGNING_KEY_MANAGEMENT}" = "local" ] || [ "${INPUT_SIGNING_KEY_MANAGEMENT}" = "remote" ] || { + err "Unknown signing key management: ${INPUT_SIGNING_KEY_MANAGEMENT}" +} + +[ "${INPUT_SIGNING_KEY_MANAGEMENT}" = "local" ] && { + export THISTLE_KEY="${HOME}/.minisign/minisign.key" + export THISTLE_KEY_PASS="${INPUT_SIGNING_KEY_PASSWORD}" +} + [ -z "${INPUT_BACKEND_URL}" ] || export THISTLE_BACKEND="${INPUT_BACKEND_URL}" +# Set TRH command based on signing method +TRH_COMMAND="${TRH_BINARY_PATH} --signing-method=${INPUT_SIGNING_KEY_MANAGEMENT}" +readonly TRH_COMMAND + +# Functions err() { echo -e "$*" exit 1 @@ -54,9 +68,12 @@ get_manifest_template_hack() { local persist_dir="${INPUT_PERSIST_DIR_ON_DEVICE:-}" [ -z "${persist_dir}" ] && err "No persist directory provided" - mkdir -p "$(dirname "${THISTLE_KEY}")" - echo "${INPUT_SIGNING_KEY}" > "${THISTLE_KEY}" - "${TRH_BINARY_PATH}" init --persist="${persist_dir}" > /dev/null + if [ "${INPUT_SIGNING_KEY_MANAGEMENT}" = "local" ]; then + mkdir -p "$(dirname "${THISTLE_KEY}")" + echo "${INPUT_SIGNING_KEY}" > "${THISTLE_KEY}" + fi + + eval "${TRH_COMMAND}" init --persist="${persist_dir}" > /dev/null } file_release() { @@ -70,12 +87,12 @@ file_release() { local base_install_path="${INPUT_BASE_INSTALL_PATH_ON_DEVICE:-}" [ -z "${base_install_path}" ] && err "No base install path provided" - "${TRH_BINARY_PATH}" prepare --target="${artifacts_dir}" --file-base-path="${base_install_path}" + eval "${TRH_COMMAND}" prepare --target="${artifacts_dir}" --file-base-path="${base_install_path}" if [ -n "${release_name}" ]; then - "${TRH_BINARY_PATH}" release --name="${release_name}" + eval "${TRH_COMMAND}" release --name="${release_name}" else - "${TRH_BINARY_PATH}" release + eval "${TRH_COMMAND}" release fi echo "done" @@ -89,12 +106,12 @@ rootfs_release() { local rootfs_img_path="${INPUT_ROOTFS_IMG_PATH:-}" [ -z "${rootfs_img_path}" ] && err "No rootfs image path provided" - "${TRH_BINARY_PATH}" prepare --target="${rootfs_img_path}" + eval "${TRH_COMMAND}" prepare --target="${rootfs_img_path}" if [ -n "${release_name}" ]; then - "${TRH_BINARY_PATH}" release --name="${release_name}" + eval "${TRH_COMMAND}" release --name="${release_name}" else - "${TRH_BINARY_PATH}" release + eval "${TRH_COMMAND}" release fi echo "done" @@ -111,12 +128,12 @@ zip_archive_release() { local base_install_path="${INPUT_BASE_INSTALL_PATH_ON_DEVICE:-}" [ -z "${base_install_path}" ] && err "No base install path provided" - "${TRH_BINARY_PATH}" prepare --zip-target --target="${zip_archive_dir}" --file-base-path="${base_install_path}" + eval "${TRH_COMMAND}" prepare --zip-target --target="${zip_archive_dir}" --file-base-path="${base_install_path}" if [ -n "${release_name}" ]; then - "${TRH_BINARY_PATH}" release --name="${release_name}" + eval "${TRH_COMMAND}" release --name="${release_name}" else - "${TRH_BINARY_PATH}" release + eval "${TRH_COMMAND}" release fi echo "done" From e490263645f85df52b9cc9bbb787d6f9e791ce1e Mon Sep 17 00:00:00 2001 From: Ning Shang Date: Thu, 8 Aug 2024 15:12:42 -0700 Subject: [PATCH 2/4] Fix qutoes in eval Fix quotes in eval commands. --- entrypoint.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index adb157e..3b3248a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -87,10 +87,10 @@ file_release() { local base_install_path="${INPUT_BASE_INSTALL_PATH_ON_DEVICE:-}" [ -z "${base_install_path}" ] && err "No base install path provided" - eval "${TRH_COMMAND}" prepare --target="${artifacts_dir}" --file-base-path="${base_install_path}" + eval "${TRH_COMMAND}" prepare --target="\"${artifacts_dir}\"" --file-base-path="\"${base_install_path}\"" if [ -n "${release_name}" ]; then - eval "${TRH_COMMAND}" release --name="${release_name}" + eval "${TRH_COMMAND}" release --name="\"${release_name}\"" else eval "${TRH_COMMAND}" release fi @@ -106,10 +106,10 @@ rootfs_release() { local rootfs_img_path="${INPUT_ROOTFS_IMG_PATH:-}" [ -z "${rootfs_img_path}" ] && err "No rootfs image path provided" - eval "${TRH_COMMAND}" prepare --target="${rootfs_img_path}" + eval "${TRH_COMMAND}" prepare --target="\"${rootfs_img_path}\"" if [ -n "${release_name}" ]; then - eval "${TRH_COMMAND}" release --name="${release_name}" + eval "${TRH_COMMAND}" release --name="\"${release_name}\"" else eval "${TRH_COMMAND}" release fi @@ -128,10 +128,10 @@ zip_archive_release() { local base_install_path="${INPUT_BASE_INSTALL_PATH_ON_DEVICE:-}" [ -z "${base_install_path}" ] && err "No base install path provided" - eval "${TRH_COMMAND}" prepare --zip-target --target="${zip_archive_dir}" --file-base-path="${base_install_path}" + eval "${TRH_COMMAND}" prepare --zip-target --target="\"${zip_archive_dir}\"" --file-base-path="\"${base_install_path}\"" if [ -n "${release_name}" ]; then - eval "${TRH_COMMAND}" release --name="${release_name}" + eval "${TRH_COMMAND}" release --name="\"${release_name}\"" else eval "${TRH_COMMAND}" release fi From 0c90937254df106ea2ec94696328c14cf0369fed Mon Sep 17 00:00:00 2001 From: Ning Shang Date: Thu, 8 Aug 2024 17:13:33 -0700 Subject: [PATCH 3/4] GHA: add remote signing tests in CI Add test jobs for remote signing key management. --- .github/workflows/test.yml | 98 +++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6259cd7..0dec236 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,12 +10,12 @@ on: jobs: test_ota_release_file: - name: 'Test OTA Release Action (release_type: file)' + name: 'Test OTA Release Action (release_type: file, signing_key_management: local)' runs-on: 'ubuntu-latest' steps: - name: 'Checkout' uses: 'actions/checkout@v4' - + - name: 'Create artifacts for test' run: | mkdir -p artifacts @@ -38,9 +38,37 @@ jobs: signing_key: ${{ secrets.TEST_SIGNING_KEY }} signing_key_password: ${{ secrets.TEST_SIGNING_KEY_PASSWORD }} - test_ota_release_zip_archive: - name: 'Test OTA Release Action (release_type: zip_archive)' + test_ota_release_file_remote_signing: needs: 'test_ota_release_file' + name: 'Test OTA Release Action (release_type: file, signing_key_management: remote)' + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout' + uses: 'actions/checkout@v4' + + - name: 'Create artifacts for test' + run: | + mkdir -p artifacts + hello_time="$(date +'%Y-%m-%dT%H:%M:%S%z')" + echo "${hello_time}: Hello, World!" > artifacts/hello.txt + sleep 4.2 + goodbye_time="$(date +'%Y-%m-%dT%H:%M:%S%z')" + echo "${goodbye_time}: Goodbye, World!" > artifacts/goodbye.txt + + - name: 'Test OTA Release' + uses: './' # Use an action in the root directory + with: + # Not setting release_name to test the default behavior + release_type: 'file' + artifacts_dir: 'artifacts' + persist_dir_on_device: '/tmp/persist' + base_install_path_on_device: '/tmp/ota' + project_access_token: ${{ secrets.TEST_PROJECT_ACCESS_TOKEN }} + signing_key_management: 'remote' + + test_ota_release_zip_archive: + needs: 'test_ota_release_file_remote_signing' + name: 'Test OTA Release Action (release_type: zip_archive, signing_key_management: local)' runs-on: 'ubuntu-latest' steps: - name: 'Checkout' @@ -72,9 +100,41 @@ jobs: signing_key: ${{ secrets.TEST_SIGNING_KEY }} signing_key_password: ${{ secrets.TEST_SIGNING_KEY_PASSWORD }} - test_ota_release_rootfs: - name: 'Test OTA Release Action (release_type: rootfs)' + test_ota_release_zip_archive_remote_signing: needs: 'test_ota_release_zip_archive' + name: 'Test OTA Release Action (release_type: zip_archive, signing_key_management: remote)' + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout' + uses: 'actions/checkout@v4' + + - name: 'Set up environment' + run: | + echo "TIMESTAMP=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> $GITHUB_ENV + + - name: 'Create artifacts for test' + run: | + mkdir -p artifacts + hello_time="$(date +'%Y-%m-%dT%H:%M:%S%z')" + echo "${hello_time}: Hello, World!" > artifacts/hello.txt + sleep 4.2 + goodbye_time="$(date +'%Y-%m-%dT%H:%M:%S%z')" + echo "${goodbye_time}: Goodbye, World!" > artifacts/goodbye.txt + + - name: 'Test OTA Release' + uses: './' # Use an action in the root directory + with: + release_name: 'archive-release ${{ env.TIMESTAMP }}' + release_type: 'zip_archive' + zip_archive_dir: 'artifacts' + persist_dir_on_device: '/tmp/persist' + base_install_path_on_device: '/tmp/ota' + project_access_token: ${{ secrets.TEST_PROJECT_ACCESS_TOKEN }} + signing_key_management: 'remote' + + test_ota_release_rootfs: + needs: 'test_ota_release_zip_archive_remote_signing' + name: 'Test OTA Release Action (release_type: rootfs, signing_key_management: local)' runs-on: 'ubuntu-latest' steps: - name: 'Checkout' @@ -99,3 +159,29 @@ jobs: signing_key_management: 'local' signing_key: ${{ secrets.TEST_SIGNING_KEY }} signing_key_password: ${{ secrets.TEST_SIGNING_KEY_PASSWORD }} + + test_ota_release_rootfs_remote_signing: + needs: 'test_ota_release_rootfs' + name: 'Test OTA Release Action (release_type: rootfs, signing_key_management: remote)' + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout' + uses: 'actions/checkout@v4' + + - name: 'Set up environment' + run: | + echo "TIMESTAMP=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> $GITHUB_ENV + + - name: 'Create (bogus) rootfs for test' + run: | + echo "Create fake rootfs at ${{ env.TIMESTAMP }}" > rootfs.img + + - name: 'Test OTA Release' + uses: './' # Use an action in the root directory + with: + release_name: 'rootfs-release ${{ env.TIMESTAMP }}' + release_type: 'rootfs' + rootfs_img_path: 'rootfs.img' + persist_dir_on_device: '/tmp/persist' + project_access_token: ${{ secrets.TEST_PROJECT_ACCESS_TOKEN }} + signing_key_management: 'remote' From 88a50b3358a116a37556162025de68efbf16637d Mon Sep 17 00:00:00 2001 From: Ning Shang Date: Thu, 8 Aug 2024 17:35:05 -0700 Subject: [PATCH 4/4] Update README.md to add remote signing examples Add example steps for configuring workflows for remote signing. Also move away from using the /tmp directory in sample code. --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f6959c2..1bf2ba7 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,16 @@ Update Client (TUC)](https://docs.thistle.tech/update/cli#update-client-usage). To use this action, one needs to create an account in the [Thistle Control Center](https://app.thistle.tech), and obtain the API token ("Project Access -Token"). In case a locally managed OTA update signing key is used (which is the -only supported option currently), one needs run the `trh keygen` (requiring +Token"). In case a locally managed OTA update signing key is used, one needs run +the `trh keygen` (requiring [TRH](https://docs.thistle.tech/download#thistle-update-client-tuc-and-release-helper-trh) v1.1.0 or above) command to create a password-protected -[Minisign](https://jedisct1.github.io/minisign/) private key. +[Minisign](https://jedisct1.github.io/minisign/) private key. For remote signing +(with Thistle-managed Cloud KMS-backed keys, supported in version 1.4.0 and +above), the keygen step isn't needed. + + +### Signing OTA bundle with a locally managed signing key An example workflow for [file update](https://docs.thistle.tech/update/get_started/file_update) is as follows. @@ -57,8 +62,8 @@ jobs: release_name: 'OPTIONAL RELEASE NAME' release_type: 'file' artifacts_dir: 'artifacts' - persist_dir_on_device: '/tmp/persist' - base_install_path_on_device: '/tmp/ota' + persist_dir_on_device: '/ota/persist' + base_install_path_on_device: '/ota/bin' project_access_token: ${{ secrets.PROJECT_ACCESS_TOKEN }} signing_key_management: 'local' signing_key: ${{ secrets.SIGNING_KEY }} @@ -75,7 +80,7 @@ the "OTA Release" step as release_name: 'OPTIONAL RELEASE NAME' release_type: 'rootfs' rootfs_img_path: '/path/to/rootfs.img' - persist_dir_on_device: '/tmp/persist' + persist_dir_on_device: '/ota/persist' project_access_token: ${{ secrets.PROJECT_ACCESS_TOKEN }} signing_key_management: 'local' signing_key: ${{ secrets.SIGNING_KEY }} @@ -91,14 +96,90 @@ For zip archive update, configure the "OTA Release" step as release_name: 'OPTIONAL RELEASE NAME' release_type: 'zip_archive' zip_archive_dir: '/path/to/uncompressed_artifacts_dir' - persist_dir_on_device: '/tmp/persist' - base_install_path_on_device: '/tmp/ota' + persist_dir_on_device: '/ota/persist' + base_install_path_on_device: '/ota/bin' project_access_token: ${{ secrets.PROJECT_ACCESS_TOKEN }} signing_key_management: 'local' signing_key: ${{ secrets.SIGNING_KEY }} signing_key_password: ${{ secrets.SIGNING_KEY_PASSWORD }} ``` +### Signing OTA bundle with a remotely managed signing key + +For [file update](https://docs.thistle.tech/update/get_started/file_update), an +example workflow is as follows. + +```yaml +name: 'OTA Release' + +on: + push: + tags: + # Trigger release by tagging + - 'release-v*' + +jobs: + ota_release: + name: 'OTA Release' + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout source' + uses: 'actions/checkout@v4' + + - name: 'Create artifacts for OTA release' + run: | + ... + [build artifacts from source] + [run tests on artifacts] + ... + rm -rf artifacts + mkdir -p artifacts + ... + [copy built artifacts to directory artifacts/] + ... + + - name: 'OTA Release' + uses: 'thistletech/ota-release-action@v1' + with: + release_name: 'OPTIONAL RELEASE NAME' + release_type: 'file' + artifacts_dir: 'artifacts' + persist_dir_on_device: '/ota/persist' + base_install_path_on_device: '/ota/bin' + project_access_token: ${{ secrets.PROJECT_ACCESS_TOKEN }} + signing_key_management: 'remote' +``` + +For [rootfs update](https://docs.thistle.tech/update/get_started/rpi), configure +the "OTA Release" step as + +```yaml + - name: 'OTA Release' + uses: 'thistletech/ota-release-action@v1' + with: + release_name: 'OPTIONAL RELEASE NAME' + release_type: 'rootfs' + rootfs_img_path: '/path/to/rootfs.img' + persist_dir_on_device: '/ota/persist' + project_access_token: ${{ secrets.PROJECT_ACCESS_TOKEN }} + signing_key_management: 'remote' +``` + +For zip archive update, configure the "OTA Release" step as + +```yaml + - name: 'OTA Release' + uses: 'thistletech/ota-release-action@v1' + with: + release_name: 'OPTIONAL RELEASE NAME' + release_type: 'zip_archive' + zip_archive_dir: '/path/to/uncompressed_artifacts_dir' + persist_dir_on_device: '/ota/persist' + base_install_path_on_device: '/ota/bin' + project_access_token: ${{ secrets.PROJECT_ACCESS_TOKEN }} + signing_key_management: 'remote' +``` + ## Inputs