From 2f6eecdc96fcd693cecef8011d8f9500c7872fc7 Mon Sep 17 00:00:00 2001 From: Denis Shienkov Date: Mon, 29 Mar 2021 18:15:17 +0300 Subject: codesign: Long live `signtool` signing on Windows Change-Id: I320cd1a1f3d8a1eed11d1c70007214f19a109b6e Reviewed-by: Christian Kandeler Reviewed-by: Ivan Komissarov --- .github/workflows/main.yml | 6 + doc/reference/modules/codesign-module.qdoc | 118 +++++++++++--- share/qbs/modules/codesign/CodeSignModule.qbs | 6 + share/qbs/modules/codesign/apple.qbs | 2 +- share/qbs/modules/codesign/codesign.js | 119 +++++++++++++- share/qbs/modules/codesign/signtool.qbs | 94 +++++++++++ share/qbs/modules/cpp/msvc.js | 7 + share/qbs/modules/cpp/windows-msvc-base.qbs | 24 ++- tests/auto/auto.qbs | 3 +- tests/auto/blackbox/CMakeLists.txt | 9 ++ tests/auto/blackbox/blackbox-windows.pro | 18 +++ tests/auto/blackbox/blackbox-windows.qbs | 21 +++ .../blackbox/testdata-windows/codesign/app.cpp | 1 + .../testdata-windows/codesign/codesign.qbs | 37 +++++ tests/auto/blackbox/tst_blackboxwindows.cpp | 174 +++++++++++++++++++++ tests/auto/blackbox/tst_blackboxwindows.h | 51 ++++++ 16 files changed, 658 insertions(+), 32 deletions(-) create mode 100644 share/qbs/modules/codesign/signtool.qbs create mode 100644 tests/auto/blackbox/blackbox-windows.pro create mode 100644 tests/auto/blackbox/blackbox-windows.qbs create mode 100644 tests/auto/blackbox/testdata-windows/codesign/app.cpp create mode 100644 tests/auto/blackbox/testdata-windows/codesign/codesign.qbs create mode 100644 tests/auto/blackbox/tst_blackboxwindows.cpp create mode 100644 tests/auto/blackbox/tst_blackboxwindows.h 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 */ @@ -163,21 +170,6 @@ \appleproperty */ -/*! - \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 @@ -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 +** 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 +** 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 + +#include +#include + +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("result"); + QTest::addColumn("hashAlgorithm"); + QTest::addColumn("subjectName"); + QTest::addColumn("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 +** 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 -- cgit v1.2.3