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 11 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 }