diff --git a/scripts/ci/_common.sh b/scripts/ci/_common.sh index 1b6e44d7..528b3f85 100755 --- a/scripts/ci/_common.sh +++ b/scripts/ci/_common.sh @@ -673,7 +673,7 @@ print_canonical_ios_app_hash() { return 0 fi - digest="$(python3 "${REPO_ROOT}/scripts/ci/canonical-ios-app-hash.py" "${bundle}")" + digest="$(canonical_apple_bundle_hash_from_path_digest "${bundle}")" printf 'sha256-ios-app-canonical %s %s\n' "${digest}" "${label}" } @@ -1056,6 +1056,37 @@ stage_appdir_for_appimage() { esac } +sanitize_appdir_executable() { + local appdir="\$1" + local exe="\${appdir}/usr/bin/maple" + local description interpreter rpath + + if [ ! -f "\${exe}" ]; then + return 0 + fi + + description="\$(LC_ALL=C file "\${exe}")" + case "\${description}" in + *"x86-64"*) + interpreter="/lib64/ld-linux-x86-64.so.2" + ;; + *"aarch64"*) + interpreter="/lib/ld-linux-aarch64.so.1" + ;; + *) + echo "Unsupported AppImage ELF architecture: \${description}" >&2 + return 1 + ;; + esac + + patchelf --set-interpreter "\${interpreter}" --remove-rpath "\${exe}" + rpath="\$(patchelf --print-rpath "\${exe}" 2>/dev/null || true)" + printf 'verified-linux-appimage-appdir-elf interpreter=%s rpath=%s %s\n' \ + "\${interpreter}" \ + "\${rpath:-}" \ + "\${exe}" +} + run_appimage_plugin() { local tmp_dir="" tmp_appdir="" output_parent="" status file previous arg local -a plugin_args=() @@ -1142,6 +1173,7 @@ done if [ -n "\${appdir}" ]; then rm -f "\${appdir}/.DirIcon" + sanitize_appdir_executable "\${appdir}" fi script_dir="\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)" @@ -1190,6 +1222,37 @@ stage_appdir_for_appimage() { esac } +sanitize_appdir_executable() { + local appdir="\$1" + local exe="\${appdir}/usr/bin/maple" + local description interpreter rpath + + if [ ! -f "\${exe}" ]; then + return 0 + fi + + description="\$(LC_ALL=C file "\${exe}")" + case "\${description}" in + *"x86-64"*) + interpreter="/lib64/ld-linux-x86-64.so.2" + ;; + *"aarch64"*) + interpreter="/lib/ld-linux-aarch64.so.1" + ;; + *) + echo "Unsupported AppImage ELF architecture: \${description}" >&2 + return 1 + ;; + esac + + patchelf --set-interpreter "\${interpreter}" --remove-rpath "\${exe}" + rpath="\$(patchelf --print-rpath "\${exe}" 2>/dev/null || true)" + printf 'verified-linux-appimage-appdir-elf interpreter=%s rpath=%s %s\n' \ + "\${interpreter}" \ + "\${rpath:-}" \ + "\${exe}" +} + run_appimage_plugin() { local tmp_dir="" tmp_appdir="" output_parent="" status file previous arg local -a plugin_args=() @@ -1276,6 +1339,7 @@ done if [ -n "\${appdir}" ]; then rm -f "\${appdir}/.DirIcon" + sanitize_appdir_executable "\${appdir}" fi script_dir="\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)" @@ -2239,6 +2303,125 @@ archive_tree_as_root_tar_gz() { ) | "${gzip_cmd}" -n > "${out}" } +linux_system_elf_interpreter_for_file() { + local elf="$1" + local description + + description="$(LC_ALL=C file "${elf}")" + case "${description}" in + *"x86-64"*) + printf '%s\n' "/lib64/ld-linux-x86-64.so.2" + ;; + *"aarch64"*) + printf '%s\n' "/lib/ld-linux-aarch64.so.1" + ;; + *) + echo "Unsupported Linux ELF architecture for distributable package: ${description}" >&2 + return 1 + ;; + esac +} + +verify_linux_package_executable_metadata() { + local exe="$1" + local expected_interpreter actual_interpreter rpath + + if [ ! -f "${exe}" ]; then + echo "Missing Linux package executable: ${exe}" >&2 + return 1 + fi + + expected_interpreter="$(linux_system_elf_interpreter_for_file "${exe}")" + actual_interpreter="$(patchelf --print-interpreter "${exe}")" + if [ "${actual_interpreter}" != "${expected_interpreter}" ]; then + echo "Linux package executable uses a non-distributable ELF interpreter." >&2 + echo "expected=${expected_interpreter}" >&2 + echo "actual=${actual_interpreter}" >&2 + echo "file=${exe}" >&2 + return 1 + fi + + rpath="$(patchelf --print-rpath "${exe}" 2>/dev/null || true)" + case "${rpath}" in + *"/nix/store/"* | *"/home/runner/work/"* | *"/Users/runner/work/"*) + echo "Linux package executable contains build-host paths in RPATH/RUNPATH." >&2 + echo "rpath=${rpath}" >&2 + echo "file=${exe}" >&2 + return 1 + ;; + esac + + printf 'verified-linux-package-elf interpreter=%s rpath=%s %s\n' \ + "${actual_interpreter}" \ + "${rpath:-}" \ + "${exe}" +} + +sanitize_linux_package_executable() { + local exe="$1" + local interpreter + + interpreter="$(linux_system_elf_interpreter_for_file "${exe}")" + patchelf --set-interpreter "${interpreter}" --remove-rpath "${exe}" + verify_linux_package_executable_metadata "${exe}" +} + +sanitize_linux_deb_payload() { + local payload="$1" + + sanitize_linux_package_executable "${payload}/usr/bin/maple" +} + +sanitize_linux_target_release_executable() { + sanitize_linux_package_executable "${TAURI_DIR}/target/release/maple" +} + +verify_linux_deb_package_executable_metadata() { + local deb="$1" + local tmp status + + tmp="$(mktemp -d)" + status=0 + ( + cd "${tmp}" + ar x "${deb}" + mkdir data + tar -xzf data.tar.gz -C data + verify_linux_package_executable_metadata "${tmp}/data/usr/bin/maple" + ) || status=$? + rm -rf "${tmp}" + return "${status}" +} + +verify_linux_appimage_executable_metadata() { + local appimage="$1" + local tmp status + + tmp="$(mktemp -d)" + status=0 + ( + extract_appimage_tool "${appimage}" "${tmp}/AppDir" + verify_linux_package_executable_metadata "${tmp}/AppDir/usr/bin/maple" + ) || status=$? + rm -rf "${tmp}" + return "${status}" +} + +verify_linux_desktop_package_metadata() { + local artifact + + for artifact in "$@"; do + case "${artifact}" in + *Maple_*.AppImage) + verify_linux_appimage_executable_metadata "${artifact}" + ;; + *.deb) + verify_linux_deb_package_executable_metadata "${artifact}" + ;; + esac + done +} + normalize_deb_package() { local deb="$1" local tmp @@ -2250,6 +2433,7 @@ normalize_deb_package() { mkdir control data tar -xzf control.tar.gz -C control tar -xzf data.tar.gz -C data + sanitize_linux_deb_payload "${tmp}/data" archive_tree_as_root_tar_gz "${tmp}/control" "${tmp}/control.tar.gz" archive_tree_as_root_tar_gz "${tmp}/data" "${tmp}/data.tar.gz" printf '2.0\n' > debian-binary diff --git a/scripts/ci/desktop-pr.sh b/scripts/ci/desktop-pr.sh index 757022bf..cff34003 100755 --- a/scripts/ci/desktop-pr.sh +++ b/scripts/ci/desktop-pr.sh @@ -27,6 +27,8 @@ case "$(host_os)" in remove_build_tree "${TAURI_DIR}/target/release/bundle/appimage" remove_build_tree "${TAURI_DIR}/target/release/bundle/deb" remove_build_tree "${TAURI_DIR}/target/release/bundle/rpm" + (cd "${TAURI_DIR}" && cargo build --bins --features tauri/custom-protocol --release) + sanitize_linux_target_release_executable run_with_nix_usr_bin bun tauri build --verbose --no-sign --config "$(linux_tauri_pr_config)" restore_linux_runtime_library_path normalize_linux_desktop_packages @@ -34,7 +36,9 @@ case "$(host_os)" in desktop_artifacts=() while IFS= read -r -d '' file; do desktop_artifacts+=("${file}") - done < <(find "${TAURI_DIR}/target/release/bundle" -type f \( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' \) -print0 | LC_ALL=C sort -z) + done < <(find "${TAURI_DIR}/target/release/bundle" -type f \( -name 'Maple_*.AppImage' -o -name '*.deb' -o -name '*.rpm' \) -print0 | LC_ALL=C sort -z) + verify_linux_desktop_package_metadata "${desktop_artifacts[@]}" + repro_dir="${TAURI_DIR}/target/reproducibility" write_sha256_manifest "${repro_dir}/desktop-pr-linux-final.sha256" "${desktop_artifacts[@]}" "${TAURI_DIR}/target/release/maple" print_file_hashes "${desktop_artifacts[@]}" diff --git a/scripts/ci/desktop-release.sh b/scripts/ci/desktop-release.sh index 21a9de64..190b4e4a 100755 --- a/scripts/ci/desktop-release.sh +++ b/scripts/ci/desktop-release.sh @@ -49,6 +49,7 @@ case "$(host_os)" in release_config="$(linux_tauri_release_config)" run_with_nix_usr_bin pkg-config --modversion glib-2.0 (cd "${TAURI_DIR}" && cargo build --bins --features tauri/custom-protocol --release) + sanitize_linux_target_release_executable run_with_nix_usr_bin bun tauri build --verbose --config "${release_config}" restore_linux_runtime_library_path normalize_linux_desktop_packages @@ -63,14 +64,15 @@ case "$(host_os)" in while IFS= read -r -d '' file; do desktop_artifacts+=("${file}") done < <(find "${TAURI_DIR}/target/release/bundle" -type f \( \ - -name '*.AppImage' -o \ - -name '*.AppImage.sig' -o \ + -name 'Maple_*.AppImage' -o \ + -name 'Maple_*.AppImage.sig' -o \ -name '*.deb' -o \ -name '*.deb.sig' -o \ -name '*.rpm' -o \ -name '*.rpm.sig' \ \) -print0 | LC_ALL=C sort -z) + verify_linux_desktop_package_metadata "${desktop_artifacts[@]}" verify_tauri_updater_signature_files "${desktop_artifacts[@]}" write_sha256_manifest "${repro_dir}/desktop-release-linux-final.sha256" "${desktop_artifacts[@]}" print_file_hashes "${desktop_artifacts[@]}" diff --git a/scripts/ci/ios-release.sh b/scripts/ci/ios-release.sh index 5865e390..6802e89e 100755 --- a/scripts/ci/ios-release.sh +++ b/scripts/ci/ios-release.sh @@ -122,8 +122,13 @@ find_ios_release_app() { write_ios_canonical_app_file_manifest() { local app="$1" local out="$2" + local tmp - python3 "${REPO_ROOT}/scripts/ci/canonical-ios-app-hash.py" --manifest "${app}" > "${out}" + tmp="$(mktemp -d)" + cp -a "${app}" "${tmp}/app" + remove_apple_signing_metadata "${tmp}/app" + python3 "${REPO_ROOT}/scripts/ci/canonical-ios-app-hash.py" --manifest "${tmp}/app" > "${out}" + rm -rf "${tmp}" } write_ios_ipa_payload_canonical_file_manifest() { @@ -156,6 +161,9 @@ write_ios_canonical_app_manifest_diff() { sed -E 's/^([0-9a-f]{64}) (.*)$/\2 \1/' "${unsigned_manifest}" | LC_ALL=C sort > "${unsigned_by_path}" sed -E 's/^([0-9a-f]{64}) (.*)$/\2 \1/' "${signed_manifest}" | LC_ALL=C sort > "${signed_by_path}" comm -3 "${unsigned_by_path}" "${signed_by_path}" > "${out}" + if [ ! -s "${out}" ]; then + printf 'No canonical file manifest differences after stripping Apple signing metadata.\n' > "${out}" + fi rm -f "${unsigned_by_path}" "${signed_by_path}" } @@ -179,24 +187,22 @@ if [ -z "${signed_app}" ]; then echo "Could not find signed iOS app build product under ${TAURI_DIR}/gen/apple/build." >&2 exit 1 fi -signed_app_canonical_hash="$(print_canonical_ios_app_hash "${signed_app}" "$(repo_relative_path "${signed_app}")" | tee "${repro_dir}/ios-release-signed-app-canonical.sha256" | awk '{ print $2 }')" -cat "${repro_dir}/ios-release-signed-app-canonical.sha256" -write_ios_canonical_app_file_manifest "${signed_app}" "${repro_dir}/ios-release-signed-app-canonical-files.sha256" +signed_app_canonical_hash="$(print_canonical_ios_app_hash "${signed_app}" "$(repo_relative_path "${signed_app}")" | tee "${repro_dir}/ios-release-archive-app-canonical.sha256" | awk '{ print $2 }')" +cat "${repro_dir}/ios-release-archive-app-canonical.sha256" +write_ios_canonical_app_file_manifest "${signed_app}" "${repro_dir}/ios-release-archive-app-canonical-files.sha256" write_ios_canonical_app_manifest_diff \ "${repro_dir}/ios-release-unsigned-app-canonical-files.sha256" \ - "${repro_dir}/ios-release-signed-app-canonical-files.sha256" \ - "${repro_dir}/ios-release-signed-vs-unsigned-canonical.diff.txt" + "${repro_dir}/ios-release-archive-app-canonical-files.sha256" \ + "${repro_dir}/ios-release-archive-app-vs-unsigned-canonical.diff.txt" if [ "${signed_app_canonical_hash}" != "${unsigned_app_canonical_hash}" ]; then - echo "warning-ios-signed-app-canonical-mismatch signed iOS app does not strip back to the unsigned app tree." >&2 + echo "diagnostic-ios-archive-app-canonical-mismatch archived iOS app does not strip back to the unsigned app tree." >&2 echo "unsigned=${unsigned_app_canonical_hash}" >&2 - echo "signed_canonical=${signed_app_canonical_hash}" >&2 - if [ -s "${repro_dir}/ios-release-signed-vs-unsigned-canonical.diff.txt" ]; then - echo "First canonical iOS file manifest differences:" >&2 - sed -n '1,80p' "${repro_dir}/ios-release-signed-vs-unsigned-canonical.diff.txt" >&2 - fi + echo "archive_canonical=${signed_app_canonical_hash}" >&2 + echo "Canonical iOS archive app file manifest diff diagnostic:" >&2 + sed -n '1,80p' "${repro_dir}/ios-release-archive-app-vs-unsigned-canonical.diff.txt" >&2 else - printf 'verified-ios-signed-app %s %s\n' "${signed_app_canonical_hash}" "$(repo_relative_path "${signed_app}")" + printf 'verified-ios-archive-app %s %s\n' "${signed_app_canonical_hash}" "$(repo_relative_path "${signed_app}")" fi ios_artifacts=() @@ -228,10 +234,8 @@ for artifact in "${ios_artifacts[@]}"; do echo "warning-ios-exported-payload-canonical-mismatch exported iOS IPA payload does not strip back to the unsigned app build product." >&2 echo "unsigned=${unsigned_app_canonical_hash}" >&2 echo "ipa_payload=${ipa_canonical_hash}" >&2 - if [ -s "${payload_diff}" ]; then - echo "First canonical iOS IPA payload differences:" >&2 - sed -n '1,80p' "${payload_diff}" >&2 - fi + echo "Canonical iOS IPA payload file manifest diff diagnostic:" >&2 + sed -n '1,80p' "${payload_diff}" >&2 if [ "${MAPLE_ENFORCE_IOS_SIGNED_REPRODUCIBILITY:-0}" = "1" ]; then exit 1 fi diff --git a/scripts/ci/verify-release-artifacts.sh b/scripts/ci/verify-release-artifacts.sh index be3447aa..6d38059b 100755 --- a/scripts/ci/verify-release-artifacts.sh +++ b/scripts/ci/verify-release-artifacts.sh @@ -15,8 +15,9 @@ The all target requires every release proof class. The present target verifies only the proof classes found in the artifact directory, which is useful for partial PR artifact bundles. -Set MAPLE_VERIFY_ALLOW_PLATFORM_SKIPS=1 to skip host-specific Apple container -checks, such as DMG or IPA canonicalization on non-macOS hosts. +Set MAPLE_VERIFY_ALLOW_PLATFORM_SKIPS=1 to skip host-specific container checks, +such as DMG or IPA canonicalization on non-macOS hosts and Linux installer +metadata checks on non-Linux hosts. EOF } @@ -274,6 +275,47 @@ verify_tauri_signatures_in_artifacts() { done < <(find "${artifacts_dir}" -type f -name '*.sig' -print0 | LC_ALL=C sort -z) } +verify_linux_installer_metadata() { + local appimage deb + local -a appimages=() + local -a debs=() + + if [ "$(host_os)" != "linux" ]; then + if [ "${allow_platform_skips}" = "1" ]; then + printf 'skipped-linux-installer-metadata requires-linux\n' + return 0 + fi + echo "Linux installer metadata verification requires Linux." >&2 + return 1 + fi + + while IFS= read -r -d '' appimage; do + appimages+=("${appimage}") + done < <(find "${artifacts_dir}" -type f -name 'Maple_*.AppImage' -print0 | LC_ALL=C sort -z) + + while IFS= read -r -d '' deb; do + debs+=("${deb}") + done < <(find "${artifacts_dir}" -type f -name '*.deb' -print0 | LC_ALL=C sort -z) + + if [ "${#appimages[@]}" -eq 0 ]; then + echo "No Linux Maple AppImage artifact found for installer metadata verification." >&2 + return 1 + fi + + if [ "${#debs[@]}" -eq 0 ]; then + echo "No Linux .deb artifact found for installer metadata verification." >&2 + return 1 + fi + + for appimage in "${appimages[@]}"; do + verify_linux_appimage_executable_metadata "${appimage}" + done + + for deb in "${debs[@]}"; do + verify_linux_deb_package_executable_metadata "${deb}" + done +} + verify_linux_manifest() { local final_manifest="$1" local fake_pub_manifest @@ -284,6 +326,7 @@ verify_linux_manifest() { fi verify_file_manifest "${final_manifest}" + verify_linux_installer_metadata verify_tauri_signatures_in_artifacts } @@ -494,32 +537,37 @@ verify_macos() { } verify_ios() { - local final_manifest unsigned_manifest signed_manifest payload_manifest - local unsigned_digest signed_digest payload_digest payload_seen payload_mismatch + local final_manifest unsigned_manifest archive_manifest payload_manifest + local unsigned_digest archive_digest payload_digest payload_seen payload_mismatch final_manifest="$(proof_file_required ios-release-final.sha256)" unsigned_manifest="$(proof_file_required ios-release-unsigned-app-canonical.sha256)" - signed_manifest="$(proof_file_required ios-release-signed-app-canonical.sha256)" + archive_manifest="$(proof_file_optional ios-release-archive-app-canonical.sha256)" + if [ -z "${archive_manifest}" ]; then + archive_manifest="$(proof_file_optional ios-release-signed-app-canonical.sha256)" + fi payload_manifest="$(proof_file_required ios-release-canonical-payload.sha256)" verify_file_manifest "${final_manifest}" unsigned_digest="$(manifest_single_digest "${unsigned_manifest}")" - signed_digest="$(manifest_single_digest "${signed_manifest}")" - if [ -z "${unsigned_digest}" ] || [ -z "${signed_digest}" ]; then - echo "iOS signed app canonical proof is missing." >&2 + if [ -z "${unsigned_digest}" ]; then + echo "iOS unsigned app canonical proof is missing." >&2 echo "unsigned=${unsigned_digest:-missing}" >&2 - echo "signed=${signed_digest:-missing}" >&2 return 1 fi - if [ "${unsigned_digest}" != "${signed_digest}" ]; then - echo "iOS signed app canonical proof does not match unsigned proof." >&2 - echo "unsigned=${unsigned_digest:-missing}" >&2 - echo "signed=${signed_digest:-missing}" >&2 - printf 'warning-ios-signed-app-proof-mismatch unsigned=%s signed=%s\n' "${unsigned_digest}" "${signed_digest}" - else - printf 'verified-ios-signed-app-proof %s\n' "${signed_digest}" + if [ -n "${archive_manifest}" ]; then + archive_digest="$(manifest_single_digest "${archive_manifest}")" + if [ -z "${archive_digest}" ]; then + echo "iOS archive app canonical diagnostic proof is empty." >&2 + return 1 + fi + if [ "${unsigned_digest}" != "${archive_digest}" ]; then + printf 'diagnostic-ios-archive-app-proof-mismatch unsigned=%s archive=%s\n' "${unsigned_digest}" "${archive_digest}" + else + printf 'verified-ios-archive-app-proof %s\n' "${archive_digest}" + fi fi verify_canonical_apple_manifest "${payload_manifest}"