2
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "TestPasskeys.h"
19
#include "browser/BrowserCbor.h"
20
#include "browser/BrowserMessageBuilder.h"
21
#include "browser/BrowserPasskeysClient.h"
22
#include "browser/BrowserService.h"
23
#include "browser/PasskeyUtils.h"
24
#include "core/Database.h"
25
#include "core/Entry.h"
26
#include "core/Group.h"
27
#include "crypto/Crypto.h"
30
#include <QJsonDocument>
32
#include <botan/sodium.h>
34
using namespace Botan::Sodium;
36
QTEST_GUILESS_MAIN(TestPasskeys)
40
const QString PublicKeyCredentialOptions = R"(
42
"attestation": "none",
43
"authenticatorSelection": {
44
"residentKey": "preferred",
45
"requireResidentKey": false,
46
"userVerification": "required"
48
"challenge": "lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw",
60
"name": "webauthn.io",
64
"excludeCredentials": [],
66
"displayName": "Test User",
67
"id": "VkdWemRDQlZjMlZ5",
74
const QString PublicKeyCredential = R"(
76
"authenticatorAttachment": "platform",
77
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
78
"rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
80
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIAbsrzRbYpFhbRlZA6ZQKsoxxJWoaeXwh-XUuDLNCIXdIlgg4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M",
81
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
88
const QString PublicKeyCredentialRequestOptions = R"(
92
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
93
"transports": ["internal"],
97
"challenge": "9z36vTfQTL95Lf7WnZgyte7ohGeF-XRiLxkL-LuGU1zopRmMIUA1LVwzGpyIm1fOBn1QnRa0QH27ADAaJGHysQ",
98
"rpId": "webauthn.io",
100
"userVerification": "required"
104
const QJsonArray validPubKeyCredParams = {
106
{"type", "public-key"},
110
{"type", "public-key"},
117
void TestPasskeys::initTestCase()
119
QVERIFY(Crypto::init());
122
void TestPasskeys::init()
126
void TestPasskeys::testBase64WithHexStrings()
128
const size_t bufSize = 64;
129
unsigned char buf[bufSize] = {31, 141, 30, 29, 142, 73, 5, 239, 242, 84, 187, 202, 40, 54, 15, 223,
130
201, 0, 108, 109, 209, 104, 207, 239, 160, 89, 208, 117, 134, 66, 42, 12,
131
31, 66, 163, 248, 221, 88, 241, 164, 6, 55, 182, 97, 186, 243, 162, 162,
132
81, 220, 55, 60, 93, 207, 170, 222, 56, 234, 227, 45, 115, 175, 138, 182};
134
auto base64FromArray = browserMessageBuilder()->getBase64FromArray(reinterpret_cast<const char*>(buf), bufSize);
135
QCOMPARE(base64FromArray,
136
QString("H40eHY5JBe_yVLvKKDYP38kAbG3RaM_voFnQdYZCKgwfQqP43VjxpAY3tmG686KiUdw3PF3Pqt446uMtc6-Ktg"));
138
auto arrayFromBase64 = browserMessageBuilder()->getArrayFromBase64(base64FromArray);
139
QCOMPARE(arrayFromBase64.size(), bufSize);
141
for (size_t i = 0; i < bufSize; i++) {
142
QCOMPARE(static_cast<unsigned char>(arrayFromBase64.at(i)), buf[i]);
145
auto randomDataBase64 = browserMessageBuilder()->getRandomBytesAsBase64(24);
146
QCOMPARE(randomDataBase64.isEmpty(), false);
149
void TestPasskeys::testDecodeResponseData()
151
const auto publicKeyCredential = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
152
auto response = publicKeyCredential["response"].toObject();
153
auto clientDataJson = response["clientDataJSON"].toString();
154
auto attestationObject = response["attestationObject"].toString();
156
QVERIFY(!clientDataJson.isEmpty());
157
QVERIFY(!attestationObject.isEmpty());
159
// Parse clientDataJSON
160
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
161
auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
162
QCOMPARE(clientDataJsonObject["challenge"],
163
QString("lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw"));
164
QCOMPARE(clientDataJsonObject["origin"], QString("https://webauthn.io"));
165
QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
167
// Parse attestationObject (CBOR decoding needed)
168
BrowserCbor browserCbor;
169
auto attestationByteArray = browserMessageBuilder()->getArrayFromBase64(attestationObject);
170
auto attestationJsonObject = browserCbor.getJsonFromCborData(attestationByteArray);
173
auto authDataJsonObject = attestationJsonObject["authData"].toString();
174
auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
175
QVERIFY(authDataArray.size() >= 37);
177
auto authData = browserPasskeys()->parseAuthData(authDataArray);
178
auto credentialData = authData["credentialData"].toObject();
179
auto flags = authData["flags"].toObject();
180
auto publicKey = credentialData["publicKey"].toObject();
182
// The attestationObject should include the same ID after decoding with the response root
183
QCOMPARE(credentialData["credentialId"].toString(), publicKeyCredential["id"].toString());
184
QCOMPARE(credentialData["aaguid"].toString(), QString("_bFBsl2ERD6KNUaYwgWlAg"));
185
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
186
QCOMPARE(flags["AT"], true);
187
QCOMPARE(flags["UP"], true);
188
QCOMPARE(publicKey["1"], 2);
189
QCOMPARE(publicKey["3"], -7);
190
QCOMPARE(publicKey["-1"], 1);
191
QCOMPARE(publicKey["-2"], QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0"));
192
QCOMPARE(publicKey["-3"], QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M"));
195
void TestPasskeys::testLoadingECPrivateKeyFromPem()
197
#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2, 14, 0)
198
QSKIP("ECDSA Signature is broken on Botan < 2.14.0");
200
const auto publicKeyCredentialRequestOptions =
201
browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
202
const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
203
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DX2R6I37nMSZqCp"
204
"XfHlE3UeitkGGE03FqGsdfxIBoOhRANCAAQG7K80W2KRYW0ZWQOmUCrKMcSVqGnl"
205
"8Ifl1LgyzQiF3eLuf+kPDukdB4NKAwnbQiSxC9Ml/xgy4VOZtx1CBOeD"
206
"-----END PRIVATE KEY-----");
208
const auto authenticatorData =
209
browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
210
const auto clientData = browserMessageBuilder()->getArrayFromBase64(
211
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
212
"1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
215
const auto signature = browserPasskeys()->buildSignature(authenticatorData, clientData, privateKeyPem);
217
browserMessageBuilder()->getBase64FromArray(signature),
218
QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
221
void TestPasskeys::testLoadingRSAPrivateKeyFromPem()
223
const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
224
"MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5OHjBHQaRfxxX\n4WHRmqq7e7JgT"
225
"FRs1bd4dIOFAOZnhNE3vAg2IF5VurmeB+9ye9xh7frw8ubrL0cv\nsBWiJfN5CY3SYGRLbGTtBC0fZ6"
226
"OhhhjwvVM1GW6nVeRU66atzuo4NBfYXJWIYECd\npRBU4+xsDL4vJnn1mj05+v/Tqp6Uo1HrEPx9+Dc"
227
"oYJD+cw7+OQ83XeGmjD+Dtm5z\nNIyYdweaafVR4PEUlB3CYZuOq9xcpxay3ps2MuYT1zGoiQqk6fla"
228
"d+0tBWGY8Lwp\nCVulXCv7ljNJ4gxgQtOqWX8j2hC0hBxeqNYDYbrkECid3TsMTEMcV5uaVJXULg4t"
229
"\nn6UItA11AgMBAAECggEAC3B0WBxHuieIfllOOOC4H9/7S7fDH2f7+W2cFtQ6pqo9\nCq0WBmkYMmw"
230
"Xx9hpHoq4TnhhHyL9WzPzuKYD0Vx4gvacV/ckkppFScnQKJ2hF+99\nLax1DbU+UImSknfDDFPYbYld"
231
"b1CD2rpJG1i6X2fRQ6NuK+F7jE05mqcIyE+ZajK+\nIpx8XFmE+tI1EEWsn3CzxMLiTQfXyFt/drM9i"
232
"GYfcDjYY+q5vzGU3Kxj68gjc96A\nOra79DGOmwX+4zIwo5sSzI3noHnhWPLsaRtE5jWu21Qkb+1BvB"
233
"jPmbQfN274OQfy\n8/BNNR/NZM1mJm/8x4Mt+h5d946XlIo0AkyYZXY/UQKBgQDYI3G3BCwaYk6MDMe"
234
"T\nIamRZo25phPtr3if47dhT2xFWJplIt35sW+6KjD6c1Qpb2aGOUh7JPmb57H54OgJ\nmojkS5tv9Y"
235
"EQZFfgCCZoeuqBx+ArqtJdkXOiNEFS0dpt44I+eO3Do5pnwKRemH+Y\ncqJ/eMH96UMzYDO7WNsyOyo"
236
"5UQKBgQDbYU0KbGbTrMEV4T9Q41rZ2TnWzs5moqn2\nCRtB8LOdKAZUG7FRsw5KgC1CvFn3Xuk+qphY"
237
"GUQeJvv7FjxMRUv4BktNpXju6eUj\n3tWHzI2QOkHaeq/XibwbNomfkdyTjtLX2+v8DBHcZnCSlukxc"
238
"JISyPqZ6CnTjXGE\nEGB+itBI5QKBgQCA+gWttOusguVkZWvivL+3aH9CPXy+5WsR3o1boE13xDu+Bm"
239
"R3\n0A5gBTVc/t1GLJf9mMlL0vCwvD5UYoWU1YbC1OtYkCQIaBiYM8TXrCGseF2pMTJ/\na4CZVp10k"
240
"o3J7W2XYgpgKIzHRQnQ+SeLDT0y3BjHMB9N1SaJsah7/RphQQKBgQCr\nL+4yKAzFOJUjQbVqpT8Lp5"
241
"qeqJofNOdzef+vIOjHxafKkiF4I0UPlZ276cY6ZfGU\nWQKwHGcvMDSI5fz/d0OksySn3mvT4uhPaV8"
242
"urMv6s7sXhY0Zn/0NLy2NOwDolBar\nIo2vDKwTVEyb1u75CWKzDemfl66ryj++Uhk6JZAKkQKBgQCc"
243
"NYVe7m648DzD0nu9\n3lgetBTaAS1zZmMs8Cinj44v0ksfqxrRBzBZcO9kCQqiJZ7uCAaVYcQ+PwkY+"
244
"05C\n+w1+KvdGcKM+8TQYTQM3s2B9IyKExRS/dbQf9F7stJL+k5vbt6OUerwfmbNI9R3t\ngDZ4DEfo"
245
"pPivs9dnequ9wfaPOw=="
246
"-----END PRIVATE KEY-----");
248
const auto authenticatorData =
249
browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
250
const auto clientData = browserMessageBuilder()->getArrayFromBase64(
251
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
252
"1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
255
const auto signature = browserPasskeys()->buildSignature(authenticatorData, clientData, privateKeyPem);
257
browserMessageBuilder()->getBase64FromArray(signature),
258
QString("MOGw6KrerCgPf2mPig7FOTFIUDXYAU1v2uZj89_NgQTg2UddWnAB3JId3pa4zXghj8CkjjadVOI_LvweJGCEpmPQnRby71yFXnja6j"
259
"Y3woX2b2klG2fB2alGZHHrVg6yVEmnAii4kYSdmoWxI7SmzLftoZfCJNFPFHujx2Pbr-6dIB02sZhtncetT0cpyWobtj9r7C5dIGfm"
260
"J5n-LccP-F9gXGqtbN605VrIkC2WNztjdk3dAt5FGM_dlIwSe-vP1dKfIuNqAEbgr2IVZAUFn_ZfzUo-XbXTysksuz9JZfEopJBiUi"
261
"9tjQDNvrYQFqB6wDPqkZAomkbRCohUb3TzCg"));
264
void TestPasskeys::testCreatingAttestationObjectWithEC()
266
// Predefined values for a desired outcome
267
const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
268
const auto predefinedFirst = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
269
const auto predefinedSecond = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
271
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
272
QJsonObject credentialCreationOptions;
273
browserPasskeysClient()->getCredentialCreationOptions(
274
publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
276
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
277
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
279
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond};
280
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
281
const auto credentialPrivateKey =
282
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedFirst, predefinedSecond);
283
auto result = browserPasskeys()->buildAttestationObject(
284
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
288
"cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
289
"9\x7F)%\x0B`\x84\x1E\xF0"
290
"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"
291
"6\x17\xD6\xDE\x01\x11|\xEA\x94\r\xA0R\xC0\x80_\xF3r\xFBr\xB5\x02\x03:"
292
"\xBAr\x0Fi\x81\xFE\xA5\x01\x02\x03& \x01!X "
293
"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~"
294
"\"X \\\x8E\x1E@\xDB\x97T-\xF8\x9B\xB0\xAD"
295
"5\xDC\x12^\xC3\x95\x05\xC6\xDF^\x03\xCB\xB4Q\x91\xFF|\xDB\x94\xB7"));
297
// Double check that the result can be decoded
298
BrowserCbor browserCbor;
299
auto attestationJsonObject = browserCbor.getJsonFromCborData(result);
302
auto authDataJsonObject = attestationJsonObject["authData"].toString();
303
auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
304
QVERIFY(authDataArray.size() >= 37);
306
auto authData = browserPasskeys()->parseAuthData(authDataArray);
307
auto credentialData = authData["credentialData"].toObject();
308
auto flags = authData["flags"].toObject();
309
auto publicKey = credentialData["publicKey"].toObject();
311
// The attestationObject should include the same ID after decoding with the response root
312
QCOMPARE(credentialData["credentialId"].toString(), QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
313
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
314
QCOMPARE(flags["AT"], true);
315
QCOMPARE(flags["UP"], true);
316
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::EC2);
317
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::ES256);
318
QCOMPARE(publicKey["-1"], 1);
319
QCOMPARE(publicKey["-2"], predefinedFirst);
320
QCOMPARE(publicKey["-3"], predefinedSecond);
323
void TestPasskeys::testCreatingAttestationObjectWithRSA()
325
// Predefined values for a desired outcome
326
const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
327
const auto predefinedModulus = QString("vUhOZnyn8yn7U-nuHlsXZ6WDWLuYvevWWnwtoHxDEQq27vlp7yAfeVvAPkcvhxRcwoCEUespoa5"
328
"5IDbkpp2Ypd6b15KbB4C-_4gM4r2FK9gfXghLPAXsMhstYv4keNFb4ghdlY5oUU3JCqUSMyOpmd"
329
"HeX-RikLL0wgGv_tLT2DaDiWeyQCAtiDblr6COuTAU2kTpLc3Bn35geV9Iqw4iT8DwBQ-f8vjnI"
330
"EDANXKUiRPojfy1q7WwEl-zMv6Ke2jFHxf68u82BSy3u9DOQaa24FAHoCm8Yd0n5IazMyoxyttl"
331
"tRt8un8myVOGxcXMiR9_kQb9pu1RRLQMQLd-icE1Qw");
332
const auto predefinedExponent = QString("AQAB");
334
// Force algorithm to RSA
335
QJsonArray pubKeyCredParams;
336
pubKeyCredParams.append(QJsonObject({{"type", "public-key"}, {"alg", -257}}));
338
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
339
QJsonObject credentialCreationOptions;
340
browserPasskeysClient()->getCredentialCreationOptions(
341
publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
342
credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams;
344
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
345
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
347
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent};
348
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
349
auto credentialPrivateKey =
350
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedModulus, predefinedExponent);
351
auto result = browserPasskeys()->buildAttestationObject(
352
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
354
// Double check that the result can be decoded
355
BrowserCbor browserCbor;
356
auto attestationJsonObject = browserCbor.getJsonFromCborData(result);
359
auto authDataJsonObject = attestationJsonObject["authData"].toString();
360
auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
361
QVERIFY(authDataArray.size() >= 37);
363
auto authData = browserPasskeys()->parseAuthData(authDataArray);
364
auto credentialData = authData["credentialData"].toObject();
365
auto flags = authData["flags"].toObject();
366
auto publicKey = credentialData["publicKey"].toObject();
368
// The attestationObject should include the same ID after decoding with the response root
369
QCOMPARE(credentialData["credentialId"].toString(), QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
370
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
371
QCOMPARE(flags["AT"], true);
372
QCOMPARE(flags["UP"], true);
373
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::RSA);
374
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::RS256);
375
QCOMPARE(publicKey["-1"], predefinedModulus);
376
QCOMPARE(publicKey["-2"], predefinedExponent);
379
void TestPasskeys::testRegister()
381
// Predefined values for a desired outcome
382
const auto predefinedId = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
383
const auto predefinedX = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
384
const auto predefinedY = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
385
const auto origin = QString("https://webauthn.io");
386
const auto testDataPublicKey = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
387
const auto testDataResponse = testDataPublicKey["response"];
388
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
390
QJsonObject credentialCreationOptions;
391
const auto creationResult = browserPasskeysClient()->getCredentialCreationOptions(
392
publicKeyCredentialOptions, origin, &credentialCreationOptions);
393
QVERIFY(creationResult == 0);
395
TestingVariables testingVariables = {predefinedId, predefinedX, predefinedY};
396
auto result = browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions, testingVariables);
397
auto publicKeyCredential = result.response;
398
QCOMPARE(publicKeyCredential["type"], QString("public-key"));
399
QCOMPARE(publicKeyCredential["authenticatorAttachment"], QString("platform"));
400
QCOMPARE(publicKeyCredential["id"], QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
402
auto response = publicKeyCredential["response"].toObject();
403
auto attestationObject = response["attestationObject"].toString();
404
auto clientDataJson = response["clientDataJSON"].toString();
405
QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
407
// Parse clientDataJSON
408
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
409
auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
410
QCOMPARE(clientDataJsonObject["challenge"],
411
QString("lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw"));
412
QCOMPARE(clientDataJsonObject["origin"], origin);
413
QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
416
void TestPasskeys::testGet()
418
#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2, 14, 0)
419
QSKIP("ECDSA Signature is broken on Botan < 2.14.0");
421
const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
422
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DX2R6I37nMSZqCp"
423
"XfHlE3UeitkGGE03FqGsdfxIBoOhRANCAAQG7K80W2KRYW0ZWQOmUCrKMcSVqGnl"
424
"8Ifl1LgyzQiF3eLuf+kPDukdB4NKAwnbQiSxC9Ml/xgy4VOZtx1CBOeD"
425
"-----END PRIVATE KEY-----");
426
const auto origin = QString("https://webauthn.io");
427
const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
428
const auto publicKeyCredentialRequestOptions =
429
browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
431
QJsonObject assertionOptions;
432
const auto assertionResult =
433
browserPasskeysClient()->getAssertionOptions(publicKeyCredentialRequestOptions, origin, &assertionOptions);
434
QVERIFY(assertionResult == 0);
436
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, id, {}, privateKeyPem);
437
QVERIFY(!publicKeyCredential.isEmpty());
438
QCOMPARE(publicKeyCredential["id"].toString(), id);
440
auto response = publicKeyCredential["response"].toObject();
441
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA"));
442
QCOMPARE(response["clientDataJSON"].toString(),
443
QString("eyJjaGFsbGVuZ2UiOiI5ejM2dlRmUVRMOTVMZjdXblpneXRlN29oR2VGLVhSaUx4a0wtTHVHVTF6b3BSbU1JVUExTFZ3ekdwe"
444
"UltMWZPQm4xUW5SYTBRSDI3QURBYUpHSHlzUSIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdX"
445
"Robi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ"));
447
response["signature"].toString(),
448
QString("MEUCIHFv0lOOGGloi_XoH5s3QDSs__8yAp9ZTMEjNiacMpOxAiEA04LAfO6TE7j12XNxd3zHQpn4kZN82jQFPntPiPBSD5c"));
450
auto clientDataJson = response["clientDataJSON"].toString();
451
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
452
auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
453
QCOMPARE(clientDataJsonObject["challenge"].toString(), publicKeyCredentialRequestOptions["challenge"].toString());
456
void TestPasskeys::testExtensions()
458
auto extensions = QJsonObject({{"credProps", true}, {"uvm", true}});
459
auto result = passkeyUtils()->buildExtensionData(extensions);
462
auto extensionJson = cbor.getJsonFromCborData(result.extensionData);
463
auto uvmArray = result.extensionObject["uvm"].toArray();
464
QCOMPARE(result.extensionObject["credProps"].toObject()["rk"].toBool(), true);
465
QCOMPARE(uvmArray.size(), 1);
466
QCOMPARE(uvmArray.first().toArray().size(), 3);
468
auto partial = QJsonObject({{"props", true}, {"uvm", true}});
469
auto faulty = QJsonObject({{"uvx", true}});
470
auto partialData = passkeyUtils()->buildExtensionData(partial);
471
auto faultyData = passkeyUtils()->buildExtensionData(faulty);
473
auto partialJson = cbor.getJsonFromCborData(partialData.extensionData);
474
QCOMPARE(partialJson["uvm"].toArray().size(), 1);
476
auto faultyJson = cbor.getJsonFromCborData(faultyData.extensionData);
477
QCOMPARE(faultyJson.size(), 0);
480
void TestPasskeys::testParseFlags()
482
auto registerResult = browserPasskeys()->parseFlags("\x45");
483
QCOMPARE(registerResult["ED"], false);
484
QCOMPARE(registerResult["AT"], true);
485
QCOMPARE(registerResult["BS"], false);
486
QCOMPARE(registerResult["BE"], false);
487
QCOMPARE(registerResult["UV"], true);
488
QCOMPARE(registerResult["UP"], true);
490
auto getResult = browserPasskeys()->parseFlags("\x05"); // Only UP and UV
491
QCOMPARE(getResult["ED"], false);
492
QCOMPARE(getResult["AT"], false);
493
QCOMPARE(getResult["BS"], false);
494
QCOMPARE(getResult["BE"], false);
495
QCOMPARE(getResult["UV"], true);
496
QCOMPARE(getResult["UP"], true);
499
void TestPasskeys::testSetFlags()
502
QJsonObject({{"ED", false}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
503
auto registerResult = browserPasskeys()->setFlagsFromJson(registerJson);
504
QCOMPARE(registerResult, 0x45);
507
QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
508
auto getResult = browserPasskeys()->setFlagsFromJson(getJson);
509
QCOMPARE(getResult, 0x05);
511
// With "discouraged", so UV is false
512
auto discouragedJson =
513
QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", false}, {"UP", true}});
514
auto discouragedResult = browserPasskeys()->setFlagsFromJson(discouragedJson);
515
QCOMPARE(discouragedResult, 0x01);
518
void TestPasskeys::testEntry()
521
auto* root = db.rootGroup();
522
root->setUuid(QUuid::createUuid());
524
auto* group1 = new Group();
525
group1->setUuid(QUuid::createUuid());
526
group1->setParent(root);
528
auto* entry = new Entry();
529
entry->setGroup(root);
531
browserService()->addPasskeyToEntry(entry,
532
QString("example.com"),
533
QString("example.com"),
536
QString("userHandle"),
537
QString("privateKey"));
539
QVERIFY(entry->hasPasskey());
542
void TestPasskeys::testIsDomain()
544
QVERIFY(passkeyUtils()->isDomain("test.example.com"));
545
QVERIFY(passkeyUtils()->isDomain("example.com"));
547
QVERIFY(!passkeyUtils()->isDomain("exa[mple.org"));
548
QVERIFY(!passkeyUtils()->isDomain("example.com."));
549
QVERIFY(!passkeyUtils()->isDomain("127.0.0.1"));
550
QVERIFY(!passkeyUtils()->isDomain("127.0.0.1."));
553
// List from https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
554
void TestPasskeys::testRegistrableDomainSuffix()
556
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("example.com"), QString("example.com")));
557
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("example.com"), QString("example.com.")));
558
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("example.com."), QString("example.com")));
559
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("example.com"), QString("www.example.com")));
560
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("com"), QString("example.com")));
561
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("example"), QString("example")));
563
!passkeyUtils()->isRegistrableDomainSuffix(QString("s3.amazonaws.com"), QString("example.s3.amazonaws.com")));
564
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("example.compute.amazonaws.com"),
565
QString("www.example.compute.amazonaws.com")));
566
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("amazonaws.com"),
567
QString("www.example.compute.amazonaws.com")));
568
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("amazonaws.com"), QString("test.amazonaws.com")));
571
void TestPasskeys::testRpIdValidation()
574
auto allowedIdentical = passkeyUtils()->validateRpId(QString("example.com"), QString("example.com"), &result);
575
QCOMPARE(result, QString("example.com"));
576
QVERIFY(allowedIdentical == PASSKEYS_SUCCESS);
579
auto allowedSubdomain = passkeyUtils()->validateRpId(QString("example.com"), QString("www.example.com"), &result);
580
QCOMPARE(result, QString("example.com"));
581
QVERIFY(allowedSubdomain == PASSKEYS_SUCCESS);
584
QJsonValue emptyValue;
585
auto emptyRpId = passkeyUtils()->validateRpId(emptyValue, QString("example.com"), &result);
586
QCOMPARE(result, QString("example.com"));
587
QVERIFY(emptyRpId == PASSKEYS_SUCCESS);
590
auto ipRpId = passkeyUtils()->validateRpId(QString("127.0.0.1"), QString("example.com"), &result);
591
QCOMPARE(result, QString(""));
592
QVERIFY(ipRpId == ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
595
auto emptyOrigin = passkeyUtils()->validateRpId(QString("example.com"), QString(""), &result);
596
QVERIFY(result.isEmpty());
597
QCOMPARE(emptyOrigin, ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED);
600
auto ipOrigin = passkeyUtils()->validateRpId(QString("example.com"), QString("127.0.0.1"), &result);
601
QVERIFY(result.isEmpty());
602
QCOMPARE(ipOrigin, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
605
auto invalidRpId = passkeyUtils()->validateRpId(QString(".com"), QString("example.com"), &result);
606
QVERIFY(result.isEmpty());
607
QCOMPARE(invalidRpId, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
610
auto malformedOrigin = passkeyUtils()->validateRpId(QString("example.com."), QString("example.com."), &result);
611
QVERIFY(result.isEmpty());
612
QCOMPARE(malformedOrigin, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
615
auto malformed = passkeyUtils()->validateRpId(QString("...com."), QString("example...com"), &result);
616
QVERIFY(result.isEmpty());
617
QCOMPARE(malformed, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
620
auto differentDomain = passkeyUtils()->validateRpId(QString("another.com"), QString("example.com"), &result);
621
QVERIFY(result.isEmpty());
622
QCOMPARE(differentDomain, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
625
void TestPasskeys::testParseAttestation()
627
QVERIFY(passkeyUtils()->parseAttestation(QString("")) == QString("none"));
628
QVERIFY(passkeyUtils()->parseAttestation(QString("direct")) == QString("direct"));
629
QVERIFY(passkeyUtils()->parseAttestation(QString("none")) == QString("none"));
630
QVERIFY(passkeyUtils()->parseAttestation(QString("indirect")) == QString("none"));
631
QVERIFY(passkeyUtils()->parseAttestation(QString("invalidvalue")) == QString("none"));
634
void TestPasskeys::testParseCredentialTypes()
636
const QJsonArray invalidPubKeyCredParams = {
637
QJsonObject({{"type", "private-key"}, {"alg", -7}}),
638
QJsonObject({{"type", "private-key"}, {"alg", -257}}),
641
const QJsonArray partiallyInvalidPubKeyCredParams = {
642
QJsonObject({{"type", "private-key"}, {"alg", -7}}),
643
QJsonObject({{"type", "public-key"}, {"alg", -257}}),
646
auto validResponse = passkeyUtils()->parseCredentialTypes(validPubKeyCredParams);
647
QVERIFY(validResponse == validPubKeyCredParams);
649
auto invalidResponse = passkeyUtils()->parseCredentialTypes(invalidPubKeyCredParams);
650
QVERIFY(invalidResponse.isEmpty());
652
auto partiallyInvalidResponse = passkeyUtils()->parseCredentialTypes(partiallyInvalidPubKeyCredParams);
653
QVERIFY(partiallyInvalidResponse != validPubKeyCredParams);
654
QVERIFY(partiallyInvalidResponse.size() == 1);
655
QVERIFY(partiallyInvalidResponse.first()["type"].toString() == QString("public-key"));
656
QVERIFY(partiallyInvalidResponse.first()["alg"].toInt() == -257);
658
auto emptyResponse = passkeyUtils()->parseCredentialTypes({});
659
QVERIFY(emptyResponse == validPubKeyCredParams);
661
const auto publicKeyOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
662
auto responseFromPublicKey = passkeyUtils()->parseCredentialTypes(publicKeyOptions["pubKeyCredParams"].toArray());
663
QVERIFY(responseFromPublicKey == validPubKeyCredParams);
666
void TestPasskeys::testIsAuthenticatorSelectionValid()
668
QVERIFY(passkeyUtils()->isAuthenticatorSelectionValid({}));
669
QVERIFY(passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "platform"}})));
671
passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "cross-platform"}})));
672
QVERIFY(!passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "something"}})));
675
void TestPasskeys::testIsResidentKeyRequired()
677
QVERIFY(passkeyUtils()->isResidentKeyRequired(QJsonObject({{"residentKey", "required"}})));
678
QVERIFY(passkeyUtils()->isResidentKeyRequired(QJsonObject({{"residentKey", "preferred"}})));
679
QVERIFY(!passkeyUtils()->isResidentKeyRequired(QJsonObject({{"residentKey", "discouraged"}})));
680
QVERIFY(passkeyUtils()->isResidentKeyRequired(QJsonObject({{"requireResidentKey", true}})));
683
void TestPasskeys::testIsUserVerificationRequired()
685
QVERIFY(passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "required"}})));
686
QVERIFY(passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "preferred"}})));
687
QVERIFY(!passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "discouraged"}})));
690
void TestPasskeys::testAllowLocalhostWithPasskeys()
692
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(false, "https://example.com"));
693
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(false, "http://example.com"));
694
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(true, "https://example.com"));
695
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://example.com"));
696
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(false, "http://localhost"));
697
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://localhost"));
698
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://localhosting"));
699
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://test.localhost"));
700
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(false, "http://test.localhost"));
701
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://localhost.example.com"));