keepassxc

Форк
0
/
TestPasskeys.cpp 
702 строки · 34.8 Кб
1
/*
2
 *  Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
3
 *
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.
8
 *
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.
13
 *
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/>.
16
 */
17

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"
28

29
#include <QJsonArray>
30
#include <QJsonDocument>
31
#include <QTest>
32
#include <botan/sodium.h>
33

34
using namespace Botan::Sodium;
35

36
QTEST_GUILESS_MAIN(TestPasskeys)
37

38
// Register request
39
// clang-format off
40
const QString PublicKeyCredentialOptions = R"(
41
    {
42
        "attestation": "none",
43
        "authenticatorSelection": {
44
            "residentKey": "preferred",
45
            "requireResidentKey": false,
46
            "userVerification": "required"
47
        },
48
        "challenge": "lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw",
49
        "pubKeyCredParams": [
50
            {
51
                "type": "public-key",
52
                "alg": -7
53
            },
54
            {
55
                "type": "public-key",
56
                "alg": -257
57
            }
58
        ],
59
        "rp": {
60
            "name": "webauthn.io",
61
            "id": "webauthn.io"
62
        },
63
        "timeout": 60000,
64
        "excludeCredentials": [],
65
        "user": {
66
            "displayName": "Test User",
67
            "id": "VkdWemRDQlZjMlZ5",
68
            "name": "Test User"
69
        }
70
    }
71
)";
72

73
// Register response
74
const QString PublicKeyCredential = R"(
75
    {
76
        "authenticatorAttachment": "platform",
77
        "id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
78
        "rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
79
        "response": {
80
            "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIAbsrzRbYpFhbRlZA6ZQKsoxxJWoaeXwh-XUuDLNCIXdIlgg4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M",
81
            "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
82
        },
83
        "type": "public-key"
84
    }
85
)";
86

87
// Get request
88
const QString PublicKeyCredentialRequestOptions = R"(
89
    {
90
        "allowCredentials": [
91
            {
92
                "id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
93
                "transports": ["internal"],
94
                "type": "public-key"
95
            }
96
        ],
97
        "challenge": "9z36vTfQTL95Lf7WnZgyte7ohGeF-XRiLxkL-LuGU1zopRmMIUA1LVwzGpyIm1fOBn1QnRa0QH27ADAaJGHysQ",
98
        "rpId": "webauthn.io",
99
        "timeout": 60000,
100
        "userVerification": "required"
101
    }
102
)";
103

104
const QJsonArray validPubKeyCredParams = {
105
    QJsonObject({
106
       {"type", "public-key"},
107
       {"alg", -7}
108
    }),
109
    QJsonObject({
110
        {"type", "public-key"},
111
        {"alg", -257}
112
    }),
113
};
114

115
// clang-format on
116

117
void TestPasskeys::initTestCase()
118
{
119
    QVERIFY(Crypto::init());
120
}
121

122
void TestPasskeys::init()
123
{
124
}
125

126
void TestPasskeys::testBase64WithHexStrings()
127
{
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};
133

134
    auto base64FromArray = browserMessageBuilder()->getBase64FromArray(reinterpret_cast<const char*>(buf), bufSize);
135
    QCOMPARE(base64FromArray,
136
             QString("H40eHY5JBe_yVLvKKDYP38kAbG3RaM_voFnQdYZCKgwfQqP43VjxpAY3tmG686KiUdw3PF3Pqt446uMtc6-Ktg"));
137

138
    auto arrayFromBase64 = browserMessageBuilder()->getArrayFromBase64(base64FromArray);
139
    QCOMPARE(arrayFromBase64.size(), bufSize);
140

141
    for (size_t i = 0; i < bufSize; i++) {
142
        QCOMPARE(static_cast<unsigned char>(arrayFromBase64.at(i)), buf[i]);
143
    }
144

145
    auto randomDataBase64 = browserMessageBuilder()->getRandomBytesAsBase64(24);
146
    QCOMPARE(randomDataBase64.isEmpty(), false);
147
}
148

