Browse Source

ci: Extend packaging pipeline to sign Windows binaries automatically

Split packaging on Windows into dedicated jobs that run with access to
an EV signing certificate.

Prior to commit 0929221ca3 (gitlab-ci: Simplify Windows packaging
pipeline, 2023-02-28, v3.26.0-rc5~3^2~3) we had separate packaging jobs,
but they did not run in release packaging pipelines.  Restore them, and
run them in both nightly and release packaging pipelines.
Brad King 7 months ago
parent
commit
1f46bc299b

+ 44 - 6
.gitlab-ci.yml

@@ -16,6 +16,7 @@ stages:
     - build
     - test
     - test-ext
+    - package
     - upload
 
 variables:
@@ -40,6 +41,7 @@ variables:
 
 # Job prefixes:
 #   - `b:` build
+#   - `k:` package
 #   - `l:` lint
 #   - `p:` prep
 #   - `t:` test
@@ -1436,11 +1438,23 @@ b:windows-x86_64-package:
     extends:
         - .windows_x86_64_package
         - .cmake_build_windows
-        - .cmake_release_artifacts
+        - .cmake_build_package_artifacts
         - .windows_x86_64_tags_nonconcurrent_vs2022
         - .run_only_for_package
     needs:
         - p:doc-package
+    variables:
+        CMAKE_CI_ARTIFACTS_NAME: "artifacts-windows-x86_64-build"
+
+k:windows-x86_64-package:
+    extends:
+        - .windows_x86_64_package
+        - .cmake_package_windows
+        - .cmake_release_artifacts
+        - .windows_x86_64_tags_nonconcurrent_sign
+        - .run_only_for_package
+    needs:
+        - b:windows-x86_64-package
     variables:
         CMAKE_CI_ARTIFACTS_NAME: "artifacts-windows-x86_64"
 
@@ -1449,17 +1463,29 @@ u:windows-x86_64-package:
         - .rsync_upload_package
         - .run_only_for_package
     needs:
-        - b:windows-x86_64-package
+        - k:windows-x86_64-package
 
 b:windows-i386-package:
     extends:
         - .windows_i386_package
         - .cmake_build_windows
-        - .cmake_release_artifacts
+        - .cmake_build_package_artifacts
         - .windows_x86_64_tags_nonconcurrent_vs2022
         - .run_only_for_package
     needs:
         - p:doc-package
+    variables:
+        CMAKE_CI_ARTIFACTS_NAME: "artifacts-windows-i386-build"
+
+k:windows-i386-package:
+    extends:
+        - .windows_i386_package
+        - .cmake_package_windows
+        - .cmake_release_artifacts
+        - .windows_x86_64_tags_nonconcurrent_sign
+        - .run_only_for_package
+    needs:
+        - b:windows-i386-package
     variables:
         CMAKE_CI_ARTIFACTS_NAME: "artifacts-windows-i386"
 
@@ -1468,17 +1494,29 @@ u:windows-i386-package:
         - .rsync_upload_package
         - .run_only_for_package
     needs:
-        - b:windows-i386-package
+        - k:windows-i386-package
 
 b:windows-arm64-package:
     extends:
         - .windows_arm64_package
         - .cmake_build_windows
-        - .cmake_release_artifacts
+        - .cmake_build_package_artifacts
         - .windows_x86_64_tags_nonconcurrent_vs2022_arm64
         - .run_only_for_package
     needs:
         - p:doc-package
+    variables:
+        CMAKE_CI_ARTIFACTS_NAME: "artifacts-windows-arm64-build"
+
+k:windows-arm64-package:
+    extends:
+        - .windows_arm64_package
+        - .cmake_package_windows
+        - .cmake_release_artifacts
+        - .windows_x86_64_tags_nonconcurrent_sign
+        - .run_only_for_package
+    needs:
+        - b:windows-arm64-package
     variables:
         CMAKE_CI_ARTIFACTS_NAME: "artifacts-windows-arm64"
 
@@ -1487,4 +1525,4 @@ u:windows-arm64-package:
         - .rsync_upload_package
         - .run_only_for_package
     needs:
-        - b:windows-arm64-package
+        - k:windows-arm64-package

+ 30 - 0
.gitlab/artifacts.yml

@@ -67,6 +67,36 @@
             annotations:
                 - ${CMAKE_CI_BUILD_DIR}/annotations.json
 
