#!/bin/sh # Alpine Reproducible Build Script # Builds packages with deterministic settings for fingerprint generation # # Usage: build.sh [build|diff] [patch_url...] # # Examples: # build.sh build openssl 3.0.7-r0 # build.sh diff openssl 3.0.7-r0 3.0.8-r0 # build.sh build openssl 3.0.7-r0 https://patch.url/CVE-2023-1234.patch set -eu COMMAND="${1:-help}" PACKAGE="${2:-}" VERSION="${3:-}" OUTPUT_DIR="${OUTPUT_DIR:-/output}" log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >&2 } show_help() { cat < [patch_urls...] Build a package with reproducible settings build.sh diff Build two versions and compute fingerprint diff build.sh --help Show this help message Environment: SOURCE_DATE_EPOCH Override timestamp (extracted from APKBUILD if not set) OUTPUT_DIR Output directory (default: /output) CFLAGS Additional compiler flags LDFLAGS Additional linker flags Examples: build.sh build openssl 3.0.7-r0 build.sh build curl 8.1.0-r0 https://patch/CVE-2023-1234.patch build.sh diff openssl 3.0.7-r0 3.0.8-r0 EOF } setup_reproducible_env() { local pkg="$1" local ver="$2" # Extract SOURCE_DATE_EPOCH from APKBUILD if not set if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then if [ -f "aports/main/$pkg/APKBUILD" ]; then # Use pkgrel date or fallback to current SOURCE_DATE_EPOCH=$(stat -c %Y "aports/main/$pkg/APKBUILD" 2>/dev/null || date +%s) else SOURCE_DATE_EPOCH=$(date +%s) fi export SOURCE_DATE_EPOCH fi log "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" # Reproducible compiler flags export CFLAGS="${CFLAGS:-} -fno-record-gcc-switches -fdebug-prefix-map=$(pwd)=/build" export CXXFLAGS="${CXXFLAGS:-} ${CFLAGS}" export LDFLAGS="${LDFLAGS:-}" # Locale for deterministic sorting export LC_ALL=C.UTF-8 export TZ=UTC } fetch_source() { local pkg="$1" local ver="$2" log "Fetching source for $pkg-$ver" # Clone aports if needed if [ ! -d "aports" ]; then git clone --depth 1 https://gitlab.alpinelinux.org/alpine/aports.git fi # Find package local pkg_dir="" for repo in main community testing; do if [ -d "aports/$repo/$pkg" ]; then pkg_dir="aports/$repo/$pkg" break fi done if [ -z "$pkg_dir" ]; then log "ERROR: Package $pkg not found in aports" return 1 fi # Checkout specific version if needed cd "$pkg_dir" abuild fetch abuild unpack } apply_patches() { local src_dir="$1" shift for patch_url in "$@"; do log "Applying patch: $patch_url" curl -sSL "$patch_url" | patch -d "$src_dir" -p1 done } build_package() { local pkg="$1" local ver="$2" shift 2 local patches="$@" log "Building $pkg-$ver" setup_reproducible_env "$pkg" "$ver" cd /build fetch_source "$pkg" "$ver" if [ -n "$patches" ]; then apply_patches "src/$pkg-*" $patches fi # Build with reproducible settings abuild -r # Copy output local out_dir="$OUTPUT_DIR/$pkg-$ver" mkdir -p "$out_dir" cp -r ~/packages/*/*.apk "$out_dir/" 2>/dev/null || true # Extract binaries and fingerprints for apk in "$out_dir"/*.apk; do [ -f "$apk" ] || continue local apk_name=$(basename "$apk" .apk) mkdir -p "$out_dir/extracted/$apk_name" tar -xzf "$apk" -C "$out_dir/extracted/$apk_name" # Extract function fingerprints /usr/local/bin/extract-functions.sh "$out_dir/extracted/$apk_name" > "$out_dir/$apk_name.functions.json" done log "Build complete: $out_dir" } diff_versions() { local pkg="$1" local vuln_ver="$2" local patched_ver="$3" log "Building and diffing $pkg: $vuln_ver vs $patched_ver" # Build vulnerable version build_package "$pkg" "$vuln_ver" # Build patched version build_package "$pkg" "$patched_ver" # Compute diff local diff_out="$OUTPUT_DIR/$pkg-diff-$vuln_ver-vs-$patched_ver.json" # Simple diff of function fingerprints jq -s ' .[0] as $vuln | .[1] as $patched | { package: "'"$pkg"'", vulnerable_version: "'"$vuln_ver"'", patched_version: "'"$patched_ver"'", vulnerable_functions: ($vuln | length), patched_functions: ($patched | length), added: [($patched[] | select(.name as $n | ($vuln | map(.name) | index($n)) == null))], removed: [($vuln[] | select(.name as $n | ($patched | map(.name) | index($n)) == null))], modified: [ $vuln[] | .name as $n | .hash as $h | ($patched[] | select(.name == $n and .hash != $h)) | {name: $n, vuln_hash: $h, patched_hash: .hash} ] } ' \ "$OUTPUT_DIR/$pkg-$vuln_ver"/*.functions.json \ "$OUTPUT_DIR/$pkg-$patched_ver"/*.functions.json \ > "$diff_out" log "Diff complete: $diff_out" } case "$COMMAND" in build) if [ -z "$PACKAGE" ] || [ -z "$VERSION" ]; then log "ERROR: Package and version required" show_help exit 1 fi shift 2 # Remove command, package, version build_package "$PACKAGE" "$VERSION" "$@" ;; diff) PATCHED_VERSION="${4:-}" if [ -z "$PACKAGE" ] || [ -z "$VERSION" ] || [ -z "$PATCHED_VERSION" ]; then log "ERROR: Package, vulnerable version, and patched version required" show_help exit 1 fi diff_versions "$PACKAGE" "$VERSION" "$PATCHED_VERSION" ;; --help|help) show_help ;; *) log "ERROR: Unknown command: $COMMAND" show_help exit 1 ;; esac