From 4e59c1c579c222be979da3f7b54f53999038f49d Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 15 Nov 2025 02:02:59 +0100 Subject: [PATCH] Integrate macOS code signing into CMake Moves code signing from the release-tool to CMake and unifies the Windows-equivalent code. --- CMakeLists.txt | 11 +- cmake/FindBotan.cmake | 4 +- cmake/FindQREncode.cmake | 2 +- cmake/MacOSCodesign.cmake.in | 102 +++++++++++++ cmake/WindowsCodesign.cmake.in | 79 ++++++++++ cmake/WindowsPostInstall.cmake.in | 71 --------- release-tool.py | 242 +++++++++++------------------- src/CMakeLists.txt | 22 ++- 8 files changed, 301 insertions(+), 232 deletions(-) create mode 100644 cmake/MacOSCodesign.cmake.in create mode 100644 cmake/WindowsCodesign.cmake.in delete mode 100644 cmake/WindowsPostInstall.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index e7183f169..77b789207 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,10 +60,17 @@ option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF) option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON) if(UNIX AND NOT APPLE) option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF) + set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps") endif() option(WITH_XC_DOCS "Enable building of documentation" ON) - -set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps") +if(WIN32 OR APPLE) + set(WITH_XC_CODESIGN_IDENTITY "" CACHE STRING "Certificate to be used for signing binaries before packaging.") + if(WIN32) + set(WITH_XC_CODESIGN_TIMESTAMP_URL "http://timestamp.sectigo.com" CACHE STRING "Timestamp URL for Windows code signing.") + elseif(APPLE) + set(WITH_XC_NOTARY_KEYCHAIN_PROFILE "" CACHE STRING "Keychain profile name for stored Apple notarization credentials.") + endif() +endif() if(APPLE) # Perform the platform checks before applying the stricter compiler flags. diff --git a/cmake/FindBotan.cmake b/cmake/FindBotan.cmake index 94d9df98a..dfa415c1d 100644 --- a/cmake/FindBotan.cmake +++ b/cmake/FindBotan.cmake @@ -36,7 +36,7 @@ find_library( NAMES ${BOTAN_NAMES} PATH_SUFFIXES release/lib lib DOC "The Botan (release) library") -if(MSVC) +if(WIN32 AND NOT MINGW) find_library( BOTAN_LIBRARY_DEBUG NAMES ${BOTAN_NAMES_DEBUG} @@ -55,7 +55,7 @@ endif() if(BOTAN_FOUND) set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR}) - if(MSVC) + if(WIN32 AND NOT MINGW) set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG}) else() set(BOTAN_LIBRARIES ${BOTAN_LIBRARY}) diff --git a/cmake/FindQREncode.cmake b/cmake/FindQREncode.cmake index fdd98278c..9f12def98 100644 --- a/cmake/FindQREncode.cmake +++ b/cmake/FindQREncode.cmake @@ -15,7 +15,7 @@ find_path(QRENCODE_INCLUDE_DIR NAMES qrencode.h) -if(WIN32 AND MSVC) +if(WIN32 AND NOT MINGW) find_library(QRENCODE_LIBRARY_RELEASE qrencode) find_library(QRENCODE_LIBRARY_DEBUG qrencoded) set(QRENCODE_LIBRARY optimized ${QRENCODE_LIBRARY_RELEASE} debug ${QRENCODE_LIBRARY_DEBUG}) diff --git a/cmake/MacOSCodesign.cmake.in b/cmake/MacOSCodesign.cmake.in new file mode 100644 index 000000000..bd38a31df --- /dev/null +++ b/cmake/MacOSCodesign.cmake.in @@ -0,0 +1,102 @@ +# Copyright (C) 2025 KeePassXC Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 or (at your option) +# version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# CPACK_PACKAGE_FILES is set only during POST_BUILD +if(NOT CPACK_PACKAGE_FILES) # PRE_BUILD: Sign binaries + + set(PROGNAME "@PROGNAME@") + set(CODESIGN_IDENTITY "@WITH_XC_CODESIGN_IDENTITY@") + set(ENTITLEMENTS @MACOSX_BUNDLE_APPLE_ENTITLEMENTS@) + set(APP_DIR "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/ALL_IN_ONE/${PROGNAME}.app") + + if(NOT CODESIGN_IDENTITY) + message(FATAL_ERROR "No codesign identity specified.") + endif() + + message(STATUS "Codesign identity used: ${CODESIGN_IDENTITY}") + message(STATUS "Signing ${PROGNAME}.app, this may take while...") + + # Sign all binaries + execute_process( + COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep ${APP_DIR} + RESULT_VARIABLE SIGN_RESULT + OUTPUT_VARIABLE SIGN_OUTPUT + ERROR_VARIABLE SIGN_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ECHO_OUTPUT_VARIABLE + ) + if (NOT SIGN_RESULT EQUAL 0) + message(FATAL_ERROR "Signing binaries failed: ${SIGN_ERROR}") + endif() + + # (Re-)Sign main executable with --entitlements + execute_process( + COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep --entitlements=${ENTITLEMENTS} ${APP_DIR} + RESULT_VARIABLE SIGN_RESULT + OUTPUT_VARIABLE SIGN_OUTPUT + ERROR_VARIABLE SIGN_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ECHO_OUTPUT_VARIABLE + ) + if (NOT SIGN_RESULT EQUAL 0) + message(FATAL_ERROR "Signing main binary failed: ${SIGN_ERROR}") + endif() + + message(STATUS "${PROGNAME}.app signed successfully.") + +else() # POST_BUILD: Notarize DMG + set(KEYCHAIN_PROFILE "@WITH_XC_NOTARY_KEYCHAIN_PROFILE@") + file(GLOB_RECURSE DMG_FILE "${CPACK_PACKAGE_DIRECTORY}/${CPACK_PACKAGE_FILE_NAME}.dmg") + + if(NOT KEYCHAIN_PROFILE) + message(FATAL_ERROR "No notarization credentials keychain profile specified.") + endif() + + # Submit for notarization + message(STATUS "Submitting DMG bundle for notarization, this may take while...") + execute_process( + COMMAND xcrun notarytool submit --keychain-profile=${KEYCHAIN_PROFILE} --wait ${DMG_FILE} + RESULT_VARIABLE NOTARIZE_RESULT + OUTPUT_VARIABLE NOTARIZE_OUTPUT + ERROR_VARIABLE NOTARIZE_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ECHO_OUTPUT_VARIABLE + ) + if (NOT NOTARIZE_RESULT EQUAL 0) + message(FATAL_ERROR "Notarization failed: ${NOTARIZE_ERROR}") + endif() + message(STATUS "DMG bundle notarized successfully.") + + # Staple tickets + message(STATUS "Stapling notarization ticket...") + execute_process( + COMMAND xcrun stapler staple ${DMG_FILE} && xcrun stapler validate ${DMG_FILE} + RESULT_VARIABLE STAPLE_RESULT + OUTPUT_VARIABLE STAPLE_OUTPUT + ERROR_VARIABLE STAPLE_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ECHO_OUTPUT_VARIABLE + ) + if (NOT STAPLE_RESULT EQUAL 0) + message(FATAL_ERROR "Stapling failed: ${STAPLE_ERROR}") + endif() + message(STATUS "DMG bundle notarization ticket stapled successfully.") + +endif() \ No newline at end of file diff --git a/cmake/WindowsCodesign.cmake.in b/cmake/WindowsCodesign.cmake.in new file mode 100644 index 000000000..fb59440f0 --- /dev/null +++ b/cmake/WindowsCodesign.cmake.in @@ -0,0 +1,79 @@ +# Copyright (C) 2025 KeePassXC Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 or (at your option) +# version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(INSTALL_DIR ${CPACK_TEMPORARY_INSTALL_DIRECTORY}) +set(CODESIGN_IDENTITY @WITH_XC_CODESIGN_IDENTITY@) +set(TIMESTAMP_URL @WITH_XC_CODESIGN_TIMESTAMP_URL@) + +if(CPACK_PACKAGE_FILES) + # This variable is set only during POST_BUILD, reset SIGN_FILES first + set(SIGN_FILES "") + foreach(PACKAGE_FILE ${CPACK_PACKAGE_FILES}) + # Check each package file to see if it can be signed + if(PACKAGE_FILE MATCHES "\\.msix?$" OR PACKAGE_FILE MATCHES "\\.exe$") + message(STATUS "Adding ${PACKAGE_FILE} for signature") + list(APPEND SIGN_FILES "${PACKAGE_FILE}") + endif() + endforeach() +else() + # Setup portable zip file if building one + if(INSTALL_DIR MATCHES "/ZIP/") + file(TOUCH "${INSTALL_DIR}/.portable") + message(STATUS "Injected portable marker into ZIP file.") + endif() + + # Find all dll and exe files in the install directory + file(GLOB_RECURSE SIGN_FILES + RELATIVE "${INSTALL_DIR}" + "${INSTALL_DIR}/*.dll" + "${INSTALL_DIR}/*.exe" + ) +endif() + +# Sign relevant binaries if requested +if(CODESIGN_IDENTITY AND SIGN_FILES) + # Find signtool in PATH or error out + find_program(SIGNTOOL signtool.exe QUIET) + if(NOT SIGNTOOL) + message(FATAL_ERROR "signtool.exe not found in PATH, correct or unset WITH_XC_CODESIGN_IDENTITY") + endif() + + # Check that a certificate thumbprint was provided or error out + if(CODESIGN_IDENTITY STREQUAL "auto") + message(STATUS "Signing using best available certificate.") + set(CERT_OPTS /a) + else () + message(STATUS "Signing using certificate with fingerprint ${CODESIGN_IDENTITY}.") + set(CERT_OPTS /sha1 ${CODESIGN_IDENTITY}) + endif() + + message(STATUS "Signing binary files, this may take a while...") + # Use cmd /c to enable pop-up for pin entry if needed + execute_process( + COMMAND cmd /c ${SIGNTOOL} sign /fd SHA256 ${CERT_OPTS} /tr ${TIMESTAMP_URL} /td SHA256 /d ${CPACK_PACKAGE_FILE_NAME} ${SIGN_FILES} + WORKING_DIRECTORY "${INSTALL_DIR}" + RESULT_VARIABLE SIGN_RESULT + OUTPUT_VARIABLE SIGN_OUTPUT + ERROR_VARIABLE SIGN_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ECHO_OUTPUT_VARIABLE + ) + if(NOT SIGN_RESULT EQUAL 0) + message(FATAL_ERROR "Signing binary files failed: ${SIGN_ERROR}") + endif() + + message(STATUS "Binary files signed successfully.") +endif() diff --git a/cmake/WindowsPostInstall.cmake.in b/cmake/WindowsPostInstall.cmake.in deleted file mode 100644 index 6d70ec71d..000000000 --- a/cmake/WindowsPostInstall.cmake.in +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2025 KeePassXC Team -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 or (at your option) -# version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -set(_installdir ${CPACK_TEMPORARY_INSTALL_DIRECTORY}) -set(_sign @WITH_XC_SIGNINSTALL@) -set(_cert_thumbprint @WITH_XC_SIGNINSTALL_CERT@) -set(_timestamp_url @WITH_XC_SIGNINSTALL_TIMESTAMP_URL@) - -# Setup portable zip file if building one -if(_installdir MATCHES "/ZIP/") - file(TOUCH "${_installdir}/.portable") - message(STATUS "Injected portable zip file.") -endif() - -# Find all dll and exe files in the install directory -file(GLOB_RECURSE _sign_files - RELATIVE "${_installdir}" - "${_installdir}/*.dll" - "${_installdir}/*.exe" -) - -# Sign relevant binaries if requested -if(_sign AND _sign_files) - # Find signtool in PATH or error out - find_program(_signtool signtool.exe QUIET) - if(NOT _signtool) - message(FATAL_ERROR "signtool.exe not found in PATH, correct or unset WITH_XC_SIGNINSTALL") - endif() - - # Set a default timestamp URL if none was provided - if (NOT _timestamp_url) - set(_timestamp_url "http://timestamp.sectigo.com") - endif() - - # Check that a certificate thumbprint was provided or error out - if (NOT _cert_thumbprint) - message(STATUS "Signing using best available certificate.") - set(_certopt /a) - else() - message(STATUS "Signing using certificate with thumbprint ${_cert_thumbprint}.") - set(_certopt /sha1 ${_cert_thumbprint}) - endif() - - message(STATUS "Signing binary files with signtool, this may take a while...") - # Use cmd /c to enable pop-up for pin entry if needed - execute_process( - COMMAND cmd /c ${_signtool} sign /fd SHA256 ${_certopt} /tr ${_timestamp_url} /td SHA256 ${_sign_files} - WORKING_DIRECTORY "${_installdir}" - RESULT_VARIABLE sign_result - OUTPUT_VARIABLE sign_output - ERROR_VARIABLE sign_error - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_STRIP_TRAILING_WHITESPACE - ECHO_OUTPUT_VARIABLE - ) - if (NOT sign_result EQUAL 0) - message(FATAL_ERROR "signtool failed: ${sign_error}") - endif() -endif() diff --git a/release-tool.py b/release-tool.py index bed4367a4..7c0c8b724 100755 --- a/release-tool.py +++ b/release-tool.py @@ -342,6 +342,48 @@ def _capture_vs_env(arch='amd64'): return env +def _macos_get_codesigning_identity(user_choice=None): + """ + Select an Apple codesigning certificate to be used for signing the macOS binaries. + If only one identity was found on the system, it is returned automatically. If multiple identities are + found, an interactive selection is shown. A user choice can be supplied to skip the selection. + If the user choice refers to an invalid identity, an error is raised. + """ + Check.check_xcode_setup() + result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True) + identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]] + identities = [i.split(' ', 2)[1:] for i in identities] + if not identities: + raise Error('No codesigning identities found.') + + if not user_choice and len(identities) == 1: + logger.info('Using codesigning identity %s.', identities[0][1]) + return identities[0][0] + elif not user_choice: + return identities[_choice_prompt( + 'The following code signing identities were found. Which one do you want to use?', + [' '.join(i) for i in identities])][0] + else: + for i in identities: + # Exact match of ID or substring match of description + if user_choice == i[0] or user_choice in i[1]: + return i[0] + raise Error('Invalid identity: %s', user_choice) + + +def _macos_validate_keychain_profile(keychain_profile): + """ + Validate that a given keychain profile with stored notarization credentials exists and is valid. + If no such profile is found, an error is raised with instructions on how to set one up. + """ + if _run(['security', 'find-generic-password', '-a', + f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0: + raise Error(f'Keychain profile "%s" not found! Run\n' + f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n' + f'to store your Apple notary service credentials in a keychain as "%s".', + keychain_profile, keychain_profile) + + ########################################################################################### # CLI Commands ########################################################################################### @@ -585,6 +627,13 @@ class Build(Command): help='macOS deployment target version (default: %(default)s).') parser.add_argument('-p', '--platform-target', default=platform.uname().machine, help='Build target platform (default: %(default)s).', choices=['x86_64', 'arm64']) + parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true') + parser.add_argument('--sign-identity', + help='Apple Developer identity name used for signing binaries (default: ask).') + parser.add_argument('--notarize', help='Notarize signed file(s).', action='store_true') + parser.add_argument('--keychain-profile', default='notarization-creds', + help='Read Apple credentials for notarization from a keychain (default: %(default)s).') + parser.set_defaults(cmake_generator='Ninja') elif sys.platform == 'linux': parser.add_argument('-d', '--docker-image', help='Run build in Docker image (overrides --use-system-deps).') parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).', @@ -594,8 +643,10 @@ class Build(Command): parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).', choices=['amd64', 'arm64'], default='amd64') parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true') - parser.add_argument('--sign-cert', help='SHA1 fingerprint of the signing certificate (optional).') - parser.set_defaults(cmake_generator='Ninja', no_source_tarball=True) + parser.add_argument('--sign-identity', help='SHA1 fingerprint of the signing certificate.') + parser.add_argument('--sign-timestamp-url', help='Timestamp URL for signing binaries.', + default='http://timestamp.sectigo.com') + parser.set_defaults(cmake_generator='Ninja') parser.add_argument('-c', '--cmake-opts', nargs=argparse.REMAINDER, help='Additional CMake options (no other arguments can be specified after this).') @@ -674,15 +725,15 @@ class Build(Command): # noinspection PyMethodMayBeStatic def build_windows(self, version, src_dir, output_dir, *, parallelism, cmake_opts, platform_target, - sign, sign_cert, with_tests, **_): + sign, sign_identity, sign_timestamp_url, with_tests, **_): # Check for required tools if not _cmd_exists('candle.exe') or not _cmd_exists('light.exe') or not _cmd_exists('heat.exe'): raise Error('WiX Toolset not found on the PATH (candle.exe, light.exe, heat.exe).') # Setup build signing if requested if sign: - cmake_opts.append('-DWITH_XC_SIGNINSTALL=ON') - cmake_opts.append(f'-DWITH_XC_SIGNINSTALL_CERT={sign_cert}') + cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}') + cmake_opts.append(f'-WITH_XC_CODESIGN_TIMESTAMP_URL={sign_timestamp_url}') # Use vcpkg for dependency deployment cmake_opts.append('-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON') @@ -716,13 +767,20 @@ class Build(Command): # noinspection PyMethodMayBeStatic def build_macos(self, version, src_dir, output_dir, *, use_system_deps, parallelism, cmake_opts, - macos_target, platform_target, with_tests, **_): + macos_target, platform_target, with_tests, sign, sign_identity, notarize, keychain_profile, **_): if not use_system_deps: cmake_opts.append(f'-DVCPKG_TARGET_TRIPLET={platform_target.replace("86_", "")}-osx-dynamic-release') cmake_opts.append(f'-DCMAKE_OSX_DEPLOYMENT_TARGET={macos_target}') cmake_opts.append(f'-DCMAKE_OSX_ARCHITECTURES={platform_target}') with tempfile.TemporaryDirectory() as build_dir: + if sign: + sign_identity = _macos_get_codesigning_identity(sign_identity) + cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}') + if notarize: + _macos_validate_keychain_profile(keychain_profile) + cmake_opts.append(f'-DWITH_XC_NOTARY_KEYCHAIN_PROFILE={keychain_profile}') + logger.info('Configuring build...') _run(['cmake', *cmake_opts, str(src_dir)], cwd=build_dir, capture_output=False) @@ -736,9 +794,13 @@ class Build(Command): _run(['cpack', '-G', 'DragNDrop'], cwd=build_dir, capture_output=False) output_file = Path(build_dir) / f'KeePassXC-{version}.dmg' - output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}-unsigned.dmg') + unsigned_suffix = '-unsigned' if not sign else '' + output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}{unsigned_suffix}.dmg') - logger.info('All done! Please don\'t forget to sign the binaries before distribution.') + if sign: + logger.info('All done!') + else: + logger.info('All done! Please don\'t forget to sign the binaries before distribution.') @staticmethod def _download_tools_if_not_available(toolname, bin_dir, url, docker_args=None): @@ -888,162 +950,37 @@ class BuildSrc(Command): tmp_comp.rename(output_file) -class AppSign(Command): - """Sign binaries with code signing certificates on Windows and macOS.""" +class Notarize(Command): + """Notarize a signed macOS DMG app bundle.""" @classmethod def setup_arg_parser(cls, parser: argparse.ArgumentParser): - parser.add_argument('file', help='Input file(s) to sign.', nargs='+') - parser.add_argument('-i', '--identity', help='Key or identity used for the signature (default: ask).') - parser.add_argument('-s', '--src-dir', help='Source directory (default: %(default)s).', default='.') + parser.add_argument('file', help='Input DMG file(s) to notarize.', nargs='+') + parser.add_argument('-p', '--keychain-profile', default='notarization-creds', + help='Read Apple credentials for notarization from a keychain (default: %(default)s).') - if sys.platform == 'darwin': - parser.add_argument('-n', '--notarize', help='Notarize signed file(s).', action='store_true') - parser.add_argument('-c', '--keychain-profile', default='notarization-creds', - help='Read Apple credentials for notarization from a keychain (default: %(default)s).') + def run(self, file, keychain_profile, **_): + if sys.platform != 'darwin': + raise Error('Unsupported platform.') - def run(self, file, identity, src_dir, **kwargs): + logger.warning('This tool is meant primarily for testing purposes. ' + 'For production use, add the --notarize flag to the build command.') + + _macos_validate_keychain_profile(keychain_profile) for i, f in enumerate(file): f = Path(f) if not f.exists(): raise Error('Input file does not exist: %s', f) + if f.suffix != '.dmg': + raise Error('Input file is not a DMG image: %s', f) file[i] = f - - if sys.platform == 'win32': - for f in file: - self.sign_windows(f, identity, Path(src_dir)) - - elif sys.platform == 'darwin': - Check.check_xcode_setup() - if kwargs['notarize']: - self._macos_validate_keychain_profile(kwargs['keychain_profile']) - identity = self._macos_get_codesigning_identity(identity) - for f in file: - out_file = self.sign_macos(f, identity, Path(src_dir)) - if out_file and kwargs['notarize'] and out_file.suffix == '.dmg': - self.notarize_macos(out_file, kwargs['keychain_profile']) - - else: - raise Error('Unsupported platform.') + self.notarize_macos(f, keychain_profile) logger.info('All done.') - # noinspection PyMethodMayBeStatic - def sign_windows(self, file, identity, src_dir): - # Check for signtool - if not _cmd_exists('signtool.exe'): - raise Error('signtool was not found on the PATH.') - - signtool_args = ['signtool', 'sign', '/fd', 'sha256', '/tr', 'http://timestamp.digicert.com', '/td', 'sha256'] - if not identity: - logger.info('Using automatic selection of signing certificate.') - signtool_args += ['/a'] - else: - logger.info('Using specified signing certificate: %s', identity) - signtool_args += ['/sha1', identity] - signtool_args += ['/d', file.name, str(file.resolve())] - - _run(signtool_args, cwd=src_dir, capture_output=False) - - # noinspection PyMethodMayBeStatic - def _macos_validate_keychain_profile(self, keychain_profile): - if _run(['security', 'find-generic-password', '-a', - f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0: - raise Error(f'Keychain profile "%s" not found! Run\n' - f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n' - f'to store your Apple notary service credentials in a keychain as "%s".', - keychain_profile, keychain_profile) - - # noinspection PyMethodMayBeStatic - def _macos_get_codesigning_identity(self, user_choice=None): - result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True) - identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]] - identities = [i.split(' ', 2)[1:] for i in identities] - if not identities: - raise Error('No codesigning identities found.') - - if not user_choice and len(identities) == 1: - logger.info('Using codesigning identity %s.', identities[0][1]) - return identities[0][0] - elif not user_choice: - return identities[_choice_prompt( - 'The following code signing identities were found. Which one do you want to use?', - [' '.join(i) for i in identities])][0] - else: - for i in identities: - # Exact match of ID or substring match of description - if user_choice == i[0] or user_choice in i[1]: - return i[0] - raise Error('Invalid identity: %s', user_choice) - - # noinspection PyMethodMayBeStatic - def sign_macos(self, file, identity, src_dir): - logger.info('Signing "%s"', file) - - with tempfile.TemporaryDirectory() as tmp: - tmp = Path(tmp).absolute() - app_dir = tmp / 'app' - out_file = file.parent / file.name.replace('-unsigned', '') - - if file.is_file() and file.suffix == '.dmg': - logger.debug('Unpacking disk image...') - mnt = tmp / 'mnt' - mnt.mkdir() - try: - _run(['hdiutil', 'attach', '-noautoopen', '-mountpoint', mnt.as_posix(), file.as_posix()], cwd=None) - shutil.copytree(mnt, app_dir, symlinks=True) - finally: - _run(['hdiutil', 'detach', mnt.as_posix()], cwd=None) - elif file.is_dir() and file.suffix == '.app': - logger.debug('Copying .app directory...') - shutil.copytree(file, app_dir, symlinks=True) - else: - logger.warning('Skipping non-app file "%s"', file) - return None - - app_dir_app = list(app_dir.glob('*.app'))[0] - - logger.debug('Signing libraries and frameworks...') - _run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime', '--deep', - app_dir_app.as_posix()], cwd=None) - - # (Re-)Sign main executable with --entitlements - logger.debug('Signing main executable...') - _run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime', - '--entitlements', (src_dir / 'share/macosx/keepassxc.entitlements').as_posix(), - (app_dir_app / 'Contents/MacOS/KeePassXC').as_posix()], cwd=None) - - tmp_out = out_file.with_suffix(f'.{"".join(random.choices(string.ascii_letters, k=8))}{file.suffix}') - try: - if file.suffix == '.dmg': - logger.debug('Repackaging disk image...') - dmg_size = sum(f.stat().st_size for f in app_dir.rglob('*')) - _run(['hdiutil', 'create', '-volname', 'KeePassXC', '-srcfolder', app_dir.as_posix(), - '-fs', 'HFS+', '-fsargs', '-c c=64,a=16,e=16', '-format', 'UDBZ', - '-size', f'{dmg_size}k', tmp_out.as_posix()], - cwd=None) - elif file.suffix == '.app': - shutil.copytree(app_dir, tmp_out, symlinks=True) - except Exception: - if tmp_out.is_file(): - tmp_out.unlink() - elif tmp_out.is_dir(): - shutil.rmtree(tmp_out, ignore_errors=True) - raise - finally: - # Replace original file if all went well - if tmp_out.exists(): - if tmp_out.is_dir(): - shutil.rmtree(file) - else: - file.unlink() - tmp_out.rename(out_file) - - logger.info('File signed successfully and written to: "%s".', out_file) - return out_file - # noinspection PyMethodMayBeStatic def notarize_macos(self, file, keychain_profile): + logger.info('Submitting "%s" for notarization...', file) _run(['xcrun', 'notarytool', 'submit', f'--keychain-profile={keychain_profile}', '--wait', file.as_posix()], cwd=None, capture_output=False) @@ -1271,9 +1208,10 @@ def main(): BuildSrc.setup_arg_parser(build_src_parser) build_src_parser.set_defaults(_cmd=BuildSrc) - appsign_parser = subparsers.add_parser('appsign', help=AppSign.__doc__) - AppSign.setup_arg_parser(appsign_parser) - appsign_parser.set_defaults(_cmd=AppSign) + if sys.platform == 'darwin': + notarize_parser = subparsers.add_parser('notarize', help=Notarize.__doc__) + Notarize.setup_arg_parser(notarize_parser) + notarize_parser.set_defaults(_cmd=Notarize) gpgsign_parser = subparsers.add_parser('gpgsign', help=GPGSign.__doc__) GPGSign.setup_arg_parser(gpgsign_parser) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3aa82dc30..6257451a5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -426,7 +426,7 @@ target_link_libraries(${PROGNAME} keepassxc_gui) set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON) if(WIN32) - set_target_properties(${PROGNAME} PROPERTIES WIN32 ON) + set_target_properties(${PROGNAME} PROPERTIES WIN32_EXECUTABLE ON) include(GenerateProductVersion) generate_product_version( WIN32_ResourceFiles @@ -442,6 +442,7 @@ if(WIN32) elseif(APPLE AND WITH_APP_BUNDLE) set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc) set(MACOSX_BUNDLE_ICON_NAME keepassxc) + set(MACOSX_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements") configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR}) set(MACOSX_BUNDLE_RESOURCE_FILES @@ -451,7 +452,7 @@ elseif(APPLE AND WITH_APP_BUNDLE) set_target_properties(${PROGNAME} PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist" - CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements" + CPACK_BUNDLE_APPLE_ENTITLEMENTS "${MACOSX_BUNDLE_APPLE_ENTITLEMENTS}" RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}" ) target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES}) @@ -461,6 +462,18 @@ elseif(APPLE AND WITH_APP_BUNDLE) DESTINATION "${DATA_INSTALL_DIR}") endif() + # Sign binaries + if(WITH_XC_CODESIGN_IDENTITY) + configure_file("${CMAKE_SOURCE_DIR}/cmake/MacOSCodesign.cmake.in" "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake" @ONLY) + set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake") + if(WITH_XC_NOTARY_KEYCHAIN_PROFILE) + configure_file("${CMAKE_SOURCE_DIR}/cmake/MacOSCodesign.cmake.in" "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake" @ONLY) + set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake") + else() + message(INFO "Do not forget to notarize DMG package before distribution!") + endif() + endif() + set(CPACK_GENERATOR "DragNDrop") set(CPACK_DMG_FORMAT "UDBZ") set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/share/macosx/DS_Store.in") @@ -492,8 +505,9 @@ if(WIN32) "${CMAKE_CURRENT_BINARY_DIR}/INSTALLER_LICENSE.txt") # Prepare post-install script and set to run prior to building cpack installers - configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsPostInstall.cmake.in" "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake" @ONLY) - set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake") + configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsCodesign.cmake.in" "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake" @ONLY) + set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake") + set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake") string(REGEX REPLACE "-.*$" "" KEEPASSXC_VERSION_CLEAN ${KEEPASSXC_VERSION})