Passkeys: Fix ordering of clientDataJSON

This commit is contained in:
varjolintu 2025-05-22 21:20:03 +03:00 committed by Jonathan White
parent f32ed71dfc
commit 5cb6ad6335
4 changed files with 21 additions and 25 deletions

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -71,7 +71,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
} }
const auto authenticatorAttachment = credentialCreationOptions["authenticatorAttachment"]; const auto authenticatorAttachment = credentialCreationOptions["authenticatorAttachment"];
const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toObject(); const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toString();
const auto extensions = credentialCreationOptions["extensions"].toString(); const auto extensions = credentialCreationOptions["extensions"].toString();
const auto credentialId = testingVariables.credentialId.isEmpty() const auto credentialId = testingVariables.credentialId.isEmpty()
? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES) ? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
@ -98,7 +98,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
// Response // Response
QJsonObject responseObject; QJsonObject responseObject;
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject); responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson); responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataJson.toUtf8());
responseObject["clientExtensionResults"] = credentialCreationOptions["clientExtensionResults"]; responseObject["clientExtensionResults"] = credentialCreationOptions["clientExtensionResults"];
// Additions for extension side functions // Additions for extension side functions
@ -130,8 +130,8 @@ QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& asse
const auto authenticatorData = const auto authenticatorData =
buildAuthenticatorData(assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString()); buildAuthenticatorData(assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString());
const auto clientDataJson = assertionOptions["clientDataJson"].toObject(); const auto clientDataJson = assertionOptions["clientDataJson"].toString();
const auto clientDataArray = QJsonDocument(clientDataJson).toJson(QJsonDocument::Compact); const auto clientDataArray = clientDataJson.toUtf8();
const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem); const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem);
if (signature.isEmpty()) { if (signature.isEmpty()) {
@ -140,7 +140,7 @@ QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& asse
QJsonObject responseObject; QJsonObject responseObject;
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData); responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson); responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
responseObject["clientExtensionResults"] = assertionOptions["clientExtensionResults"]; responseObject["clientExtensionResults"] = assertionOptions["clientExtensionResults"];
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature); responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
responseObject["userHandle"] = userHandle; responseObject["userHandle"] = userHandle;

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -53,8 +53,8 @@ bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialC
{ {
if (!credentialCreationOptions["attestation"].isString() if (!credentialCreationOptions["attestation"].isString()
|| credentialCreationOptions["attestation"].toString().isEmpty() || credentialCreationOptions["attestation"].toString().isEmpty()
|| !credentialCreationOptions["clientDataJSON"].isObject() || !credentialCreationOptions["clientDataJSON"].isString()
|| credentialCreationOptions["clientDataJSON"].toObject().isEmpty() || credentialCreationOptions["clientDataJSON"].toString().isEmpty()
|| !credentialCreationOptions["rp"].isObject() || credentialCreationOptions["rp"].toObject().isEmpty() || !credentialCreationOptions["rp"].isObject() || credentialCreationOptions["rp"].toObject().isEmpty()
|| !credentialCreationOptions["user"].isObject() || credentialCreationOptions["user"].toObject().isEmpty() || !credentialCreationOptions["user"].isObject() || credentialCreationOptions["user"].toObject().isEmpty()
|| !credentialCreationOptions["residentKey"].isBool() || credentialCreationOptions["residentKey"].isUndefined() || !credentialCreationOptions["residentKey"].isBool() || credentialCreationOptions["residentKey"].isUndefined()
@ -75,7 +75,7 @@ bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialC
// Basic check for the object that it contains necessary variables in a correct form // Basic check for the object that it contains necessary variables in a correct form
bool PasskeyUtils::checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const bool PasskeyUtils::checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const
{ {
if (!assertionOptions["clientDataJson"].isObject() || assertionOptions["clientDataJson"].toObject().isEmpty() if (!assertionOptions["clientDataJson"].isString() || assertionOptions["clientDataJson"].toString().isEmpty()
|| !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty() || !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty()
|| !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined() || !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined()
|| !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) { || !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) {
@ -352,15 +352,11 @@ ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) c
return {}; return {};
} }
QJsonObject PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const // Serialization order: https://w3c.github.io/webauthn/#clientdatajson-serialization
QString PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
{ {
QJsonObject clientData; return QString("{\"type\":\"%1\",\"challenge\":\"%2\",\"origin\":\"%3\",\"crossOrigin\":false}")
clientData["challenge"] = publicKey["challenge"]; .arg((get ? QString("webauthn.get") : QString("webauthn.create")), publicKey["challenge"].toString(), origin);
clientData["crossOrigin"] = false;
clientData["origin"] = origin;
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
return clientData;
} }
QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -58,7 +58,7 @@ public:
bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const; bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const;
bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const; bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const;
ExtensionResult buildExtensionData(QJsonObject& extensionObject) const; ExtensionResult buildExtensionData(QJsonObject& extensionObject) const;
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const; QString buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const;
QStringList getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const; QStringList getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const;
QString getCredentialIdFromEntry(const Entry* entry) const; QString getCredentialIdFromEntry(const Entry* entry) const;
QString getUsernameFromEntry(const Entry* entry) const; QString getUsernameFromEntry(const Entry* entry) const;

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -440,12 +440,12 @@ void TestPasskeys::testGet()
auto response = publicKeyCredential["response"].toObject(); auto response = publicKeyCredential["response"].toObject();
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA")); QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA"));
QCOMPARE(response["clientDataJSON"].toString(), QCOMPARE(response["clientDataJSON"].toString(),
QString("eyJjaGFsbGVuZ2UiOiI5ejM2dlRmUVRMOTVMZjdXblpneXRlN29oR2VGLVhSaUx4a0wtTHVHVTF6b3BSbU1JVUExTFZ3ekdwe" QString("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtML"
"UltMWZPQm4xUW5SYTBRSDI3QURBYUpHSHlzUSIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdX" "Ux1R1Uxem9wUm1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYm"
"Robi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ")); "F1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"));
QCOMPARE( QCOMPARE(
response["signature"].toString(), response["signature"].toString(),
QString("MEUCIHFv0lOOGGloi_XoH5s3QDSs__8yAp9ZTMEjNiacMpOxAiEA04LAfO6TE7j12XNxd3zHQpn4kZN82jQFPntPiPBSD5c")); QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
auto clientDataJson = response["clientDataJSON"].toString(); auto clientDataJson = response["clientDataJSON"].toString();
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson); auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);