This commit is contained in:
Sami Vänttinen 2026-03-09 18:59:34 +01:00 committed by GitHub
commit 4d2d0a82df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 222 additions and 38 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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);
}

View file

@ -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,

View file

@ -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
{

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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}};

View file

@ -36,6 +36,7 @@ private slots:
void testDecryptMessage();
void testGetBase64FromKey();
void testIncrementNonce();
void testGetStringListFromJsonArray();
void testBuildResponse();
void testSortPriority();
void testSortPriority_data();

View file

@ -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"));

View file

@ -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();

View file

@ -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

View file

@ -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();