diff --git a/.gitignore b/.gitignore index f0cb420b..11201ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ # CLion .idea/ +# Python build/test artifacts +__pycache__/ +*.py[cod] + # VC Linux build artifacts *.o *.o0 diff --git a/src/Build/Packaging/openwrt/README.md b/src/Build/Packaging/openwrt/README.md new file mode 100644 index 00000000..ac28edd4 --- /dev/null +++ b/src/Build/Packaging/openwrt/README.md @@ -0,0 +1,163 @@ +# OpenWrt packaging + +This directory contains the canonical OpenWrt package template and local test +configuration for building VeraCrypt console-only with the OpenWrt SDK. +It is a maintainer build-and-test harness for the VeraCrypt working tree, not +an OpenWrt packages-feed submission recipe. + +The current supported target is `x86/64`, matching the QEMU runtime smoke test. +The build uses: + +- OpenWrt 24.10.6 x86/64 SDK by default +- musl through the OpenWrt toolchain +- `NOGUI=1` +- `WITHFUSE3=1` +- `WXSTATIC=1` +- wxWidgets 3.2.10 built as static wxBase +- `NOTEST=1` during cross compilation +- `NOSTRIP=1`, with OpenWrt package stripping disabled for maintainer + diagnostics + +The package installs only: + +- `/usr/bin/veracrypt` +- `/sbin/mount.veracrypt` +- `/usr/share/licenses/veracrypt/License.txt` + +`mount.veracrypt` uses a Bash shebang, so the OpenWrt package declares `bash` +as a runtime dependency. + +## Build + +From the VeraCrypt checkout root: + +```sh +src/Build/build_veracrypt_openwrt.sh +``` + +The script downloads and verifies the OpenWrt SDK against a pinned SHA-256 for +the supported release/target, downloads and verifies wxWidgets 3.2.10, installs +the required OpenWrt feeds, renders +`package/utils/veracrypt/Makefile` inside the SDK, builds the `.ipk`, and +also builds the local userland `bash`, `fuse3`, `util-linux`, and `lvm2` feed +packages used by the QEMU runtime test. Kernel modules for the stock OpenWrt +image are resolved by the test from the official OpenWrt kmod feed for the +selected release and target. + +Default output location: + +```text +../openwrt-veracrypt/openwrt-sdk-24.10.6-x86-64_gcc-13.3.0_musl.Linux-x86_64/bin/packages/x86_64/base/veracrypt_-r1_x86_64.ipk +``` + +Useful options: + +```sh +src/Build/build_veracrypt_openwrt.sh --fresh-sdk +src/Build/build_veracrypt_openwrt.sh --work-dir /tmp/veracrypt-openwrt +src/Build/build_veracrypt_openwrt.sh --sdk-dir /path/to/openwrt-sdk +src/Build/build_veracrypt_openwrt.sh --sdk-url URL --sdk-sha256 HASH +src/Build/build_veracrypt_openwrt.sh --wx-version 3.2.10 +``` + +Custom SDK URLs or unsupported OpenWrt release/target combinations must pass +`--sdk-sha256`; the build script does not trust an unsigned `sha256sums` file as +the sole integrity source for SDK archives. + +If the host only has `mawk`, the build script downloads and builds GNU awk +locally under the OpenWrt work directory because OpenWrt feed scripts require +GNU awk behavior on some hosts. + +## QEMU Runtime Test + +Install or provide `qemu-system-x86_64`. On Debian/Ubuntu hosts: + +```sh +sudo apt install qemu-system-x86 +``` + +Then run: + +```sh +python3 src/Build/test_veracrypt_openwrt_qemu.py \ + --ipk ../openwrt-veracrypt/openwrt-sdk-24.10.6-x86-64_gcc-13.3.0_musl.Linux-x86_64/bin/packages/x86_64/base/veracrypt_-r1_x86_64.ipk +``` + +The test script downloads and verifies the matching OpenWrt x86/64 ext4 image, +boots it with QEMU user networking, waits for OpenWrt network init to settle, +gets a DHCP lease on `br-lan` or `eth0`, resolves the needed dependency closure +from local SDK `.ipk` control metadata plus the official OpenWrt kmod feed, +serves those packages to the guest, and installs them with `opkg`. It then runs: + +```sh +veracrypt --text --version +veracrypt --text --test +``` + +By default it also creates a 16 MiB AES/SHA-512 test container, opens it with +`--filesystem=none`, verifies it appears in `veracrypt --text --list`, and +unmounts it. Use `--skip-container` to run only the package install, version, +and algorithm self-test path. + +If QEMU was extracted locally instead of installed system-wide, pass the binary +and firmware directory explicitly: + +```sh +LD_LIBRARY_PATH=/path/to/qemu-libs \ +python3 src/Build/test_veracrypt_openwrt_qemu.py \ + --qemu /path/to/qemu-system-x86_64 \ + --qemu-data-dir /path/to/pc-bios \ + --ipk /path/to/veracrypt_-r1_x86_64.ipk +``` + +The runner defaults to one QEMU vCPU because TCG with multiple vCPUs can +intermittently trip x86 APIC timer startup in the stock OpenWrt image. +The runner does not require external package feed access from the guest; all +runtime packages are served over the host-to-guest QEMU user-networking link. +Local userland packages come from the SDK `bin/` directory, while stock-image +kmods are downloaded by the host from the official OpenWrt kmod feed and staged +locally. If the VeraCrypt `.ipk` is not below the SDK `bin/` directory, pass +`--package-bin-dir /path/to/sdk/bin`. + +For custom images whose kernel does not match the official release feed, pass +`--kmod-feed-url` for the matching kmod feed, or `--local-kmods` to resolve +kmods from `--package-bin-dir`. + +The test log is written to: + +```text +../openwrt-veracrypt/openwrt-qemu-test.log +``` + +## Runtime Packages + +The VeraCrypt package itself declares the direct userland dependencies using +OpenWrt package symbols: + +- `libstdcpp` +- `libfuse3` +- `bash` + +OpenWrt's FUSE3 recipe defines `Package/libfuse3` with ABI version `3`, so the +binary IPK and package-index metadata are emitted as `libfuse3-3` while still +providing `libfuse3`. The QEMU test installs the seed runtime support normally +needed for useful mounts using package-index names: + +- `libfuse3-3` +- `fuse3-utils` +- `kmod-fuse` +- `kmod-loop` +- `lvm2` +- `kmod-dm` +- `losetup` +- `blkid` +- `mount-utils` +- `kmod-crypto-misc` + +The test resolver also stages required transitive dependencies from package +metadata, such as `libdevmapper` when pulled by `lvm2`. + +Filesystem-specific mounts also need the corresponding OpenWrt filesystem +kernel modules and tools. Smart-card and EMV keyfile support should install +`libpcsclite`, `pcscd`, and the appropriate reader driver such as `ccid`; these +are optional and are not part of the base package dependency set. diff --git a/src/Build/Packaging/openwrt/configs/x86_64-minimal.config b/src/Build/Packaging/openwrt/configs/x86_64-minimal.config new file mode 100644 index 00000000..0b8e74b2 --- /dev/null +++ b/src/Build/Packaging/openwrt/configs/x86_64-minimal.config @@ -0,0 +1,23 @@ +CONFIG_TARGET_x86=y +CONFIG_TARGET_x86_64=y +# CONFIG_TARGET_MULTI_PROFILE is not set +# CONFIG_TARGET_ALL_PROFILES is not set +CONFIG_TARGET_DEVICE_x86_64_DEVICE_generic=y + +# CONFIG_ALL is not set +# CONFIG_ALL_KMODS is not set +# CONFIG_ALL_NONSHARED is not set +# CONFIG_DEVEL is not set + +CONFIG_PACKAGE_veracrypt=m +CONFIG_PACKAGE_bash=m +CONFIG_PACKAGE_libfuse3=m +CONFIG_PACKAGE_fuse3-utils=m +CONFIG_PACKAGE_kmod-fuse=m +CONFIG_PACKAGE_kmod-loop=m +CONFIG_PACKAGE_kmod-dm=m +CONFIG_PACKAGE_kmod-crypto-misc=m +CONFIG_PACKAGE_lvm2=m +CONFIG_PACKAGE_losetup=m +CONFIG_PACKAGE_blkid=m +CONFIG_PACKAGE_mount-utils=m diff --git a/src/Build/Packaging/openwrt/package/utils/veracrypt/Makefile.in b/src/Build/Packaging/openwrt/package/utils/veracrypt/Makefile.in new file mode 100644 index 00000000..780bc6b7 --- /dev/null +++ b/src/Build/Packaging/openwrt/package/utils/veracrypt/Makefile.in @@ -0,0 +1,91 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=veracrypt +PKG_VERSION:=@VERACRYPT_VERSION@ +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 AND LicenseRef-TrueCrypt +PKG_LICENSE_FILES:=veracrypt/src/License.txt +PKG_MAINTAINER:=Mounir IDRASSI + +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) +PKG_BUILD_PARALLEL:=1 +PKG_BUILD_DEPENDS:=fuse3 pcsc-lite + +WXWIDGETS_VERSION:=@WXWIDGETS_VERSION@ +VERACRYPT_SOURCE_DIR:=@VERACRYPT_SOURCE_DIR@ +WXWIDGETS_SOURCE_DIR:=@WXWIDGETS_SOURCE_DIR@ + +include $(INCLUDE_DIR)/package.mk + +RSTRIP:=: +STRIP:=: + +define Package/veracrypt + SECTION:=utils + CATEGORY:=Utilities + SUBMENU:=Filesystem + TITLE:=VeraCrypt console + URL:=https://www.veracrypt.fr/ + DEPENDS:=+libstdcpp +libfuse3 +bash +endef + +define Package/veracrypt/description + Console-only VeraCrypt build for OpenWrt using FUSE3 and a static wxBase. +endef + +define Build/Prepare + rm -rf $(PKG_BUILD_DIR) + $(INSTALL_DIR) $(PKG_BUILD_DIR) + rsync -a --delete \ + --exclude .git \ + --exclude 'src/wxrelease' \ + --exclude 'src/wxdebug' \ + --exclude 'src/Main/veracrypt' \ + --exclude 'src/Setup/Linux/usr' \ + --exclude '*.o' \ + --exclude '*.d' \ + --exclude '*.a' \ + $(VERACRYPT_SOURCE_DIR)/ $(PKG_BUILD_DIR)/veracrypt/ + rsync -a --delete $(WXWIDGETS_SOURCE_DIR)/ $(PKG_BUILD_DIR)/wxWidgets-$(WXWIDGETS_VERSION)/ +endef + +define Build/Configure +endef + +VC_COMMON_MAKE_FLAGS = \ + AR="$(TARGET_AR)" \ + CC="$(TARGET_CC)" \ + CXX="$(TARGET_CXX)" \ + AS="yasm" \ + RANLIB="$(TARGET_RANLIB)" \ + PKG_CONFIG="$(PKG_CONFIG)" \ + PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" \ + WX_ROOT="$(PKG_BUILD_DIR)/wxWidgets-$(WXWIDGETS_VERSION)" \ + WX_BUILD_DIR="$(PKG_BUILD_DIR)/wxBuildConsole" \ + WX_CONFIGURE_EXTRA_FLAGS="--target=$(GNU_TARGET_NAME) --host=$(GNU_TARGET_NAME) --build=$(GNU_HOST_NAME) --prefix=/usr --exec-prefix=/usr --disable-rpath" \ + TC_EXTRA_CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS)" \ + TC_EXTRA_CXXFLAGS="$(TARGET_CXXFLAGS) $(TARGET_CPPFLAGS)" \ + TC_EXTRA_LFLAGS="$(TARGET_LDFLAGS)" \ + NOGUI=1 \ + WITHFUSE3=1 \ + WXSTATIC=1 \ + NOTEST=1 \ + NOSTRIP=1 \ + VERBOSE=1 + +define Build/Compile + +$(MAKE) -C $(PKG_BUILD_DIR)/veracrypt/src $(VC_COMMON_MAKE_FLAGS) clean + +$(MAKE) -C $(PKG_BUILD_DIR)/veracrypt/src $(VC_COMMON_MAKE_FLAGS) wxbuild + +$(MAKE) -C $(PKG_BUILD_DIR)/veracrypt/src $(PKG_JOBS) $(VC_COMMON_MAKE_FLAGS) +endef + +define Package/veracrypt/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/veracrypt/src/Main/veracrypt $(1)/usr/bin/veracrypt + $(INSTALL_DIR) $(1)/sbin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/veracrypt/src/Setup/Linux/mount.veracrypt $(1)/sbin/mount.veracrypt + $(INSTALL_DIR) $(1)/usr/share/licenses/veracrypt + $(INSTALL_DATA) $(PKG_BUILD_DIR)/veracrypt/src/License.txt $(1)/usr/share/licenses/veracrypt/License.txt +endef + +$(eval $(call BuildPackage,veracrypt)) diff --git a/src/Build/build_veracrypt_openwrt.sh b/src/Build/build_veracrypt_openwrt.sh new file mode 100755 index 00000000..ea10a777 --- /dev/null +++ b/src/Build/build_veracrypt_openwrt.sh @@ -0,0 +1,407 @@ +#!/bin/sh +# +# Copyright (c) 2026 AM Crypto +# Governed by the Apache License 2.0 the full text of which is contained +# in the file License.txt included in VeraCrypt binary and source +# code distribution packages. +# + +set -eu +umask 022 + +OPENWRT_VERSION=24.10.6 +OPENWRT_TARGET=x86/64 +WX_VERSION=3.2.10 +WX_URL= +WX_SHA256=d66e929569947a4a5920699539089a9bda83a93e5f4917fb313a61f0c344b896 +SDK_URL= +SDK_SHA256= +SDK_DIR= +FRESH_SDK=0 + +SCRIPT=$(readlink -f "$0") +SCRIPTPATH=$(dirname "$SCRIPT") +REPOROOT=$(readlink -f "$SCRIPTPATH/../..") +SOURCEPATH="$REPOROOT/src" +PARENTDIR=$(readlink -f "$SCRIPTPATH/../../..") +WORK_DIR="$PARENTDIR/openwrt-veracrypt" +JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + +GAWK_VERSION=5.3.2 +GAWK_URL="https://ftp.gnu.org/gnu/gawk/gawk-$GAWK_VERSION.tar.xz" +GAWK_SHA256=f8c3486509de705192138b00ef2c00bbbdd0e84c30d5c07d23fc73a9dc4cc9cc + +usage() { + cat <&2 + exit 1 +} + +need_tool() { + command -v "$1" >/dev/null 2>&1 || die "Required tool '$1' was not found" +} + +require_option_arg() { + [ $# -ge 2 ] || die "Option $1 requires an argument" +} + +download_file() { + url=$1 + out=$2 + expected_sha=$3 + tmp="$out.tmp.$$" + + mkdir -p "$(dirname "$out")" + if [ -f "$out" ]; then + if [ -z "$expected_sha" ]; then + return + fi + + actual_sha=$(sha256sum "$out" | awk '{print $1}') + if [ "$actual_sha" = "$expected_sha" ]; then + return + fi + + echo "Checksum mismatch for existing $out; re-downloading" >&2 + rm -f "$out" + fi + + rm -f "$tmp" + echo "Downloading $url" + if ! wget -O "$tmp" "$url"; then + rm -f "$tmp" + die "Download failed: $url" + fi + + if [ -n "$expected_sha" ]; then + actual_sha=$(sha256sum "$tmp" | awk '{print $1}') + if [ "$actual_sha" != "$expected_sha" ]; then + rm -f "$tmp" + die "SHA-256 mismatch for $out: expected $expected_sha, got $actual_sha" + fi + fi + + mv "$tmp" "$out" +} + +pinned_sdk_sha256() { + archive_name=$1 + + case "$OPENWRT_VERSION:$OPENWRT_TARGET:$archive_name" in + 24.10.6:x86/64:openwrt-sdk-24.10.6-x86-64_gcc-13.3.0_musl.Linux-x86_64.tar.zst) + printf '%s\n' "9e398ea7efc098e4a986f97efff595e32d08c615fe356bcb3d885d7ad3a39ac0" + ;; + esac +} + +target_defaults() { + case "$OPENWRT_TARGET" in + x86/64) + OPENWRT_TARGET_SLUG=x86-64 + OPENWRT_CONFIG="$REPOROOT/src/Build/Packaging/openwrt/configs/x86_64-minimal.config" + ;; + *) + die "Unsupported target '$OPENWRT_TARGET'. Add a config under src/Build/Packaging/openwrt/configs first." + ;; + esac +} + +openwrt_base_url() { + printf 'https://downloads.openwrt.org/releases/%s/targets/%s\n' "$OPENWRT_VERSION" "$OPENWRT_TARGET" +} + +resolve_sdk() { + if [ -n "$SDK_DIR" ]; then + SDK_DIR=$(readlink -f "$SDK_DIR") + [ -d "$SDK_DIR" ] || die "SDK directory does not exist: $SDK_DIR" + return + fi + + base_url=$(openwrt_base_url) + + mkdir -p "$WORK_DIR/downloads" + if [ -z "$SDK_URL" ]; then + index_file="$WORK_DIR/downloads/openwrt-$OPENWRT_VERSION-$OPENWRT_TARGET_SLUG-index.html" + wget -q -O "$index_file" "$base_url/" + sdk_archive=$(sed -n "s/.*href=\"\\(openwrt-sdk-$OPENWRT_VERSION-${OPENWRT_TARGET_SLUG}_[^\"]*\\.Linux-x86_64\\.tar\\.zst\\)\".*/\\1/p" "$index_file" | head -n 1) + [ -n "$sdk_archive" ] || die "Could not find an SDK archive at $base_url/" + SDK_URL="$base_url/$sdk_archive" + else + sdk_archive=$(basename "$SDK_URL") + fi + + if [ -z "$SDK_SHA256" ]; then + SDK_SHA256=$(pinned_sdk_sha256 "$sdk_archive") + fi + + [ -n "$SDK_SHA256" ] || die "No trusted SDK SHA-256 is available for $sdk_archive; pass --sdk-sha256 for custom SDKs" + + SDK_ARCHIVE_PATH="$WORK_DIR/downloads/$sdk_archive" + download_file "$SDK_URL" "$SDK_ARCHIVE_PATH" "$SDK_SHA256" + + sdk_top=$(zstd -dc "$SDK_ARCHIVE_PATH" | tar -tf - | sed -n '1{s,/.*,,;p;q;}') + [ -n "$sdk_top" ] || die "Could not determine SDK archive top-level directory" + SDK_DIR="$WORK_DIR/$sdk_top" + + if [ "$FRESH_SDK" = "1" ]; then + rm -rf "$SDK_DIR" + fi + + if [ ! -d "$SDK_DIR" ]; then + echo "Extracting $SDK_ARCHIVE_PATH" + zstd -dc "$SDK_ARCHIVE_PATH" | tar -xf - -C "$WORK_DIR" + fi +} + +ensure_gawk() { + HOST_TOOLS="$WORK_DIR/host-tools" + if command -v gawk >/dev/null 2>&1; then + mkdir -p "$HOST_TOOLS/bin" + ln -sf "$(command -v gawk)" "$HOST_TOOLS/bin/gawk" + ln -sf "$(command -v gawk)" "$HOST_TOOLS/bin/awk" + return + fi + + if [ -x "$HOST_TOOLS/prefix/bin/gawk" ]; then + mkdir -p "$HOST_TOOLS/bin" + ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/gawk" + ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/awk" + return + fi + + need_tool make + if ! command -v gcc >/dev/null 2>&1 && ! command -v cc >/dev/null 2>&1; then + die "GNU awk is not installed and no C compiler was found to build it" + fi + + mkdir -p "$HOST_TOOLS/src" + gawk_archive="$HOST_TOOLS/src/gawk-$GAWK_VERSION.tar.xz" + download_file "$GAWK_URL" "$gawk_archive" "$GAWK_SHA256" + + rm -rf "$HOST_TOOLS/src/gawk-$GAWK_VERSION" + tar -xf "$gawk_archive" -C "$HOST_TOOLS/src" + echo "Building GNU awk $GAWK_VERSION for OpenWrt feed scripts" + ( + cd "$HOST_TOOLS/src/gawk-$GAWK_VERSION" + ./configure --prefix="$HOST_TOOLS/prefix" >/dev/null + make -j "$JOBS" >/dev/null + make install >/dev/null + ) + mkdir -p "$HOST_TOOLS/bin" + ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/gawk" + ln -sf ../prefix/bin/gawk "$HOST_TOOLS/bin/awk" +} + +prepare_wxwidgets() { + if [ -z "$WX_URL" ]; then + WX_URL="https://github.com/wxWidgets/wxWidgets/releases/download/v$WX_VERSION/wxWidgets-$WX_VERSION.tar.bz2" + fi + + wx_archive="$WORK_DIR/downloads/wxWidgets-$WX_VERSION.tar.bz2" + WX_SOURCE_DIR="$WORK_DIR/sources/wxWidgets-$WX_VERSION" + download_file "$WX_URL" "$wx_archive" "$WX_SHA256" + + if [ ! -f "$WX_SOURCE_DIR/configure" ]; then + rm -rf "$WX_SOURCE_DIR" + mkdir -p "$WORK_DIR/sources" + echo "Extracting $wx_archive" + tar -xjf "$wx_archive" -C "$WORK_DIR/sources" + fi +} + +sed_escape() { + printf '%s' "$1" | sed 's/[&|]/\\&/g' +} + +render_package_makefile() { + version=$(sed -n 's/^#define[[:space:]][[:space:]]*VERSION_STRING[[:space:]][[:space:]]*"\([^"]*\)".*/\1/p' "$SOURCEPATH/Common/Tcdefs.h" | head -n 1) + [ -n "$version" ] || die "Could not determine VeraCrypt version from src/Common/Tcdefs.h" + + package_dir="$SDK_DIR/package/utils/veracrypt" + template="$REPOROOT/src/Build/Packaging/openwrt/package/utils/veracrypt/Makefile.in" + + rm -rf "$package_dir" + mkdir -p "$package_dir" + sed \ + -e "s|@VERACRYPT_VERSION@|$(sed_escape "$version")|g" \ + -e "s|@VERACRYPT_SOURCE_DIR@|$(sed_escape "$REPOROOT")|g" \ + -e "s|@WXWIDGETS_VERSION@|$(sed_escape "$WX_VERSION")|g" \ + -e "s|@WXWIDGETS_SOURCE_DIR@|$(sed_escape "$WX_SOURCE_DIR")|g" \ + "$template" > "$package_dir/Makefile" + + VERACRYPT_VERSION=$version +} + +configure_sdk() { + [ -f "$OPENWRT_CONFIG" ] || die "Missing OpenWrt config seed: $OPENWRT_CONFIG" + + cp "$OPENWRT_CONFIG" "$SDK_DIR/.config" + ( + cd "$SDK_DIR" + if ! PATH="$HOST_TOOLS/bin:$PATH" ./scripts/feeds update packages base; then + echo "Feed update failed; removing stale feed checkouts and retrying" >&2 + rm -rf feeds/base feeds/packages + PATH="$HOST_TOOLS/bin:$PATH" ./scripts/feeds update packages base + fi + PATH="$HOST_TOOLS/bin:$PATH" ./scripts/feeds install fuse3 lvm2 util-linux pcsc-lite bash + PATH="$HOST_TOOLS/bin:$PATH" make defconfig + ) +} + +build_package() { + ( + cd "$SDK_DIR" + PATH="$HOST_TOOLS/bin:$PATH" make package/utils/veracrypt/clean V=s + PATH="$HOST_TOOLS/bin:$PATH" make package/utils/veracrypt/compile V=s -j "$JOBS" + ) + + IPK_PATH=$(find "$SDK_DIR/bin/packages" "$SDK_DIR/bin/targets" -name "veracrypt_${VERACRYPT_VERSION}-*.ipk" 2>/dev/null | sort | tail -n 1) + [ -n "$IPK_PATH" ] || die "Build completed but no VeraCrypt .ipk was found" +} + +build_runtime_packages() { + ( + cd "$SDK_DIR" + for target in \ + package/feeds/packages/bash/compile \ + package/feeds/packages/fuse3/compile \ + package/feeds/base/util-linux/compile \ + package/feeds/packages/lvm2/compile + do + echo "Building OpenWrt runtime dependency: $target" + PATH="$HOST_TOOLS/bin:$PATH" make "$target" V=s -j "$JOBS" + done + ) +} + +while [ $# -gt 0 ]; do + case "$1" in + --openwrt-version) + require_option_arg "$@" + OPENWRT_VERSION=$2 + shift 2 + ;; + --target) + require_option_arg "$@" + OPENWRT_TARGET=$2 + shift 2 + ;; + --work-dir) + require_option_arg "$@" + WORK_DIR=$(readlink -m "$2") + shift 2 + ;; + --sdk-url) + require_option_arg "$@" + SDK_URL=$2 + shift 2 + ;; + --sdk-sha256) + require_option_arg "$@" + SDK_SHA256=$2 + shift 2 + ;; + --sdk-dir) + require_option_arg "$@" + SDK_DIR=$2 + shift 2 + ;; + --fresh-sdk) + FRESH_SDK=1 + shift + ;; + --wx-version) + require_option_arg "$@" + WX_VERSION=$2 + shift 2 + ;; + --wx-url) + require_option_arg "$@" + WX_URL=$2 + shift 2 + ;; + --wx-sha256) + require_option_arg "$@" + WX_SHA256=$2 + shift 2 + ;; + -j|--jobs) + require_option_arg "$@" + JOBS=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "Unknown option: $1" + ;; + esac +done + +case "$JOBS" in + ''|*[!0-9]*) + die "jobs must be a positive integer" + ;; +esac +[ "$JOBS" -gt 0 ] || die "jobs must be a positive integer" + +need_tool awk +need_tool find +need_tool make +need_tool rsync +need_tool sed +need_tool sha256sum +need_tool tar +need_tool wget +need_tool yasm +need_tool zstd + +mkdir -p "$WORK_DIR" +target_defaults +resolve_sdk +ensure_gawk +prepare_wxwidgets +render_package_makefile +configure_sdk +build_package +build_runtime_packages + +echo +echo "OpenWrt release: $OPENWRT_VERSION $OPENWRT_TARGET" +if [ -n "$SDK_URL" ]; then + echo "OpenWrt SDK: $SDK_URL" +else + echo "OpenWrt SDK: existing directory supplied with --sdk-dir" +fi +echo "SDK directory: $SDK_DIR" +echo "wxWidgets: $WX_URL" +echo "VeraCrypt package: $IPK_PATH" +sha256sum "$IPK_PATH" +echo +echo "Run the QEMU runtime test with:" +echo " python3 \"$SCRIPTPATH/test_veracrypt_openwrt_qemu.py\" --ipk \"$IPK_PATH\" --work-dir \"$WORK_DIR\"" diff --git a/src/Build/test_veracrypt_openwrt_qemu.py b/src/Build/test_veracrypt_openwrt_qemu.py new file mode 100755 index 00000000..67499c9c --- /dev/null +++ b/src/Build/test_veracrypt_openwrt_qemu.py @@ -0,0 +1,863 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026 AM Crypto +# Governed by the Apache License 2.0 the full text of which is contained +# in the file License.txt included in VeraCrypt binary and source +# code distribution packages. +# + +import argparse +import gzip +import hashlib +import http.server +import io +import lzma +import os +import re +import selectors +import shutil +import socketserver +import subprocess +import sys +import tarfile +import tempfile +import threading +import time +import urllib.parse +import urllib.request +from pathlib import Path + + +DEFAULT_OPENWRT_VERSION = "24.10.6" +DEFAULT_TARGET = "x86/64" +DEFAULT_PASSWORD = "OpenWrt-VeraCrypt-Test-Password-123456" +SHELL_PROMPT = ":~#" +PREINSTALLED_PACKAGES = { + "base-files", + "busybox", + "kernel", + "libc", + "libgcc1", + "libpthread", + "librt", + "opkg", +} +DEFAULT_RUNTIME_PACKAGES = [ + "bash", + # OpenWrt emits the FUSE3 library IPK with its ABI suffix; it still + # Provides: libfuse3. + "libfuse3-3", + "fuse3-utils", + "lvm2", + "losetup", + "blkid", + "mount-utils", + "kmod-fuse", + "kmod-loop", + "kmod-dm", + "kmod-crypto-misc", + "veracrypt", +] + + +class TestError(Exception): + pass + + +class ReusableTCPServer(socketserver.TCPServer): + allow_reuse_address = True + + +class PackageMetadata: + def __init__(self, path, fields, url=None, source="local"): + self.path = path + self.url = url + self.source = source + self.package = fields.get("Package", "") + self.version = fields.get("Version", "") + self.filename = fields.get("Filename", "") + self.sha256sum = fields.get("SHA256sum", "") + self.depends = parse_depends(fields.get("Depends", "")) + self.provides = parse_name_list(fields.get("Provides", "")) + + def identity(self): + if self.path: + return f"local:{self.path.resolve()}" + return f"remote:{self.url}" + + def display_location(self): + return str(self.path) if self.path else self.url + + def package_file_name(self): + if self.path: + return self.path.name + url_path = urllib.parse.urlparse(self.url).path + return Path(url_path).name + + +class Console: + def __init__(self, proc, log_path): + self.proc = proc + self.selector = selectors.DefaultSelector() + self.selector.register(proc.stdout, selectors.EVENT_READ) + os.set_blocking(proc.stdout.fileno(), False) + self.buffer = "" + self.log = open(log_path, "w", encoding="utf-8", errors="replace") + self.command_index = 0 + + def close(self): + self.log.close() + + def _record(self, data): + text = data.decode("utf-8", errors="replace") + self.buffer += text + self.log.write(text) + self.log.flush() + sys.stdout.write(text) + sys.stdout.flush() + + def send(self, text): + self.proc.stdin.write(text.encode("utf-8")) + self.proc.stdin.flush() + + def read_until(self, patterns, timeout, start=0): + if isinstance(patterns, str): + patterns = [patterns] + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + tail = self.buffer[start:] + for pattern in patterns: + if pattern in tail: + return pattern + if self.proc.poll() is not None: + raise TestError(f"QEMU exited before seeing {patterns}") + events = self.selector.select(0.25) + for key, _ in events: + try: + data = os.read(key.fileobj.fileno(), 8192) + except BlockingIOError: + continue + if data: + self._record(data) + raise TestError(f"Timed out waiting for {patterns}") + + def run(self, command, timeout=120): + self.command_index += 1 + marker = f"__VC_STATUS_{self.command_index:03d}__" + start = len(self.buffer) + wrapped = ( + f"printf '\\n__VC_BEGIN_{self.command_index:03d}__\\n'\n" + "{\n" + f"{command}\n" + "}\n" + f"echo {marker}:$?\n" + ) + self.send(wrapped) + deadline = time.monotonic() + timeout + status_re = re.compile(rf"{re.escape(marker)}:(\d+)") + while time.monotonic() < deadline: + tail = self.buffer[start:] + match = status_re.search(tail) + if match: + self.read_until(SHELL_PROMPT, 60, start + match.end()) + tail = self.buffer[start:] + status = int(match.group(1)) + if status != 0: + raise TestError(f"Command failed with status {status}: {command}") + return tail + if self.proc.poll() is not None: + raise TestError(f"QEMU exited while running: {command}") + events = self.selector.select(0.25) + for key, _ in events: + try: + data = os.read(key.fileobj.fileno(), 8192) + except BlockingIOError: + continue + if data: + self._record(data) + raise TestError(f"Timed out running: {command}") + + +def target_info(target, version): + if target != "x86/64": + raise TestError("Only x86/64 is currently supported by this QEMU test") + return { + "slug": "x86-64", + "image": f"openwrt-{version}-x86-64-generic-ext4-combined.img.gz", + "manifest": f"openwrt-{version}-x86-64.manifest", + "base_url": f"https://downloads.openwrt.org/releases/{version}/targets/x86/64", + } + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def download(url, path, expected_sha256=None): + if path.exists() and expected_sha256: + actual_sha = sha256_file(path) + if actual_sha == expected_sha256: + return + print(f"Checksum mismatch for existing {path}; re-downloading", file=sys.stderr) + path.unlink() + elif path.exists(): + return + + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name(f"{path.name}.tmp-{os.getpid()}") + if tmp.exists(): + tmp.unlink() + + print(f"Downloading {url}") + try: + with urllib.request.urlopen(url) as response, open(tmp, "wb") as out: + shutil.copyfileobj(response, out) + if expected_sha256: + actual_sha = sha256_file(tmp) + if actual_sha != expected_sha256: + raise TestError(f"SHA-256 mismatch for {path}: expected {expected_sha256}, got {actual_sha}") + tmp.replace(path) + finally: + if tmp.exists(): + tmp.unlink() + + +def read_text_archive(path): + data = path.read_bytes() + if path.name.endswith(".gz"): + return gzip.decompress(data).decode("utf-8", errors="replace") + if path.name.endswith(".xz"): + return lzma.decompress(data).decode("utf-8", errors="replace") + return data.decode("utf-8", errors="replace") + + +def expected_sha_from_sums(sums_path, filename): + with open(sums_path, "r", encoding="utf-8") as fh: + for line in fh: + parts = line.split() + if len(parts) >= 2 and parts[1].lstrip("*") == filename: + return parts[0] + return None + + +def sh_quote(text): + return "'" + str(text).replace("'", "'\"'\"'") + "'" + + +def read_ar_control_archive(ipk_path): + with open(ipk_path, "rb") as fh: + magic = fh.read(8) + if magic != b"!\n": + raise TestError(f"Unsupported .ipk format for {ipk_path}") + + while True: + header = fh.read(60) + if not header: + break + if len(header) != 60 or header[58:60] != b"`\n": + raise TestError(f"Malformed ar archive in {ipk_path}") + + name = header[:16].decode("utf-8", errors="replace").strip().rstrip("/") + size = int(header[48:58].decode("ascii").strip()) + data = fh.read(size) + if size % 2: + fh.read(1) + + if Path(name).name.startswith("control.tar"): + return name, data + + raise TestError(f"No control archive found in {ipk_path}") + + +def extract_top_level_control_archive(ipk_path): + with open(ipk_path, "rb") as fh: + magic = fh.read(8) + + if magic == b"!\n": + return read_ar_control_archive(ipk_path) + + try: + with tarfile.open(ipk_path, "r:*") as outer: + for member in outer: + if Path(member.name).name.startswith("control.tar"): + member_file = outer.extractfile(member) + if member_file: + return member.name, member_file.read() + except tarfile.TarError as exc: + raise TestError(f"Unsupported .ipk format for {ipk_path}: {exc}") from exc + + raise TestError(f"No control archive found in {ipk_path}") + + +def open_control_tar(name, data): + try: + return tarfile.open(fileobj=io.BytesIO(data), mode="r:*") + except tarfile.TarError: + pass + + if name.endswith(".zst"): + zstd = shutil.which("zstd") + if not zstd: + raise TestError(f"{name} is zstd-compressed, but zstd was not found") + result = subprocess.run([zstd, "-dc"], input=data, stdout=subprocess.PIPE, check=False) + if result.returncode != 0: + raise TestError(f"zstd failed while reading {name}") + return tarfile.open(fileobj=io.BytesIO(result.stdout), mode="r:") + + if name.endswith(".gz"): + return tarfile.open(fileobj=io.BytesIO(gzip.decompress(data)), mode="r:") + if name.endswith(".xz"): + return tarfile.open(fileobj=io.BytesIO(lzma.decompress(data)), mode="r:") + + raise TestError(f"Unsupported control archive compression: {name}") + + +def read_control_fields(ipk_path): + control_name, control_data = extract_top_level_control_archive(ipk_path) + with open_control_tar(control_name, control_data) as control_tar: + for member in control_tar: + if member.name in ("control", "./control") or member.name.endswith("/control"): + member_file = control_tar.extractfile(member) + if member_file: + text = member_file.read().decode("utf-8", errors="replace") + return parse_control_fields(text) + raise TestError(f"No control file found in {ipk_path}") + + +def parse_control_fields(text): + fields = {} + current = None + for line in text.splitlines(): + if not line: + current = None + continue + if line[0].isspace() and current: + fields[current] += "\n" + line.strip() + continue + key, sep, value = line.partition(":") + if not sep: + continue + current = key + fields[key] = value.strip() + return fields + + +def parse_control_paragraphs(text): + paragraphs = [] + lines = [] + for line in text.splitlines(): + if line.strip(): + lines.append(line) + continue + if lines: + paragraphs.append(parse_control_fields("\n".join(lines))) + lines = [] + if lines: + paragraphs.append(parse_control_fields("\n".join(lines))) + return paragraphs + + +def parse_package_name(text): + text = re.sub(r"\s*\([^)]*\)", "", text).strip() + return text.split()[0] if text else "" + + +def parse_depends(value): + groups = [] + for item in value.replace("\n", " ").split(","): + alternatives = [parse_package_name(part) for part in item.split("|")] + alternatives = [name for name in alternatives if name] + if alternatives: + groups.append(alternatives) + return groups + + +def parse_name_list(value): + names = [] + for item in value.replace("\n", " ").split(","): + name = parse_package_name(item) + if name: + names.append(name) + return names + + +def infer_package_bin_dir(ipk_path): + for parent in [ipk_path.parent] + list(ipk_path.parents): + if parent.name == "bin": + return parent + return ipk_path.parent + + +def read_package_metadata(ipk_path): + meta = PackageMetadata(ipk_path, read_control_fields(ipk_path)) + if not meta.package: + raise TestError(f"Package metadata in {ipk_path} has no Package field") + return meta + + +def add_package_metadata(index, meta, override=False): + for name in [meta.package] + meta.provides: + if name and (override or name not in index): + index[name] = meta + + +def build_package_index(package_bin_dir, veracrypt_ipk, skip_local_kmods=False): + if not package_bin_dir.is_dir(): + raise TestError(f"Package bin directory does not exist: {package_bin_dir}") + + index = {} + + for ipk in sorted(package_bin_dir.rglob("*.ipk")): + if skip_local_kmods and ipk.name.startswith("kmod-"): + continue + add_package_metadata(index, read_package_metadata(ipk)) + + add_package_metadata(index, read_package_metadata(veracrypt_ipk), override=True) + return index + + +def parse_packages_index(text, feed_url, source): + index = {} + feed_url = feed_url.rstrip("/") + "/" + for fields in parse_control_paragraphs(text): + package = fields.get("Package", "") + filename = fields.get("Filename", "") + if not package or not filename: + continue + url = urllib.parse.urljoin(feed_url, filename) + meta = PackageMetadata(None, fields, url=url, source=source) + add_package_metadata(index, meta) + return index + + +def download_packages_index(index_url, cache_path): + download(index_url, cache_path) + return read_text_archive(cache_path) + + +def official_index_cache_path(work_dir, info, name): + safe_name = re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_") + return work_dir / "package-indexes" / info["slug"] / safe_name + + +def kmod_dir_from_kernel_version(version): + match = re.match(r"^([^~]+)~([0-9a-fA-F]+)-r([0-9A-Za-z_.+-]+)$", version) + if not match: + return None + linux_version, vermagic, release = match.groups() + return f"{linux_version}-{release}-{vermagic}" + + +def official_manifest_kernel_version(args, info): + manifest_url = f"{info['base_url']}/{info['manifest']}" + cache_path = official_index_cache_path(args.work_dir, info, info["manifest"]) + download(manifest_url, cache_path) + manifest = read_text_archive(cache_path) + for line in manifest.splitlines(): + name, sep, version = line.partition(" - ") + if sep and name == "kernel": + return version.strip() + return None + + +def discover_single_kmod_feed_url(info): + kmods_url = f"{info['base_url']}/kmods/" + print(f"Discovering OpenWrt kmod feed from {kmods_url}") + with urllib.request.urlopen(kmods_url) as response: + html = response.read().decode("utf-8", errors="replace") + candidates = sorted(set(re.findall(r'href="([^"/]+-[^"/]+-[0-9a-fA-F]+/)"', html))) + if len(candidates) == 1: + return urllib.parse.urljoin(kmods_url, candidates[0]).rstrip("/") + if not candidates: + raise TestError(f"Could not discover an OpenWrt kmod feed at {kmods_url}") + raise TestError( + "Multiple OpenWrt kmod feeds are available; pass --kmod-feed-url explicitly: " + + ", ".join(urllib.parse.urljoin(kmods_url, candidate).rstrip("/") for candidate in candidates) + ) + + +def resolve_official_kmod_feed_url(args, info): + if args.kmod_feed_url: + return args.kmod_feed_url.rstrip("/") + + kernel_version = official_manifest_kernel_version(args, info) + if kernel_version: + kmod_dir = kmod_dir_from_kernel_version(kernel_version) + if kmod_dir: + return f"{info['base_url']}/kmods/{kmod_dir}" + + return discover_single_kmod_feed_url(info) + + +def official_kmod_package_index(args): + info = target_info(args.target, args.openwrt_version) + feed_url = resolve_official_kmod_feed_url(args, info) + index_url = f"{feed_url}/Packages.gz" + cache_path = official_index_cache_path(args.work_dir, info, f"kmods-{Path(feed_url).name}-Packages.gz") + text = download_packages_index(index_url, cache_path) + index = parse_packages_index(text, feed_url, f"official kmods {feed_url}") + if not index: + raise TestError(f"No packages were found in OpenWrt kmod feed {index_url}") + return index, feed_url + + +def overlay_package_index(index, overlay): + seen = set() + for meta in overlay.values(): + meta_key = meta.identity() + if meta_key in seen: + continue + seen.add(meta_key) + add_package_metadata(index, meta, override=True) + + +def resolve_runtime_packages(package_index, seed_packages): + resolved = [] + resolved_paths = set() + visiting = set() + + def visit(name, chain): + if name in PREINSTALLED_PACKAGES: + return + meta = package_index.get(name) + if not meta: + chain_text = " -> ".join(chain + [name]) + raise TestError(f"Missing .ipk metadata for dependency '{name}' while resolving {chain_text}") + + meta_key = meta.identity() + if meta_key in resolved_paths: + return + if meta_key in visiting: + return + + visiting.add(meta_key) + for alternatives in meta.depends: + selected = None + for alternative in alternatives: + if alternative in PREINSTALLED_PACKAGES or alternative in package_index: + selected = alternative + break + if not selected: + chain_text = " -> ".join(chain + [meta.package]) + raise TestError( + f"Missing .ipk metadata for dependency '{alternatives[0]}' required by {chain_text}" + ) + visit(selected, chain + [meta.package]) + + visiting.remove(meta_key) + resolved_paths.add(meta_key) + resolved.append(meta) + + for package in seed_packages: + visit(package, []) + + return resolved + + +def staged_package_name(index, meta): + return f"{index:03d}-{meta.package_file_name()}" + + +def ensure_remote_package(meta, cache_dir): + if not meta.url: + raise TestError(f"Package {meta.package} has no local path or remote URL") + + file_name = meta.package_file_name() + cache_name = f"{hashlib.sha256(meta.url.encode('utf-8')).hexdigest()[:12]}-{file_name}" + cached = cache_dir / cache_name + + def verify_cached(): + if meta.sha256sum and sha256_file(cached) != meta.sha256sum: + cached.unlink() + return False + return True + + if cached.exists() and verify_cached(): + return cached + + download(meta.url, cached, meta.sha256sum) + if meta.sha256sum and sha256_file(cached) != meta.sha256sum: + raise TestError(f"SHA-256 mismatch for {cached} downloaded from {meta.url}") + return cached + + +def stage_packages(packages, directory, cache_dir): + directory.mkdir(parents=True, exist_ok=True) + cache_dir.mkdir(parents=True, exist_ok=True) + for index, meta in enumerate(packages): + source = meta.path if meta.path else ensure_remote_package(meta, cache_dir) + shutil.copy2(source, directory / staged_package_name(index, meta)) + + +def package_download_command(packages, http_port): + lines = [ + "set -e", + "rm -rf /tmp/veracrypt-ipks", + "mkdir -p /tmp/veracrypt-ipks", + ] + for index, meta in enumerate(packages): + package_name = staged_package_name(index, meta) + url_path = urllib.parse.quote(package_name, safe="") + lines.append( + f"wget -O {sh_quote(f'/tmp/veracrypt-ipks/{package_name}')} " + f"{sh_quote(f'http://10.0.2.2:{http_port}/{url_path}')}" + ) + lines.append("opkg install /tmp/veracrypt-ipks/*.ipk") + return "\n".join(lines) + + +def prepare_image(args): + if args.image: + image = Path(args.image).resolve() + if not image.exists(): + raise TestError(f"Image does not exist: {image}") + return image + + info = target_info(args.target, args.openwrt_version) + image_gz = args.work_dir / "images" / info["image"] + image = image_gz.with_suffix("") + sums = args.work_dir / "images" / f"sha256sums-{args.openwrt_version}-{info['slug']}" + + download(f"{info['base_url']}/sha256sums", sums) + expected_sha = expected_sha_from_sums(sums, info["image"]) + if not expected_sha: + raise TestError(f"Could not find {info['image']} in {sums}") + download(f"{info['base_url']}/{info['image']}", image_gz, expected_sha) + actual_sha = sha256_file(image_gz) + if actual_sha != expected_sha: + raise TestError(f"SHA-256 mismatch for {image_gz}: expected {expected_sha}, got {actual_sha}") + + if not image.exists(): + print(f"Extracting {image_gz}") + with open(image, "wb") as out: + result = subprocess.run(["gzip", "-cd", str(image_gz)], stdout=out) + if result.returncode not in (0, 2): + raise TestError(f"gzip failed while extracting {image_gz}") + + return image + + +def start_http_server(directory, address, port): + handler = lambda *args, **kwargs: http.server.SimpleHTTPRequestHandler( + *args, directory=str(directory), **kwargs + ) + server = ReusableTCPServer((address, port), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + return server + + +def boot_qemu(args, image): + qemu = shutil.which(args.qemu) if os.sep not in args.qemu else args.qemu + if not qemu: + raise TestError("qemu-system-x86_64 was not found; install QEMU or pass --qemu") + + netdev = "user,id=net0" + if args.ssh_port is not None: + netdev += f",hostfwd=tcp::{args.ssh_port}-:22" + + qemu_cmd = [ + qemu, + "-accel", args.accel, + "-M", "pc", + "-cpu", args.cpu, + "-m", args.memory, + "-smp", str(args.smp), + "-nographic", + "-drive", f"file={image},format=raw,if=virtio", + "-netdev", netdev, + "-device", "virtio-net-pci,netdev=net0", + ] + if args.qemu_data_dir: + qemu_cmd[1:1] = ["-L", str(args.qemu_data_dir)] + + print("Starting QEMU:") + print(" ".join(qemu_cmd)) + return subprocess.Popen( + qemu_cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + +def run_guest_tests(args, console, http_port, packages): + console.read_until(["Please press Enter", SHELL_PROMPT], args.boot_timeout) + prompt_start = len(console.buffer) + console.send("\n") + console.read_until(SHELL_PROMPT, 90, prompt_start) + + console.run( + "sleep 20\n" + "if ip link show br-lan >/dev/null 2>&1; then NETDEV=br-lan; else NETDEV=eth0; fi\n" + "ip addr flush dev \"$NETDEV\" || true\n" + "ip link set \"$NETDEV\" up\n" + "udhcpc -n -q -t 10 -i \"$NETDEV\"\n" + "ip -4 addr show \"$NETDEV\"\n" + "ping -c 1 10.0.2.2", + timeout=120, + ) + + console.run(package_download_command(packages, http_port), timeout=900) + + version_output = console.run("veracrypt --text --version", timeout=120) + if "VeraCrypt " not in version_output: + raise TestError("version command did not print a VeraCrypt version") + + test_output = console.run("veracrypt --text --test", timeout=240) + if "Self-tests of all algorithms passed" not in test_output: + raise TestError("algorithm self-test did not report success") + + if not args.skip_container: + escaped_password = args.password.replace("'", "'\"'\"'") + console.run("dd if=/dev/urandom of=/tmp/vc-random.bin bs=1M count=1", timeout=120) + console.run( + "veracrypt --text --create /tmp/openwrt-test.hc " + f"--size={args.container_size} " + f"--password='{escaped_password}' " + "--encryption=AES --hash=SHA-512 --filesystem=none " + "--volume-type=normal --random-source=/tmp/vc-random.bin " + "--quick --force --non-interactive", + timeout=360, + ) + console.run("mkdir -p /mnt/veracrypt-test", timeout=60) + console.run( + "veracrypt --text --mount /tmp/openwrt-test.hc /mnt/veracrypt-test " + f"--password='{escaped_password}' " + "--pim=0 --keyfiles='' --protect-hidden=no --filesystem=none --non-interactive", + timeout=240, + ) + list_output = console.run("veracrypt --text --list", timeout=120) + if "/dev/mapper/veracrypt" not in list_output: + raise TestError("container did not appear in veracrypt --list output") + console.run("veracrypt --text --unmount /tmp/openwrt-test.hc", timeout=180) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Boot OpenWrt in QEMU and test a VeraCrypt .ipk") + parser.add_argument("--ipk", required=True, type=Path, help="Path to veracrypt_*.ipk") + parser.add_argument( + "--package-bin-dir", + type=Path, + help="SDK bin directory containing local dependency .ipk files; defaults to the nearest bin parent of --ipk", + ) + parser.add_argument("--openwrt-version", default=DEFAULT_OPENWRT_VERSION) + parser.add_argument("--target", default=DEFAULT_TARGET) + parser.add_argument("--work-dir", type=Path, default=None) + parser.add_argument("--image", type=Path, help="Use an already extracted OpenWrt raw image") + parser.add_argument("--qemu", default="qemu-system-x86_64") + parser.add_argument("--qemu-data-dir", type=Path, help="QEMU pc-bios directory for locally extracted QEMU builds") + parser.add_argument("--accel", default="tcg") + parser.add_argument("--cpu", default="max") + parser.add_argument("--memory", default="512M") + parser.add_argument("--smp", default=1, type=int) + parser.add_argument( + "--ssh-port", + type=int, + metavar="PORT", + help="Forward host TCP PORT to guest SSH; disabled by default", + ) + parser.add_argument("--http-port", default=0, type=int) + parser.add_argument( + "--http-bind-address", + default="127.0.0.1", + help="Host address for the temporary package server (default: 127.0.0.1)", + ) + parser.add_argument( + "--kmod-feed-url", + help="OpenWrt kmod feed URL; defaults to the official feed matching --openwrt-version and --target", + ) + parser.add_argument( + "--local-kmods", + action="store_true", + help="Resolve kmod-* packages from --package-bin-dir instead of the official OpenWrt kmod feed", + ) + parser.add_argument("--boot-timeout", default=180, type=int) + parser.add_argument("--container-size", default="16M") + parser.add_argument("--password", default=DEFAULT_PASSWORD) + parser.add_argument("--skip-container", action="store_true") + parser.add_argument("--keep-image", action="store_true") + return parser.parse_args() + + +def main(): + args = parse_args() + args.ipk = args.ipk.resolve() + if not args.ipk.exists(): + raise TestError(f"Package does not exist: {args.ipk}") + if args.package_bin_dir is None: + args.package_bin_dir = infer_package_bin_dir(args.ipk) + args.package_bin_dir = args.package_bin_dir.resolve() + + repo_root = Path(__file__).resolve().parents[2] + if args.work_dir is None: + args.work_dir = repo_root.parent / "openwrt-veracrypt" + args.work_dir = args.work_dir.resolve() + args.work_dir.mkdir(parents=True, exist_ok=True) + + package_index = build_package_index( + args.package_bin_dir, + args.ipk, + skip_local_kmods=not args.local_kmods, + ) + if not args.local_kmods: + kmod_index, kmod_feed_url = official_kmod_package_index(args) + overlay_package_index(package_index, kmod_index) + print(f"Using OpenWrt kmod feed: {kmod_feed_url}") + packages = resolve_runtime_packages(package_index, DEFAULT_RUNTIME_PACKAGES) + print("Resolved OpenWrt packages:") + for index, meta in enumerate(packages): + print(f" {index:02d} {meta.package}: {meta.display_location()}") + + base_image = prepare_image(args) + test_image = args.work_dir / "images" / f"{base_image.stem}-veracrypt-test.img" + test_image.parent.mkdir(parents=True, exist_ok=True) + if test_image.exists(): + test_image.unlink() + shutil.copyfile(base_image, test_image) + + with tempfile.TemporaryDirectory(prefix="veracrypt-ipks-", dir=args.work_dir) as package_dir: + server_root = Path(package_dir) + stage_packages(packages, server_root, args.work_dir / "package-cache") + server = start_http_server(server_root, args.http_bind_address, args.http_port) + http_port = server.server_address[1] + print(f"Serving staged packages from {server_root} on http://{args.http_bind_address}:{http_port}/") + + log_path = args.work_dir / "openwrt-qemu-test.log" + proc = None + console = None + try: + proc = boot_qemu(args, test_image) + console = Console(proc, log_path) + run_guest_tests(args, console, http_port, packages) + console.send("poweroff\n") + try: + proc.wait(timeout=90) + except subprocess.TimeoutExpired: + proc.terminate() + proc.wait(timeout=30) + finally: + server.shutdown() + server.server_close() + if console: + console.close() + if proc and proc.poll() is None: + proc.terminate() + if not args.keep_image and test_image.exists(): + test_image.unlink() + + print() + print("OpenWrt QEMU test passed") + print(f"Log: {log_path}") + + +if __name__ == "__main__": + try: + main() + except TestError as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1) diff --git a/src/Makefile b/src/Makefile index d96b713c..0130ae66 100644 --- a/src/Makefile +++ b/src/Makefile @@ -19,6 +19,8 @@ # NOTEST: Do not test release binary # RESOURCEDIR: Run-time resource directory # VERBOSE: Enable verbose messages +# WITHFUSE3: Build with FUSE3 support instead of FUSE2 +# WX_CONFIGURE_EXTRA_FLAGS: Extra flags passed to wxWidgets configure # WXSTATIC: Use static wxWidgets library # SSSE3: Enable SSSE3 support in compiler # SSE41: Enable SSE4.1 support in compiler @@ -736,7 +738,7 @@ endif WX_CONFIGURE_LEGACY_FLAGS="$$WX_CONFIGURE_LEGACY_FLAGS --enable-$$option"; \ fi; \ done; \ - "$(WX_ROOT)/configure" $(WX_CONFIGURE_FLAGS) $$WX_CONFIGURE_LEGACY_FLAGS >/dev/null + "$(WX_ROOT)/configure" $(WX_CONFIGURE_FLAGS) $(WX_CONFIGURE_EXTRA_FLAGS) $$WX_CONFIGURE_LEGACY_FLAGS >/dev/null @echo Building wxWidgets library... cd "$(WX_BUILD_DIR)" && $(MAKE) -j 4