From db352083cfe010623dc2d8be193a8792f8865acd Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sat, 12 Sep 2026 05:05:58 +0000 Subject: [PATCH] Rebase opt-in fingerprint workaround onto KScreenLocker 6.7.5 --- plasma-fingerprint-workaround/README.md | 22 ++- .../kscreenlocker/kscreenlocker.spec | 7 +- plasma-fingerprint-workaround/payload.conf | 4 +- .../plasma-fingerprint-workaround.spec | 5 +- .../tests/authentication/CMakeLists.txt | 32 ++++ .../tests/authentication/retrytest.cpp | 160 ++++++++++++++++++ scripts/build-patched-kscreenlocker-rpm | 8 +- 7 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 plasma-fingerprint-workaround/tests/authentication/CMakeLists.txt create mode 100644 plasma-fingerprint-workaround/tests/authentication/retrytest.cpp diff --git a/plasma-fingerprint-workaround/README.md b/plasma-fingerprint-workaround/README.md index 90bcc90..81acc4a 100644 --- a/plasma-fingerprint-workaround/README.md +++ b/plasma-fingerprint-workaround/README.md @@ -21,7 +21,7 @@ restores the current KScreenLocker package from Fedora's `fedora` or `updates` repository. The provided build currently supports Fedora 44 on x86_64 and is based on -`kscreenlocker-6.7.4-1.fc44`. A different base version is rejected by default. +`kscreenlocker-6.7.5-1.fc44`. A different base version is rejected by default. To intentionally install the older provided build after a KScreenLocker update: ```bash @@ -41,7 +41,7 @@ Install the resulting package directly through the controller: ```bash sudo plasma-fingerprint-workaround enable \ - --rpm rpmbuild/RPMS/x86_64/kscreenlocker-6.7.4-1.fc44.ajp5.x86_64.rpm + --rpm rpmbuild/RPMS/x86_64/kscreenlocker-6.7.5-1.fc44.ajp6.x86_64.rpm ``` For another upstream version, supply its version, Fedora base release, and @@ -58,3 +58,21 @@ A locally built package based on a different installed version requires `--force`. This flag permits the version mismatch and allows DNF to downgrade; it does not bypass the package identity, architecture, integrity, or workaround metadata checks. + +## Authentication regression tests + +With an extracted source tree and the patches applied in numeric order: + +```bash +cmake -S plasma-fingerprint-workaround/tests/authentication \ + -B local/fingerprint-tests -DKSCREENLOCKER_SOURCE=/path/to/patched/kscreenlocker +cmake --build local/fingerprint-tests +ctest --test-dir local/fingerprint-tests --output-on-failure +``` + +These tests compile the upstream authentication classes against a simulated +PAM backend. They check retries after fingerprint timeout or temporary +unavailability, preservation of the password conversation, absent smartcards, +missing modules, the grace-period guard and incorrect passwords. They never +access the system's authentication configuration. Actual reader behaviour and +the lock-screen interface still require testing on the laptop. diff --git a/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec b/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec index 915c12f..f22df5a 100644 --- a/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec +++ b/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec @@ -1,6 +1,6 @@ -%{!?upstream_version:%global upstream_version 6.7.4} +%{!?upstream_version:%global upstream_version 6.7.5} %{!?base_release:%global base_release 1} -%{!?workaround_release:%global workaround_release 5} +%{!?workaround_release:%global workaround_release 6} Name: kscreenlocker Version: %{upstream_version} @@ -103,6 +103,9 @@ install -D -m 0755 %{SOURCE1} \ %{_datadir}/dbus-1/interfaces/*.xml %changelog +* Sat Sep 12 2026 Anton - 6.7.5-1.fc44.ajp6 +- Rebase the existing fingerprint workaround onto Fedora's Plasma 6.7.5 + * Fri Sep 04 2026 Anton - 6.7.4-1.fc44.ajp5 - Package the fprintd resume hook and compatibility metadata - Apply the tested fingerprint retry patch set diff --git a/plasma-fingerprint-workaround/payload.conf b/plasma-fingerprint-workaround/payload.conf index cdd18ae..9e757d1 100644 --- a/plasma-fingerprint-workaround/payload.conf +++ b/plasma-fingerprint-workaround/payload.conf @@ -1,2 +1,2 @@ -payload_url=https://git.ajpanton.se/api/packages/ajp_anton/generic/plasma-fingerprint-workaround/6.7.4-1.fc44.ajp5/kscreenlocker-6.7.4-1.fc44.ajp5.x86_64.rpm -payload_sha256=d64515f3c7cb9c44b1c8fc194e90503be5944b1d24094aab2f68d989b3eca381 +payload_url=https://git.ajpanton.se/api/packages/ajp_anton/generic/plasma-fingerprint-workaround/6.7.5-1.fc44.ajp6/kscreenlocker-6.7.5-1.fc44.ajp6.x86_64.rpm +payload_sha256=ce454bfb8bcdb7c80e698fb0549f7765bc4570f5383ad7314eb2c292b7708532 diff --git a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec index 48a7997..549e7d9 100644 --- a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec +++ b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec @@ -1,6 +1,6 @@ Name: plasma-fingerprint-workaround Version: 0.1.0 -Release: 3%{?dist} +Release: 4%{?dist} Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend License: MIT @@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \ %{_datadir}/plasma-fingerprint-workaround/payload.conf %changelog +* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-4 +- Update the opt-in payload to Fedora KScreenLocker 6.7.5 + * Sat Sep 05 2026 fedora-tools contributors - 0.1.0-3 - Clarify what the workaround patches diff --git a/plasma-fingerprint-workaround/tests/authentication/CMakeLists.txt b/plasma-fingerprint-workaround/tests/authentication/CMakeLists.txt new file mode 100644 index 0000000..68d2421 --- /dev/null +++ b/plasma-fingerprint-workaround/tests/authentication/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.25) +project(fingerprintRetryTests LANGUAGES CXX) + +# Compile the actual patched upstream classes, but never use the host's PAM +# configuration or credentials. The test executable supplies a fake PAM API. +set(KSCREENLOCKER_SOURCE "" CACHE PATH "Extracted, patched KScreenLocker source") +if(NOT EXISTS "${KSCREENLOCKER_SOURCE}/greeter/pamauthenticator.cpp") + message(FATAL_ERROR "Set KSCREENLOCKER_SOURCE to the patched source directory") +endif() +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Test) +find_package(ECM REQUIRED NO_MODULE) +list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}") +include(ECMQtDeclareLoggingCategory) + +set(sources retrytest.cpp + ${KSCREENLOCKER_SOURCE}/greeter/pamauthenticator.cpp + ${KSCREENLOCKER_SOURCE}/greeter/pamauthenticators.cpp) +ecm_qt_declare_logging_category(sources + HEADER kscreenlocker_greet_logging.h + IDENTIFIER KSCREENLOCKER_GREET + CATEGORY_NAME kscreenlocker_greet) +add_executable(retrytest ${sources}) +target_include_directories(retrytest PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} ${KSCREENLOCKER_SOURCE}/greeter) +target_compile_definitions(retrytest PRIVATE HAVE_PAM_FAIL_DELAY) +target_link_libraries(retrytest PRIVATE Qt6::Core Qt6::Test) +enable_testing() +add_test(NAME fingerprint-retry COMMAND retrytest) +set_tests_properties(fingerprint-retry PROPERTIES TIMEOUT 30) diff --git a/plasma-fingerprint-workaround/tests/authentication/retrytest.cpp b/plasma-fingerprint-workaround/tests/authentication/retrytest.cpp new file mode 100644 index 0000000..ad02fa1 --- /dev/null +++ b/plasma-fingerprint-workaround/tests/authentication/retrytest.cpp @@ -0,0 +1,160 @@ +#include "pamauthenticators.h" + +#include +#include +#include +#include +#include + +// Test-only PAM implementation. No real authentication or system files are used. +struct pam_handle { + QByteArray service; + pam_conv conversation; + int attempts = 0; +}; + +int pam_start(const char *service, const char *, const pam_conv *conversation, pam_handle_t **handle) +{ + *handle = new pam_handle{service, *conversation}; + return PAM_SUCCESS; +} + +int pam_end(pam_handle_t *handle, int) +{ + delete handle; + return PAM_SUCCESS; +} + +int pam_set_item(pam_handle_t *, int, const void *) { return PAM_SUCCESS; } +int pam_setcred(pam_handle_t *, int) { return PAM_SUCCESS; } +const char *pam_strerror(pam_handle_t *, int) { return "simulated PAM result"; } + +int pam_authenticate(pam_handle_t *handle, int) +{ + ++handle->attempts; + if (handle->service == "missing") { + return PAM_MODULE_UNKNOWN; + } + if (handle->service == "transient" && handle->attempts == 1) { + return PAM_AUTHINFO_UNAVAIL; + } + if (handle->service == "timeout" && handle->attempts == 1) { + return PAM_MAXTRIES; + } + pam_message message{PAM_PROMPT_ECHO_OFF, "Test credential:"}; + const pam_message *messages = &message; + pam_response *response = nullptr; + const auto &conversation = handle->conversation; + const int result = conversation.conv(1, &messages, &response, conversation.appdata_ptr); + if (result != PAM_SUCCESS) { + return result; + } + const bool accepted = response && response->resp && std::strcmp(response->resp, "correct") == 0; + if (response) { + std::free(response->resp); + std::free(response); + } + return accepted ? PAM_SUCCESS : PAM_AUTH_ERR; +} + +class RetryTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void retryWithActivePassword_data() + { + QTest::addColumn("service"); + QTest::newRow("temporary unavailability") << QStringLiteral("transient"); + QTest::newRow("attempt timeout") << QStringLiteral("timeout"); + } + + void retryWithActivePassword() + { + QFETCH(QString, service); + auto password = std::make_unique(QStringLiteral("password"), QStringLiteral("test")); + auto fingerprint = std::make_unique(service, QStringLiteral("test"), PamAuthenticator::Fingerprint); + auto *fingerprintPtr = fingerprint.get(); + QSignalSpy passwordPrompts(password.get(), &PamAuthenticator::promptForSecret); + QSignalSpy fingerprintPrompts(fingerprintPtr, &PamAuthenticator::promptForSecret); + QSignalSpy fingerprintFailures(fingerprintPtr, &PamAuthenticator::failed); + std::vector> others; + others.push_back(std::move(fingerprint)); + PamAuthenticators authenticators(std::move(password), std::move(others)); + QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded); + + authenticators.startAuthenticating(); + QTRY_COMPARE(passwordPrompts.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(fingerprintFailures.count(), 1, 2000); + QTRY_VERIFY(!fingerprintPtr->isAvailable()); + QCOMPARE(authenticators.state(), PamAuthenticators::Authenticating); + QVERIFY(!authenticators.isUnlocked()); + + authenticators.startAuthenticating(); + QTRY_COMPARE_WITH_TIMEOUT(fingerprintPrompts.count(), 1, 2000); + QTRY_VERIFY(fingerprintPtr->isAvailable()); + // Repeated starts must not duplicate either active conversation. + authenticators.startAuthenticating(); + QTest::qWait(100); + QCOMPARE(passwordPrompts.count(), 1); + QCOMPARE(fingerprintPrompts.count(), 1); + QCOMPARE(successes.count(), 0); + + // The original password conversation still accepts a response. + authenticators.respond("correct"); + QTRY_COMPARE(successes.count(), 1); + QVERIFY(authenticators.isUnlocked()); + } + + void permanentUnavailability_data() + { + QTest::addColumn("service"); + QTest::addColumn("type"); + QTest::newRow("absent smartcard") << QStringLiteral("transient") << int(PamAuthenticator::Smartcard); + QTest::newRow("missing fingerprint module") << QStringLiteral("missing") << int(PamAuthenticator::Fingerprint); + } + + void permanentUnavailability() + { + QFETCH(QString, service); + QFETCH(int, type); + PamAuthenticator auth(service, QStringLiteral("test"), PamAuthenticator::NoninteractiveAuthenticatorType(type)); + QSignalSpy availability(&auth, &PamAuthenticator::availableChanged); + QSignalSpy prompts(&auth, &PamAuthenticator::promptForSecret); + auth.tryUnlock(); + QTRY_VERIFY(availability.count() >= 3); + QVERIFY(!auth.isAvailable()); + availability.clear(); + auth.tryUnlock(); + QTest::qWait(100); + QCOMPARE(availability.count(), 0); + QCOMPARE(prompts.count(), 0); + QVERIFY(!auth.isUnlocked()); + } + + void graceAndIncorrectPassword() + { + auto password = std::make_unique(QStringLiteral("password"), QStringLiteral("test")); + PamAuthenticators authenticators(std::move(password), {}); + QSignalSpy prompts(&authenticators, &PamAuthenticators::promptForSecretChanged); + QSignalSpy failures(&authenticators, &PamAuthenticators::failed); + QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded); + authenticators.setGraceLocked(true); + authenticators.startAuthenticating(); + QTest::qWait(100); + QCOMPARE(prompts.count(), 0); + authenticators.setGraceLocked(false); + authenticators.startAuthenticating(); + QTRY_COMPARE(prompts.count(), 1); + authenticators.respond("incorrect"); + QTRY_COMPARE(failures.count(), 1); + QCOMPARE(successes.count(), 0); + QVERIFY(!authenticators.isUnlocked()); + authenticators.startAuthenticating(); + QTRY_COMPARE(prompts.count(), 2); + authenticators.respond("correct"); + QTRY_COMPARE(successes.count(), 1); + } +}; + +QTEST_GUILESS_MAIN(RetryTest) +#include "retrytest.moc" diff --git a/scripts/build-patched-kscreenlocker-rpm b/scripts/build-patched-kscreenlocker-rpm index bde5f67..e657e84 100755 --- a/scripts/build-patched-kscreenlocker-rpm +++ b/scripts/build-patched-kscreenlocker-rpm @@ -5,13 +5,15 @@ set -euo pipefail repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) source_dir="$repo_root/plasma-fingerprint-workaround/kscreenlocker" topdir="$repo_root/rpmbuild" -version=${KSCREENLOCKER_VERSION:-6.7.4} +version=${KSCREENLOCKER_VERSION:-6.7.5} base_release=${KSCREENLOCKER_BASE_RELEASE:-1} -workaround_release=${KSCREENLOCKER_WORKAROUND_RELEASE:-5} +workaround_release=${KSCREENLOCKER_WORKAROUND_RELEASE:-6} archive="$topdir/SOURCES/kscreenlocker-$version.tar.xz" archive_url="https://download.kde.org/stable/plasma/$version/kscreenlocker-$version.tar.xz" -if [[ $version == 6.7.4 ]]; then +if [[ $version == 6.7.5 ]]; then + default_archive_sha256=6c41ce2cb79d2b85d4fbe5a3a61c09e40c70e5a869ae6f0352c7e1cb3b44606b +elif [[ $version == 6.7.4 ]]; then default_archive_sha256=f9cd525d501f08931f2c0b4d08f5384588967bc89fac62e39ed4ae2da2925a39 else default_archive_sha256=