+.cmake_build_package_artifacts:
+    artifacts:
+        expire_in: 1d
+        name: "$CMAKE_CI_ARTIFACTS_NAME"
+        paths:
+            # Allow CPack to find CMAKE_ROOT and license text.
+            - build/CMakeFiles/CMakeSourceDir.txt
+            - build/CMakeFiles/LICENSE.txt
+
+            # Install rules.
+            - build/**/cmake_install.cmake
+
+            # We need the main binaries.
+            - build/bin/
+
+            # Pass through the documentation.
+            - build/install-doc/
+
+            # CPack configuration.
+            - build/CPackConfig.cmake
+            - build/CMakeCPackOptions.cmake
+            - build/Source/QtDialog/QtDialogCPack.cmake
+
+            # CPack/IFW packaging files.
+            - build/CMake*.qs
+
+            # CPack/WIX packaging files.
+            - build/Utilities/Release/WiX/custom_action_dll*.wxs
+            - build/Utilities/Release/WiX/CustomAction/CMakeWiXCustomActions.*
+
 .cmake_release_artifacts:
     artifacts:
         expire_in: 5d

+ 0 - 3
.gitlab/ci/CMakeCPack.cmake

@@ -1,3 +0,0 @@
-if(NOT "$ENV{CMAKE_CI_PACKAGE}" MATCHES "^(dev)?$")
-  configure_file(${CMAKE_CURRENT_LIST_DIR}/package_info.cmake.in ${CMake_BINARY_DIR}/ci_package_info.cmake @ONLY)
-endif()

+ 0 - 2
.gitlab/ci/configure_windows_package_common.cmake

@@ -21,6 +21,4 @@ set(Python_FIND_REGISTRY NEVER CACHE STRING "")
 
 set(CMake_BUILD_WIX_CUSTOM_ACTION ON CACHE BOOL "")
 
