【问题标题】:How to sign Windows binaries and NSIS installers when building with cmake + cpack使用 cmake + cpack 构建时如何签署 Windows 二进制文件和 NSIS 安装程序
【发布时间】:2022-06-26 20:54:31
【问题描述】:

我正在构建一个 NSIS 安装程序,其中包括一个共享库和一组使用该库的工具。我需要对所有内容进行签名,以便用户可以安装它而不会收到来自 Windows 的可怕警告。

在我对这个问题及其变体的所有搜索中,我只能找到一些答案,甚至那些都不完整。例如。 “您必须使用自定义后期构建命令”,但没有详细信息。此外,“因为 NSIS 在编译签名期间构建了卸载可执行文件,安装程序很复杂”,它指向一个 NSIS url,解释了直接使用 NSIS 时的过程。在该页面的顶部,它说版本 3.08 有一个新的uninstfinalize 命令,它废弃了这里描述的过程。但没有说明如何使用。

【问题讨论】:

    标签: cmake nsis code-signing cpack


    【解决方案1】:

    MI 发布了这个问题,以便我可以与其他提出此问题的人分享我从签名工作中学到了什么。我不维护博客,所以 SO 似乎是与相关受众分享并为我从网络上学到的许多东西带来一点回报的好方法。来了……

    在 Windows 上签名

    如果您不熟悉在 Windows 上进行签名,则需要了解相关命令。实际签名是使用 Windows SDK 中的signtool 完成的。这可以在 Windows 的证书存储中找到证书,或者您可以通过命令行在 PFX (.p12) 文件中提供它。

    Windows 有三个用于管理证书存储的命令:certmgrcertlmcertutil。前两个是交互式的,第三个是命令行实用程序。 certmgr 用于管理当前用户存储。 certlm 用于管理本地机器存储。 certutil 默认在本地计算机存储上运行,但在指定-user 选项时在当前用户存储上运行。

    注意:如何获取证书不在本回答的范围内。

    使用 CMake 签名

    要对 CMake 构建的可执行文件和 dll 进行签名,您需要为每个要签名的目标添加自定义构建后命令。这是我用来向目标添加一个的宏:

    macro (set_code_sign target)
      if (WIN32 AND WIN_CODE_SIGN_IDENTITY)
        find_package(signtool REQUIRED)
    
        if (signtool_EXECUTABLE)
          configure_sign_params()
          add_custom_command( TARGET ${target}
           POST_BUILD
           COMMAND ${signtool_EXECUTABLE} sign ${SIGN_PARAMS} $<TARGET_FILE:${target}>
           VERBATIM
          )
        endif()
      endif()
    endmacro (set_code_sign)
    

    这是上述宏的典型用法:

    add_executable( mycheck
        ...
    )
    set_code_sign(mycheck)
    
    
    

    为了定位signtool,我创建了Findsigntool.cmake:

    #[============================================================================
    # Copyright 2022, Khronos Group, Inc.
    # SPDX-License-Identifier: Apache-2.0
    #============================================================================]
    
    #  Functions to convert unix-style paths into paths useable by cmake on windows.
    #[=======================================================================[.rst:
    Findsigntool
    -------
    
    Finds the signtool executable used for codesigning on Windows.
    
    Note that signtool does not offer a way to make it print its version
    so version selection and reporting is not possible.
    
    Result Variables
    ^^^^^^^^^^^^^^^^
    
    This will define the following variables:
    
    ``signtool_FOUND``
      True if the system has the signtool executable.
    ``signtool_EXECUTABLE``
      The signtool command executable.
    
    #]=======================================================================]
    
    if (WIN32 AND CMAKE_HOST_SYSTEM_NAME MATCHES "CYGWIN.*")
      find_program(CYGPATH
          NAMES cygpath
          HINTS [HKEY_LOCAL_MACHINE\\Software\\Cygwin\\setup;rootdir]/bin
          PATHS C:/cygwin64/bin
                C:/cygwin/bin
      )
    endif ()
    
    function(convert_cygwin_path _pathvar)
      if (WIN32 AND CYGPATH)
        execute_process(
            COMMAND         "${CYGPATH}" -m "${${_pathvar}}"
            OUTPUT_VARIABLE ${_pathvar}
            OUTPUT_STRIP_TRAILING_WHITESPACE
        )
        set(${_pathvar} "${${_pathvar}}" PARENT_SCOPE)
      endif ()
    endfunction()
    
    function(convert_windows_path _pathvar)
      if (CYGPATH)
        execute_process(
            COMMAND         "${CYGPATH}" "${${_pathvar}}"
            OUTPUT_VARIABLE ${_pathvar}
            OUTPUT_STRIP_TRAILING_WHITESPACE
        )
        set(${_pathvar} "${${_pathvar}}" PARENT_SCOPE)
      endif ()
    endfunction()
    
    # Make a list of Windows Kit versions with newer versions first.
    #
    # _winver   string          Windows version whose signtool to find.
    # _versions variable name   Variable in which to return the list of versions.
    #
    function(find_kits _winver _kit_versions)
      set(${_kit_versions})
      set(_kit_root "KitsRoot${_winver}")
      set(regkey "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots")
      set(regval ${_kit_root})
      if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
        # Note: must be a cache operation in order to read from the registry.
        get_filename_component(_kits_path "[${regkey};${regval}]"
            ABSOLUTE CACHE
        )
      elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "CYGWIN.*")
        # On Cygwin, CMake's built-in registry query won't work.
        # Use Cygwin utility "regtool" instead.
        execute_process(COMMAND regtool get "\\${regkey}\\${regval}"
          OUTPUT_VARIABLE _kits_path}
          ERROR_QUIET
          OUTPUT_STRIP_TRAILING_WHITESPACE
        )
        if (_kits_path)
          convert_windows_path(_kits_path)
        endif ()
      endif()
      if (_kits_path)
          file(GLOB ${_kit_versions} "${_kits_path}/bin/${_winver}.*")
          # Reverse list, so newer versions (higher-numbered) appear first.
          list(REVERSE ${_kit_versions})
      endif ()
      unset(_kits_path CACHE)
      set(${_kit_versions} ${${_kit_versions}} PARENT_SCOPE)
    endfunction()
    
    if (WIN32 AND NOT signtool_EXECUTABLE)
      if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "AMD64")
        set(arch "x64")
      else()
        set(arch ${CMAKE_HOST_SYSTEM_PROCESSOR})
      endif()
    
      # Look for latest signtool
      foreach(winver 11 10)
        find_kits(${winver} kit_versions)
        if (kit_versions)
          find_program(signtool_EXECUTABLE
              NAMES           signtool
              PATHS           ${kit_versions}
              PATH_SUFFIXES   ${arch}
                              bin/${arch}
                              bin
              NO_DEFAULT_PATH
          )
          if (signtool_EXECUTABLE)
            break()
          endif()
        endif()
      endforeach()
    
      if (signtool_EXECUTABLE)
        mark_as_advanced (signtool_EXECUTABLE)
      endif ()
    
      # handle the QUIETLY and REQUIRED arguments and set *_FOUND to TRUE
      # if all listed variables are found or TRUE
      include (FindPackageHandleStandardArgs)
    
      find_package_handle_standard_args (
        signtool
        REQUIRED_VARS
          signtool_EXECUTABLE
        FAIL_MESSAGE
          "Could NOT find signtool. Will be unable to sign Windows binaries."
      )
    endif()
    

    把它放在你项目的 cmake 模块路径中。

    这里是配置签名参数的功能。我使用了一个变量,因为我们需要在命令中重复相同的参数来签署安装程序:

    function(configure_sign_params)
      if (NOT SIGN_PARAMS)
        # Default to looking for cert. in user's store but let user tell us
        # to look in Local Computer store. User store is preferred because importing
        # the cert. does not need admin elevation.
        if (WIN_CS_CERT_SEARCH_MACHINE_STORE)
          set(store "/sm")
        endif()
        set(SIGN_PARAMS ${store} /fd sha256 /n "${WIN_CODE_SIGN_IDENTITY}"
            /tr http://ts.ssl.com /td sha256
            /d "My Software" /du https://github.com/Me/My-Software
            PARENT_SCOPE)
      endif()
    endfunction()
    

    如果您将证书导入到本地计算机存储区,那么您需要 /sm 参数,如果在 cmake 配置期间打开了 `WIN_CS_CERT_SEARCH_MACHINE_STORE 选项,则此代码将设置该参数。

    [由于在 CI 环境中通过 certutil 将我们的证书导入当前用户存储的问题,我添加了使用本地计算机存储的选项。]

    如果您的证书在 PFX 文件中,请将 /n "code sign identity" 替换为 -f your_cert.p12 -p &lt;your private key password&gt;

    这是项目顶层 CMakeLists.txt 的摘录,其中设置了签名相关选项:

    if (WIN32)
        set( WIN_CODE_SIGN_IDENTITY "" CACHE STRING "Subject Name of Windows code signing certificate. Displayed in 'Issued To' column of cert{lm,mgr}.")
        CMAKE_DEPENDENT_OPTION( WIN_CS_CERT_SEARCH_MACHINE_STORE
         "When set, machine store will be searched for signing certificate instead of user store."
         OFF
         WIN_CODE_SIGN_IDENTITY
         OFF
        )
    endif()
    

    通过 CPack 签署 NSIS 安装程序

    NSIS 和可能的其他安装程序会在将卸载可执行文件包含在安装程序中之前即时构建它。这也必须签字。这样做以前很困难,但 NSIS 3.08 中添加了一个新的uninstfinalize 命令,使其变得简单。现有的instfinalize 命令用于对安装程序进行签名。标准 CMake 不支持这些命令,因此您必须按照NSISAdvancedTips 中的说明制作自定义 NSIS 脚本。

    将文件 NSIS.template.in 从 CMake 安装的模块路径复制到项目的模块路径中。添加以下行

    ;--------------------------------
    ;Signing
    
      !finalize '@CPACK_NSIS_FINALIZE_CMD@'
      !uninstfinalize '@CPACK_NSIS_FINALIZE_CMD@'
    

    我不认为文件中的位置特别重要。我将它们放在 Include Modern UIGeneral 部分之间。

    cpack 生成安装程序脚本时,它会将@CPACK_NSIS_FINALIZE_CMD@ 替换为相应的CMake 变量的值(如果有)。这是一个定义变量的函数:

    function(set_nsis_installer_codesign_cmd)
      if (WIN32 AND WIN_CODE_SIGN_IDENTITY)
        # To make calls to the set_code_sign macro and this order independent ...
        find_package(signtool REQUIRED)
        if (signtool_EXECUTABLE)
          configure_sign_params()
          # CPACK_NSIS_FINALIZE_CMD is a variable whose value is to be substituted
          # into the !finalize and !uninstfinalize commands in
          # cmake/modules/NSIS.template.in. This variable is ours. It is not a
          # standard CPACK variable. The name MUST start with CPACK otherwise
          # it will not be defined when cpack runs its configure_file step.
          foreach(param IN LISTS SIGN_PARAMS)
            # Quote the parameters because at least one of them,
            # WIN_CODE_SIGN_IDENTITY, has spaces. It is easier to quote
            # all of them than determine which have spaces.
            #
            # Insane escaping is needed due to the 2-step process used to
            # configure the final output. First cpack creates CPackConfig.cmake
            # in which the value set here appears, inside quotes, as the
            # argument to a cmake `set` command. That variable's value
            # is then substituted into the output.
            string(APPEND NSIS_SIGN_PARAMS "\\\"${param}\\\" ")
          endforeach()
    
          # Note 1: cpack/NSIS does not show any output when running signtool,
          # whether it succeeds or fails.
          #
          # Note 2: Do not move the %1 to NSIS.template.in. We need an empty
          # command there when we aren't signing. %1 is replaced by the name
          # of the installer or uninstaller during NSIS compilation.
          set(CPACK_NSIS_FINALIZE_CMD "\\\"${signtool_EXECUTABLE}\\\" sign ${NSIS_SIGN_PARAMS} %1"
            PARENT_SCOPE
          )
          unset(NSIS_SIGN_PARAMS)
        endif()
      endif()
    endfunction()
    

    注意上面函数中的cmets。

    最后我们需要调用这个函数。这是我在项目的 CMakeLists.txt 部分所做的,我在其中设置了所有感兴趣的标准 CPACK_* 变量:

        if (WIN_CODE_SIGN_IDENTITY)
            set_nsis_installer_codesign_cmd()
        else()
            # We're not signing the package so provide a checksum file.
            set(CPACK_PACKAGE_CHECKSUM SHA1)
        endif()
    

    你有它。最后也没那么难。

    【讨论】:

      猜你喜欢
      • 2012-10-20
      • 1970-01-01
      • 2021-06-18
      • 2022-08-18
      • 1970-01-01
      • 1970-01-01
      • 2015-02-21
      • 2021-11-01
      • 1970-01-01
      相关资源
      最近更新 更多