From 23e8b187a45127d7f629f4a270ef190cb01201ce Mon Sep 17 00:00:00 2001 From: "John W. Parent" <45471568+johnwparent@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:21:18 -0400 Subject: [PATCH] Add basic signing of app bundle and binaries (#2472) Adds verification functionality to codesign script Adds required context to enable XCode to perform the signing Adds install time check + signing for all binaries Adds instructions allowing macdeployqt to sign the finalized app bundle Signed-off-by: John Parent --- .circleci/continue_config.yml | 10 ++++++ gpt4all-chat/CMakeLists.txt | 42 +++++++++++++++++++++++ gpt4all-chat/cmake/deploy-qt-mac.cmake.in | 3 +- gpt4all-chat/cmake/sign_dmg.py | 41 +++++++++++++++++++++- 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 1c50ddbe..b32c12e6 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -56,6 +56,15 @@ jobs: key: macos-qt-cache-v3 paths: - ~/Qt + - run: + name: Setup Keychain + command: | + echo $MAC_SIGNING_CERT | base64 --decode > cert.p12 + security create-keychain -p "$MAC_KEYCHAIN_KEY" sign.keychain + security default-keychain -s sign.keychain + security unlock-keychain -p "$MAC_KEYCHAIN_KEY" sign.keychain + security import cert.p12 -k sign.keychain -P "$MAC_SIGNING_CERT_PWD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MAC_KEYCHAIN_KEY" sign.keychain - run: name: Build command: | @@ -67,6 +76,7 @@ jobs: -DBUILD_UNIVERSAL=ON \ -DMACDEPLOYQT=~/Qt/6.5.1/macos/bin/macdeployqt \ -DGPT4ALL_OFFLINE_INSTALLER=ON \ + -DGPT4ALL_SIGN_INSTALL=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.5.1/macos/lib/cmake/Qt6 \ -DCMAKE_MAKE_PROGRAM:FILEPATH=~/Qt/Tools/Ninja/ninja \ diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index a3e01904..0e7bdb89 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -33,6 +33,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) option(GPT4ALL_LOCALHOST OFF "Build installer for localhost repo") option(GPT4ALL_OFFLINE_INSTALLER "Build an offline installer" OFF) +option(GPT4ALL_SIGN_INSTALL "Sign installed binaries and installers (requires signing identities)" OFF) # Generate a header file with the version number configure_file( @@ -220,6 +221,13 @@ set_target_properties(chat PROPERTIES WIN32_EXECUTABLE TRUE ) +macro(REPORT_MISSING_SIGNING_CONTEXT) + message(FATAL_ERROR [=[ + Signing requested but no identity configured. + Please set the correct env variable or provide the MAC_SIGNING_IDENTITY argument on the command line + ]=]) +endmacro() + if (APPLE) set_target_properties(chat PROPERTIES MACOSX_BUNDLE TRUE @@ -230,6 +238,28 @@ if (APPLE) OUTPUT_NAME gpt4all ) add_dependencies(chat ggml-metal) + + if(NOT MAC_SIGNING_IDENTITY) + if(NOT DEFINED ENV{MAC_SIGNING_CERT_NAME} AND GPT4ALL_SIGN_INSTALL) + REPORT_MISSING_SIGNING_CONTEXT() + endif() + set(MAC_SIGNING_IDENTITY $ENV{MAC_SIGNING_CERT_NAME}) + endif() + if(NOT MAC_SIGNING_TID) + if(NOT DEFINED ENV{MAC_NOTARIZATION_TID} AND GPT4ALL_SIGN_INSTALL) + REPORT_MISSING_SIGNING_CONTEXT() + endif() + set(MAC_SIGNING_TID $ENV{MAC_NOTARIZATION_TID}) + endif() + + # Setup MacOS signing for individual binaries + set_target_properties(chat PROPERTIES + XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual" + XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${MAC_SIGNING_TID} + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ${MAC_SIGNING_IDENTITY} + XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED True + XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--timestamp=http://timestamp.apple.com/ts01 --options=runtime,library" + ) endif() target_compile_definitions(chat @@ -254,6 +284,10 @@ target_link_libraries(chat # -- install -- +function(install_sign_osx tgt) + install(CODE "execute_process(COMMAND codesign --options runtime --timestamp -s \"${MAC_SIGNING_IDENTITY}\" $)") +endfunction() + set(COMPONENT_NAME_MAIN ${PROJECT_NAME}) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) @@ -296,6 +330,14 @@ install( RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .dll ) +if(APPLE AND GPT4ALL_SIGN_INSTALL) + install_sign_osx(chat) + install_sign_osx(llmodel) + foreach(tgt ${MODEL_IMPL_TARGETS}) + install_sign_osx(${tgt}) + endforeach() +endif() + if (LLMODEL_CUDA) set_property(TARGET llamamodel-mainline-cuda llamamodel-mainline-cuda-avxonly APPEND PROPERTY INSTALL_RPATH "$ORIGIN") diff --git a/gpt4all-chat/cmake/deploy-qt-mac.cmake.in b/gpt4all-chat/cmake/deploy-qt-mac.cmake.in index d4a637db..b16f0b34 100644 --- a/gpt4all-chat/cmake/deploy-qt-mac.cmake.in +++ b/gpt4all-chat/cmake/deploy-qt-mac.cmake.in @@ -1,7 +1,8 @@ set(MACDEPLOYQT "@MACDEPLOYQT@") set(COMPONENT_NAME_MAIN "@COMPONENT_NAME_MAIN@") set(CMAKE_CURRENT_SOURCE_DIR "@CMAKE_CURRENT_SOURCE_DIR@") -execute_process(COMMAND ${MACDEPLOYQT} ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/bin/gpt4all.app -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -verbose=2) +set(GPT4ALL_SIGNING_ID "@MAC_SIGNING_IDENTITY@") +execute_process(COMMAND ${MACDEPLOYQT} ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/bin/gpt4all.app -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -verbose=2 -sign-for-notarization=${GPT4ALL_SIGNING_ID}) file(GLOB MYGPTJLIBS ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/lib/libgptj*) file(GLOB MYLLAMALIBS ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/lib/libllama*) file(GLOB MYLLMODELLIBS ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/lib/libllmodel.*) diff --git a/gpt4all-chat/cmake/sign_dmg.py b/gpt4all-chat/cmake/sign_dmg.py index 08cbeab9..91de5a96 100755 --- a/gpt4all-chat/cmake/sign_dmg.py +++ b/gpt4all-chat/cmake/sign_dmg.py @@ -4,6 +4,7 @@ import subprocess import tempfile import shutil import click +import re from typing import Optional # Requires click @@ -20,7 +21,8 @@ from typing import Optional @click.option('--output-dmg', required=True, help='Path to the output signed DMG file.') @click.option('--sha1-hash', help='SHA-1 hash of the Developer ID Application certificate') @click.option('--signing-identity', default=None, help='Common name of the Developer ID Application certificate') -def sign_dmg(input_dmg: str, output_dmg: str, signing_identity: Optional[str] = None, sha1_hash: Optional[str] = None) -> None: +@click.option('--verify', is_flag=True, show_default=True, required=False, default=False, help='Perform verification of signed app bundle' ) +def sign_dmg(input_dmg: str, output_dmg: str, signing_identity: Optional[str] = None, sha1_hash: Optional[str] = None, verify: Optional[bool] = False) -> None: if not signing_identity and not sha1_hash: print("Error: Either --signing-identity or --sha1-hash must be provided.") exit(1) @@ -64,6 +66,43 @@ def sign_dmg(input_dmg: str, output_dmg: str, signing_identity: Optional[str] = shutil.rmtree(mount_point) exit(1) + # Validate signature and entitlements of signed app bundle + if verify: + try: + code_ver_proc = subprocess.run([ + 'codesign', + '--deep', + '--verify', + '--verbose=2', + '--strict', + app_bundle + ], check=True, capture_output=True) + if not re.search(fr"{app_bundle}: valid", code_ver_proc.stdout.decode()): + raise RuntimeError(f"codesign validation failed: {code_ver_proc.stdout.decode()}") + except subprocess.CalledProcessError as e: + print(f"Error during codesign validation: {e}") + # Clean up temporary directories + shutil.rmtree(temp_dir) + shutil.rmtree(mount_point) + exit(1) + try: + spctl_proc = subprocess.run([ + 'spctl', + '-a', + '-t', + 'exec', + '-vv', + app_bundle + ], check=True, capture_output=True) + if not re.search(fr"{app_bundle}: accepted", spctl_proc.stdout.decode()): + raise RuntimeError(f"spctl validation failed: {spctl_proc.stdout.decode()}") + except subprocess.CalledProcessError as e: + print(f"Error during spctl validation: {e}") + # Clean up temporary directories + shutil.rmtree(temp_dir) + shutil.rmtree(mount_point) + exit(1) + # Create a new DMG containing the signed .app bundle subprocess.run([ 'hdiutil', 'create',