mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2026-03-11 08:54:48 +00:00
Merge 61cb573f62 into d9be1b0682
This commit is contained in:
commit
4d2d0a82df
17 changed files with 222 additions and 38 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -546,8 +546,11 @@ QJsonObject BrowserAction::handlePasskeysGet(const QJsonObject& json, const QStr
|
|||
return getErrorReply(action, ERROR_PASSKEYS_INVALID_URL_PROVIDED);
|
||||
}
|
||||
|
||||
const auto relatedOrigins =
|
||||
browserMessageBuilder()->getStringListFromJsonArray(browserRequest.getArray("relatedOrigins"));
|
||||
const auto keyList = getConnectionKeys(browserRequest);
|
||||
const auto response = browserService()->showPasskeysAuthenticationPrompt(publicKey, origin, keyList);
|
||||
const auto response =
|
||||
browserService()->showPasskeysAuthenticationPrompt(publicKey, origin, relatedOrigins, keyList);
|
||||
|
||||
const Parameters params{{"response", response}};
|
||||
return buildResponse(action, browserRequest.incrementedNonce, params);
|
||||
|
|
@ -580,8 +583,11 @@ QJsonObject BrowserAction::handlePasskeysRegister(const QJsonObject& json, const
|
|||
}
|
||||
|
||||
const auto groupName = browserRequest.getString("groupName");
|
||||
const auto relatedOrigins =
|
||||
browserMessageBuilder()->getStringListFromJsonArray(browserRequest.getArray("relatedOrigins"));
|
||||
const auto keyList = getConnectionKeys(browserRequest);
|
||||
const auto response = browserService()->showPasskeysRegisterPrompt(publicKey, origin, groupName, keyList);
|
||||
const auto response =
|
||||
browserService()->showPasskeysRegisterPrompt(publicKey, origin, relatedOrigins, groupName, keyList);
|
||||
|
||||
const Parameters params{{"response", response}};
|
||||
return buildResponse(action, browserRequest.incrementedNonce, params);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -370,3 +370,15 @@ QString BrowserMessageBuilder::getSha256HashAsBase64(const QString& str) const
|
|||
{
|
||||
return getBase64FromArray(QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Sha256));
|
||||
}
|
||||
|
||||
QStringList BrowserMessageBuilder::getStringListFromJsonArray(const QJsonArray& jsonArray) const
|
||||
{
|
||||
QStringList stringList;
|
||||
for (const auto& item : jsonArray) {
|
||||
if (item.isString()) {
|
||||
stringList << item.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return stringList;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -106,6 +106,7 @@ public:
|
|||
QByteArray getArrayFromBase64(const QString& base64str) const;
|
||||
QByteArray getSha256Hash(const QString& str) const;
|
||||
QString getSha256HashAsBase64(const QString& str) const;
|
||||
QStringList getStringListFromJsonArray(const QJsonArray& jsonArray) const;
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(BrowserMessageBuilder);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -33,6 +33,7 @@ BrowserPasskeysClient* BrowserPasskeysClient::instance()
|
|||
// https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#createCredential
|
||||
int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
QJsonObject* result) const
|
||||
{
|
||||
if (!result || publicKeyOptions.isEmpty()) {
|
||||
|
|
@ -41,14 +42,14 @@ int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publi
|
|||
|
||||
// Check validity of some basic values
|
||||
const auto checkResultError = passkeyUtils()->checkLimits(publicKeyOptions);
|
||||
if (checkResultError > 0) {
|
||||
if (checkResultError != PASSKEYS_SUCCESS) {
|
||||
return checkResultError;
|
||||
}
|
||||
|
||||
// Get effective domain
|
||||
QString effectiveDomain;
|
||||
const auto effectiveDomainResponse = passkeyUtils()->getEffectiveDomain(origin, &effectiveDomain);
|
||||
if (effectiveDomainResponse > 0) {
|
||||
if (effectiveDomainResponse != PASSKEYS_SUCCESS) {
|
||||
return effectiveDomainResponse;
|
||||
}
|
||||
|
||||
|
|
@ -56,8 +57,11 @@ int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publi
|
|||
QString rpId;
|
||||
const auto rpName = publicKeyOptions["rp"]["name"].toString();
|
||||
const auto rpIdResponse = passkeyUtils()->validateRpId(publicKeyOptions["rp"]["id"], effectiveDomain, &rpId);
|
||||
if (rpIdResponse > 0) {
|
||||
return rpIdResponse;
|
||||
if (rpIdResponse != PASSKEYS_SUCCESS) {
|
||||
// Validate Related Origin Requests if found
|
||||
if (relatedOrigins.isEmpty() || !passkeyUtils()->validateRelatedOrigins(relatedOrigins, origin)) {
|
||||
return rpIdResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Check PublicKeyCredentialTypes
|
||||
|
|
@ -126,6 +130,7 @@ int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publi
|
|||
// https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#getAssertion
|
||||
int BrowserPasskeysClient::getAssertionOptions(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
QJsonObject* result) const
|
||||
{
|
||||
if (!result || publicKeyOptions.isEmpty()) {
|
||||
|
|
@ -135,15 +140,18 @@ int BrowserPasskeysClient::getAssertionOptions(const QJsonObject& publicKeyOptio
|
|||
// Get effective domain
|
||||
QString effectiveDomain;
|
||||
const auto effectiveDomainResponse = passkeyUtils()->getEffectiveDomain(origin, &effectiveDomain);
|
||||
if (effectiveDomainResponse > 0) {
|
||||
if (effectiveDomainResponse != PASSKEYS_SUCCESS) {
|
||||
return effectiveDomainResponse;
|
||||
}
|
||||
|
||||
// Validate RP ID
|
||||
QString rpId;
|
||||
const auto rpIdResponse = passkeyUtils()->validateRpId(publicKeyOptions["rpId"], effectiveDomain, &rpId);
|
||||
if (rpIdResponse > 0) {
|
||||
return rpIdResponse;
|
||||
if (rpIdResponse != PASSKEYS_SUCCESS) {
|
||||
// Validate Related Origin Requests if found
|
||||
if (relatedOrigins.isEmpty() || !passkeyUtils()->validateRelatedOrigins(relatedOrigins, origin)) {
|
||||
return rpIdResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -31,9 +31,14 @@ public:
|
|||
~BrowserPasskeysClient() = default;
|
||||
static BrowserPasskeysClient* instance();
|
||||
|
||||
int
|
||||
getCredentialCreationOptions(const QJsonObject& publicKeyOptions, const QString& origin, QJsonObject* result) const;
|
||||
int getAssertionOptions(const QJsonObject& publicKeyOptions, const QString& origin, QJsonObject* result) const;
|
||||
int getCredentialCreationOptions(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
QJsonObject* result) const;
|
||||
int getAssertionOptions(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
QJsonObject* result) const;
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(BrowserPasskeysClient);
|
||||
|
|
|
|||
|
|
@ -638,6 +638,7 @@ QString BrowserService::getKey(const QString& id)
|
|||
// Passkey registration
|
||||
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
const QString& groupName,
|
||||
const StringPairList& keyList)
|
||||
{
|
||||
|
|
@ -647,9 +648,9 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
|
|||
}
|
||||
|
||||
QJsonObject credentialCreationOptions;
|
||||
const auto pkOptionsResult =
|
||||
browserPasskeysClient()->getCredentialCreationOptions(publicKeyOptions, origin, &credentialCreationOptions);
|
||||
if (pkOptionsResult > 0 || credentialCreationOptions.isEmpty()) {
|
||||
const auto pkOptionsResult = browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyOptions, origin, relatedOrigins, &credentialCreationOptions);
|
||||
if (pkOptionsResult != PASSKEYS_SUCCESS || credentialCreationOptions.isEmpty()) {
|
||||
return getPasskeyError(pkOptionsResult);
|
||||
}
|
||||
|
||||
|
|
@ -737,6 +738,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
|
|||
// Passkey authentication
|
||||
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
const StringPairList& keyList)
|
||||
{
|
||||
auto db = getDatabase();
|
||||
|
|
@ -746,8 +748,8 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
|
|||
|
||||
QJsonObject assertionOptions;
|
||||
const auto assertionResult =
|
||||
browserPasskeysClient()->getAssertionOptions(publicKeyOptions, origin, &assertionOptions);
|
||||
if (assertionResult > 0 || assertionOptions.isEmpty()) {
|
||||
browserPasskeysClient()->getAssertionOptions(publicKeyOptions, origin, relatedOrigins, &assertionOptions);
|
||||
if (assertionResult != PASSKEYS_SUCCESS || assertionOptions.isEmpty()) {
|
||||
return getPasskeyError(assertionResult);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
|
||||
* Copyright (C) 2013 Francois Ferrand
|
||||
*
|
||||
|
|
@ -90,10 +90,12 @@ public:
|
|||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
QJsonObject showPasskeysRegisterPrompt(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
const QString& groupName,
|
||||
const StringPairList& keyList);
|
||||
QJsonObject showPasskeysAuthenticationPrompt(const QJsonObject& publicKeyOptions,
|
||||
const QString& origin,
|
||||
const QStringList& relatedOrigins,
|
||||
const StringPairList& keyList);
|
||||
void addPasskeyToGroup(const QSharedPointer<Database>& db,
|
||||
Group* group,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <QList>
|
||||
#include <QUrl>
|
||||
|
||||
#define MAX_SEEN_LABELS 10
|
||||
|
||||
Q_GLOBAL_STATIC(PasskeyUtils, s_passkeyUtils);
|
||||
|
||||
PasskeyUtils* PasskeyUtils::instance()
|
||||
|
|
@ -135,6 +137,37 @@ int PasskeyUtils::validateRpId(const QJsonValue& rpIdValue, const QString& effec
|
|||
return PASSKEYS_SUCCESS;
|
||||
}
|
||||
|
||||
// The steps for validation: https://www.w3.org/TR/webauthn-3/#sctn-validating-relation-origin
|
||||
bool PasskeyUtils::validateRelatedOrigins(const QStringList& relatedOrigins, const QString& origin) const
|
||||
{
|
||||
QSet<QString> labelsSeen;
|
||||
|
||||
for (const auto& originItem : relatedOrigins) {
|
||||
QString effectiveDomain;
|
||||
if (passkeyUtils()->getEffectiveDomain(originItem, &effectiveDomain) != PASSKEYS_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto label = urlTools()->getBaseDomainFromUrl(originItem, true);
|
||||
if (label.isNull() || label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (labelsSeen.size() >= MAX_SEEN_LABELS && !labelsSeen.contains(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (originItem == origin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (labelsSeen.size() < MAX_SEEN_LABELS) {
|
||||
labelsSeen.insert(label);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dom-publickeycredentialcreationoptions-attestation
|
||||
QString PasskeyUtils::parseAttestation(const QString& attestation) const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -50,6 +50,7 @@ public:
|
|||
bool checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const;
|
||||
int getEffectiveDomain(const QString& origin, QString* result) const;
|
||||
int validateRpId(const QJsonValue& rpIdValue, const QString& effectiveDomain, QString* result) const;
|
||||
bool validateRelatedOrigins(const QStringList& relatedOrigins, const QString& origin) const;
|
||||
QString parseAttestation(const QString& attestation) const;
|
||||
QJsonArray parseCredentialTypes(const QJsonArray& credentialTypes) const;
|
||||
bool isAuthenticatorSelectionValid(const QJsonObject& authenticatorSelection) const;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -53,10 +53,13 @@ QUrl UrlTools::getRedirectTarget(QNetworkReply* reply) const
|
|||
/**
|
||||
* Gets the base domain of URL or hostname.
|
||||
*
|
||||
* If returnOnlyLabel is true, return only the Registrable Origin Label:
|
||||
* https://www.w3.org/TR/webauthn-3/#registrable-origin-label
|
||||
*
|
||||
* Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk
|
||||
* Up-to-date list can be found: https://publicsuffix.org/list/public_suffix_list.dat
|
||||
*/
|
||||
QString UrlTools::getBaseDomainFromUrl(const QString& url) const
|
||||
QString UrlTools::getBaseDomainFromUrl(const QString& url, bool returnOnlyLabel) const
|
||||
{
|
||||
auto qUrl = QUrl::fromUserInput(url);
|
||||
|
||||
|
|
@ -74,8 +77,11 @@ QString UrlTools::getBaseDomainFromUrl(const QString& url) const
|
|||
host.chop(tld.length() + 1);
|
||||
// Split the URL and select the last part, e.g. https://another.example -> example
|
||||
QString baseDomain = host.split('.').last();
|
||||
// Append the top level domain back to the URL, e.g. example -> example.co.uk
|
||||
baseDomain.append(QString(".%1").arg(tld));
|
||||
|
||||
if (!returnOnlyLabel) {
|
||||
// Append the top level domain back to the URL, e.g. example -> example.co.uk
|
||||
baseDomain.append(QString(".%1").arg(tld));
|
||||
}
|
||||
|
||||
return baseDomain;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -36,7 +36,7 @@ public:
|
|||
|
||||
#if defined(WITH_XC_NETWORKING) || defined(WITH_XC_BROWSER)
|
||||
QUrl getRedirectTarget(QNetworkReply* reply) const;
|
||||
QString getBaseDomainFromUrl(const QString& url) const;
|
||||
QString getBaseDomainFromUrl(const QString& url, bool returnOnlyLabel = false) const;
|
||||
QString getTopLevelDomainFromUrl(const QString& url) const;
|
||||
bool isIpAddress(const QString& host) const;
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@
|
|||
#include "core/Tools.h"
|
||||
#include "crypto/Crypto.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStringList>
|
||||
#include <QTest>
|
||||
|
||||
#include <botan/sodium.h>
|
||||
|
|
@ -111,6 +113,17 @@ void TestBrowser::testIncrementNonce()
|
|||
QCOMPARE(result, INCREMENTEDNONCE);
|
||||
}
|
||||
|
||||
void TestBrowser::testGetStringListFromJsonArray()
|
||||
{
|
||||
QJsonArray array = {QString("first"), QString("second")};
|
||||
QJsonArray mixedArray = {1, 2.2, QString()};
|
||||
QJsonArray emptyArray = {};
|
||||
|
||||
QCOMPARE(browserMessageBuilder()->getStringListFromJsonArray(array), QStringList({"first", "second"}));
|
||||
QCOMPARE(browserMessageBuilder()->getStringListFromJsonArray(mixedArray), QStringList({""}));
|
||||
QCOMPARE(browserMessageBuilder()->getStringListFromJsonArray(emptyArray), QStringList({}));
|
||||
}
|
||||
|
||||
void TestBrowser::testBuildResponse()
|
||||
{
|
||||
const auto object = QJsonObject{{"test", true}};
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ private slots:
|
|||
void testDecryptMessage();
|
||||
void testGetBase64FromKey();
|
||||
void testIncrementNonce();
|
||||
void testGetStringListFromJsonArray();
|
||||
void testBuildResponse();
|
||||
void testSortPriority();
|
||||
void testSortPriority_data();
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
|
|||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
QJsonObject credentialCreationOptions;
|
||||
browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
|
||||
publicKeyCredentialOptions, QString("https://webauthn.io"), {}, &credentialCreationOptions);
|
||||
|
||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
|
|
@ -346,7 +346,7 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA()
|
|||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
QJsonObject credentialCreationOptions;
|
||||
browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
|
||||
publicKeyCredentialOptions, QString("https://webauthn.io"), {}, &credentialCreationOptions);
|
||||
credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams;
|
||||
|
||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||
|
|
@ -397,7 +397,7 @@ void TestPasskeys::testRegister()
|
|||
|
||||
QJsonObject credentialCreationOptions;
|
||||
const auto creationResult = browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, origin, &credentialCreationOptions);
|
||||
publicKeyCredentialOptions, origin, {}, &credentialCreationOptions);
|
||||
QVERIFY(creationResult == 0);
|
||||
|
||||
TestingVariables testingVariables = {predefinedId, QString(), QString(), predefinedData};
|
||||
|
|
@ -424,6 +424,22 @@ void TestPasskeys::testRegister()
|
|||
QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
|
||||
}
|
||||
|
||||
void TestPasskeys::testRegisterWithRelatedOrigins()
|
||||
{
|
||||
const auto origin = QString("https://webauthn.io");
|
||||
|
||||
// Modify the RP ID not to match with the origin. Actual accepted origin is inside Related Origins list.
|
||||
const auto credentialOptions = QString(PublicKeyCredentialOptions).replace("webauthn.io", "example.io");
|
||||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(credentialOptions.toUtf8());
|
||||
|
||||
// Only check that the creation succeeds with the Related Origins list
|
||||
const auto relatedOrigins = QStringList({"https://webauthn.io"});
|
||||
QJsonObject credentialCreationOptions;
|
||||
const auto creationResult = browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, origin, relatedOrigins, &credentialCreationOptions);
|
||||
QVERIFY(creationResult == 0);
|
||||
}
|
||||
|
||||
void TestPasskeys::testGet()
|
||||
{
|
||||
#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2, 14, 0)
|
||||
|
|
@ -441,7 +457,7 @@ void TestPasskeys::testGet()
|
|||
|
||||
QJsonObject assertionOptions;
|
||||
const auto assertionResult =
|
||||
browserPasskeysClient()->getAssertionOptions(publicKeyCredentialRequestOptions, origin, &assertionOptions);
|
||||
browserPasskeysClient()->getAssertionOptions(publicKeyCredentialRequestOptions, origin, {}, &assertionOptions);
|
||||
QVERIFY(assertionResult == 0);
|
||||
|
||||
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, id, {}, privateKeyPem);
|
||||
|
|
@ -464,6 +480,22 @@ void TestPasskeys::testGet()
|
|||
QCOMPARE(clientDataJsonObject["challenge"].toString(), publicKeyCredentialRequestOptions["challenge"].toString());
|
||||
}
|
||||
|
||||
void TestPasskeys::testGetWithRelatedOrigins()
|
||||
{
|
||||
const auto origin = QString("https://webauthn.io");
|
||||
|
||||
// Modify the RP ID not to match with the origin. Actual accepted origin is inside Related Origins list.
|
||||
const auto credentialOptions = QString(PublicKeyCredentialRequestOptions).replace("webauthn.io", "example.io");
|
||||
const auto publicKeyCredentialRequestOptions = browserMessageBuilder()->getJsonObject(credentialOptions.toUtf8());
|
||||
|
||||
// Only check that the assertion succeeds with the Related Origins list
|
||||
const auto relatedOrigins = QStringList({"https://webauthn.io"});
|
||||
QJsonObject assertionOptions;
|
||||
const auto assertionResult = browserPasskeysClient()->getAssertionOptions(
|
||||
publicKeyCredentialRequestOptions, origin, relatedOrigins, &assertionOptions);
|
||||
QVERIFY(assertionResult == 0);
|
||||
}
|
||||
|
||||
void TestPasskeys::testExtensions()
|
||||
{
|
||||
auto extensions = QJsonObject({{"credProps", true}, {"uvm", true}});
|
||||
|
|
@ -633,6 +665,52 @@ void TestPasskeys::testRpIdValidation()
|
|||
QCOMPARE(differentDomain, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
}
|
||||
|
||||
void TestPasskeys::testRelatedOriginsValidation()
|
||||
{
|
||||
// A valid case. Matches with https://accountscenter.facebook.com when RP ID is e.g. accounts.meta.com.
|
||||
QString origin = "https://accountscenter.facebook.com";
|
||||
const QStringList relatedOrigins = {"https://messenger.com",
|
||||
"https://www.messenger.com",
|
||||
"https://facebook.com",
|
||||
"https://web.facebook.com",
|
||||
"https://www.facebook.com",
|
||||
"https://m.facebook.com",
|
||||
"https://business.facebook.com",
|
||||
"https://accountscenter.facebook.com",
|
||||
"https://accounts.meta.com",
|
||||
"https://accountscenter.meta.com"};
|
||||
QVERIFY(passkeyUtils()->validateRelatedOrigins(relatedOrigins, origin));
|
||||
|
||||
// Failed case. The origin differs from all related origins.
|
||||
origin = "https://accountscenter.example.com";
|
||||
QVERIFY(!passkeyUtils()->validateRelatedOrigins(relatedOrigins, origin));
|
||||
|
||||
// Failed case where MAX_SEEN_LABELS has been met (too many different labels)
|
||||
const QStringList failedRelatedOrigins = {
|
||||
"https://messenge1r.com",
|
||||
"https://www.messenger2.com",
|
||||
"https://facebook3.com",
|
||||
"https://web.facebook4.com",
|
||||
"https://www.facebook5.com",
|
||||
"https://m.facebook6.com",
|
||||
"https://business.facebook7.com",
|
||||
"https://accountscenter.meta.com"
|
||||
"https://accounts.meta1.com",
|
||||
"https://accounts.meta2.com",
|
||||
"https://accounts.met3a.com",
|
||||
"https://accounts.meta4.com",
|
||||
"https://accounts.meta5.com",
|
||||
"https://accounts.meta6.com",
|
||||
"https://accounts.meta7.com",
|
||||
"https://accounts.meta8.com",
|
||||
"https://accounts.meta9.com",
|
||||
"https://accounts.meta10.com",
|
||||
"https://accounts.meta11.com",
|
||||
"https://accountscenter.facebook.com",
|
||||
};
|
||||
QVERIFY(!passkeyUtils()->validateRelatedOrigins(failedRelatedOrigins, origin));
|
||||
}
|
||||
|
||||
void TestPasskeys::testParseAttestation()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->parseAttestation(QString("")) == QString("none"));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -38,7 +38,9 @@ private slots:
|
|||
void testCreatingAttestationObjectWithEC();
|
||||
void testCreatingAttestationObjectWithRSA();
|
||||
void testRegister();
|
||||
void testRegisterWithRelatedOrigins();
|
||||
void testGet();
|
||||
void testGetWithRelatedOrigins();
|
||||
|
||||
void testExtensions();
|
||||
void testParseFlags();
|
||||
|
|
@ -48,6 +50,7 @@ private slots:
|
|||
void testIsDomain();
|
||||
void testRegistrableDomainSuffix();
|
||||
void testRpIdValidation();
|
||||
void testRelatedOriginsValidation();
|
||||
void testParseAttestation();
|
||||
void testParseCredentialTypes();
|
||||
void testIsAuthenticatorSelectionValid();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -70,6 +70,18 @@ void TestUrlTools::testTopLevelDomain()
|
|||
}
|
||||
}
|
||||
|
||||
void TestUrlTools::testTopLevelDomainWithReturnOnlyLabel()
|
||||
{
|
||||
QList<QPair<QString, QString>> urls{
|
||||
{QString("https://another.example.co.uk"), QString("example")},
|
||||
{QString("https://www.example.com"), QString("example")},
|
||||
};
|
||||
|
||||
for (const auto& u : urls) {
|
||||
QCOMPARE(urlTools()->getBaseDomainFromUrl(u.first, true), u.second);
|
||||
}
|
||||
}
|
||||
|
||||
void TestUrlTools::testIsIpAddress()
|
||||
{
|
||||
auto host1 = "example.com"; // Not valid
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
|
|
@ -31,6 +31,7 @@ private slots:
|
|||
void init();
|
||||
|
||||
void testTopLevelDomain();
|
||||
void testTopLevelDomainWithReturnOnlyLabel();
|
||||
void testIsIpAddress();
|
||||
void testIsUrlIdentical();
|
||||
void testIsUrlValid();
|
||||
|
|
|
|||
Loading…
Reference in a new issue