From 9535e65bd888a64b6a72fb102f136600997fd83c Mon Sep 17 00:00:00 2001 From: curious-rabbit Date: Mon, 18 May 2026 13:54:13 +0200 Subject: [PATCH] Ensure reproducible builds on Linux (#1731) * ensure reproducible builds * improve patch * improve patch * Narrow reproducibility scope to legacy and DEB Keep the verified Linux legacy Makefile and DEB reproducibility paths, but remove the unverified RPM/openSUSE timestamp changes and AppImage reproducibility behavior from this PR. The CPack mtime/mode clamp is now installed only for Debian/Ubuntu packaging, matching the scope covered by the provided reproducibility logs. Retain umask 022 in the RPM/openSUSE wrappers so staged package permissions do not depend on a restrictive caller umask. * Harden reproducible build cleanup Validate SOURCE_DATE_EPOCH before interpolating it into Make, CMake or shell packaging paths. Refuse live DESTDIR values in the CPack mtime clamp and pass makeself options through normal argv construction instead of eval. --------- Co-authored-by: curious-rabbit Co-authored-by: Mounir IDRASSI --- src/Build/CMakeLists.txt | 42 +++++++ src/Build/Include/Makefile.inc | 15 ++- .../Tools/cmake_repro_clamp_mtimes.cmake.in | 108 ++++++++++++++++++ src/Build/Tools/makeself_repro_finalize.py | 55 +++++++++ src/Build/build_cmake_deb.sh | 24 +++- src/Build/build_cmake_opensuse.sh | 3 + src/Build/build_cmake_rpm.sh | 3 + src/Main/Main.make | 89 ++++++++++++++- src/Makefile | 69 +++++++++++ 9 files changed, 402 insertions(+), 6 deletions(-) create mode 100644 src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in create mode 100755 src/Build/Tools/makeself_repro_finalize.py diff --git a/src/Build/CMakeLists.txt b/src/Build/CMakeLists.txt index 8e3fabce..2634cd34 100644 --- a/src/Build/CMakeLists.txt +++ b/src/Build/CMakeLists.txt @@ -27,6 +27,34 @@ else() endif() project(${PROJECT_NAME}) +# SOURCE_DATE_EPOCH for the cpack-driven DEB pipeline. +# Precedence: -DSOURCE_DATE_EPOCH=N, env, git HEAD, fixed fallback. +# Re-exported to ENV so dpkg-deb/tar inherit it. +if(NOT DEFINED SOURCE_DATE_EPOCH) + if(DEFINED ENV{SOURCE_DATE_EPOCH}) + set(SOURCE_DATE_EPOCH "$ENV{SOURCE_DATE_EPOCH}") + else() + execute_process( + COMMAND git -C "$ENV{SOURCEPATH}" log -1 --pretty=%ct + OUTPUT_VARIABLE _git_ct + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _git_rc) + if(_git_rc EQUAL 0 AND _git_ct) + set(SOURCE_DATE_EPOCH "${_git_ct}") + else() + set(SOURCE_DATE_EPOCH "1577836800") + endif() + endif() +endif() +if(NOT SOURCE_DATE_EPOCH MATCHES "^[0-9]+$") + MESSAGE(FATAL_ERROR "SOURCE_DATE_EPOCH must be a non-negative Unix timestamp") +endif() +message(STATUS "SOURCE_DATE_EPOCH = ${SOURCE_DATE_EPOCH}") +set(ENV{SOURCE_DATE_EPOCH} "${SOURCE_DATE_EPOCH}") +# Avoid nondeterministic ordering from cpack 3.18+ parallel compression. +set(CPACK_THREADS 1) + # - Check whether 'Tcdefs.h' and 'License.txt' exist if(NOT EXISTS "$ENV{SOURCEPATH}/Common/Tcdefs.h") MESSAGE(FATAL_ERROR "Tcdefs.h does not exist.") @@ -254,6 +282,19 @@ if ( ( PLATFORM STREQUAL "Debian" ) OR ( PLATFORM STREQUAL "Ubuntu" ) ) set( DEBIAN_PRERM ${CMAKE_CURRENT_BINARY_DIR}/Packaging/debian-control/prerm) set( CPACK_GENERATOR "DEB" ) # mandatory + + # Reproducible DEB: clamp the just-installed staging tree's mtimes + # and modes so the payload is independent of wall-clock time and + # the build umask. Placed AFTER install(DIRECTORY) so it runs against a + # populated tree (install rules execute in declaration order). The script + # acts only on a real package staging root and refuses a live prefix; + # see the script header for the staging-root detection rules. + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/Tools/cmake_repro_clamp_mtimes.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_repro_clamp_mtimes.cmake" + @ONLY) + install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/cmake_repro_clamp_mtimes.cmake") + set( CPACK_DEBIAN_PACKAGE_NAME ${CPACK_PACKAGE_NAME} ) # mandatory set( CPACK_DEBIAN_FILE_NAME ${CPACK_PACKAGE_FILE_NAME}.deb ) # mandatory # -- Use a distro-specific version string to avoid repository conflicts -- @@ -357,6 +398,7 @@ elseif ( ( PLATFORM STREQUAL "CentOS" ) OR ( PLATFORM STREQUAL "openSUSE" ) OR ( set( CPACK_RPM_PACKAGE_GROUP "Applications/System" ) # mandatory, https://fedoraproject.org/wiki/RPMGroups set( CPACK_RPM_PACKAGE_VENDOR ${CPACK_PACKAGE_VENDOR} ) # mandatory set( CPACK_RPM_PACKAGE_AUTOREQ "no" ) # disable automatic shared libraries dependency detection (most of the time buggy) + if (VC_WITH_FUSE3) set(VC_RPM_FUSE_PACKAGE "fuse3") else () diff --git a/src/Build/Include/Makefile.inc b/src/Build/Include/Makefile.inc index 2f3aee68..52e0d650 100644 --- a/src/Build/Include/Makefile.inc +++ b/src/Build/Include/Makefile.inc @@ -119,7 +119,18 @@ TR_SED_BIN := tr '\n' ' ' | tr -s ' ' ',' | sed -e 's/^,//g' -e 's/,$$/n/' | tr -include $(OBJS:.o=.d) $(OBJSEX:.oo=.d) $(OBJSNOOPT:.o0=.d) $(OBJSHANI:.oshani=.d) $(OBJAESNI:.oaesni=.d) $(OBJSSSE41:.osse41=.d) $(OBJSSSSE3:.ossse3=.d) $(OBJSAVX2:.oavx2=.d) $(OBJARMV8CRYPTO:.oarmv8crypto=.d) +# Deterministic static library: the 'D' modifier zeroes member mtime/uid/gid +# and 'ranlib -D' writes a deterministic index. Both are probed functionally +# (running them on a throwaway archive) rather than by parsing --help, whose +# wording varies between binutils versions. Very old binutils that lack the +# feature simply falls back to a normal, still-correct archive. +# Probe also covers BSD ar / macOS libtool ar (neither supports -D): both +# variables come out empty there and the original ar/ranlib calls are used. +AR_DETERMINISTIC := $(shell t=$$(mktemp); rm -f $$t.a; $(AR) Drc $$t.a $$t >/dev/null 2>&1 && echo D; rm -f $$t $$t.a) +RANLIB_DETERMINISTIC := $(shell t=$$(mktemp); rm -f $$t.a; $(AR) rc $$t.a $$t >/dev/null 2>&1; $(RANLIB) -D $$t.a >/dev/null 2>&1 && echo -D; rm -f $$t $$t.a) + $(NAME).a: $(OBJS) $(OBJSEX) $(OBJSNOOPT) $(OBJSHANI) $(OBJAESNI) $(OBJSSSE41) $(OBJSSSSE3) $(OBJSAVX2) $(OBJARMV8CRYPTO) @echo Updating library $@ - $(AR) $(AFLAGS) -rc $@ $(OBJS) $(OBJSEX) $(OBJSNOOPT) $(OBJSHANI) $(OBJAESNI) $(OBJSSSE41) $(OBJSSSSE3) $(OBJSAVX2) $(OBJARMV8CRYPTO) - $(RANLIB) $@ + rm -f $@ + $(AR) $(AFLAGS) $(AR_DETERMINISTIC)rc $@ $(OBJS) $(OBJSEX) $(OBJSNOOPT) $(OBJSHANI) $(OBJAESNI) $(OBJSSSE41) $(OBJSSSSE3) $(OBJSAVX2) $(OBJARMV8CRYPTO) + $(RANLIB) $(RANLIB_DETERMINISTIC) $@ diff --git a/src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in b/src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in new file mode 100644 index 00000000..33d93740 --- /dev/null +++ b/src/Build/Tools/cmake_repro_clamp_mtimes.cmake.in @@ -0,0 +1,108 @@ +# Copyright (c) 2026 VeraCrypt +# Governed by the Apache License 2.0. +# +# Run at install time by install(SCRIPT ...) AFTER all install(DIRECTORY) +# rules, so the staging tree is fully populated. Clamps every file's mtime +# and permission bits so the CPack DEB payload is reproducible. +# +# Safety: only a package staging tree may be modified, never a live host +# tree. Two recognised staging conventions: +# 1. CPack: it installs into its private +# /_CPack_Packages/.../${CPACK_PACKAGING_INSTALL_PREFIX} +# directory, exposed here as CMAKE_INSTALL_PREFIX. We require the +# path to contain a "_CPack_Packages" component. +# 2. "make DESTDIR= install" style: $ENV{DESTDIR} is a non-live +# staging root and $ENV{DESTDIR}${CMAKE_INSTALL_PREFIX} is clamped. +# Anything else (bare "cmake --install" into /usr or /usr/local) is +# refused so root cannot rewrite mtimes/modes outside a package build. + +if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + return() +endif() + +set(_destdir "$ENV{DESTDIR}") +set(_prefix "${CMAKE_INSTALL_PREFIX}") + +if(NOT _destdir STREQUAL "") + get_filename_component(_destdir_abs "${_destdir}" ABSOLUTE) + foreach(_live_destdir "/" "/usr" "/usr/local" "/opt") + if(_destdir_abs STREQUAL "${_live_destdir}") + message(FATAL_ERROR "Reproducible build: refusing to clamp live " + "DESTDIR '${_destdir}'") + endif() + endforeach() + set(_staging "${_destdir}${_prefix}") +elseif(_prefix MATCHES "/_CPack_Packages/") + set(_staging "${_prefix}") +else() + message(STATUS "Reproducible build: not a package staging install " + "(DESTDIR empty and prefix '${_prefix}' is not a CPack " + "staging tree); skipping mtime/mode clamp") + return() +endif() +get_filename_component(_staging "${_staging}" ABSOLUTE) + +if(NOT IS_DIRECTORY "${_staging}") + message(STATUS "Reproducible build: staging root '${_staging}' absent, " + "skipping mtime/mode clamp") + return() +endif() + +# SOURCE_DATE_EPOCH is baked in by configure_file-style substitution from +# the parent CMakeLists (see install(SCRIPT) call site). +set(_epoch "@SOURCE_DATE_EPOCH@") +if(NOT _epoch MATCHES "^[0-9]+$") + message(FATAL_ERROR "Reproducible build: SOURCE_DATE_EPOCH must be a " + "non-negative Unix timestamp") +endif() + +# Probe GNU touch on a private temp file, not /dev/null: /dev/null is +# root-owned, so probing it fails for normal users and rewrites the +# device node's mtime as root (the bug the review flagged). +execute_process( + COMMAND mktemp + OUTPUT_VARIABLE _probe + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _mktemp_rc) +if(NOT _mktemp_rc EQUAL 0) + message(STATUS "Reproducible build: mktemp failed, skipping mtime clamp") + return() +endif() +execute_process( + COMMAND touch --no-dereference --date=@0 "${_probe}" + RESULT_VARIABLE _touch_rc + OUTPUT_QUIET ERROR_QUIET) +file(REMOVE "${_probe}") +if(NOT _touch_rc EQUAL 0) + message(STATUS "Reproducible build: GNU touch unavailable, " + "skipping mtime clamp") + return() +endif() + +# Normalise permission bits first: the make-side prepare creates staging +# dirs with "mkdir -p" (umask-dependent) and CPack's tar records those +# modes verbatim via USE_SOURCE_PERMISSIONS, so umask 027 -> 0750 vs +# umask 022 -> 0755 breaks reproducibility. Match the legacy tar's +# --mode=go-w,a+rX: dirs/exes 0755, regular files 0644. +execute_process( + COMMAND find "${_staging}" -type d -exec chmod 0755 {} + + RESULT_VARIABLE _cd_rc OUTPUT_QUIET ERROR_QUIET) +execute_process( + COMMAND find "${_staging}" -type f -perm -u+x -exec chmod 0755 {} + + RESULT_VARIABLE _cx_rc OUTPUT_QUIET ERROR_QUIET) +execute_process( + COMMAND find "${_staging}" -type f -not -perm -u+x -exec chmod 0644 {} + + RESULT_VARIABLE _cf_rc OUTPUT_QUIET ERROR_QUIET) + +# Clamp mtimes last so this is the final metadata write. +execute_process( + COMMAND find "${_staging}" -exec + touch --no-dereference --date=@${_epoch} {} + + RESULT_VARIABLE _find_rc + OUTPUT_QUIET ERROR_QUIET) + +if(_find_rc EQUAL 0 AND _cd_rc EQUAL 0 AND _cx_rc EQUAL 0 AND _cf_rc EQUAL 0) + message(STATUS "Reproducible build: clamped mtimes and modes under ${_staging}") +else() + message(WARNING "Reproducible build: mtime/mode clamp incomplete under ${_staging}") +endif() diff --git a/src/Build/Tools/makeself_repro_finalize.py b/src/Build/Tools/makeself_repro_finalize.py new file mode 100755 index 00000000..9514211f --- /dev/null +++ b/src/Build/Tools/makeself_repro_finalize.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026 VeraCrypt +# Governed by the Apache License 2.0. +# +# Zero the gzip mtime in a makeself archive and refresh its integrity +# fields. makeself runs `gzip -c9 < tmpfile' which writes tmpfile's +# mtime into the gzip header (gzip ignores SOURCE_DATE_EPOCH for +# redirected stdin), so the installer is otherwise not reproducible. +# +# After editing the payload the recorded checksums are refreshed: +# - CRCsum is set to "0000000000". Makeself stores a POSIX cksum(1) +# value there, not a zlib CRC-32 (the two differ); an all-zero +# CRCsum makes its extractor skip the redundant CRC check. +# - MD5 is recomputed, which the extractor still verifies. +# +# Usage: makeself_repro_finalize.py + +import hashlib +import re +import sys + + +def finalize(path): + with open(path, "rb") as f: + raw = bytearray(f.read()) + text = raw.decode("latin1", errors="replace") + # Locate payload start by line count, mirroring makeself's own extractor. + m = re.search(r'^skip="(\d+)"', text, re.MULTILINE) + if not m: + sys.exit(f"{path}: no skip= line in makeself header") + skip = int(m.group(1)) + header_text = "\n".join(text.split("\n")[:skip]) + "\n" + offset = len(header_text.encode("latin1")) + if bytes(raw[offset:offset + 3]) != b"\x1f\x8b\x08": + sys.exit(f"{path}: no gzip magic at payload offset {offset}") + # gzip header mtime: 4-byte LE uint at offset+4 (RFC 1952 section 2.3.1). + raw[offset + 4:offset + 8] = b"\x00\x00\x00\x00" + payload = bytes(raw[offset:]) + new_md5 = hashlib.md5(payload).hexdigest() + # CRCsum -> all zeros (extractor then skips the CRC check); MD5 -> fresh. + new_header = re.sub(r'CRCsum="[^"]*"', 'CRCsum="0000000000"', header_text) + new_header = re.sub(r'MD5="[0-9a-fA-F]+"', f'MD5="{new_md5}"', new_header) + new_bytes = new_header.encode("latin1") + # Line count must stay the same so makeself's "skip=" remains accurate. + if new_bytes.count(b"\n") != skip: + sys.exit(f"{path}: header line count changed during rewrite") + with open(path, "wb") as f: + f.write(new_bytes + payload) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit("Usage: makeself_repro_finalize.py ") + finalize(sys.argv[1]) diff --git a/src/Build/build_cmake_deb.sh b/src/Build/build_cmake_deb.sh index 64cdf559..d85cdded 100755 --- a/src/Build/build_cmake_deb.sh +++ b/src/Build/build_cmake_deb.sh @@ -9,6 +9,26 @@ # Errors should cause script to exit set -e +# Deterministic umask: dpkg-deb records the mode of the temporary ar +# members (debian-binary, control.tar.gz, data.tar.gz) it creates, so a +# caller umask of 027 yields 0640 where 022 yields 0644 and the .deb is +# not reproducible. Pin it for the whole packaging run. +umask 022 + +# Compute and export SOURCE_DATE_EPOCH so cmake/cpack inherit it (they get +# an empty env from this shell otherwise). Precedence: caller, git HEAD, +# fallback constant matching src/Makefile and CMakeLists.txt. +if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then + SOURCE_DATE_EPOCH=$(git -C "$(dirname "$0")/../.." log -1 --pretty=%ct 2>/dev/null || echo 1577836800) +fi +case "$SOURCE_DATE_EPOCH" in + ''|*[!0-9]*) + echo "Error: SOURCE_DATE_EPOCH must be a non-negative Unix timestamp" >&2 + exit 1 + ;; +esac +export SOURCE_DATE_EPOCH + # Absolute path to this script export SCRIPT=$(readlink -f "$0") # Absolute path this script is in @@ -158,8 +178,8 @@ rm -rf $PARENTDIR/VeraCrypt_Packaging mkdir -p $PARENTDIR/VeraCrypt_Packaging/GUI mkdir -p $PARENTDIR/VeraCrypt_Packaging/Console -cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/GUI -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/GUI" -DNOGUI=FALSE $FUSE3_CMAKE_FLAG || exit 1 +cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/GUI -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/GUI" -DNOGUI=FALSE -DSOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH $FUSE3_CMAKE_FLAG || exit 1 cpack --config $PARENTDIR/VeraCrypt_Packaging/GUI/CPackConfig.cmake || exit 1 -cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/Console -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/Console" -DNOGUI=TRUE $FUSE3_CMAKE_FLAG || exit 1 +cmake -H$SCRIPTPATH -B$PARENTDIR/VeraCrypt_Packaging/Console -DVERACRYPT_BUILD_DIR="$PARENTDIR/VeraCrypt_Setup/Console" -DNOGUI=TRUE -DSOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH $FUSE3_CMAKE_FLAG || exit 1 cpack --config $PARENTDIR/VeraCrypt_Packaging/Console/CPackConfig.cmake || exit 1 diff --git a/src/Build/build_cmake_opensuse.sh b/src/Build/build_cmake_opensuse.sh index 87c5a75e..6db94d35 100644 --- a/src/Build/build_cmake_opensuse.sh +++ b/src/Build/build_cmake_opensuse.sh @@ -9,6 +9,9 @@ # Errors should cause script to exit set -e +# Keep staged RPM payload permissions independent of the caller's umask. +umask 022 + # Absolute path to this script export SCRIPT=$(readlink -f "$0") # Absolute path this script is in diff --git a/src/Build/build_cmake_rpm.sh b/src/Build/build_cmake_rpm.sh index 499a1c61..d40fb79a 100644 --- a/src/Build/build_cmake_rpm.sh +++ b/src/Build/build_cmake_rpm.sh @@ -9,6 +9,9 @@ # Errors should cause script to exit set -e +# Keep staged RPM payload permissions independent of the caller's umask. +umask 022 + # Absolute path to this script export SCRIPT=$(readlink -f "$0") # Absolute path this script is in diff --git a/src/Main/Main.make b/src/Main/Main.make index fbabcb31..36c71f09 100755 --- a/src/Main/Main.make +++ b/src/Main/Main.make @@ -194,13 +194,18 @@ endif endif #----------------------------------- +# Probe strip --enable-deterministic-archives against a private object +# rather than "-V" (which can fail in getopt before printing). All +# platforms, so non-Linux behaviour matches the original PR. +STRIP_DETERMINISTIC := $(strip $(shell d=$$(mktemp -d 2>/dev/null) && printf 'int x;\n' | $(CC) -x c -c - -o "$$d/o.o" >/dev/null 2>&1 && strip --enable-deterministic-archives "$$d/o.o" >/dev/null 2>&1 && echo --enable-deterministic-archives; rm -rf "$$d")) + $(APPNAME): $(LIBS) $(OBJS) @echo Linking $@ $(CXX) -o $(APPNAME) $(OBJS) $(LIBS) $(AYATANA_LIBS) $(FUSE_LIBS) $(WX_LIBS) $(LFLAGS) ifeq "$(TC_BUILD_CONFIG)" "Release" ifndef NOSTRIP - strip $(APPNAME) + strip $(STRIP_DETERMINISTIC) $(APPNAME) endif ifndef NOTEST @@ -294,6 +299,23 @@ endif ifeq "$(PLATFORM)" "Linux" + +# Packaging-tool feature probes. Empty result = host lacks the feature; +# the recipe falls back to the pre-PR (non-deterministic) form with a +# warning. $(strip) because $(shell) keeps trailing whitespace which +# would break the "= yes" equality test in the recipe. +# +# All probes act on a private $(mktemp) file and clean it up. touch +# probing /dev/null fails (EPERM) for unprivileged users and rewrites +# the device node as root. The tar option set requires GNU tar >= 1.28, +# so the probe exercises it exactly rather than matching "GNU tar". +# MAKESELF_TAR_EXTRA needs Makeself >= 2.3.1 (cited in the review). +TOUCH_REPRODUCIBLE := $(strip $(shell t=$$(mktemp 2>/dev/null) && touch --no-dereference --date=@0 "$$t" >/dev/null 2>&1 && echo yes; rm -f "$$t")) +TAR_DETERMINISTIC := $(strip $(shell t=$$(mktemp 2>/dev/null) && tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner --mode='go-w,a+rX' --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime -cf "$$t" --files-from /dev/null >/dev/null 2>&1 && echo yes; rm -f "$$t")) +GZIP_NO_TIMESTAMP := $(strip $(shell printf x | gzip -n -c >/dev/null 2>&1 && echo yes)) +MAKESELF_PACKAGING_DATE := $(strip $(shell makeself --help 2>&1 | grep -q -- '--packaging-date' && echo yes)) +MAKESELF_TAR_EXTRA := $(strip $(shell makeself --help 2>&1 | grep -q -- '--tar-extra' && echo yes)) + prepare: $(APPNAME) rm -fr $(BASE_DIR)/Setup/Linux/usr mkdir -p $(BASE_DIR)/Setup/Linux/usr/bin @@ -331,6 +353,22 @@ ifndef TC_NO_GUI cp -r $(BASE_DIR)/Setup/Linux/usr $(BASE_DIR)/Setup/Linux/veracrypt.AppDir/. ln -sf usr/share/icons/hicolor/1024x1024/apps/$(APPNAME).png $(BASE_DIR)/Setup/Linux/veracrypt.AppDir/$(APPNAME).png endif + # Normalise modification times of every staged file. cp preserves the + # checkout-time mtimes of the source tree, which would otherwise leak + # into the tar/makeself archives and break reproducibility. + # Only run when GNU touch supports the option set. Keep AppImage + # outside this narrowed reproducibility scope: appimagetool is not + # verified here, so do not pre-clamp veracrypt.AppDir for that target. +ifeq "$(TOUCH_REPRODUCIBLE)" "yes" + _appdir="$(BASE_DIR)/Setup/Linux/veracrypt.AppDir"; \ + if [ -n "$(filter appimage,$(MAKECMDGOALS))" ] || [ ! -d "$$_appdir" ]; then \ + _appdir=""; \ + fi; \ + find $(BASE_DIR)/Setup/Linux/usr $$_appdir \ + -exec touch --no-dereference --date=@$(SOURCE_DATE_EPOCH) {} + +else + @echo "Reproducible build: GNU touch unavailable, skipping mtime normalisation" +endif install: prepare @@ -341,7 +379,25 @@ endif ifeq "$(TC_BUILD_CONFIG)" "Release" package: prepare + # Deterministic tarball: sort members, pin mtime to SOURCE_DATE_EPOCH, + # drop owner/group identity, and use gzip -n so no timestamp/name is + # stored in the gzip header. + # --mode= normalises permission bits so the host umask cannot leak + # into them. Falls back to plain tar cfz when probes fail. +ifeq "$(TAR_DETERMINISTIC)$(GZIP_NO_TIMESTAMP)" "yesyes" + tar --sort=name --mtime=@$(SOURCE_DATE_EPOCH) \ + --owner=0 --group=0 --numeric-owner \ + --mode='go-w,a+rX' \ + --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \ + -cf $(BASE_DIR)/Setup/Linux/$(APPNAME)_$(TC_VERSION).tar \ + --directory $(BASE_DIR)/Setup/Linux usr + gzip -9 -n -c $(BASE_DIR)/Setup/Linux/$(APPNAME)_$(TC_VERSION).tar \ + > $(BASE_DIR)/Setup/Linux/$(PACKAGE_NAME) + rm -f $(BASE_DIR)/Setup/Linux/$(APPNAME)_$(TC_VERSION).tar +else + @echo "Reproducible build: non-deterministic tar.gz fallback (GNU tar=$(if $(TAR_DETERMINISTIC),yes,no), gzip -n=$(if $(GZIP_NO_TIMESTAMP),yes,no))" tar cfz $(BASE_DIR)/Setup/Linux/$(PACKAGE_NAME) --directory $(BASE_DIR)/Setup/Linux usr +endif @rm -fr $(INTERNAL_INSTALLER_NAME) @echo "#!/bin/sh" > $(INTERNAL_INSTALLER_NAME) @@ -359,7 +415,33 @@ package: prepare rm -fr $(BASE_DIR)/Setup/Linux/packaging mkdir -p $(BASE_DIR)/Setup/Linux/packaging cp $(INTERNAL_INSTALLER_NAME) $(BASE_DIR)/Setup/Linux/packaging/. - makeself $(BASE_DIR)/Setup/Linux/packaging $(BASE_DIR)/Setup/Linux/$(INSTALLER_NAME) "VeraCrypt $(TC_VERSION) Installer" ./$(INTERNAL_INSTALLER_NAME) + # makeself: --packaging-date pins the banner date, SOURCE_DATE_EPOCH is + # honoured by the embedded tar/gzip, and the archive is sorted so the + # self-extracting installer is byte-identical across builds. + # Flags gated per probe; invoked from Setup/Linux with relative paths + # so the build path does not end up in makeself's echoed argv. + @cd $(BASE_DIR)/Setup/Linux && set --; \ + if [ "$(MAKESELF_PACKAGING_DATE)" = yes ]; then \ + set -- "$$@" --packaging-date "@$(SOURCE_DATE_EPOCH)"; \ + fi; \ + if [ "$(MAKESELF_TAR_EXTRA)" = yes ] && [ "$(TAR_DETERMINISTIC)" = yes ]; then \ + set -- "$$@" --tar-extra "--sort=name --mtime=@$(SOURCE_DATE_EPOCH) --owner=0 --group=0 --numeric-owner --mode=go-w,a+rX"; \ + fi; \ + if [ "$$#" -eq 0 ]; then \ + echo "Reproducible build: makeself flags unavailable, installer will not be byte-identical"; \ + fi; \ + makeself "$$@" \ + packaging "$(INSTALLER_NAME)" \ + "VeraCrypt $(TC_VERSION) Installer" "./$(INTERNAL_INSTALLER_NAME)" + # makeself runs 'gzip -c9 < tmpfile' which writes tmpfile's mtime into + # the gzip header (SOURCE_DATE_EPOCH is ignored for redirected stdin). + # Zero the mtime and refresh CRCsum/MD5; installer --check still passes. + @if command -v python3 >/dev/null 2>&1; then \ + python3 $(BASE_DIR)/Build/Tools/makeself_repro_finalize.py \ + $(BASE_DIR)/Setup/Linux/$(INSTALLER_NAME); \ + else \ + echo "Reproducible build: python3 unavailable, skipping makeself finalize"; \ + fi appimage: prepare @set -e; \ @@ -398,6 +480,9 @@ appimage: prepare wget --quiet -O "$${_appimagetool_executable_path}" "$${_appimagetool_url}"; \ chmod +x "$${_appimagetool_executable_path}"; \ echo "Creating AppImage $${_final_appimage_path}..."; \ + if [ "$(VC_SOURCE_DATE_EPOCH_AUTO)" = "1" ]; then \ + unset SOURCE_DATE_EPOCH; \ + fi; \ ARCH="$${_final_appimage_arch_suffix}" "$${_appimagetool_executable_path}" "$(BASE_DIR)/Setup/Linux/veracrypt.AppDir" "$${_final_appimage_path}"; \ echo "AppImage created: $${_final_appimage_path}"; \ echo "Cleaning up appimagetool..."; \ diff --git a/src/Makefile b/src/Makefile index a406bd45..d96b713c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -582,6 +582,75 @@ CFLAGS := $(C_CXX_FLAGS) $(CFLAGS) $(TC_EXTRA_CFLAGS) CXXFLAGS := $(C_CXX_FLAGS) $(CXXFLAGS) $(TC_EXTRA_CXXFLAGS) LFLAGS := $(LFLAGS) $(TC_EXTRA_LFLAGS) +#------ Reproducible build configuration ------ +# Goal: byte-identical binaries from identical sources regardless of build +# path, build host, build user or wall-clock time. +# +# Every flag below is probed for compiler support before use, so this block +# is a safe no-op on older toolchains (VeraCrypt still supports GCC 4.x) and +# never breaks a build that would otherwise have succeeded. +# +# SOURCE_DATE_EPOCH (https://reproducible-builds.org/specs/source-date-epoch/) +# is honoured by GCC/Clang for any residual __DATE__/__TIME__ expansion, by +# ar/ranlib in deterministic mode, and by tar, gzip and makeself for archive +# member timestamps. If the caller does not set it, derive a stable value +# from the HEAD commit; fall back to a fixed constant for tarball builds with +# no git tree so that unattended builds are still deterministic. +ifndef SOURCE_DATE_EPOCH +export SOURCE_DATE_EPOCH := $(shell git -C $(BASE_DIR) log -1 --pretty=%ct 2>/dev/null || echo 1577836800) +export VC_SOURCE_DATE_EPOCH_AUTO := 1 +endif +override export SOURCE_DATE_EPOCH := $(value SOURCE_DATE_EPOCH) +SOURCE_DATE_EPOCH_REMAINDER := $(value SOURCE_DATE_EPOCH) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 0,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 1,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 2,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 3,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 4,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 5,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 6,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 7,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 8,,$(SOURCE_DATE_EPOCH_REMAINDER)) +SOURCE_DATE_EPOCH_REMAINDER := $(subst 9,,$(SOURCE_DATE_EPOCH_REMAINDER)) +ifeq "$(SOURCE_DATE_EPOCH)" "" +$(error SOURCE_DATE_EPOCH must be a non-negative Unix timestamp) +endif +ifneq "$(SOURCE_DATE_EPOCH_REMAINDER)" "" +$(error SOURCE_DATE_EPOCH must contain decimal digits only) +endif + +# cc-option: return $(1) if the C compiler accepts it, empty otherwise. +cc-option = $(shell printf 'int main(void){return 0;}' | $(CC) $(1) -x c -c - -o /dev/null >/dev/null 2>&1 && echo $(1)) + +# Normalise build paths embedded in debug info, assertions and __FILE__ so +# the binary does not depend on where the source tree lives. -ffile-prefix-map +# needs GCC >= 8 / Clang >= 10; fall back to the older -fdebug-prefix-map +# (GCC >= 4.3) which at least normalises the path recorded in debug info. +# Linux-only block: out of scope for FreeBSD/macOS. $(abspath) because +# -ffile-prefix-map needs an absolute prefix to match. +ifeq "$(PLATFORM)" "Linux" +REPRO_BASE_DIR := $(abspath $(BASE_DIR)) +REPRODUCIBLE_FLAGS := $(call cc-option,-ffile-prefix-map=$(REPRO_BASE_DIR)=.) +ifeq "$(REPRODUCIBLE_FLAGS)" "" +REPRODUCIBLE_FLAGS := $(call cc-option,-fdebug-prefix-map=$(REPRO_BASE_DIR)=.) +endif +# Drop the recorded compiler command line, which embeds absolute paths and +# host-specific options into .comment / .GCC.command.line. +REPRODUCIBLE_FLAGS += $(call cc-option,-fno-record-gcc-switches) + +CFLAGS += $(REPRODUCIBLE_FLAGS) +CXXFLAGS += $(REPRODUCIBLE_FLAGS) +WXCONFIG_CFLAGS += $(REPRODUCIBLE_FLAGS) +WXCONFIG_CXXFLAGS += $(REPRODUCIBLE_FLAGS) + +# Deterministic linking: pin the GNU build-id to a stable hash of the output +# instead of letting it vary with non-deterministic linker inputs. Only added +# when the linker actually accepts it. +REPRODUCIBLE_LFLAGS := $(shell printf 'int main(void){return 0;}' | $(CC) -Wl,--build-id=sha1 -x c - -o /dev/null >/dev/null 2>&1 && echo -Wl,--build-id=sha1) +LFLAGS += $(REPRODUCIBLE_LFLAGS) +endif + + WX_CONFIGURE_FLAGS += -disable-shared --disable-dependency-tracking --enable-exceptions --enable-dataobj --enable-mimetype ifdef VC_WX_MINIMAL