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.
This commit is contained in:
Julius Joska
2026-10-23 09:15:15 +02:00
parent ab0e35ddf4
commit ae127decd9
2 changed files with 112 additions and 0 deletions
+25
View File
@@ -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
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Checks the libsignal native library against the pins in native_libs.sha256.
#
# verify_native_libs.sh <app.aab|app.apk> 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