nid-webauthn-emulator 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +3 -0
- package/README.md +3 -0
- package/dist/spec/authenticator/authenticator-emulator.spec.js +147 -0
- package/dist/spec/libs/browser-injection.spec.js +44 -0
- package/dist/spec/libs/cbor.spec.js +9 -0
- package/dist/spec/webauthn/webauthn-emulator.spec.js +325 -87
- package/dist/spec/webauthn/webauthn-model-json.spec.js +67 -23
- package/dist/spec/webauthn/webauthn-model.spec.js +34 -0
- package/dist/spec/webauthn/webauthn-test-server.js +9 -2
- package/dist/src/authenticator/authenticator-emulator.d.ts +3 -0
- package/dist/src/authenticator/authenticator-emulator.js +71 -8
- package/dist/src/libs/cbor.d.ts +4 -0
- package/dist/src/libs/cbor.js +9 -0
- package/dist/src/libs/encode-utils.d.ts +2 -1
- package/dist/src/libs/encode-utils.js +1 -0
- package/dist/src/repository/credentials-file-repository.js +2 -1
- package/dist/src/webauthn/cose-key.d.ts +1 -0
- package/dist/src/webauthn/cose-key.js +6 -16
- package/dist/src/webauthn/webauthn-emulator.js +100 -3
- package/dist/src/webauthn/webauthn-model-json.d.ts +5 -0
- package/dist/src/webauthn/webauthn-model-json.js +89 -8
- package/dist/src/webauthn/webauthn-model.d.ts +2 -0
- package/dist/src/webauthn/webauthn-model.js +28 -7
- package/package.json +8 -9
package/README.ja.md
CHANGED
|
@@ -62,11 +62,14 @@ const authenticator = new AuthenticatorEmulator({
|
|
|
62
62
|
userPresent: false,
|
|
63
63
|
},
|
|
64
64
|
signCounterIncrement: 0,
|
|
65
|
+
hmacSecret: "hmac-secret-mc",
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
const webAuthnEmulator = new WebAuthnEmulator(authenticator);
|
|
68
69
|
```
|
|
69
70
|
|
|
71
|
+
`hmacSecret` オプションは、WebAuthn の [PRF 拡張](https://www.w3.org/TR/webauthn-3/#prf-extension) の実装のバックエンドとなりうる CTAP2 拡張の有無を制御します。認証時にのみ評価するなら `"hmac-secret"`、登録時にも評価するなら(CTAP 2.2)`"hmac-secret-mc"`、拡張を使わないなら `"none"`(デフォルト)を指定します。
|
|
72
|
+
|
|
70
73
|
`AuthenticatorEmulator` は FIDO2/CTAP の仕様のうち下記のコマンドを実装しています。
|
|
71
74
|
|
|
72
75
|
- `authenticatorMakeCredential` (CTAP2) : 認証情報の作成
|
package/README.md
CHANGED
|
@@ -63,11 +63,14 @@ const authenticator = new AuthenticatorEmulator({
|
|
|
63
63
|
userPresent: false,
|
|
64
64
|
},
|
|
65
65
|
signCounterIncrement: 0,
|
|
66
|
+
hmacSecret: "hmac-secret-mc",
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
const webAuthnEmulator = new WebAuthnEmulator(authenticator);
|
|
69
70
|
```
|
|
70
71
|
|
|
72
|
+
The `hmacSecret` option controls the CTAP2 extension that backs the WebAuthn [PRF extension](https://www.w3.org/TR/webauthn-3/#prf-extension). Use `"hmac-secret"` to evaluate at authentication, `"hmac-secret-mc"` (CTAP 2.2) to also evaluate at registration, or `"none"` to disable (default).
|
|
73
|
+
|
|
71
74
|
`AuthenticatorEmulator` implements the following commands from the FIDO2/CTAP specification:
|
|
72
75
|
|
|
73
76
|
- `authenticatorMakeCredential` (CTAP2): Creating credentials
|
|
@@ -9,6 +9,7 @@ const src_1 = require("../../src");
|
|
|
9
9
|
const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
10
10
|
const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
|
|
11
11
|
const credentials_memory_repository_1 = require("../../src/repository/credentials-memory-repository");
|
|
12
|
+
const webauthn_model_1 = require("../../src/webauthn/webauthn-model");
|
|
12
13
|
(0, node_test_1.describe)("Authenticator Emulator Exceptional Test", () => {
|
|
13
14
|
// Success case has been tested in the webauthn-emulator.spec.ts
|
|
14
15
|
(0, node_test_1.test)("Unknown command _ CTAP Error", async () => {
|
|
@@ -310,3 +311,149 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
310
311
|
});
|
|
311
312
|
});
|
|
312
313
|
});
|
|
314
|
+
(0, node_test_1.describe)("hmac-secret extension", () => {
|
|
315
|
+
const rpId = "example.com";
|
|
316
|
+
const user = { id: encode_utils_1.default.strToUint8Array("hmac-user"), name: "user", displayName: "User" };
|
|
317
|
+
const clientDataHash = new Uint8Array(32).fill(1);
|
|
318
|
+
const pubKeyCredParams = [{ type: "public-key", alg: -7 }];
|
|
319
|
+
const salt1 = new Uint8Array(32).fill(2);
|
|
320
|
+
const salt2 = new Uint8Array(32).fill(3);
|
|
321
|
+
function extensionOutput(authData) {
|
|
322
|
+
const extensions = (0, webauthn_model_1.unpackAuthenticatorData)(authData).extensions;
|
|
323
|
+
return extensions?.["hmac-secret"];
|
|
324
|
+
}
|
|
325
|
+
function assertUint8Array(value) {
|
|
326
|
+
if (!(value instanceof Uint8Array)) {
|
|
327
|
+
strict_1.default.fail("expected Uint8Array");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function credentialIdOf(authData) {
|
|
331
|
+
const attested = (0, webauthn_model_1.unpackAuthenticatorData)(authData).attestedCredentialData;
|
|
332
|
+
if (!attested)
|
|
333
|
+
throw new Error("expected attested credential data");
|
|
334
|
+
return attested.credentialId;
|
|
335
|
+
}
|
|
336
|
+
function makeCredential(authenticator, credUser, extensions) {
|
|
337
|
+
return authenticator.authenticatorMakeCredential({
|
|
338
|
+
clientDataHash,
|
|
339
|
+
rp: { id: rpId, name: rpId },
|
|
340
|
+
user: credUser,
|
|
341
|
+
pubKeyCredParams,
|
|
342
|
+
extensions,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function getAssertion(authenticator, credentialId, extensions) {
|
|
346
|
+
return authenticator.authenticatorGetAssertion({
|
|
347
|
+
rpId,
|
|
348
|
+
clientDataHash,
|
|
349
|
+
allowList: [{ type: "public-key", id: credentialId }],
|
|
350
|
+
extensions,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
(0, node_test_1.test)("authenticator info returns enabled extensions", () => {
|
|
354
|
+
strict_1.default.equal(new src_1.AuthenticatorEmulator({ hmacSecret: "none" }).authenticatorGetInfo().extensions, undefined);
|
|
355
|
+
strict_1.default.deepEqual(new src_1.AuthenticatorEmulator({ hmacSecret: "hmac-secret" }).authenticatorGetInfo().extensions, [
|
|
356
|
+
"hmac-secret",
|
|
357
|
+
]);
|
|
358
|
+
strict_1.default.deepEqual(new src_1.AuthenticatorEmulator({ hmacSecret: "hmac-secret-mc" }).authenticatorGetInfo().extensions, [
|
|
359
|
+
"hmac-secret",
|
|
360
|
+
"hmac-secret-mc",
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
(0, node_test_1.test)("hmac-secret makeCredential produces matching getAssertion output", () => {
|
|
364
|
+
const authenticator = new src_1.AuthenticatorEmulator({
|
|
365
|
+
hmacSecret: "hmac-secret",
|
|
366
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
367
|
+
});
|
|
368
|
+
const credential = makeCredential(authenticator, user, { "hmac-secret": true });
|
|
369
|
+
strict_1.default.equal(extensionOutput(credential.authData), true);
|
|
370
|
+
const credentialId = credentialIdOf(credential.authData);
|
|
371
|
+
const output1 = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
372
|
+
assertUint8Array(output1);
|
|
373
|
+
strict_1.default.equal(output1.length, 32);
|
|
374
|
+
const output2 = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
375
|
+
strict_1.default.deepEqual(output2, output1);
|
|
376
|
+
});
|
|
377
|
+
(0, node_test_1.test)("hmac-secret-mc makeCredential produces matching getAssertion output", () => {
|
|
378
|
+
const authenticator = new src_1.AuthenticatorEmulator({
|
|
379
|
+
hmacSecret: "hmac-secret-mc",
|
|
380
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
381
|
+
});
|
|
382
|
+
const credential = makeCredential(authenticator, user, { "hmac-secret": true, "hmac-secret-mc": salt1 });
|
|
383
|
+
const output1 = extensionOutput(credential.authData);
|
|
384
|
+
assertUint8Array(output1);
|
|
385
|
+
const credentialId = credentialIdOf(credential.authData);
|
|
386
|
+
const output2 = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
387
|
+
strict_1.default.deepEqual(output1, output2);
|
|
388
|
+
const output3 = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
389
|
+
strict_1.default.deepEqual(output2, output3);
|
|
390
|
+
});
|
|
391
|
+
(0, node_test_1.test)("credRandom survives the stateless credentialId blob", () => {
|
|
392
|
+
const authenticator = new src_1.AuthenticatorEmulator({ hmacSecret: "hmac-secret-mc", stateless: true });
|
|
393
|
+
const make = makeCredential(authenticator, user, { "hmac-secret": true, "hmac-secret-mc": salt1 });
|
|
394
|
+
const createOutput = extensionOutput(make.authData);
|
|
395
|
+
assertUint8Array(createOutput);
|
|
396
|
+
const credentialId = credentialIdOf(make.authData);
|
|
397
|
+
const assertOutput = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
398
|
+
strict_1.default.deepEqual(assertOutput, createOutput);
|
|
399
|
+
});
|
|
400
|
+
(0, node_test_1.test)("output differs per salt and per credential", () => {
|
|
401
|
+
const authenticator = new src_1.AuthenticatorEmulator({
|
|
402
|
+
hmacSecret: "hmac-secret",
|
|
403
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
404
|
+
});
|
|
405
|
+
const user2 = { id: encode_utils_1.default.strToUint8Array("hmac-user-2"), name: "user2", displayName: "User Two" };
|
|
406
|
+
const id1 = credentialIdOf(makeCredential(authenticator, user, { "hmac-secret": true }).authData);
|
|
407
|
+
const id2 = credentialIdOf(makeCredential(authenticator, user2, { "hmac-secret": true }).authData);
|
|
408
|
+
const out1Salt1 = extensionOutput(getAssertion(authenticator, id1, { "hmac-secret": salt1 }).authData);
|
|
409
|
+
const out1Salt2 = extensionOutput(getAssertion(authenticator, id1, { "hmac-secret": salt2 }).authData);
|
|
410
|
+
const out2Salt1 = extensionOutput(getAssertion(authenticator, id2, { "hmac-secret": salt1 }).authData);
|
|
411
|
+
strict_1.default.notDeepEqual(out1Salt1, out1Salt2);
|
|
412
|
+
strict_1.default.notDeepEqual(out1Salt1, out2Salt1);
|
|
413
|
+
});
|
|
414
|
+
(0, node_test_1.test)("two salts produce the two concatenated outputs", () => {
|
|
415
|
+
const authenticator = new src_1.AuthenticatorEmulator({
|
|
416
|
+
hmacSecret: "hmac-secret",
|
|
417
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
418
|
+
});
|
|
419
|
+
const credentialId = credentialIdOf(makeCredential(authenticator, user, { "hmac-secret": true }).authData);
|
|
420
|
+
const both = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": new Uint8Array([...salt1, ...salt2]) }).authData);
|
|
421
|
+
assertUint8Array(both);
|
|
422
|
+
strict_1.default.equal(both.length, 64);
|
|
423
|
+
const first = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
424
|
+
const second = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt2 }).authData);
|
|
425
|
+
strict_1.default.deepEqual(both.slice(0, 32), first);
|
|
426
|
+
strict_1.default.deepEqual(both.slice(32), second);
|
|
427
|
+
});
|
|
428
|
+
(0, node_test_1.test)("rejects a salt that is not 32 or 64 bytes", () => {
|
|
429
|
+
const authenticator = new src_1.AuthenticatorEmulator({
|
|
430
|
+
hmacSecret: "hmac-secret",
|
|
431
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
432
|
+
});
|
|
433
|
+
const credentialId = credentialIdOf(makeCredential(authenticator, user, { "hmac-secret": true }).authData);
|
|
434
|
+
strict_1.default.throws(() => getAssertion(authenticator, credentialId, { "hmac-secret": new Uint8Array(33) }), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
435
|
+
});
|
|
436
|
+
(0, node_test_1.test)("no output for a credential created without hmac-secret", () => {
|
|
437
|
+
let authenticator = new src_1.AuthenticatorEmulator({
|
|
438
|
+
hmacSecret: "hmac-secret",
|
|
439
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
440
|
+
});
|
|
441
|
+
let credentialId = credentialIdOf(makeCredential(authenticator, user).authData);
|
|
442
|
+
let output = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
443
|
+
strict_1.default.equal(output, undefined);
|
|
444
|
+
authenticator = new src_1.AuthenticatorEmulator({
|
|
445
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
446
|
+
});
|
|
447
|
+
credentialId = credentialIdOf(makeCredential(authenticator, user, { "hmac-secret": true }).authData);
|
|
448
|
+
output = extensionOutput(getAssertion(authenticator, credentialId, { "hmac-secret": salt1 }).authData);
|
|
449
|
+
strict_1.default.equal(output, undefined);
|
|
450
|
+
});
|
|
451
|
+
(0, node_test_1.test)("hmac-secret makeCredential ignores makeCredential salt", () => {
|
|
452
|
+
const authenticator = new src_1.AuthenticatorEmulator({
|
|
453
|
+
hmacSecret: "hmac-secret",
|
|
454
|
+
credentialsRepository: new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository(),
|
|
455
|
+
});
|
|
456
|
+
const credential = makeCredential(authenticator, user, { "hmac-secret": true, "hmac-secret-mc": salt1 });
|
|
457
|
+
strict_1.default.equal(extensionOutput(credential.authData), true);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
@@ -5,7 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
7
|
const node_test_1 = require("node:test");
|
|
8
|
+
const authenticator_emulator_1 = require("../../src/authenticator/authenticator-emulator");
|
|
9
|
+
const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
|
|
8
10
|
const browser_injection_1 = require("../../src/test-utils/browser-injection");
|
|
11
|
+
const webauthn_emulator_1 = require("../../src/webauthn/webauthn-emulator");
|
|
9
12
|
(0, node_test_1.describe)("Browser Injection Test", () => {
|
|
10
13
|
(0, node_test_1.test)("Browser Injection Test", async () => {
|
|
11
14
|
const window = { navigator: { credentials: { create: undefined, get: undefined } } };
|
|
@@ -20,4 +23,45 @@ const browser_injection_1 = require("../../src/test-utils/browser-injection");
|
|
|
20
23
|
strict_1.default.notEqual(window.navigator.credentials.get, undefined);
|
|
21
24
|
strict_1.default.notEqual(PublicKeyCredential.signalUnknownCredential, undefined);
|
|
22
25
|
});
|
|
26
|
+
// The hooked create/get run in-page, so every helper they reach must be exported.
|
|
27
|
+
(0, node_test_1.test)("hooked create and get round-trip prf through the emulator", async () => {
|
|
28
|
+
const origin = "https://test-rp.org";
|
|
29
|
+
const emulator = new webauthn_emulator_1.WebAuthnEmulator(new authenticator_emulator_1.AuthenticatorEmulator({ hmacSecret: "hmac-secret-mc" }));
|
|
30
|
+
const window = {
|
|
31
|
+
navigator: { credentials: { create: undefined, get: undefined } },
|
|
32
|
+
webAuthnEmulatorCreate: async (optionsJSON) => emulator.createJSON(origin, optionsJSON),
|
|
33
|
+
webAuthnEmulatorGet: async (optionsJSON) => emulator.getJSON(origin, optionsJSON),
|
|
34
|
+
};
|
|
35
|
+
const PublicKeyCredential = { isConditionalMediationAvailable: async () => false };
|
|
36
|
+
// biome-ignore lint/security/noGlobalEval: This is a test code.
|
|
37
|
+
eval(browser_injection_1.HookWebAuthnApis);
|
|
38
|
+
strict_1.default.ok(await PublicKeyCredential.isConditionalMediationAvailable());
|
|
39
|
+
const first = encode_utils_1.default.strToUint8Array("prf-input-first");
|
|
40
|
+
const create = window.navigator.credentials.create;
|
|
41
|
+
strict_1.default.ok(create);
|
|
42
|
+
const created = await create({
|
|
43
|
+
publicKey: {
|
|
44
|
+
rp: { id: "test-rp.org", name: "rp" },
|
|
45
|
+
user: { id: encode_utils_1.default.strToUint8Array("user"), name: "user", displayName: "user" },
|
|
46
|
+
challenge: encode_utils_1.default.strToUint8Array("challenge"),
|
|
47
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
|
48
|
+
extensions: { prf: { eval: { first } } },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const createdPrf = created.getClientExtensionResults().prf?.results?.first;
|
|
52
|
+
strict_1.default.ok(createdPrf instanceof ArrayBuffer);
|
|
53
|
+
strict_1.default.equal(createdPrf.byteLength, 32);
|
|
54
|
+
const get = window.navigator.credentials.get;
|
|
55
|
+
strict_1.default.ok(get);
|
|
56
|
+
const asserted = await get({
|
|
57
|
+
publicKey: {
|
|
58
|
+
rpId: "test-rp.org",
|
|
59
|
+
challenge: encode_utils_1.default.strToUint8Array("challenge-2"),
|
|
60
|
+
extensions: { prf: { eval: { first } } },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const assertedPrf = asserted.getClientExtensionResults().prf?.results?.first;
|
|
64
|
+
strict_1.default.ok(assertedPrf instanceof ArrayBuffer);
|
|
65
|
+
strict_1.default.deepEqual(new Uint8Array(assertedPrf), new Uint8Array(createdPrf));
|
|
66
|
+
});
|
|
23
67
|
});
|
|
@@ -62,6 +62,15 @@ const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
|
|
|
62
62
|
strict_1.default.equal(encode_utils_1.default.decodeCbor(new Uint8Array([0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])), 0x1_0000_0000);
|
|
63
63
|
strict_1.default.equal(encode_utils_1.default.decodeCbor(new Uint8Array([0xf6])), null);
|
|
64
64
|
});
|
|
65
|
+
(0, node_test_1.test)("decodeCborWithRemainder returns the first value and trailing bytes", () => {
|
|
66
|
+
const first = encode_utils_1.default.encodeCbor({ 1: "a", [-2]: new Uint8Array([9, 9]) });
|
|
67
|
+
const second = encode_utils_1.default.encodeCbor({ 2: "b" });
|
|
68
|
+
const combined = new Uint8Array([...first, ...second]);
|
|
69
|
+
const { value, remainder } = encode_utils_1.default.decodeCborWithRemainder(combined);
|
|
70
|
+
strict_1.default.equal(value[1], "a");
|
|
71
|
+
strict_1.default.deepEqual(value[-2], new Uint8Array([9, 9]));
|
|
72
|
+
strict_1.default.deepEqual(remainder, second);
|
|
73
|
+
});
|
|
65
74
|
(0, node_test_1.test)("rejects unsupported values and malformed input", () => {
|
|
66
75
|
strict_1.default.throws(() => encode_utils_1.default.encodeCbor(Symbol("unsupported")), {
|
|
67
76
|
message: "Unsupported CBOR value type: symbol",
|