149
void TestPasskeys::testDecodeResponseData()
150
{
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();
155

156
    QVERIFY(!clientDataJson.isEmpty());
157
    QVERIFY(!attestationObject.isEmpty());
158

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

167
    // Parse attestationObject (CBOR decoding needed)
168
    BrowserCbor browserCbor;
169
    auto attestationByteArray = browserMessageBuilder()->getArrayFromBase64(attestationObject);
170
    auto attestationJsonObject = browserCbor.getJsonFromCborData(attestationByteArray);
171

172
    // Parse authData
173
    auto authDataJsonObject = attestationJsonObject["authData"].toString();
174
    auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
175
    QVERIFY(authDataArray.size() >= 37);
176

177
    auto authData = browserPasskeys()->parseAuthData(authDataArray);
178
    auto credentialData = authData["credentialData"].toObject();
179
    auto flags = authData["flags"].toObject();
180
    auto publicKey = credentialData["publicKey"].toObject();
181

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"));
193
}
194

195
void TestPasskeys::testLoadingECPrivateKeyFromPem()
196
{
197
#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2, 14, 0)
198
    QSKIP("ECDSA Signature is broken on Botan < 2.14.0");
199
#endif
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-----");
207

208
    const auto authenticatorData =
209
        browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
210
    const auto clientData = browserMessageBuilder()->getArrayFromBase64(
211
        "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
212
        "1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
213
        "aW4iOmZhbHNlfQ");
214

215
    const auto signature = browserPasskeys()->buildSignature(authenticatorData, clientData, privateKeyPem);
216
    QCOMPARE(
217
        browserMessageBuilder()->getBase64FromArray(signature),
218
        QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
219
}
220

221
void TestPasskeys::testLoadingRSAPrivateKeyFromPem()
222
{
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-----");
247

248
    const auto authenticatorData =
249
        browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
250
    const auto clientData = browserMessageBuilder()->getArrayFromBase64(
251
        "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
252
        "1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
253
        "aW4iOmZhbHNlfQ");
254

255
    const auto signature = browserPasskeys()->buildSignature(authenticatorData, clientData, privateKeyPem);
256
    QCOMPARE(
257
        browserMessageBuilder()->getBase64FromArray(signature),
258
        QString("MOGw6KrerCgPf2mPig7FOTFIUDXYAU1v2uZj89_NgQTg2UddWnAB3JId3pa4zXghj8CkjjadVOI_LvweJGCEpmPQnRby71yFXnja6j"
259
                "Y3woX2b2klG2fB2alGZHHrVg6yVEmnAii4kYSdmoWxI7SmzLftoZfCJNFPFHujx2Pbr-6dIB02sZhtncetT0cpyWobtj9r7C5dIGfm"
260
                "J5n-LccP-F9gXGqtbN605VrIkC2WNztjdk3dAt5FGM_dlIwSe-vP1dKfIuNqAEbgr2IVZAUFn_ZfzUo-XbXTysksuz9JZfEopJBiUi"
261
                "9tjQDNvrYQFqB6wDPqkZAomkbRCohUb3TzCg"));
262
}
263

264
void TestPasskeys::testCreatingAttestationObjectWithEC()
265
{
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");
270

271
    const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
272
    QJsonObject credentialCreationOptions;
273
    browserPasskeysClient()->getCredentialCreationOptions(
274
        publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
275

276
    auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
277
    QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
278

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);
285
    QCOMPARE(
286
        result,
287
        QString("\xA3"
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"));
296

297
    // Double check that the result can be decoded
298
    BrowserCbor browserCbor;
299
    auto attestationJsonObject = browserCbor.getJsonFromCborData(result);
300

301
    // Parse authData
302
    auto authDataJsonObject = attestationJsonObject["authData"].toString();
303
    auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
304
    QVERIFY(authDataArray.size() >= 37);
305

306
    auto authData = browserPasskeys()->parseAuthData(authDataArray);
307
    auto credentialData = authData["credentialData"].toObject();
308
    auto flags = authData["flags"].toObject();
309
    auto publicKey = credentialData["publicKey"].toObject();
310

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);
321
}
322

323
void TestPasskeys::testCreatingAttestationObjectWithRSA()
324
{
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");
333

334
    // Force algorithm to RSA
335
    QJsonArray pubKeyCredParams;
336
    pubKeyCredParams.append(QJsonObject({{"type", "public-key"}, {"alg", -257}}));
337

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

344
    auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
345
    QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
346

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

354
    // Double check that the result can be decoded
355
    BrowserCbor browserCbor;
356
    auto attestationJsonObject = browserCbor.getJsonFromCborData(result);
357

358
    // Parse authData
359
    auto authDataJsonObject = attestationJsonObject["authData"].toString();
360
    auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
361
    QVERIFY(authDataArray.size() >= 37);
362

363
    auto authData = browserPasskeys()->parseAuthData(authDataArray);
364
    auto credentialData = authData["credentialData"].toObject();
365
    auto flags = authData["flags"].toObject();
366
    auto publicKey = credentialData["publicKey"].toObject();
367

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);
377
}
378

379
void TestPasskeys::testRegister()
380
{
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());
389

390
    QJsonObject credentialCreationOptions;
391
    const auto creationResult = browserPasskeysClient()->getCredentialCreationOptions(
392
        publicKeyCredentialOptions, origin, &credentialCreationOptions);
393
    QVERIFY(creationResult == 0);
394

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

402
    auto response = publicKeyCredential["response"].toObject();
403
    auto attestationObject = response["attestationObject"].toString();
404
    auto clientDataJson = response["clientDataJSON"].toString();
405
    QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
406

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"));
414
}
415

