From 1ce2ce9bb8ecae342ca6ce407e5b573dd0d7736c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Sun, 8 Mar 2026 14:08:15 +0200 Subject: [PATCH] Passkeys: Set BE and BS flags to true (#13042) Passkeys: Set BE flag to true --------- Co-authored-by: varjolintu --- src/browser/BrowserPasskeys.cpp | 25 +++++++++++++++++-------- src/browser/BrowserPasskeys.h | 13 ++++++++++--- src/browser/BrowserService.cpp | 16 +++++++++++++--- src/core/EntryAttributes.cpp | 4 +++- src/core/EntryAttributes.h | 4 +++- tests/TestPasskeys.cpp | 33 ++++++++++++++++++++++----------- 6 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/browser/BrowserPasskeys.cpp b/src/browser/BrowserPasskeys.cpp index 2560b9428..361bed7dd 100644 --- a/src/browser/BrowserPasskeys.cpp +++ b/src/browser/BrowserPasskeys.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -125,14 +125,16 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& assertionOptions, const QString& credentialId, const QString& userHandle, - const QString& privateKeyPem) + const QString& privateKeyPem, + const bool beFlag, + const bool bsFlag) { if (!passkeyUtils()->checkCredentialAssertionOptions(assertionOptions)) { return {}; } - const auto authenticatorData = - buildAuthenticatorData(assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString()); + const auto authenticatorData = buildAuthenticatorData( + assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString(), beFlag, bsFlag); const auto clientDataJson = assertionOptions["clientDataJson"].toString(); const auto clientDataArray = clientDataJson.toUtf8(); @@ -171,8 +173,12 @@ QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credential result.append(rpIdHash); // Use default flags - const auto flags = setFlagsFromJson(QJsonObject( - {{"ED", !extensions.isEmpty()}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}})); + const auto flags = setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()}, + {"AT", true}, + {"BS", DEFAULT_BS_FLAG}, + {"BE", DEFAULT_BE_FLAG}, + {"UV", true}, + {"UP", true}})); result.append(flags); // Signature counter (not supported, always 0 @@ -204,7 +210,10 @@ QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credential } // Build a short version of the attestation object for webauthn.get -QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QString& extensions) +QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, + const QString& extensions, + const bool beFlag, + const bool bsFlag) { QByteArray result; @@ -212,7 +221,7 @@ QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QS result.append(rpIdHash); const auto flags = setFlagsFromJson(QJsonObject( - {{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}})); + {{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", bsFlag}, {"BE", beFlag}, {"UV", true}, {"UP", true}})); result.append(flags); // Signature counter (not supported, always 0 diff --git a/src/browser/BrowserPasskeys.h b/src/browser/BrowserPasskeys.h index 230dee63f..83ba625c3 100644 --- a/src/browser/BrowserPasskeys.h +++ b/src/browser/BrowserPasskeys.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,6 +25,8 @@ #include #include +#define DEFAULT_BE_FLAG true +#define DEFAULT_BS_FLAG true #define ID_BYTES 32 #define HASH_BYTES 32 #define RSA_BITS 2048 @@ -87,7 +89,9 @@ public: QJsonObject buildGetPublicKeyCredential(const QJsonObject& assertionOptions, const QString& credentialId, const QString& userHandle, - const QString& privateKeyPem); + const QString& privateKeyPem, + const bool beFlag = DEFAULT_BE_FLAG, + const bool bsFlag = DEFAULT_BE_FLAG); static const QString AAGUID; @@ -113,7 +117,10 @@ private: const QString& credentialId, const QByteArray& cborEncodedPublicKey, const TestingVariables& testingVariables = {}); - QByteArray buildAuthenticatorData(const QString& rpId, const QString& extensions); + QByteArray buildAuthenticatorData(const QString& rpId, + const QString& extensions, + const bool beFlag = DEFAULT_BE_FLAG, + const bool bsFlag = DEFAULT_BE_FLAG); AttestationKeyPair buildCredentialPrivateKey(int alg, const TestingVariables& testingVariables = {}); QByteArray buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem); diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 9ce1293f1..a421d080f 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2017 Sami Vänttinen * Copyright (C) 2013 Francois Ferrand * @@ -775,8 +775,16 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry); const auto userHandle = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE); - auto publicKeyCredential = - browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem); + // Get BE and BS flags if present + const auto beFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BE) + ? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BE) == TRUE_STR + : DEFAULT_BE_FLAG; + const auto bsFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BS) + ? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BS) == TRUE_STR + : DEFAULT_BS_FLAG; + + auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential( + assertionOptions, credentialId, userHandle, privateKeyPem, beFlag, bsFlag); if (publicKeyCredential.isEmpty()) { return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR); } @@ -856,6 +864,8 @@ void BrowserService::addPasskeyToEntry(Entry* entry, entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true); entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId); entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true); + entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BE, TRUE_STR); + entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BS, TRUE_STR); entry->addTag(tr("Passkey")); entry->endUpdate(); diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index dba4d8962..88d32ae96 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2012 Felix Geyer * * This program is free software: you can redistribute it and/or modify @@ -46,6 +46,8 @@ const QString EntryAttributes::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX const QString EntryAttributes::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE"); const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START = QStringLiteral("-----BEGIN PRIVATE KEY-----"); const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END = QStringLiteral("-----END PRIVATE KEY-----"); +const QString EntryAttributes::KPEX_PASSKEY_FLAG_BE = QStringLiteral("KPEX_PASSKEY_FLAG_BE"); +const QString EntryAttributes::KPEX_PASSKEY_FLAG_BS = QStringLiteral("KPEX_PASSKEY_FLAG_BS"); // For compatibility with StrongBox const QString EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID"); diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h index d0767a4c1..14fd0c30e 100644 --- a/src/core/EntryAttributes.h +++ b/src/core/EntryAttributes.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2012 Felix Geyer * * This program is free software: you can redistribute it and/or modify @@ -75,6 +75,8 @@ public: static const QString KPEX_PASSKEY_USER_HANDLE; static const QString KPEX_PASSKEY_PRIVATE_KEY_START; static const QString KPEX_PASSKEY_PRIVATE_KEY_END; + static const QString KPEX_PASSKEY_FLAG_BE; + static const QString KPEX_PASSKEY_FLAG_BS; static bool isDefaultAttribute(const QString& key); static bool isPasskeyAttribute(const QString& key); diff --git a/tests/TestPasskeys.cpp b/tests/TestPasskeys.cpp index 878732e3e..82f649e9c 100644 --- a/tests/TestPasskeys.cpp +++ b/tests/TestPasskeys.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -77,7 +77,7 @@ const QString PublicKeyCredential = R"( "id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8", "rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef", "response": { - "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIHK1iVimeR02UYipyiEKrKhhfhJRMew8EbDWGKtMZ2wUIlggbtZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU", + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBdAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIHK1iVimeR02UYipyiEKrKhhfhJRMew8EbDWGKtMZ2wUIlggbtZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" }, "type": "public-key" @@ -185,6 +185,8 @@ void TestPasskeys::testDecodeResponseData() QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")); QCOMPARE(flags["AT"], true); QCOMPARE(flags["UP"], true); + QCOMPARE(flags["BE"], true); + QCOMPARE(flags["BS"], true); QCOMPARE(publicKey["1"], 2); QCOMPARE(publicKey["3"], -7); QCOMPARE(publicKey["-1"], 1); @@ -285,13 +287,18 @@ void TestPasskeys::testCreatingAttestationObjectWithEC() result, QString("\xA3" "cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0" - "9\x7F)%\x0B`\x84\x1E\xF0" - "E\x00\x00\x00\x01\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b\x00 \x8B\xB0\xCA" - "6\x17\xD6\xDE\x01\x11|\xEA\x94\r\xA0R\xC0\x80_\xF3r\xFBr\xB5\x02\x03:" - "\xBAr\x0Fi\x81\xFE\xA5\x01\x02\x03& \x01!X " - "e\xE2\xF2\x1F:cq\xD3G\xEA\xE0\xF7\x1F\xCF\xFA\\\xABO\xF6\x86\x88\x80\t\xAE\x81\x8BT\xB2\x9B\x15\x85~" - "\"X \\\x8E\x1E@\xDB\x97T-\xF8\x9B\xB0\xAD" - "5\xDC\x12^\xC3\x95\x05\xC6\xDF^\x03\xCB\xB4Q\x91\xFF|\xDB\x94\xB7")); + "9\x7F)%\x0B`\x84\x1E\xF0]\x00\x00\x00\x00\xFD\xB1" + "A\xB2]\x84" + "D>\x8A" + "5F\x98\xC2\x05\xA5\x02\x00 \xCA\xBC\xC5'\x99pr\x94\xF0`\xC3\x9D])\xB1\x17\x96\xF9q\x84%\xA8\x13" + "3m\xB5?w\xEA\x05,\xEF\xA5\x01\x02\x03& \x01!X \x06\xEC\xAF" + "4[b\x91" + "am\x19Y\x03\xA6P*\xCA" + "1\xC4\x95\xA8i\xE5\xF0\x87\xE5\xD4\xB8" + "2\xCD\b\x85\xDD\"X \xE2\xEE\x7F\xE9\x0F\x0E\xE9\x1D\x07\x83J\x03\t\xDB" + "B$\xB1\x0B\xD3%\xFF\x18" + "2\xE1S\x99\xB7\x1D" + "B\x04\xE7\x83")); // Double check that the result can be decoded BrowserCbor browserCbor; @@ -312,6 +319,8 @@ void TestPasskeys::testCreatingAttestationObjectWithEC() QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")); QCOMPARE(flags["AT"], true); QCOMPARE(flags["UP"], true); + QCOMPARE(flags["BE"], true); + QCOMPARE(flags["BS"], true); QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::EC2); QCOMPARE(publicKey["3"], WebAuthnAlgorithms::ES256); QCOMPARE(publicKey["-1"], 1); @@ -368,6 +377,8 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA() QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")); QCOMPARE(flags["AT"], true); QCOMPARE(flags["UP"], true); + QCOMPARE(flags["BE"], true); + QCOMPARE(flags["BS"], true); QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::RSA); QCOMPARE(publicKey["3"], WebAuthnAlgorithms::RS256); QCOMPARE(publicKey["-1"], predefinedModulus); @@ -438,14 +449,14 @@ void TestPasskeys::testGet() QCOMPARE(publicKeyCredential["id"].toString(), id); auto response = publicKeyCredential["response"].toObject(); - QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA")); + QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAdAAAAAA")); QCOMPARE(response["clientDataJSON"].toString(), QString("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtML" "Ux1R1Uxem9wUm1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYm" "F1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ")); QCOMPARE( response["signature"].toString(), - QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE")); + QString("MEUCIQCvg3nXO2fiNK9ockxscgPtoM9_u6ERaW2-F1L99YasOAIgNhYOjPJyKJ-W8roV531kC59ss1USas7jy8TfRnbJLtg")); auto clientDataJson = response["clientDataJSON"].toString(); auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);