Files
zeal/cmake/CodeSign.cmake
T
2026-05-23 14:20:57 +03:00

353 lines
12 KiB
CMake

#
# CodeSign.cmake - CMake helper for signing Windows executables
#
# SPDX-FileCopyrightText: Oleg Shparber, et al. <https://zealdocs.org>
# SPDX-License-Identifier: MIT
#
include_guard()
# codesign(FILES <files>...
# [DESCRIPTION] <description-string>
# [URL] <url-string>
# [CERTIFICATE_FILE] <filename>
# [PASSWORD] <password-string>
# [TIMESTAMP_URL] <url-string>
# [QUIET]
# [VERBOSE]
# [DEBUG])
function(codesign)
# Cleans up temporary files created during signing.
macro(_cleanup)
if(DEFINED _certificate_file)
file(REMOVE ${_certificate_file})
endif()
if(DEFINED _azure_metadata_file)
file(REMOVE ${_azure_metadata_file})
endif()
endmacro()
# Sets '_certificate_file' variable to a temporary file path.
macro(_set_temporary_certificate_file)
# Determine temporary file location. Try to keep it local to the build.
if(CMAKE_BINARY_DIR)
set(_temp_path ${CMAKE_BINARY_DIR})
elseif(CPACK_TEMPORARY_DIRECTORY)
set(_temp_path ${CPACK_TEMPORARY_DIRECTORY})
else()
set(_temp_path $ENV{TEMP})
endif()
set(_certificate_file "${_temp_path}/codesign.tmp")
# Remove file if left from previous run.
_cleanup()
endmacro()
if(NOT WIN32)
message(FATAL_ERROR "Code signing is only supported on Windows.")
endif()
cmake_parse_arguments(_ARG
"QUIET;VERBOSE;DEBUG" # Options.
"DESCRIPTION;URL;CERTIFICATE_FILE;PASSWORD;TIMESTAMP_URL" # Single-value keywords.
"FILES" # Multi-value keywords.
${ARGN}
)
if(NOT _ARG_FILES)
message(FATAL_ERROR "FILES argument is required.")
endif()
# Find signtool executable.
# TODO: Add option for path to signtool.exe.
# Add Windows 10 SDK paths.
get_filename_component(_w10sdk_root_path
"[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]"
ABSOLUTE CACHE
)
if(_w10sdk_root_path)
file(GLOB _w10sdk_paths "${_w10sdk_root_path}/bin/10.*")
list(REVERSE _w10sdk_paths) # Newest version first.
# Detect target architecture.
# https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details#environment-variables
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64")
set(_w10sdk_arch "x64")
elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "X86")
set(_w10sdk_arch "x86")
elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
set(_w10sdk_arch "arm64")
else()
message(WARNING "Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}.")
endif()
endif()
# TODO: Add microsoft.windows.sdk.buildtools path.
find_program(_cmd
NAMES signtool
PATHS ${_w10sdk_paths}
PATH_SUFFIXES ${_w10sdk_arch}
)
if(NOT _cmd)
message(NOTICE "signtool.exe was not found, no binaries will be signed.")
return()
endif()
message(DEBUG "Found signtool.exe: ${_cmd}")
# Start constructing command.
set(_cmd_args "sign")
list(APPEND _cmd_args "/fd" "sha256")
# Detect Azure Trusted Signing mode. If all three required variables are set,
# signing is delegated to Microsoft.Trusted.Signing.Client (loaded by signtool
# via /dlib) instead of using a local PFX certificate. Authentication to Azure
# is handled out-of-band by the Azure SDK credential chain (typically a
# workload-identity token from `azure/login@v2` in CI).
set(_azure_endpoint "${CODESIGN_AZURE_ENDPOINT}")
if(NOT _azure_endpoint AND DEFINED ENV{CODESIGN_AZURE_ENDPOINT})
set(_azure_endpoint "$ENV{CODESIGN_AZURE_ENDPOINT}")
endif()
set(_azure_account "${CODESIGN_AZURE_ACCOUNT}")
if(NOT _azure_account AND DEFINED ENV{CODESIGN_AZURE_ACCOUNT})
set(_azure_account "$ENV{CODESIGN_AZURE_ACCOUNT}")
endif()
set(_azure_profile "${CODESIGN_AZURE_PROFILE}")
if(NOT _azure_profile AND DEFINED ENV{CODESIGN_AZURE_PROFILE})
set(_azure_profile "$ENV{CODESIGN_AZURE_PROFILE}")
endif()
set(_use_azure FALSE)
if(_azure_endpoint AND _azure_account AND _azure_profile)
set(_use_azure TRUE)
elseif(_azure_endpoint OR _azure_account OR _azure_profile)
message(FATAL_ERROR
"Incomplete Azure Trusted Signing configuration. "
"CODESIGN_AZURE_ENDPOINT, CODESIGN_AZURE_ACCOUNT, and "
"CODESIGN_AZURE_PROFILE must all be set."
)
endif()
if(_use_azure)
# Resolve dlib path.
set(_azure_dlib "${CODESIGN_AZURE_DLIB}")
if(NOT _azure_dlib AND DEFINED ENV{CODESIGN_AZURE_DLIB})
set(_azure_dlib "$ENV{CODESIGN_AZURE_DLIB}")
endif()
if(NOT _azure_dlib)
message(FATAL_ERROR
"CODESIGN_AZURE_DLIB is not set. "
"Azure Trusted Signing requires the path to Azure.CodeSigning.Dlib.dll."
)
endif()
if(NOT EXISTS "${_azure_dlib}")
message(FATAL_ERROR
"Azure Trusted Signing dlib '${_azure_dlib}' does not exist."
)
endif()
# Determine temporary file location for the metadata JSON.
if(CMAKE_BINARY_DIR)
set(_temp_path ${CMAKE_BINARY_DIR})
elseif(CPACK_TEMPORARY_DIRECTORY)
set(_temp_path ${CPACK_TEMPORARY_DIRECTORY})
else()
set(_temp_path $ENV{TEMP})
endif()
set(_azure_metadata_file "${_temp_path}/codesign-azure-metadata.json")
# Remove any leftover from a previous run.
if(EXISTS ${_azure_metadata_file})
file(REMOVE ${_azure_metadata_file})
endif()
file(WRITE ${_azure_metadata_file}
"{
\"Endpoint\": \"${_azure_endpoint}\",
\"CodeSigningAccountName\": \"${_azure_account}\",
\"CertificateProfileName\": \"${_azure_profile}\"
}
")
list(APPEND _cmd_args "/dlib" ${_azure_dlib} "/dmdf" ${_azure_metadata_file})
else()
# Set certificate file.
if(NOT _ARG_CERTIFICATE_FILE)
if(CODESIGN_CERTIFICATE_FILE)
if(NOT EXISTS ${CODESIGN_CERTIFICATE_FILE})
message(NOTICE "Certificate file '${CODESIGN_CERTIFICATE_FILE}' does not exist.")
return()
endif()
set(_ARG_CERTIFICATE_FILE ${CODESIGN_CERTIFICATE_FILE})
elseif(DEFINED ENV{CODESIGN_CERTIFICATE_FILE})
if("$ENV{CODESIGN_CERTIFICATE_FILE}" STREQUAL "")
message(NOTICE "CODESIGN_CERTIFICATE_FILE is set to an empty string.")
return()
endif()
if(NOT EXISTS $ENV{CODESIGN_CERTIFICATE_FILE})
message(NOTICE
"Certificate file '$ENV{CODESIGN_CERTIFICATE_FILE}' "
"(set in CODESIGN_CERTIFICATE_FILE) does not exist."
)
return()
endif()
set(_ARG_CERTIFICATE_FILE $ENV{CODESIGN_CERTIFICATE_FILE})
elseif(DEFINED ENV{CODESIGN_CERTIFICATE})
if("$ENV{CODESIGN_CERTIFICATE}" STREQUAL "")
message(NOTICE "CODESIGN_CERTIFICATE is set to an empty string.")
return()
endif()
# Store certificate value in a temporary file for signtool to use.
_set_temporary_certificate_file()
file(WRITE ${_certificate_file} $ENV{CODESIGN_CERTIFICATE})
set(_ARG_CERTIFICATE_FILE ${_certificate_file})
elseif(DEFINED ENV{CODESIGN_CERTIFICATE_BASE64})
if("$ENV{CODESIGN_CERTIFICATE_BASE64}" STREQUAL "")
message(NOTICE "CODESIGN_CERTIFICATE_BASE64 is set to an empty string.")
return()
endif()
# Read base64-encoded certificate from environment variable,
# decode with `certutil.exe`, and store in a temporary file
# for signtool to use.
#
# This is useful for GitHub Actions, which cannot handle unencoded
# multiline secrets.
_set_temporary_certificate_file()
# Save base64-encoded certificate to file.
set(_certificate_base64_file "${_certificate_file}.base64")
file(WRITE ${_certificate_base64_file} $ENV{CODESIGN_CERTIFICATE_BASE64})
# Decode certificate.
set(_cmd_certutil_args "-decode" ${_certificate_base64_file} ${_certificate_file})
execute_process(COMMAND "certutil.exe" ${_cmd_certutil_args}
RESULT_VARIABLE _rc
OUTPUT_VARIABLE _stdout
# For some reason certutil prints errors to stdout.
# ERROR_VARIABLE _stderr
)
# Remove temporary file first.
file(REMOVE ${_certificate_base64_file})
if(NOT _rc EQUAL 0)
message(WARNING "Failed to decode certificate: ${_stdout}")
_cleanup()
return()
endif()
unset(_rc)
unset(_stdout)
set(_ARG_CERTIFICATE_FILE ${_certificate_file})
else()
message(NOTICE "Certificate is not provided, no binaries will be signed.")
return()
endif()
endif()
list(APPEND _cmd_args "/f" ${_ARG_CERTIFICATE_FILE})
# Set password.
if(NOT _ARG_PASSWORD)
if(CODESIGN_PASSWORD)
set(_ARG_PASSWORD ${CODESIGN_PASSWORD})
elseif(DEFINED ENV{CODESIGN_PASSWORD})
if("$ENV{CODESIGN_PASSWORD}" STREQUAL "")
message(NOTICE "CODESIGN_PASSWORD is set to an empty string. Unset if not used.")
_cleanup()
return()
endif()
set(_ARG_PASSWORD $ENV{CODESIGN_PASSWORD})
endif()
endif()
if(_ARG_PASSWORD)
list(APPEND _cmd_args "/p" ${_ARG_PASSWORD})
endif()
endif()
# Set description (shown as "Program name" in UAC prompts).
if(NOT _ARG_DESCRIPTION AND CPACK_PACKAGE_NAME)
set(_ARG_DESCRIPTION ${CPACK_PACKAGE_NAME})
endif()
if(_ARG_DESCRIPTION)
list(APPEND _cmd_args "/d" ${_ARG_DESCRIPTION})
endif()
# Set project URL.
if(NOT _ARG_URL AND CPACK_PACKAGE_HOMEPAGE_URL)
set(_ARG_URL ${CPACK_PACKAGE_HOMEPAGE_URL})
endif()
if(_ARG_URL)
list(APPEND _cmd_args "/du" ${_ARG_URL})
endif()
# Set timestamp server.
if(NOT _ARG_TIMESTAMP_URL)
set(_ARG_TIMESTAMP_URL "http://timestamp.digicert.com")
endif()
if(_ARG_TIMESTAMP_URL)
list(APPEND _cmd_args "/tr" ${_ARG_TIMESTAMP_URL} "/td" "sha256")
endif()
# Set quiet, verbose, or debug options.
if(_ARG_QUIET)
list(APPEND _cmd_args "/q")
endif()
if(_ARG_VERBOSE)
list(APPEND _cmd_args "/v")
endif()
if(_ARG_DEBUG)
list(APPEND _cmd_args "/debug")
endif()
foreach(_file ${_ARG_FILES})
if(NOT EXISTS ${_file})
message(NOTICE "Cannot find file to sign: ${_file}")
continue()
endif()
message(STATUS "Signing ${_file}...")
execute_process(
COMMAND "${_cmd}" ${_cmd_args} "${_file}"
RESULT_VARIABLE _rc
OUTPUT_VARIABLE _stdout
ERROR_VARIABLE _stderr
)
if(_rc EQUAL 0)
message(STATUS "Successfully signed: ${_file}")
else()
message(NOTICE "Failed to sign: ${_stderr}")
if(NOT _ARG_QUIET)
message(VERBOSE ${_stdout})
endif()
endif()
endforeach()
_cleanup()
endfunction()