416
void TestPasskeys::testGet()
417
{
418
#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2, 14, 0)
419
    QSKIP("ECDSA Signature is broken on Botan < 2.14.0");
420
#endif
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());
430

431
    QJsonObject assertionOptions;
432
    const auto assertionResult =
433
        browserPasskeysClient()->getAssertionOptions(publicKeyCredentialRequestOptions, origin, &assertionOptions);
434
    QVERIFY(assertionResult == 0);
435

436
    auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, id, {}, privateKeyPem);
437
    QVERIFY(!publicKeyCredential.isEmpty());
438
    QCOMPARE(publicKeyCredential["id"].toString(), id);
439

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"));
446
    QCOMPARE(
447
        response["signature"].toString(),
448
        QString("MEUCIHFv0lOOGGloi_XoH5s3QDSs__8yAp9ZTMEjNiacMpOxAiEA04LAfO6TE7j12XNxd3zHQpn4kZN82jQFPntPiPBSD5c"));
449

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());
454
}
455

456
void TestPasskeys::testExtensions()
457
{
458
    auto extensions = QJsonObject({{"credProps", true}, {"uvm", true}});
459
    auto result = passkeyUtils()->buildExtensionData(extensions);
460

461
    BrowserCbor cbor;
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);
467

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

473
    auto partialJson = cbor.getJsonFromCborData(partialData.extensionData);
474
    QCOMPARE(partialJson["uvm"].toArray().size(), 1);
475

476
    auto faultyJson = cbor.getJsonFromCborData(faultyData.extensionData);
477
    QCOMPARE(faultyJson.size(), 0);
478
}
479

480
void TestPasskeys::testParseFlags()
481
{
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);
489

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);
497
}
498

