From 90b9014255eaa77fba0805112da5d8aa74fca256 Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Mon, 13 Jul 2026 21:10:13 +0300 Subject: [PATCH 1/2] Update Qt 6.11.2 and workaround macOS sandbox issues IB-8980 Signed-off-by: Raul Metsma --- .github/workflows/build.yml | 23 ++++++++++++++--------- CMakeLists.txt | 2 +- README.md | 4 ++-- client/CryptoDoc.cpp | 7 ++----- client/CryptoDoc.h | 1 - client/DigiDoc.cpp | 10 ++-------- client/DigiDoc.h | 1 - client/DocumentModel.h | 1 - client/dialogs/FileDialog.cpp | 19 +++++++++++++++++++ client/dialogs/FileDialog.h | 4 ++++ client/libcdoc | 2 +- prepare_osx_build_environment.sh | 4 ++-- qdigidoc4.wxs | 3 +-- 13 files changed, 48 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d690d1c84..65a05f2b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | brew install flatbuffers - curl -O -L -s https://installer.id.ee/media/github/opensc_0.26.1.pkg + curl -O -L -s https://installer.id.ee/media/github/opensc_0.27.1-3.pkg sudo installer -verboseR -pkg libdigidocpp-pkg/build/macos/libdigidocpp*.pkg -target / sudo installer -verboseR -pkg opensc_*.pkg -target / rm -rf libdigidocpp-pkg @@ -41,12 +41,12 @@ jobs: key: vcpkg-macOS-${{ hashFiles('prepare_osx_build_environment.sh') }} - name: Build libraries run: | - mkdir -p ${LIBS_PATH}/Qt-6.10.3-OpenSSL + mkdir -p ${LIBS_PATH}/Qt-6.11.2-OpenSSL ./prepare_osx_build_environment.sh -p ${LIBS_PATH} - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: 6.10.3 + version: 6.11.2 arch: clang_64 cache: true - name: Build @@ -169,22 +169,27 @@ jobs: Rename-Item "libdigidocpp*.msi" libdigidocpp.msi $r = Start-Process msiexec -ArgumentList '/a', 'libdigidocpp.msi', '/qn', "TARGETDIR=$env:GITHUB_WORKSPACE\libs", '/l*v', 'install.log' -Wait -PassThru if ($r.ExitCode -ne 0) { Get-Content install.log; exit $r.ExitCode } - - name: Cache vcpkg + - name: Cache vcpkg and WiX uses: actions/cache@v5 with: - path: ${{ github.workspace }}/vcpkg_cache - key: vcpkg-${{ matrix.vcver }}-${{ matrix.platform }}-${{ hashFiles('client/libcdoc/vcpkg.json') }} + path: | + ${{ github.workspace }}/vcpkg_cache + ~/.dotnet/tools + ~/.wix + key: vcpkg-${{ matrix.vcver }}-${{ matrix.platform }}-${{ hashFiles('client/libcdoc/vcpkg.json') }}-wix-7.0.0 - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: 6.11.1 + version: 6.11.2 arch: ${{ matrix.platform == 'arm64' && 'win64_msvc2022_arm64_cross_compiled' || 'win64_msvc2022_64' }} cache: true aqtsource: git+https://github.com/miurahr/aqtinstall.git - name: Install WiX run: | - dotnet tool install -g wix --version 6.0.2 - wix extension -g add WixToolset.UI.wixext/6.0.2 + dotnet tool install -g wix --version 7.0.0 + wix eula accept wix7 + wix extension -g add WixToolset.UI.wixext/7.0.0 + wix extension -g add WixToolset.Util.wixext/7.0.0 - name: Build shell: cmd env: diff --git a/CMakeLists.txt b/CMakeLists.txt index d5da89210..dff25fdc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.22) if(NOT EXISTS ${CMAKE_SOURCE_DIR}/common/CMakeLists.txt) message(FATAL_ERROR "common submodule directory empty, did you 'git clone --recursive'?") endif() -project(qdigidoc4 VERSION 4.11.1 +project(qdigidoc4 VERSION 4.12.0 DESCRIPTION "DigiDoc4 application for digital signing and encryption" HOMEPAGE_URL https://github.com/open-eid/DigiDoc4-Client ) diff --git a/README.md b/README.md index f7fd729de..ca030141d 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ 3. Configure cmake -B build -S . \ - -DCMAKE_PREFIX_PATH=~/cmake_builds/Qt-6.11.1-OpenSSL \ + -DCMAKE_PREFIX_PATH=~/cmake_builds/Qt-6.11.2-OpenSSL \ -DOPENSSL_ROOT_DIR=~/cmake_build/OpenSSL \ -DLDAP_ROOT=~/cmake_build/OpenLDAP \ -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" @@ -95,7 +95,7 @@ 3. Configure cmake -G"NMAKE Makefiles" -B build -S . ` - -DCMAKE_PREFIX_PATH=C:\Qt\6.11.1\msvc2022_64 ` + -DCMAKE_PREFIX_PATH=C:\Qt\6.11.2\msvc2022_64 ` -DLibDigiDocpp_ROOT="C:\Program Files (x86)\libdigidocpp" ` -DVCPKG_MANIFEST_DIR=client/libcdoc diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index a83b10d91..2ec479718 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -171,11 +171,6 @@ quint64 CDocumentModel::fileSize(int row) const return d->files.at(row).size; } -QString CDocumentModel::mime(int row) const -{ - return FileDialog::normalized(QString::fromStdString(d->files.at(row).mime)); -} - void CDocumentModel::open(int row) { if(!d->isEncrypted()) @@ -414,6 +409,7 @@ bool CryptoDoc::encrypt(const QString &filename, const QString& label, const QBy return false; } QString writer_last_error; + auto keepAlive = FileDialog::keepAccessAlive(d->fileName); libcdoc::result_t result = waitFor([&] -> libcdoc::result_t { qCDebug(CRYPTO) << "Encrypt" << d->fileName; auto writer = std::unique_ptr(libcdoc::CDocWriter::createWriter(d->version, d->fileName.toStdString(), &d->conf, &d->crypto, &d->network)); @@ -524,6 +520,7 @@ bool CryptoDoc::saveCopy(const QString &filename) QFileInfo dst(filename); if(src == dst) return true; + auto keepAlive = FileDialog::keepAccessAlive(filename); std::error_code ec; std::filesystem::copy_file(src.filesystemFilePath(), dst.filesystemFilePath(), std::filesystem::copy_options::overwrite_existing, ec); diff --git a/client/CryptoDoc.h b/client/CryptoDoc.h index 3a0c2da71..be9ca121a 100644 --- a/client/CryptoDoc.h +++ b/client/CryptoDoc.h @@ -89,7 +89,6 @@ class CDocumentModel final: public DocumentModel bool addFile(const QString &file, const QString &mime = QStringLiteral("application/octet-stream")) final; QString data(int row) const final; quint64 fileSize(int row) const final; - QString mime(int row) const final; void open(int row) final; bool removeRow(int row) final; int rowCount() const final; diff --git a/client/DigiDoc.cpp b/client/DigiDoc.cpp index 8409b55b4..329894dc8 100644 --- a/client/DigiDoc.cpp +++ b/client/DigiDoc.cpp @@ -333,14 +333,6 @@ quint64 SDocumentModel::fileSize(int row) const return doc->b->dataFiles().at(size_t(row))->fileSize(); } -QString SDocumentModel::mime(int row) const -{ - if(row >= rowCount()) - return {}; - - return from(doc->b->dataFiles().at(size_t(row))->mediaType()); -} - bool SDocumentModel::removeRow(int row) { if(!doc->b) @@ -368,6 +360,7 @@ QString SDocumentModel::save(int row, const QString &path) const return {}; if(QFileInfo::exists(path)) return path; + auto keepAlive = FileDialog::keepAccessAlive(path); doc->b->dataFiles().at(size_t(row))->saveAs(path.toStdString()); if(!QFileInfo::exists(path)) return {}; @@ -710,6 +703,7 @@ bool DigiDoc::saveAs(const QString &filename) { try { + auto keepAlive = FileDialog::keepAccessAlive(filename); return waitFor([&] { parentContainer ? parentContainer->save(to(filename)) : b->save(to(filename)); return true; diff --git a/client/DigiDoc.h b/client/DigiDoc.h index 739deede2..0988e01da 100644 --- a/client/DigiDoc.h +++ b/client/DigiDoc.h @@ -95,7 +95,6 @@ class SDocumentModel final: public DocumentModel bool addFile(const QString &file, const QString &mime = QStringLiteral("application/octet-stream")) final; QString data(int row) const final; quint64 fileSize(int row) const final; - QString mime(int row) const final; bool removeRow(int row) final; int rowCount() const final; QString save(int row, const QString &path) const final; diff --git a/client/DocumentModel.h b/client/DocumentModel.h index 3dc2ae2ce..f17fef7c4 100644 --- a/client/DocumentModel.h +++ b/client/DocumentModel.h @@ -35,7 +35,6 @@ class DocumentModel: public QObject virtual void copyModel(DocumentModel *model); virtual QString data(int row) const = 0; virtual quint64 fileSize(int row) const = 0; - virtual QString mime(int row) const = 0; virtual void open(int row); virtual bool removeRow(int row) = 0; virtual int rowCount() const = 0; diff --git a/client/dialogs/FileDialog.cpp b/client/dialogs/FileDialog.cpp index f47151c36..5f4d11a3c 100644 --- a/client/dialogs/FileDialog.cpp +++ b/client/dialogs/FileDialog.cpp @@ -104,6 +104,25 @@ bool FileDialog::fileIsWritable( const QString &filename ) return result; } +std::unique_ptr FileDialog::keepAccessAlive(const QString &path) +{ +#ifdef Q_OS_MACOS + // Qt 6.11's macOS sandbox file engine only keeps a user-selected path's + // security-scoped access active while some QFile instance for that exact + // path exists (not merely open). Code that writes to disk without going + // through QFile (libdigidocpp, libcdoc, std::filesystem) bypasses that + // entirely, so callers doing raw I/O to a save-panel-selected path must + // keep the object this returns alive for the duration of that write. + auto file = std::make_unique(path); + bool opened = file->open(QFile::WriteOnly|QFile::Append); + Q_UNUSED(opened) + return file; +#else + Q_UNUSED(path) + return {}; +#endif +} + bool FileDialog::isSignedPDF(const QString &path) { if(!path.endsWith(QLatin1String("pdf"), Qt::CaseInsensitive)) diff --git a/client/dialogs/FileDialog.h b/client/dialogs/FileDialog.h index cb87355ff..28e0dcf25 100644 --- a/client/dialogs/FileDialog.h +++ b/client/dialogs/FileDialog.h @@ -19,8 +19,11 @@ #pragma once +#include #include +#include + class FileDialog : public QFileDialog { Q_OBJECT @@ -36,6 +39,7 @@ class FileDialog : public QFileDialog static QString createNewFileName(const QString &file, bool signature, QWidget *parent); static FileType detect(const QString &filename); static bool fileIsWritable( const QString &filename ); + static std::unique_ptr keepAccessAlive(const QString &path); static bool isSignedPDF(const QString &path); static void setFileZone(const QString &target, const QString &source); static void setReadOnly(const QString &path, bool readonly = true); diff --git a/client/libcdoc b/client/libcdoc index 83408f9fc..4f28aa8d9 160000 --- a/client/libcdoc +++ b/client/libcdoc @@ -1 +1 @@ -Subproject commit 83408f9fcf4af25e81d891c34f9cc437d23f021a +Subproject commit 4f28aa8d9f20a3dcbf7c86093496840fd34f5a33 diff --git a/prepare_osx_build_environment.sh b/prepare_osx_build_environment.sh index 26c014e3e..b3d2f15cd 100755 --- a/prepare_osx_build_environment.sh +++ b/prepare_osx_build_environment.sh @@ -4,9 +4,9 @@ set -e ######### Versions of libraries/frameworks to be compiled -QT_VER="6.10.3" +QT_VER="6.11.2" OPENSSL_VER="3.5.7" -OPENLDAP_VER="2.6.13" +OPENLDAP_VER="2.6.14" REBUILD=false BUILD_PATH=~/cmake_builds : ${MACOSX_DEPLOYMENT_TARGET:="14.0"} diff --git a/qdigidoc4.wxs b/qdigidoc4.wxs index abedfe22f..4e7249379 100644 --- a/qdigidoc4.wxs +++ b/qdigidoc4.wxs @@ -17,8 +17,7 @@ + xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"> From 73e0cbe68de31345d639ed2ba62fcc9bf747bbfe Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Tue, 25 Aug 2026 10:22:41 +0300 Subject: [PATCH 2/2] Generate ephemeral APPX signing key IB-9060 Signed-off-by: Raul Metsma --- .github/workflows/build.yml | 1 + AppxManifest-ShellExt.xml.cmake | 2 +- AppxManifest.xml.cmake | 2 +- CMakeLists.txt | 1 + SignAppx.ps1 | 50 ++++++++++++++++++++++++++++++ client/CMakeLists.txt | 11 ++++--- extensions/windows/CMakeLists.txt | 11 ++++++- qdigidoc4.pfx | Bin 2694 -> 0 bytes 8 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 SignAppx.ps1 delete mode 100644 qdigidoc4.pfx diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65a05f2b3..707729634 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -212,6 +212,7 @@ jobs: path: | build/*.msi build/*.appx + build/*.cer coverity: name: Run Coverity tests if: github.repository == 'open-eid/DigiDoc4-Client' && contains(github.ref, 'coverity_scan') diff --git a/AppxManifest-ShellExt.xml.cmake b/AppxManifest-ShellExt.xml.cmake index 72ab42401..df7810187 100644 --- a/AppxManifest-ShellExt.xml.cmake +++ b/AppxManifest-ShellExt.xml.cmake @@ -8,7 +8,7 @@ xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10" IgnorableNamespaces="com desktop4 desktop5 rescap uap uap10"> + Publisher="${APPX_PUBLISHER}" /> DigiDoc4 Shell Extension Riigi Infosüsteemi Amet diff --git a/AppxManifest.xml.cmake b/AppxManifest.xml.cmake index a9c07df02..5669719a9 100644 --- a/AppxManifest.xml.cmake +++ b/AppxManifest.xml.cmake @@ -9,7 +9,7 @@ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" IgnorableNamespaces="com desktop desktop4 desktop5 rescap uap uap3"> + Publisher="${APPX_PUBLISHER}" /> DigiDoc4 Client Riigi Infosüsteemi Amet diff --git a/CMakeLists.txt b/CMakeLists.txt index dff25fdc8..f2c6e8aae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ find_package(Qt6 6.2.0 REQUIRED COMPONENTS Core Widgets Network PrintSupport Svg if(APPLE) add_subdirectory(extensions/DigiDocQL) elseif(WIN32) + set(APPX_PUBLISHER "CN=8BBBE4D8-620A-4884-A12A-72F1A2030D8B" CACHE STRING "Publisher for test Appx and MSIX packages") add_subdirectory(extensions/windows) elseif(UNIX) option(ENABLE_KDE "Install KDE service menu (default: TRUE)" TRUE) diff --git a/SignAppx.ps1 b/SignAppx.ps1 new file mode 100644 index 000000000..2bd16b647 --- /dev/null +++ b/SignAppx.ps1 @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Estonian Information System Authority +# SPDX-License-Identifier: LGPL-2.1-or-later + +param( + [Parameter(Mandatory = $true)] + [string] $PackagePath, + + [Parameter(Mandatory = $true)] + [string] $ManifestPath, + + [Parameter(Mandatory = $true)] + [string] $CertificatePath +) + +$ErrorActionPreference = 'Stop' +$certificate = $null + +try { + [xml] $manifest = Get-Content -LiteralPath $ManifestPath -Raw + $publisher = $manifest.Package.Identity.Publisher + if ([string]::IsNullOrWhiteSpace($publisher)) { + throw "The Appx manifest does not define a publisher." + } + + $certificate = New-SelfSignedCertificate ` + -Type Custom ` + -Subject $publisher ` + -FriendlyName 'DigiDoc4 ephemeral Appx test certificate' ` + -CertStoreLocation 'Cert:\CurrentUser\My' ` + -Provider 'Microsoft Software Key Storage Provider' ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -HashAlgorithm SHA256 ` + -KeyExportPolicy NonExportable ` + -KeyUsage DigitalSignature ` + -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13', '2.5.29.19={text}') ` + -NotAfter (Get-Date).AddDays(30) + + Export-Certificate -Cert $certificate -FilePath $CertificatePath -Type CERT -Force | Out-Null + + & signtool.exe sign /v /s My /sha1 $certificate.Thumbprint /fd SHA256 $PackagePath + if ($LASTEXITCODE -ne 0) { + throw "SignTool failed with exit code $LASTEXITCODE." + } +} +finally { + if ($null -ne $certificate) { + Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -DeleteKey + } +} diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 475f57341..b460b3fdc 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -271,12 +271,13 @@ elseif(WIN32) --skip-plugin-types generic,networkinformation,iconengines --exclude-plugins qjpeg,qico,qgif,qcertonlybackend,qschannelbackend appx COMMAND ${CMAKE_COMMAND} -E copy ${LIBS_PATH}/digidoc-tool.exe appx COMMAND makeappx.exe pack -d appx -p ${MSI_FILE}.appx - # https://msdn.microsoft.com/en-us/library/windows/desktop/jj835832(v=vs.85).aspx - #Popups GUI - #COMMAND MakeCert.exe /n "CN=8BBBE4D8-620A-4884-A12A-72F1A2030D8B" /r /h 0 /eku "1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13" /m 120 /a sha256 /sv qdigidoc4.pvk qdigidoc4.cer - #COMMAND Pvk2Pfx.exe /pvk qdigidoc4.pvk /spc qdigidoc4.cer /pfx qdigidoc4.pfx /f - COMMAND signtool.exe sign -f ${CMAKE_SOURCE_DIR}/qdigidoc4.pfx -fd SHA256 -v ${MSI_FILE}.appx + COMMAND powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass + -File ${CMAKE_SOURCE_DIR}/SignAppx.ps1 + -PackagePath ${MSI_FILE}.appx + -ManifestPath AppxManifest.xml + -CertificatePath ${MSI_FILE}.cer WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + VERBATIM ) else() find_package( Threads REQUIRED ) diff --git a/extensions/windows/CMakeLists.txt b/extensions/windows/CMakeLists.txt index bc70c082a..f4d9ba656 100644 --- a/extensions/windows/CMakeLists.txt +++ b/extensions/windows/CMakeLists.txt @@ -64,7 +64,7 @@ if(SIGNCERT) execute_process( COMMAND powershell -NoProfile -NonInteractive -Command "(Get-ChildItem Cert:\\CurrentUser\\My | Where-Object { $_.Subject -like '*${SIGNCERT}*' } | Select-Object -First 1).Subject" - OUTPUT_VARIABLE SIGNCERT_SUBJECT + OUTPUT_VARIABLE APPX_PUBLISHER OUTPUT_STRIP_TRAILING_WHITESPACE ) endif() @@ -109,4 +109,13 @@ if(SIGNCERT) /tr http://timestamp.digicert.com /td SHA256 "${CMAKE_BINARY_DIR}/${SHELLEXT_MSIX_FILE}" ) +else() + add_custom_command(TARGET msixshellext POST_BUILD + COMMAND powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass + -File ${CMAKE_SOURCE_DIR}/SignAppx.ps1 + -PackagePath ${CMAKE_BINARY_DIR}/${SHELLEXT_MSIX_FILE} + -ManifestPath ${CMAKE_BINARY_DIR}/AppxManifest-ShellExt.xml + -CertificatePath ${CMAKE_BINARY_DIR}/${SHELLEXT_MSIX_FILE}.cer + VERBATIM + ) endif() diff --git a/qdigidoc4.pfx b/qdigidoc4.pfx deleted file mode 100644 index 6c4e0fe2a4c55e2cf5996223d3cd01415b0f880b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2694 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