aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenis Shienkov <denis.shienkov@gmail.com>2021-03-29 18:15:17 +0300
committerDenis Shienkov <denis.shienkov@gmail.com>2021-04-23 14:55:38 +0000
commit2f6eecdc96fcd693cecef8011d8f9500c7872fc7 (patch)
tree8bed88e65e4b3fd8c415c3beba3afd226c7d6481
parent2f34e637828f7e519a25a498bd5aa4e8f955217d (diff)
codesign: Long live `signtool` signing on Windows
Change-Id: I320cd1a1f3d8a1eed11d1c70007214f19a109b6e Reviewed-by: Christian Kandeler <christian.kandeler@qt.io> Reviewed-by: Ivan Komissarov <ABBAPOH@gmail.com>
-rw-r--r--.github/workflows/main.yml6
-rw-r--r--doc/reference/modules/codesign-module.qdoc118
-rw-r--r--share/qbs/modules/codesign/CodeSignModule.qbs6
-rw-r--r--share/qbs/modules/codesign/apple.qbs2
-rw-r--r--share/qbs/modules/codesign/codesign.js119
-rw-r--r--share/qbs/modules/codesign/signtool.qbs94
-rw-r--r--share/qbs/modules/cpp/msvc.js7
-rw-r--r--share/qbs/modules/cpp/windows-msvc-base.qbs24
-rw-r--r--tests/auto/auto.qbs3
-rw-r--r--tests/auto/blackbox/CMakeLists.txt9
-rw-r--r--tests/auto/blackbox/blackbox-windows.pro18
-rw-r--r--tests/auto/blackbox/blackbox-windows.qbs21
-rw-r--r--tests/auto/blackbox/testdata-windows/codesign/app.cpp1
-rw-r--r--tests/auto/blackbox/testdata-windows/codesign/codesign.qbs37
-rw-r--r--tests/auto/blackbox/tst_blackboxwindows.cpp174
-rw-r--r--tests/auto/blackbox/tst_blackboxwindows.h51
16 files changed, 658 insertions, 32 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 29f737a1e..953f19949 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -625,6 +625,12 @@ jobs:
qbs config defaultProfile qt
qbs config --list
shell: bash
+ - name: Setup self-signed certificate
+ run: |
+ New-SelfSignedCertificate -DnsName qbs@community.test -Type CodeSigning -CertStoreLocation cert:\CurrentUser\My
+ Export-Certificate -Cert (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)[0] -FilePath qbs-code-signing.crt
+ Import-Certificate -FilePath .\qbs-code-signing.crt -CertStoreLocation Cert:\CurrentUser\TrustedPublisher
+ shell: powershell
- name: Run Tests
run: ${{ matrix.config.script }} ./release/install-root/bin
shell: bash
diff --git a/doc/reference/modules/codesign-module.qdoc b/doc/reference/modules/codesign-module.qdoc
index 70a37477b..d0aba4688 100644
--- a/doc/reference/modules/codesign-module.qdoc
+++ b/doc/reference/modules/codesign-module.qdoc
@@ -63,15 +63,25 @@
*/
/*!
- \qmlproperty string codesign::codesignFlags
+ \qmlproperty string codesign::signingTimestamp
- Additional flags passed to the \c{codesign} tool.
+ URL of the timestamp authority RFC 3161 server to be contacted to authenticate code signing.
+ \c undefined or \c empty indicates that a system-specific default should be used;
+ \c{"none"} explicitly disables the use of timestamp services on Apple platforms.
\since Qbs 1.19
- \defaultvalue \c{undefined}
+ \defaultvalue \c "none" on Apple, \c undefined otherwise
+*/
+
+/*!
+ \qmlproperty stringList codesign::codesignFlags
- \appleproperty
+ Additional flags passed to the \c{codesign} tool.
+
+ \since Qbs 1.19
+
+ \nodefaultvalue
*/
/*!
@@ -81,9 +91,7 @@
\since Qbs 1.19
- \defaultvalue \c{"codesign"}
-
- \appleproperty
+ \defaultvalue Determined automatically
*/
/*!
@@ -94,8 +102,6 @@
\since Qbs 1.19
\defaultvalue Determined automatically
-
- \appleproperty
*/
/*!
@@ -104,6 +110,7 @@
Whether to actually perform code signing.
\since Qbs 1.19
+
\defaultvalue \c false
*/
@@ -164,21 +171,6 @@
*/
/*!
- \qmlproperty string codesign::signingTimestamp
-
- URL of the timestamp authority server to be contacted to authenticate code signing.
- \c{undefined} indicates that a system-specific default should be used, and the empty
- string indicates the default server provided by Apple. \c{"none"} explicitly disables
- the use of timestamp services and this should not usually need to be changed.
-
- \since Qbs 1.19
-
- \defaultvalue \c{"none"}
-
- \appleproperty
-*/
-
-/*!
\qmlproperty string codesign::signingType
Type of code signing to use. This should generally be used in preference to an explicit
@@ -307,3 +299,81 @@
\androidproperty
*/
+
+/*!
+ \qmlproperty string codesign::subjectName
+
+ Specifies the name of the subject of the signing certificate.
+ This value can be a substring of the entire subject name.
+
+ \since Qbs 1.19
+
+ \defaultvalue \c undefined
+
+ \windowsproperty
+*/
+
+/*!
+ \qmlproperty string codesign::rootSubjectName
+
+ Specifies the name of the subject of the root certificate that
+ the signing certificate must chain to. This value may be a substring
+ of the entire subject name of the root certificate.
+
+ \since Qbs 1.19
+
+ \defaultvalue \c undefined
+
+ \windowsproperty
+*/
+
+/*!
+ \qmlproperty string codesign::hashAlgorithm
+
+ Specifies the default hash algorithm used on the signing certificate.
+ The possible values are \c sha1, \c sha256, \c sha384, and \c sha512.
+
+ \note Only available in Windows 10 kit builds 20236 and later.
+
+ \since Qbs 1.19
+
+ \defaultvalue \c undefined
+
+ \windowsproperty
+*/
+
+/*!
+ \qmlproperty string codesign::certificatePath
+
+ Specifies the full path to the signing certificate file (*.pfx).
+
+ \since Qbs 1.19
+
+ \defaultvalue \c undefined
+
+ \windowsproperty
+*/
+
+/*!
+ \qmlproperty string codesign::certificatePassword
+
+ Specifies the password to use when opening a signing certificate file (*.pfx).
+
+ \since Qbs 1.19
+
+ \defaultvalue \c undefined
+
+ \windowsproperty
+*/
+
+/*!
+ \qmlproperty string codesign::crossCertificatePath
+
+ Specifies the full path to the additional certificate file (*.cer).
+
+ \since Qbs 1.19
+
+ \defaultvalue \c undefined
+
+ \windowsproperty
+*/
diff --git a/share/qbs/modules/codesign/CodeSignModule.qbs b/share/qbs/modules/codesign/CodeSignModule.qbs
index 1951ec374..2115baebf 100644
--- a/share/qbs/modules/codesign/CodeSignModule.qbs
+++ b/share/qbs/modules/codesign/CodeSignModule.qbs
@@ -43,5 +43,11 @@ Module {
property string codesignPath: codesignName
property stringList codesignFlags
+ property string signingTimestamp
+ PropertyOptions {
+ name: "signingTimestamp"
+ description: "URL of the RFC 3161 time stamp server."
+ }
+
property bool _canSignArtifacts: false // whether can sign individual actifacts
}
diff --git a/share/qbs/modules/codesign/apple.qbs b/share/qbs/modules/codesign/apple.qbs
index 31e2c366d..565d29080 100644
--- a/share/qbs/modules/codesign/apple.qbs
+++ b/share/qbs/modules/codesign/apple.qbs
@@ -96,7 +96,7 @@ CodeSignModule {
}
}
- property string signingTimestamp: "none"
+ signingTimestamp: "none"
property string provisioningProfile
PropertyOptions {
diff --git a/share/qbs/modules/codesign/codesign.js b/share/qbs/modules/codesign/codesign.js
index bf7e95224..5aa303c9c 100644
--- a/share/qbs/modules/codesign/codesign.js
+++ b/share/qbs/modules/codesign/codesign.js
@@ -202,6 +202,75 @@ function findBestProvisioningProfile(product, files) {
}
}
+/**
+ * Finds out the search paths for the `signtool.exe` utility supplied with
+ * the Windows SDK's.
+ */
+function findBestSignToolSearchPaths(arch) {
+ var searchPaths = [];
+ var keys = [
+ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows",
+ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Microsoft SDKs\\Windows"
+ ];
+ for (var keyIndex = 0; keyIndex < keys.length; ++keyIndex) {
+ var re = /^v([0-9]+)\.([0-9]+)$/;
+ var groups = Utilities.nativeSettingGroups(keys[keyIndex]).filter(function(version) {
+ return version.match(re);
+ });
+
+ groups.sort(function(a, b) {
+ return Utilities.versionCompare(b.substring(1), a.substring(1));
+ });
+
+ function addSearchPath(searchPath) {
+ if (File.exists(searchPath) && !searchPaths.contains(searchPath)) {
+ searchPaths.push(searchPath);
+ return true;
+ }
+ return false;
+ }
+
+ for (var groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
+ var fullKey = keys[keyIndex] + "\\" + groups[groupIndex];
+ var fullVersion = Utilities.getNativeSetting(fullKey, "ProductVersion");
+ if (fullVersion) {
+ var installRoot = FileInfo.cleanPath(
+ Utilities.getNativeSetting(fullKey, "InstallationFolder"));
+ if (installRoot) {
+ // Try to add the architecture-independent path at first.
+ var searchPath = FileInfo.joinPaths(installRoot, "App Certification Kit");
+ if (!addSearchPath(searchPath)) {
+ // Try to add the architecture-dependent paths at second.
+ var binSearchPath = FileInfo.joinPaths(installRoot, "bin/" + fullVersion);
+ if (!File.exists(binSearchPath)) {
+ binSearchPath += ".0";
+ if (!File.exists(binSearchPath))
+ continue;
+ }
+
+ function kitsArchitectureSubDirectory(arch) {
+ if (arch === "x86")
+ return "x86";
+ else if (arch === "x86_64")
+ return "x64";
+ else if (arch.startsWith("arm64"))
+ return "arm64";
+ else if (arch.startsWith("arm"))
+ return "arm";
+ }
+
+ var archDir = kitsArchitectureSubDirectory(arch);
+ searchPath = FileInfo.joinPaths(binSearchPath, archDir);
+ addSearchPath(searchPath);
+ }
+ }
+ }
+ }
+ }
+
+ return searchPaths;
+}
+
function prepareSign(project, product, inputs, outputs, input, output) {
var cmd, cmds = [];
@@ -243,10 +312,10 @@ function prepareSign(project, product, inputs, outputs, input, output) {
args.push("--force");
args.push("--sign", actualSigningIdentity.SHA1);
- // If signingTimestamp is undefined, do not specify the flag at all -
+ // If signingTimestamp is undefined or empty, do not specify the flag at all -
// this uses the system-specific default behavior
var signingTimestamp = product.codesign.signingTimestamp;
- if (signingTimestamp !== undefined) {
+ if (signingTimestamp) {
// If signingTimestamp is an empty string, specify the flag but do
// not specify a value - this uses a default Apple-provided server
var flag = "--timestamp";
@@ -349,3 +418,49 @@ function createDebugKeyStoreCommandString(keytoolFilePath, keystoreFilePath, key
"CN=Android Debug,O=Android,C=US"];
return Process.shellQuote(keytoolFilePath, args);
}
+
+function prepareSigntool(project, product, inputs, outputs, input, output) {
+ var cmd, cmds = [];
+
+ if (!product.codesign.enableCodeSigning)
+ return cmds;
+
+ var args = ["sign"].concat(product.codesign.codesignFlags || []);
+
+ var subjectName = product.codesign.subjectName;
+ if (subjectName)
+ args.push("/n", subjectName);
+
+ var rootSubjectName = product.codesign.rootSubjectName;
+ if (rootSubjectName)
+ args.push("/r", rootSubjectName);
+
+ var hashAlgorithm = product.codesign.hashAlgorithm;
+ if (hashAlgorithm)
+ args.push("/fd", hashAlgorithm);
+
+ var signingTimestamp = product.codesign.signingTimestamp;
+ if (signingTimestamp)
+ args.push("/tr", signingTimestamp);
+
+ var certificatePath = product.codesign.certificatePath;
+ if (certificatePath)
+ args.push("/f", certificatePath);
+
+ var certificatePassword = product.codesign.certificatePassword;
+ if (certificatePassword)
+ args.push("/p", certificatePassword);
+
+ var crossCertificatePath = product.codesign.crossCertificatePath;
+ if (crossCertificatePath)
+ args.push("/ac", crossCertificatePath);
+
+ var outputArtifact = outputs["codesign.signed_artifact"][0];
+ args.push(outputArtifact.filePath);
+
+ cmd = new Command(product.codesign.codesignPath, args);
+ cmd.description = "signing " + outputArtifact.fileName;
+ cmd.highlight = "linker";
+ cmds.push(cmd);
+ return cmds;
+}
diff --git a/share/qbs/modules/codesign/signtool.qbs b/share/qbs/modules/codesign/signtool.qbs
new file mode 100644
index 000000000..13933c6f6
--- /dev/null
+++ b/share/qbs/modules/codesign/signtool.qbs
@@ -0,0 +1,94 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Denis Shienkov <denis.shienkov@gmail.com>
+** Contact: http://www.qt.io/licensing
+**
+** This file is part of Qbs.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms and
+** conditions see http://www.qt.io/terms-conditions. For further information
+** use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+
+import qbs
+import qbs.File
+import qbs.ModUtils
+import qbs.Probes
+import "codesign.js" as CODESIGN
+
+CodeSignModule {
+ condition: qbs.targetOS.contains("windows") && qbs.hostOS.contains("windows")
+
+ _canSignArtifacts: true
+
+ Probes.BinaryProbe {
+ id: signtoolProbe
+ names: [codesignName]
+ searchPaths: CODESIGN.findBestSignToolSearchPaths(qbs.hostArchitecture)
+ }
+
+ codesignName: "signtool"
+ codesignPath: signtoolProbe.filePath
+
+ property string subjectName
+ PropertyOptions {
+ name: "subjectName"
+ description: "Name of the subject of the signing certificate."
+ }
+
+ property string rootSubjectName
+ PropertyOptions {
+ name: "rootSubjectName"
+ description: "Name of the subject of the root certificate that the signing " +
+ "certificate must chain to."
+ }
+
+ property string hashAlgorithm
+ PropertyOptions {
+ name: "hashAlgorithm"
+ description: "Name of the hash algorithm used on the signing certificate."
+ allowedValues: ["sha1", "sha256", "sha384", "sha512"]
+ }
+
+ property path certificatePath
+ PropertyOptions {
+ name: "certificatePath"
+ description: "Path to the signing certificate PFX file."
+ }
+
+ property path certificatePassword
+ PropertyOptions {
+ name: "certificatePassword"
+ description: "Password to use when opening a certificate PFX file."
+ }
+
+ property path crossCertificatePath
+ PropertyOptions {
+ name: "crossCertificatePath"
+ description: "Path to the additional certificate CER file."
+ }
+
+ validate: {
+ if (enableCodeSigning && !File.exists(codesignPath)) {
+ throw ModUtils.ModuleError("Could not find 'signtool' utility");
+ }
+ }
+}
diff --git a/share/qbs/modules/cpp/msvc.js b/share/qbs/modules/cpp/msvc.js
index 566059610..9f3d20282 100644
--- a/share/qbs/modules/cpp/msvc.js
+++ b/share/qbs/modules/cpp/msvc.js
@@ -28,6 +28,7 @@
**
****************************************************************************/
+var Codesign = require("../codesign/codesign.js");
var Cpp = require("cpp.js");
var File = require("qbs.File");
var FileInfo = require("qbs.FileInfo");
@@ -655,6 +656,12 @@ function prepareLinker(project, product, inputs, outputs, input, output) {
commands.push(cmd);
}
+ if (product.cpp.shouldSignArtifacts) {
+ Array.prototype.push.apply(
+ commands, Codesign.prepareSigntool(
+ project, product, inputs, outputs, input, output));
+ }
+
return commands;
}
diff --git a/share/qbs/modules/cpp/windows-msvc-base.qbs b/share/qbs/modules/cpp/windows-msvc-base.qbs
index 81fe48385..c45ec5ec3 100644
--- a/share/qbs/modules/cpp/windows-msvc-base.qbs
+++ b/share/qbs/modules/cpp/windows-msvc-base.qbs
@@ -40,6 +40,8 @@ import 'msvc.js' as MSVC
CppModule {
condition: false
+ Depends { name: "codesign" }
+
windowsApiCharacterSet: "unicode"
platformDefines: {
var defines = base.concat(WindowsUtils.characterSetDefines(windowsApiCharacterSet))
@@ -100,6 +102,8 @@ CppModule {
property var buildEnv
+ readonly property bool shouldSignArtifacts: codesign.enableCodeSigning
+
setupBuildEnvironment: {
for (var key in product.cpp.buildEnv) {
var v = new ModUtils.EnvironmentVariable(key, ';');
@@ -192,10 +196,16 @@ CppModule {
inputs: ['obj', 'native.pe.manifest', 'def']
inputsFromDependencies: ['staticlibrary', 'dynamiclibrary_import', "debuginfo_app"]
- outputFileTags: ["application", "debuginfo_app", "mem_map"]
+ outputFileTags: {
+ var tags = ["application", "debuginfo_app", "mem_map"];
+ if (shouldSignArtifacts)
+ tags.push("codesign.signed_artifact");
+ return tags;
+ }
outputArtifacts: {
var app = {
- fileTags: ["application"],
+ fileTags: ["application"].concat(
+ product.cpp.shouldSignArtifacts ? ["codesign.signed_artifact"] : []),
filePath: FileInfo.joinPaths(
product.destinationDirectory,
PathTools.applicationFilePath(product))
@@ -230,11 +240,17 @@ CppModule {
inputs: ['obj', 'native.pe.manifest', 'def']
inputsFromDependencies: ['staticlibrary', 'dynamiclibrary_import', "debuginfo_dll"]
- outputFileTags: ["dynamiclibrary", "dynamiclibrary_import", "debuginfo_dll"]
+ outputFileTags: {
+ var tags = ["dynamiclibrary", "dynamiclibrary_import", "debuginfo_dll"];
+ if (shouldSignArtifacts)
+ tags.push("codesign.signed_artifact");
+ return tags;
+ }
outputArtifacts: {
var artifacts = [
{
- fileTags: ["dynamiclibrary"],
+ fileTags: ["dynamiclibrary"].concat(
+ product.cpp.shouldSignArtifacts ? ["codesign.signed_artifact"] : []),
filePath: product.destinationDirectory + "/" + PathTools.dynamicLibraryFilePath(product)
},
{
diff --git a/tests/auto/auto.qbs b/tests/auto/auto.qbs
index 8dd301f68..0d87af9fe 100644
--- a/tests/auto/auto.qbs
+++ b/tests/auto/auto.qbs
@@ -4,7 +4,6 @@ Project {
name: "Autotests"
references: [
"api/api.qbs",
- "blackbox/blackbox.qbs",
"blackbox/blackbox-android.qbs",
"blackbox/blackbox-apple.qbs",
"blackbox/blackbox-baremetal.qbs",
@@ -13,6 +12,8 @@ Project {
"blackbox/blackbox-java.qbs",
"blackbox/blackbox-joblimits.qbs",
"blackbox/blackbox-qt.qbs",
+ "blackbox/blackbox-windows.qbs",
+ "blackbox/blackbox.qbs",
"buildgraph/buildgraph.qbs",
"cmdlineparser/cmdlineparser.qbs",
"language/language.qbs",
diff --git a/tests/auto/blackbox/CMakeLists.txt b/tests/auto/blackbox/CMakeLists.txt
index 88eed4b42..0bf79a433 100644
--- a/tests/auto/blackbox/CMakeLists.txt
+++ b/tests/auto/blackbox/CMakeLists.txt
@@ -70,3 +70,12 @@ add_qbs_test(blackbox-qt
tst_blackboxqt.cpp
tst_blackboxqt.h
)
+
+add_qbs_test(blackbox-windows
+ SOURCES
+ ../shared.h
+ tst_blackboxbase.cpp
+ tst_blackboxbase.h
+ tst_blackboxwindows.cpp
+ tst_blackboxwindows.h
+ )
diff --git a/tests/auto/blackbox/blackbox-windows.pro b/tests/auto/blackbox/blackbox-windows.pro
new file mode 100644
index 000000000..a9e8fdbd2
--- /dev/null
+++ b/tests/auto/blackbox/blackbox-windows.pro
@@ -0,0 +1,18 @@
+TARGET = tst_blackbox-windows
+
+HEADERS = tst_blackboxwindows.h tst_blackboxbase.h
+SOURCES = tst_blackboxwindows.cpp tst_blackboxbase.cpp
+OBJECTS_DIR = windows
+MOC_DIR = $${OBJECTS_DIR}-moc
+
+include(../auto.pri)
+
+DATA_DIRS = testdata-windows ../find
+
+for(data_dir, DATA_DIRS) {
+ files = $$files($$PWD/$$data_dir/*, true)
+ win32:files ~= s|\\\\|/|g
+ for(file, files):!exists($$file/*):FILES += $$file
+}
+
+OTHER_FILES += $$FILES
diff --git a/tests/auto/blackbox/blackbox-windows.qbs b/tests/auto/blackbox/blackbox-windows.qbs
new file mode 100644
index 000000000..e32421e3b
--- /dev/null
+++ b/tests/auto/blackbox/blackbox-windows.qbs
@@ -0,0 +1,21 @@
+import qbs.Utilities
+
+QbsAutotest {
+ testName: "blackbox-windows"
+ Depends { name: "qbs_app" }
+ Depends { name: "qbs-setup-toolchains" }
+ Group {
+ name: "testdata"
+ prefix: "testdata-windows/"
+ files: ["**/*"]
+ fileTags: []
+ }
+ files: [
+ "../shared.h",
+ "tst_blackboxbase.cpp",
+ "tst_blackboxbase.h",
+ "tst_blackboxwindows.cpp",
+ "tst_blackboxwindows.h",
+ ]
+ cpp.defines: base.concat(["SRCDIR=" + Utilities.cStringQuote(path)])
+}
diff --git a/tests/auto/blackbox/testdata-windows/codesign/app.cpp b/tests/auto/blackbox/testdata-windows/codesign/app.cpp
new file mode 100644
index 000000000..76e819701
--- /dev/null
+++ b/tests/auto/blackbox/testdata-windows/codesign/app.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tests/auto/blackbox/testdata-windows/codesign/codesign.qbs b/tests/auto/blackbox/testdata-windows/codesign/codesign.qbs
new file mode 100644
index 000000000..2b48c67ff
--- /dev/null
+++ b/tests/auto/blackbox/testdata-windows/codesign/codesign.qbs
@@ -0,0 +1,37 @@
+Project {
+ name: "p"
+
+ property bool enableSigning: true
+ property string hashAlgorithm
+ property string subjectName
+ property string signingTimestamp
+
+ CppApplication {
+ name: "A"
+ files: "app.cpp"
+ codesign.enableCodeSigning: project.enableSigning
+ codesign.hashAlgorithm: project.hashAlgorithm
+ codesign.subjectName: project.subjectName
+ codesign.signingTimestamp: project.signingTimestamp
+ install: true
+ installDir: ""
+ property bool dummy: {
+ console.info("signtool path: %%" + codesign.codesignPath + "%%");
+ }
+ }
+
+ DynamicLibrary {
+ Depends { name: "cpp" }
+ name: "B"
+ files: "app.cpp"
+ codesign.enableCodeSigning: project.enableSigning
+ codesign.hashAlgorithm: project.hashAlgorithm
+ codesign.subjectName: project.subjectName
+ codesign.signingTimestamp: project.signingTimestamp
+ install: true
+ installDir: ""
+ property bool dummy: {
+ console.info("signtool path: %%" + codesign.codesignPath + "%%");
+ }
+ }
+}
diff --git a/tests/auto/blackbox/tst_blackboxwindows.cpp b/tests/auto/blackbox/tst_blackboxwindows.cpp
new file mode 100644
index 000000000..0c82754fb
--- /dev/null
+++ b/tests/auto/blackbox/tst_blackboxwindows.cpp
@@ -0,0 +1,174 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Denis Shienkov <denis.shienkov@gmail.com>
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms and
+** conditions see http://www.qt.io/terms-conditions. For further information
+** use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+
+#include "tst_blackboxwindows.h"
+
+#include "../shared.h"
+
+#include <tools/hostosinfo.h>
+
+#include <QtCore/qdir.h>
+#include <QtCore/qregularexpression.h>
+
+using qbs::Internal::HostOsInfo;
+
+struct SigntoolInfo
+{
+ enum class CodeSignResult { Failed = 0, Signed, Unsigned };
+ CodeSignResult result = CodeSignResult::Failed;
+ bool timestamped = false;
+ QString hashAlgorithm;
+ QString subjectName;
+};
+
+Q_DECLARE_METATYPE(SigntoolInfo::CodeSignResult)
+
+static SigntoolInfo extractSigntoolInfo(const QString &signtoolPath, const QString &appPath)
+{
+ QProcess signtool;
+ signtool.start(signtoolPath, { QStringLiteral("verify"), QStringLiteral("/v"), appPath });
+ if (!signtool.waitForStarted() || !signtool.waitForFinished())
+ return {};
+ const auto output = signtool.readAllStandardError();
+ SigntoolInfo signtoolInfo;
+ if (output.contains("No signature found")) {
+ signtoolInfo.result = SigntoolInfo::CodeSignResult::Unsigned;
+ } else {
+ signtoolInfo.result = SigntoolInfo::CodeSignResult::Signed;
+ const auto output = signtool.readAllStandardOutput();
+ const auto lines = output.split('\n');
+ for (const auto &line: lines) {
+ {
+ const QRegularExpression re("^Hash of file \\((.+)\\):.+$");
+ const QRegularExpressionMatch match = re.match(line);
+ if (match.hasMatch()) {
+ signtoolInfo.hashAlgorithm = match.captured(1).toLocal8Bit();
+ continue;
+ }
+ }
+ {
+ const QRegularExpression re("Issued to: (.+)");
+ const QRegularExpressionMatch match = re.match(line);
+ if (match.hasMatch()) {
+ signtoolInfo.subjectName = match.captured(1).toLocal8Bit().trimmed();
+ continue;
+ }
+ }
+ if (line.startsWith("The signature is timestamped:")) {
+ signtoolInfo.timestamped = true;
+ break;
+ } else if (line.startsWith("File is not timestamped.")) {
+ break;
+ }
+ }
+ }
+ return signtoolInfo;
+}
+
+static QString extractSigntoolPath(const QByteArray &output)
+{
+ const QRegularExpression re("%%(.+)%%");
+ QRegularExpressionMatchIterator it = re.globalMatch(output);
+ if (!it.hasNext())
+ return {};
+ const QRegularExpressionMatch match = it.next();
+ return match.captured(1).toUtf8();
+}
+
+TestBlackboxWindows::TestBlackboxWindows()
+ : TestBlackboxBase (SRCDIR "/testdata-windows", "blackbox-windows")
+{
+}
+
+void TestBlackboxWindows::initTestCase()
+{
+ if (!HostOsInfo::isWindowsHost()) {
+ QSKIP("only applies on Windows");
+ return;
+ }
+
+ TestBlackboxBase::initTestCase();
+}
+
+void TestBlackboxWindows::standaloneCodesign()
+{
+ QFETCH(SigntoolInfo::CodeSignResult, result);
+ QFETCH(QString, hashAlgorithm);
+ QFETCH(QString, subjectName);
+ QFETCH(QString, signingTimestamp);
+
+ QDir::setCurrent(testDataDir + "/codesign");
+ QbsRunParameters params(QStringList{"qbs.installPrefix:''"});
+ params.arguments << QStringLiteral("project.enableSigning:%1").arg(
+ (result == SigntoolInfo::CodeSignResult::Signed) ? "true" : "false")
+ << QStringLiteral("project.hashAlgorithm:%1").arg(hashAlgorithm)
+ << QStringLiteral("project.subjectName:%1").arg(subjectName)
+ << QStringLiteral("project.signingTimestamp:%1").arg(signingTimestamp);
+
+ rmDirR(relativeBuildDir());
+ QCOMPARE(runQbs(params), 0);
+
+ if (!m_qbsStdout.contains("signtool path:"))
+ QFAIL("No current signtool path pattern exists");
+
+ const auto signtoolPath = extractSigntoolPath(m_qbsStdout);
+ QVERIFY(QFileInfo(signtoolPath).exists());
+
+ const QStringList outputBinaries = {"A.exe", "B.dll"};
+ for (const auto &outputBinary : outputBinaries) {
+ const auto outputBinaryPath = defaultInstallRoot + "/" + outputBinary;
+ QVERIFY(QFileInfo(outputBinaryPath).exists());
+
+ const SigntoolInfo signtoolInfo = extractSigntoolInfo(signtoolPath, outputBinaryPath);
+ QVERIFY(signtoolInfo.result != SigntoolInfo::CodeSignResult::Failed);
+ QCOMPARE(signtoolInfo.result, result);
+ QCOMPARE(signtoolInfo.hashAlgorithm, hashAlgorithm);
+ QCOMPARE(signtoolInfo.subjectName, subjectName);
+ QCOMPARE(signtoolInfo.timestamped, !signingTimestamp.isEmpty());
+ }
+}
+
+void TestBlackboxWindows::standaloneCodesign_data()
+{
+ QTest::addColumn<SigntoolInfo::CodeSignResult>("result");
+ QTest::addColumn<QString>("hashAlgorithm");
+ QTest::addColumn<QString>("subjectName");
+ QTest::addColumn<QString>("signingTimestamp");
+
+ QTest::newRow("standalone, unsigned")
+ << SigntoolInfo::CodeSignResult::Unsigned << "" << "" << "";
+ QTest::newRow("standalone, signed, sha1, qbs@community.test, no timestamp")
+ << SigntoolInfo::CodeSignResult::Signed << "sha1" << "qbs@community.test" << "";
+ QTest::newRow("standalone, signed, sha256, qbs@community.test, RFC3061 timestamp")
+ << SigntoolInfo::CodeSignResult::Signed << "sha256" << "qbs@community.test"
+ << "http://timestamp.digicert.com";
+}
+
+QTEST_MAIN(TestBlackboxWindows)
diff --git a/tests/auto/blackbox/tst_blackboxwindows.h b/tests/auto/blackbox/tst_blackboxwindows.h
new file mode 100644
index 000000000..fbc597313
--- /dev/null
+++ b/tests/auto/blackbox/tst_blackboxwindows.h
@@ -0,0 +1,51 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Denis Shienkov <denis.shienkov@gmail.com>
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms and
+** conditions see http://www.qt.io/terms-conditions. For further information
+** use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+
+#ifndef TST_BLACKBOXWINDOWS_H
+#define TST_BLACKBOXWINDOWS_H
+
+#include "tst_blackboxbase.h"
+
+class TestBlackboxWindows : public TestBlackboxBase
+{
+ Q_OBJECT
+
+public:
+ TestBlackboxWindows();
+
+public slots:
+ void initTestCase() override;
+
+private slots:
+ void standaloneCodesign();
+ void standaloneCodesign_data();
+};
+
+#endif // TST_BLACKBOXWINDOWS_H