499
void TestPasskeys::testSetFlags()
500
{
501
    auto registerJson =
502
        QJsonObject({{"ED", false}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
503
    auto registerResult = browserPasskeys()->setFlagsFromJson(registerJson);
504
    QCOMPARE(registerResult, 0x45);
505

506
    auto getJson =
507
        QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
508
    auto getResult = browserPasskeys()->setFlagsFromJson(getJson);
509
    QCOMPARE(getResult, 0x05);
510

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);
516
}
517

518
void TestPasskeys::testEntry()
519
{
520
    Database db;
521
    auto* root = db.rootGroup();
522
    root->setUuid(QUuid::createUuid());
523

524
    auto* group1 = new Group();
525
    group1->setUuid(QUuid::createUuid());
526
    group1->setParent(root);
527

528
    auto* entry = new Entry();
529
    entry->setGroup(root);
530

531
    browserService()->addPasskeyToEntry(entry,
532
                                        QString("example.com"),
533
                                        QString("example.com"),
534
                                        QString("username"),
535
                                        QString("userId"),
536
                                        QString("userHandle"),
537
                                        QString("privateKey"));
538

539
    QVERIFY(entry->hasPasskey());
540
}
541

542
void TestPasskeys::testIsDomain()
543
{
544
    QVERIFY(passkeyUtils()->isDomain("test.example.com"));
545
    QVERIFY(passkeyUtils()->isDomain("example.com"));
546

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."));
551
}
552

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()
555
{
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")));
562
    QVERIFY(
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")));
569
}
570

571
void TestPasskeys::testRpIdValidation()
572
{
573
    QString result;
574
    auto allowedIdentical = passkeyUtils()->validateRpId(QString("example.com"), QString("example.com"), &result);
575
    QCOMPARE(result, QString("example.com"));
576
    QVERIFY(allowedIdentical == PASSKEYS_SUCCESS);
577

578
    result.clear();
579
    auto allowedSubdomain = passkeyUtils()->validateRpId(QString("example.com"), QString("www.example.com"), &result);
580
    QCOMPARE(result, QString("example.com"));
581
    QVERIFY(allowedSubdomain == PASSKEYS_SUCCESS);
582

583
    result.clear();
584
    QJsonValue emptyValue;
585
    auto emptyRpId = passkeyUtils()->validateRpId(emptyValue, QString("example.com"), &result);
586
    QCOMPARE(result, QString("example.com"));
587
    QVERIFY(emptyRpId == PASSKEYS_SUCCESS);
588

589
    result.clear();
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);
593

594
    result.clear();
595
    auto emptyOrigin = passkeyUtils()->validateRpId(QString("example.com"), QString(""), &result);
596
    QVERIFY(result.isEmpty());
597
    QCOMPARE(emptyOrigin, ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED);
598

599
    result.clear();
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);
603

604
    result.clear();
605
    auto invalidRpId = passkeyUtils()->validateRpId(QString(".com"), QString("example.com"), &result);
606
    QVERIFY(result.isEmpty());
607
    QCOMPARE(invalidRpId, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
608

609
    result.clear();
610
    auto malformedOrigin = passkeyUtils()->validateRpId(QString("example.com."), QString("example.com."), &result);
611
    QVERIFY(result.isEmpty());
612
    QCOMPARE(malformedOrigin, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
613

614
    result.clear();
615
    auto malformed = passkeyUtils()->validateRpId(QString("...com."), QString("example...com"), &result);
616
    QVERIFY(result.isEmpty());
617
    QCOMPARE(malformed, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
618

619
    result.clear();
620
    auto differentDomain = passkeyUtils()->validateRpId(QString("another.com"), QString("example.com"), &result);
621
    QVERIFY(result.isEmpty());
622
    QCOMPARE(differentDomain, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
623
}
624

625
void TestPasskeys::testParseAttestation()
626
{
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"));
632
}
633

634
void TestPasskeys::testParseCredentialTypes()
635
{
636
    const QJsonArray invalidPubKeyCredParams = {
637
        QJsonObject({{"type", "private-key"}, {"alg", -7}}),
638
        QJsonObject({{"type", "private-key"}, {"alg", -257}}),
639
    };
640

641
    const QJsonArray partiallyInvalidPubKeyCredParams = {
642
        QJsonObject({{"type", "private-key"}, {"alg", -7}}),
643
        QJsonObject({{"type", "public-key"}, {"alg", -257}}),
644
    };
645

646
    auto validResponse = passkeyUtils()->parseCredentialTypes(validPubKeyCredParams);
647
    QVERIFY(validResponse == validPubKeyCredParams);
648

649
    auto invalidResponse = passkeyUtils()->parseCredentialTypes(invalidPubKeyCredParams);
650
    QVERIFY(invalidResponse.isEmpty());
651

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

658
    auto emptyResponse = passkeyUtils()->parseCredentialTypes({});
659
    QVERIFY(emptyResponse == validPubKeyCredParams);
660

661
    const auto publicKeyOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
662
    auto responseFromPublicKey = passkeyUtils()->parseCredentialTypes(publicKeyOptions["pubKeyCredParams"].toArray());
663
    QVERIFY(responseFromPublicKey == validPubKeyCredParams);
664
}
665

666
void TestPasskeys::testIsAuthenticatorSelectionValid()
667
{
668
    QVERIFY(passkeyUtils()->isAuthenticatorSelectionValid({}));
669
    QVERIFY(passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "platform"}})));
670
    QVERIFY(
671
        passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "cross-platform"}})));
672
    QVERIFY(!passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "something"}})));
673
}
674

675
void TestPasskeys::testIsResidentKeyRequired()
676
{
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}})));
681
}
682

683
void TestPasskeys::testIsUserVerificationRequired()
684
{
685
    QVERIFY(passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "required"}})));
686
    QVERIFY(passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "preferred"}})));
687
    QVERIFY(!passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "discouraged"}})));
688
}
689

690
void TestPasskeys::testAllowLocalhostWithPasskeys()
691
{
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"));
702
}
703

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.