From ae127decd937e3b539f4b304d7d02d7c9f6584b8 Mon Sep 17 00:00:00 2001 From: Julius Joska Date: Fri, 23 Oct 2026 09:15:15 +0200 Subject: [PATCH] build: pin the native libsignal library by SHA-256 The libsignal Dart package downloads a prebuilt libsignal_frb from the GitHub releases of djx-y-z/libsignal_dart and only checks it against a checksum file from the same release. Keep our own pins in app/tool/native_libs.sha256 and verify the finished AAB/APK with app/tool/verify_native_libs.sh; any mismatch fails the release build. The pins cover the upstream archives and the library as packaged after the Android Gradle plugin strips it. `--from-upstream` re-derives the packaged hashes from the upstream archives with llvm-strip. --- app/tool/native_libs.sha256 | 25 ++++++++++ app/tool/verify_native_libs.sh | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 app/tool/native_libs.sha256 create mode 100755 app/tool/verify_native_libs.sh diff --git a/app/tool/native_libs.sha256 b/app/tool/native_libs.sha256 new file mode 100644 index 0000000..338958c --- /dev/null +++ b/app/tool/native_libs.sha256 @@ -0,0 +1,25 @@ +# Pinned native crypto library (libsignal_frb) shipped in the Android app. +# +# The Dart package `libsignal` downloads a prebuilt `libsignal_frb` from the +# GitHub releases of djx-y-z/libsignal_dart during the build. Its own check +# only compares against a checksum file from the same release, so a replaced +# release would go unnoticed. These pins live in our repository instead and +# tool/verify_native_libs.sh fails the release build on any mismatch. +# +# package = version of the `libsignal` Dart package in pubspec.lock +# archive = SHA-256 of the upstream release archive +# https://github.com/djx-y-z/libsignal_dart/releases/tag/libsignal_frb-4.0.9 +# shipped = SHA-256 of liblibsignal_frb.so inside our AAB/APK. The Android +# Gradle plugin strips the library while packaging; the value equals +# `llvm-strip --strip-unneeded` (NDK 28) applied to the file from the +# archive. `verify_native_libs.sh --from-upstream` re-derives it. +# +# Updating libsignal = update every line here in the same commit. + +package libsignal 5.0.9 +archive android-arm64-v8a b03c1f47b07ded392a9a44b217b3d24470f0dce69901c67061c1f1295f96065f +archive android-armeabi-v7a 0dc16fbbfe2bf5b3e7758fd2b297c831fee296c55ac19b4f94a9e0086d26b9b0 +archive android-x86_64 cdb1623a855fa508dd84c025b267eaac846f882637450f00a74199bdf2d85264 +shipped arm64-v8a 39612f422ed1dc2c7576165c5c169d7911f64de093eeea4ee8d3047b884c1c0a +shipped armeabi-v7a b442795a864fe0ae15a1e04a151863244fedda3b8854b2d22ec9b23e59227e78 +shipped x86_64 59e5925c2d6f5e9496cbe86d1a7d19965dc4060efe94cd452d58ba651fe7a122 diff --git a/app/tool/verify_native_libs.sh b/app/tool/verify_native_libs.sh new file mode 100755 index 0000000..2168139 --- /dev/null +++ b/app/tool/verify_native_libs.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Checks the libsignal native library against the pins in native_libs.sha256. +# +# verify_native_libs.sh every ABI in the artifact must carry +# exactly the pinned library +# verify_native_libs.sh --from-upstream download the upstream archives, check +# their pins and re-derive the shipped +# hashes (needs llvm-strip from the NDK) +# +# Exit code 0 = match, anything else = do not ship. +set -euo pipefail + +TOOL_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PINS="$TOOL_DIR/native_libs.sha256" +LOCK="$TOOL_DIR/../pubspec.lock" +LIB=liblibsignal_frb.so + +die() { echo "verify_native_libs: $*" >&2; exit 1; } +pin() { awk -v k="$1" -v n="$2" '$1 == k && $2 == n { print $3 }' "$PINS"; } + +pinned_package=$(pin package libsignal) +[ -n "$pinned_package" ] || die "no package line in $PINS" + +# A libsignal upgrade without new pins must stop here with a clear reason, +# not later as an unexplained hash mismatch. +if [ -f "$LOCK" ]; then + locked=$(awk '/^ libsignal:$/ { f = 1 } f && /^ version:/ { gsub(/"/, "", $2); print $2; exit }' "$LOCK") + [ "$locked" = "$pinned_package" ] || + die "pubspec.lock has libsignal $locked, pins are for $pinned_package — update $PINS" +fi + +sha() { sha256sum "$1" | cut -d' ' -f1; } + +verify_artifact() { + local artifact=$1 prefix entries abi want got checked=0 + [ -f "$artifact" ] || die "file not found: $artifact" + case "$artifact" in + *.aab) prefix=base/lib ;; + *.apk) prefix=lib ;; + *) die "expected .aab or .apk: $artifact" ;; + esac + entries=$(unzip -Z1 "$artifact" | grep -E "^$prefix/[^/]+/$LIB\$" || true) + [ -n "$entries" ] || die "$LIB not found in $artifact" + for entry in $entries; do + abi=$(basename "$(dirname "$entry")") + want=$(pin shipped "$abi") + [ -n "$want" ] || die "no pin for ABI $abi" + got=$(unzip -p "$artifact" "$entry" | sha256sum | cut -d' ' -f1) + [ "$got" = "$want" ] || die "$abi: $got does not match pinned $want" + echo "ok $abi $got" + checked=$((checked + 1)) + done + # Every pinned ABI has to be present: a missing one would crash on those devices. + for abi in $(awk '$1 == "shipped" { print $2 }' "$PINS"); do + grep -q "^$prefix/$abi/$LIB\$" <<<"$entries" || die "$abi missing in $artifact" + done + echo "verify_native_libs: $checked libraries match the pins" +} + +from_upstream() { + local strip=${LLVM_STRIP:-} work version url abi want got + if [ -z "$strip" ]; then + strip=$(ls "${ANDROID_HOME:-/opt/android-sdk}"/ndk/*/toolchains/llvm/prebuilt/*/bin/llvm-strip 2>/dev/null | sort -V | tail -1) + fi + [ -x "$strip" ] || die "llvm-strip not found, set LLVM_STRIP" + # Archive names carry the version of the native crate, not of the Dart package. + version=$(sed -n 's/.*releases\/tag\/libsignal_frb-\([0-9.]*\).*/\1/p' "$PINS" | head -1) + [ -n "$version" ] || die "no release tag in $PINS" + url="https://github.com/djx-y-z/libsignal_dart/releases/download/libsignal_frb-$version" + work=$(mktemp -d); trap 'rm -rf "$work"' RETURN + for abi in $(awk '$1 == "shipped" { print $2 }' "$PINS"); do + curl -fsSL -o "$work/$abi.tar.gz" "$url/libsignal_frb-$version-android-$abi.tar.gz" + want=$(pin archive "android-$abi"); got=$(sha "$work/$abi.tar.gz") + [ "$got" = "$want" ] || die "archive android-$abi: $got does not match pinned $want" + mkdir "$work/$abi" && tar -xzf "$work/$abi.tar.gz" -C "$work/$abi" "$LIB" + "$strip" --strip-unneeded "$work/$abi/$LIB" + want=$(pin shipped "$abi"); got=$(sha "$work/$abi/$LIB") + [ "$got" = "$want" ] || die "stripped $abi: $got does not match pinned $want" + echo "ok $abi archive and stripped library match" + done +} + +case "${1:-}" in + --from-upstream) from_upstream ;; + ""|-h|--help) sed -n '2,10p' "$0"; exit 64 ;; + *) verify_artifact "$1" ;; +esac