-set(CMake_CPACK_CUSTOM_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/CMakeCPack.cmake" CACHE FILEPATH "")
-
 include("${CMAKE_CURRENT_LIST_DIR}/configure_common.cmake")

+ 0 - 1
.gitlab/ci/env_windows_arm64_package.ps1

@@ -1 +0,0 @@
-. .gitlab/ci/wix4-env.ps1

+ 0 - 1
.gitlab/ci/env_windows_i386_package.ps1

@@ -1 +0,0 @@
-. .gitlab/ci/wix4-env.ps1

+ 0 - 1
.gitlab/ci/env_windows_x86_64_package.ps1

@@ -1 +0,0 @@
-. .gitlab/ci/wix4-env.ps1

+ 0 - 1
.gitlab/ci/package_info.cmake.in

@@ -1 +0,0 @@
-set(CPACK_PACKAGE_FILE_NAME "@CPACK_PACKAGE_FILE_NAME@")

+ 3 - 7
.gitlab/ci/package_windows.ps1

@@ -1,7 +1,3 @@
-if (Test-Path -Path "build/ci_package_info.cmake" -PathType Leaf) {
-    cmake -P .gitlab/ci/package_windows_build.cmake
-} else {
-    cd build
-    cpack -G ZIP
-    cpack -G WIX
-}
+cd build
+. ../Utilities/Release/win/sign-package.ps1 -cpack cpack
+if (-not $?) { Exit $LastExitCode }

+ 0 - 42
.gitlab/ci/package_windows_build.cmake

@@ -1,42 +0,0 @@
-cmake_minimum_required(VERSION 3.29)
-include(build/ci_package_info.cmake)
-
-set(build "${CMAKE_CURRENT_BINARY_DIR}/build")
-
-file(GLOB paths RELATIVE "${CMAKE_CURRENT_BINARY_DIR}"
-  # Allow CPack to find CMAKE_ROOT and license text.
-  "${build}/CMakeFiles/CMakeSourceDir.txt"
-  "${build}/CMakeFiles/LICENSE.txt"
-
-  # We need the main binaries.
-  "${build}/bin"
-
-  # Pass through the documentation.
-  "${build}/install-doc"
-
-  # CPack configuration.
-  "${build}/CPackConfig.cmake"
-  "${build}/CMakeCPackOptions.cmake"
-  "${build}/Source/QtDialog/QtDialogCPack.cmake"
-
-  # CPack/IFW packaging files.
-  "${build}/CMake*.qs"
-
-  # CPack/WIX packaging files.
-  "${build}/Utilities/Release/WiX/custom_action_dll*.wxs"
-  "${build}/Utilities/Release/WiX/CustomAction/CMakeWiXCustomActions.*"
-  )
-
-file(GLOB_RECURSE paths_recurse RELATIVE "${CMAKE_CURRENT_BINARY_DIR}"
-  # Install rules.
-  "${build}/cmake_install.cmake"
-  "${build}/*/cmake_install.cmake"
-  )
-
-# Create a "package" containing the build-tree files needed to build a package.
-file(MAKE_DIRECTORY build/unsigned)
-file(ARCHIVE_CREATE
-  OUTPUT build/unsigned/${CPACK_PACKAGE_FILE_NAME}.build.zip
-  PATHS ${paths} ${paths_recurse}
-  FORMAT zip
-  )

+ 0 - 1
.gitlab/ci/post_build_windows_arm64_package.ps1

@@ -1 +0,0 @@
-. .gitlab/ci/package_windows.ps1

+ 0 - 1
.gitlab/ci/post_build_windows_i386_package.ps1

@@ -1 +0,0 @@
-. .gitlab/ci/package_windows.ps1

+ 0 - 1
.gitlab/ci/post_build_windows_x86_64_package.ps1

@@ -1 +0,0 @@
-. .gitlab/ci/package_windows.ps1

+ 22 - 0
.gitlab/ci/signtool-env.ps1

@@ -0,0 +1,22 @@
+if ("$env:PROCESSOR_ARCHITECTURE" -eq "AMD64") {
+    $arch = "x64"
+} elseif ("$env:PROCESSOR_ARCHITECTURE" -eq "ARM64") {
+    $arch = "arm64"
+} else {
+    throw ('unknown PROCESSOR_ARCHITECTURE: ' + "$env:PROCESSOR_ARCHITECTURE")
+}
+
+$regKey = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows\v10.0'
+$signtoolPath = $null
+if ($sdkDir = Get-ItemPropertyValue -Path $regKey -Name "InstallationFolder") {
+  if ($sdkBin = Get-ChildItem -Path "$sdkDir/bin" -Recurse -Name "$arch" |
+                  Where-Object { Test-Path -Path "$sdkDir/bin/$_/signtool.exe" -PathType Leaf } |
+                  Select-Object -Last 1) {
+    $signtoolPath = "$sdkDir/bin/$sdkBin"
+  }
+}
+if ($signtoolPath) {
+  Set-Item -Force -Path "env:PATH" -Value "$env:PATH;$signtoolPath"
+} else {
+  throw ('No signtool.exe found in Windows SDK')
+}

+ 20 - 0
.gitlab/os-windows.yml

@@ -310,6 +310,14 @@
 
 ## Tags
 
+.windows_x86_64_tags_nonconcurrent_sign:
+    tags:
+        - cmake # Since this is a bare runner, pin to a project.
+        - windows-x86_64
+        - shell
+        - sign-windows-v1
+        - nonconcurrent
+
 .windows_x86_64_tags_nonconcurrent_vs2022:
     tags:
         - cmake # Since this is a bare runner, pin to a project.
@@ -413,6 +421,18 @@
 
     interruptible: true
 
+.cmake_package_windows:
+    stage: package
+    environment:
+        name: sign-windows
+    script:
+        - . .gitlab/ci/env.ps1
+        - . .gitlab/ci/signtool-env.ps1
+        - . .gitlab/ci/cmake-env.ps1
+        - . .gitlab/ci/wix4-env.ps1
+        - . .gitlab/ci/package_windows.ps1
+    interruptible: true
+
 .cmake_test_windows:
     stage: test
 

+ 3 - 1
.gitlab/rules.yml

@@ -70,7 +70,9 @@
 
 .run_only_for_package:
     rules:
-        - if: '$CMAKE_CI_PACKAGE == "dev"'
+        - if: '$CMAKE_CI_PACKAGE == "dev" && $CI_JOB_STAGE != "upload"'
+          when: on_success
+        - if: '$CMAKE_CI_PACKAGE == "dev" && $CI_JOB_STAGE == "upload"'
           variables:
               RSYNC_DESTINATION: "[email protected]:dev/"
           when: on_success

+ 0 - 4
CMakeCPack.cmake

@@ -259,9 +259,5 @@ set(CPACK_SOURCE_IGNORE_FILES
   "~$"
   )
 
-if(CMake_CPACK_CUSTOM_SCRIPT)
-  include(${CMake_CPACK_CUSTOM_SCRIPT})
-endif()
-
 # include CPack model once all variables are set
 include(CPack)

+ 74 - 9
Utilities/Release/win/sign-package.ps1

@@ -6,24 +6,89 @@
 param (
   [string]$signtool = 'signtool',
   [string]$cpack = 'bin\cpack',
-  [switch]$trace
+  [string]$pass = $null
 )
 
-if ($trace -eq $true) {
-  Set-PSDebug -Trace 1
+$ErrorActionPreference = 'Stop'
+
+# Cleanup temporary file(s) on exit.
+$null = Register-EngineEvent PowerShell.Exiting -Action {
+  if ($certFile) {
+    Remove-Item $certFile -Force
+  }
 }
 
-$ErrorActionPreference = 'Stop'
+# If the passphrase was not provided on the command-line,
+# check for a GitLab CI variable in the environment.
+if (-not $pass) {
+  $pass = $env:SIGNTOOL_PASS
+
+  # If the environment variable looks like a GitLab CI file-type variable,
+  # replace it with the content of the file.
+  if ($pass -and
+      $pass.EndsWith("SIGNTOOL_PASS") -and
+      (Test-Path -Path "$pass" -IsValid) -and
+      (Test-Path -Path "$pass" -PathType Leaf)) {
+    $pass = Get-Content -Path "$pass"
+  }
+}
+
+# Collect signtool arguments to specify a certificate.
+$cert = @()
+
+# Select a signing certificate to pass to signtool.
+if ($certX509 = Get-ChildItem -Recurse -Path "Cert:" -CodeSigningCert |
+                  Where-Object { $_.PublicKey.Oid.FriendlyName -eq "RSA" } |
+                  Select-Object -First 1) {
+  # Identify the private key provider name and container name.
+  if ($certRSA = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certX509)) {
+    # $certRSA -is [System.Security.Cryptography.RSACng]
+    # Cryptography Next Generation (CNG) implementation
+    $csp = $certRSA.Key.Provider
+    $kc = $certRSA.Key.KeyName
+  } elseif ($certRSA = $certX509.PrivateKey) {
+    # $certRSA -is [System.Security.Cryptography.RSACryptoServiceProvider]
+    $csp = $certRSA.CspKeyContainerInfo.ProviderName
+    $kc = $certRSA.CspKeyContainerInfo.KeyContainerName
+  }
+
+  # Pass the selected certificate to signtool.
+  $certFile = New-TemporaryFile
+  $certBase64 = [System.Convert]::ToBase64String($certX509.RawData, [System.Base64FormattingOptions]::InsertLineBreaks)
+  $certPEM = "-----BEGIN CERTIFICATE-----", $certBase64, "-----END CERTIFICATE-----" -join "`n"
+  $certPEM | Out-File -FilePath "$certFile" -Encoding Ascii
+  $cert += "-f","$certFile"
+
+  # Tell signtool how to find the certificate's private key.
+  if ($csp) {
+    $cert += "-csp","$csp"
+  }
+  if ($kc) {
+    if ($pass) {
+      # The provider offers a syntax to encode the token passphrase in the key container name.
+      # https://web.archive.org/web/20250315200813/https://stackoverflow.com/questions/17927895/automate-extended-validation-ev-code-signing-with-safenet-etoken
+      $cert += "-kc","[{{$pass}}]=$kc"
+      $pass = $null
+    } else {
+      $cert += "-kc","$kc"
+    }
+  }
+} else {
+  $cert += @("-a")
+}
 
 # Sign binaries with SHA-1 for Windows 7 and below.
-& $signtool sign -v -a -t http://timestamp.digicert.com -fd sha1 bin\*.exe
+& $signtool sign -v $cert -t  http://timestamp.digicert.com -fd sha1 bin\*.exe
+if (-not $?) { Exit $LastExitCode }
 
 # Sign binaries with SHA-256 for Windows 8 and above.
-& $signtool sign -v -a -tr http://timestamp.digicert.com -fd sha256 -td sha256 -as bin\*.exe
+& $signtool sign -v $cert -tr http://timestamp.digicert.com -fd sha256 -td sha256 -as bin\*.exe
+if (-not $?) { Exit $LastExitCode }
 
 # Create packages.
-& $cpack -G ZIP
-& $cpack -G WIX
+& $cpack -G "ZIP;WIX"
+if (-not $?) { Exit $LastExitCode }
 
 # Sign installer with SHA-256.
-& $signtool sign -v -a -tr http://timestamp.digicert.com -fd sha256 -td sha256 -d "CMake Windows Installer" cmake-*-win*.msi
+& $signtool sign -v $cert -tr http://timestamp.digicert.com -fd sha256 -td sha256 -d "CMake Windows Installer" cmake-*-win*.msi
+if (-not $?) { Exit $LastExitCode }