mirror of
https://github.com/veracrypt/VeraCrypt.git
synced 2026-06-15 09:06:08 -05:00
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 <curious-rabbit@local> Co-authored-by: Mounir IDRASSI <mounir.idrassi@amcrypto.jp>
This commit is contained in:
@@ -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 ()
|
||||
|
||||
@@ -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) $@
|
||||
|
||||
@@ -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
|
||||
# <build>/_CPack_Packages/.../<pkg>${CPACK_PACKAGING_INSTALL_PREFIX}
|
||||
# directory, exposed here as CMAKE_INSTALL_PREFIX. We require the
|
||||
# path to contain a "_CPack_Packages" component.
|
||||
# 2. "make DESTDIR=<dir> 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()
|
||||
Executable
+55
@@ -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 <archive>
|
||||
|
||||
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 <archive>")
|
||||
finalize(sys.argv[1])
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user