227 lines
6.0 KiB
Bash
227 lines
6.0 KiB
Bash
#!/bin/sh
|
|
# Alpine Reproducible Build Script
|
|
# Builds packages with deterministic settings for fingerprint generation
|
|
#
|
|
# Usage: build.sh [build|diff] <package> <version> [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 <<EOF
|
|
Alpine Reproducible Builder
|
|
|
|
Usage:
|
|
build.sh build <package> <version> [patch_urls...]
|
|
Build a package with reproducible settings
|
|
|
|
build.sh diff <package> <vuln_version> <patched_version>
|
|
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
|