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 <john.parent@kitware.com>
This commit is contained in:
John W. Parent 2024-06-28 14:21:18 -04:00 committed by GitHub
parent dc6d01a0bb
commit 23e8b187a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 94 additions and 2 deletions

View File

@ -56,6 +56,15 @@ jobs:
key: macos-qt-cache-v3 key: macos-qt-cache-v3
paths: paths:
- ~/Qt - ~/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: - run:
name: Build name: Build
command: | command: |
@ -67,6 +76,7 @@ jobs:
-DBUILD_UNIVERSAL=ON \ -DBUILD_UNIVERSAL=ON \
-DMACDEPLOYQT=~/Qt/6.5.1/macos/bin/macdeployqt \ -DMACDEPLOYQT=~/Qt/6.5.1/macos/bin/macdeployqt \
-DGPT4ALL_OFFLINE_INSTALLER=ON \ -DGPT4ALL_OFFLINE_INSTALLER=ON \
-DGPT4ALL_SIGN_INSTALL=ON \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH:PATH=~/Qt/6.5.1/macos/lib/cmake/Qt6 \ -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.5.1/macos/lib/cmake/Qt6 \
-DCMAKE_MAKE_PROGRAM:FILEPATH=~/Qt/Tools/Ninja/ninja \ -DCMAKE_MAKE_PROGRAM:FILEPATH=~/Qt/Tools/Ninja/ninja \

View File

@ -33,6 +33,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(GPT4ALL_LOCALHOST OFF "Build installer for localhost repo") option(GPT4ALL_LOCALHOST OFF "Build installer for localhost repo")
option(GPT4ALL_OFFLINE_INSTALLER "Build an offline installer" OFF) 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 # Generate a header file with the version number
configure_file( configure_file(
@ -220,6 +221,13 @@ set_target_properties(chat PROPERTIES
WIN32_EXECUTABLE TRUE 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) if (APPLE)
set_target_properties(chat PROPERTIES set_target_properties(chat PROPERTIES
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
@ -230,6 +238,28 @@ if (APPLE)
OUTPUT_NAME gpt4all OUTPUT_NAME gpt4all
) )
add_dependencies(chat ggml-metal) 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() endif()
target_compile_definitions(chat target_compile_definitions(chat
@ -254,6 +284,10 @@ target_link_libraries(chat
# -- install -- # -- install --
function(install_sign_osx tgt)
install(CODE "execute_process(COMMAND codesign --options runtime --timestamp -s \"${MAC_SIGNING_IDENTITY}\" $<TARGET_FILE:${tgt}>)")
endfunction()
set(COMPONENT_NAME_MAIN ${PROJECT_NAME}) set(COMPONENT_NAME_MAIN ${PROJECT_NAME})
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
@ -296,6 +330,14 @@ install(
RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .dll 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) if (LLMODEL_CUDA)
set_property(TARGET llamamodel-mainline-cuda llamamodel-mainline-cuda-avxonly set_property(TARGET llamamodel-mainline-cuda llamamodel-mainline-cuda-avxonly
APPEND PROPERTY INSTALL_RPATH "$ORIGIN") APPEND PROPERTY INSTALL_RPATH "$ORIGIN")

View File

@ -1,7 +1,8 @@
set(MACDEPLOYQT "@MACDEPLOYQT@") set(MACDEPLOYQT "@MACDEPLOYQT@")
set(COMPONENT_NAME_MAIN "@COMPONENT_NAME_MAIN@") set(COMPONENT_NAME_MAIN "@COMPONENT_NAME_MAIN@")
set(CMAKE_CURRENT_SOURCE_DIR "@CMAKE_CURRENT_SOURCE_DIR@") 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 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 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.*) file(GLOB MYLLMODELLIBS ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/lib/libllmodel.*)

View File

@ -4,6 +4,7 @@ import subprocess
import tempfile import tempfile
import shutil import shutil
import click import click
import re
from typing import Optional from typing import Optional
# Requires click # 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('--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('--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') @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: if not signing_identity and not sha1_hash:
print("Error: Either --signing-identity or --sha1-hash must be provided.") print("Error: Either --signing-identity or --sha1-hash must be provided.")
exit(1) exit(1)
@ -64,6 +66,43 @@ def sign_dmg(input_dmg: str, output_dmg: str, signing_identity: Optional[str] =
shutil.rmtree(mount_point) shutil.rmtree(mount_point)
exit(1) 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 # Create a new DMG containing the signed .app bundle
subprocess.run([ subprocess.run([
'hdiutil', 'create', 'hdiutil', 'create',