nid-webauthn-emulator 0.2.13 → 0.2.15

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.
Files changed (36) hide show
  1. package/README.ja.md +10 -1
  2. package/README.md +10 -1
  3. package/dist/spec/authenticator/authenticator-emulator.spec.js +213 -65
  4. package/dist/spec/authenticator/ctap-model.spec.js +58 -53
  5. package/dist/spec/integration/integration.spec.js +7 -4
  6. package/dist/spec/integration/passkeys-api-client.d.ts +0 -1
  7. package/dist/spec/integration/webauthn-io.d.ts +0 -1
  8. package/dist/spec/libs/browser-injection.spec.js +55 -7
  9. package/dist/spec/libs/cbor.spec.js +57 -35
  10. package/dist/spec/libs/cose-key.spec.js +46 -40
  11. package/dist/spec/repository/credential-repository.spec.js +13 -12
  12. package/dist/spec/test-utils/unit-test.spec.js +28 -27
  13. package/dist/spec/webauthn/cose-key.spec.js +18 -17
  14. package/dist/spec/webauthn/webauthn-emulator.spec.js +376 -137
  15. package/dist/spec/webauthn/webauthn-model-json.spec.js +87 -42
  16. package/dist/spec/webauthn/webauthn-model.spec.js +59 -17
  17. package/dist/spec/webauthn/webauthn-test-server.d.ts +0 -1
  18. package/dist/spec/webauthn/webauthn-test-server.js +2 -2
  19. package/dist/src/authenticator/authenticator-emulator.d.ts +3 -0
  20. package/dist/src/authenticator/authenticator-emulator.js +71 -8
  21. package/dist/src/libs/cbor.d.ts +4 -0
  22. package/dist/src/libs/cbor.js +9 -0
  23. package/dist/src/libs/encode-utils.d.ts +2 -1
  24. package/dist/src/libs/encode-utils.js +1 -0
  25. package/dist/src/repository/credentials-file-repository.js +2 -1
  26. package/dist/src/repository/credentials-repository.d.ts +0 -1
  27. package/dist/src/test-utils/unit-test.d.ts +7 -5
  28. package/dist/src/webauthn/cose-key.d.ts +1 -0
  29. package/dist/src/webauthn/cose-key.js +6 -16
  30. package/dist/src/webauthn/webauthn-emulator.d.ts +4 -4
  31. package/dist/src/webauthn/webauthn-emulator.js +100 -3
  32. package/dist/src/webauthn/webauthn-model-json.d.ts +7 -94
  33. package/dist/src/webauthn/webauthn-model-json.js +90 -9
  34. package/dist/src/webauthn/webauthn-model.d.ts +2 -0
  35. package/dist/src/webauthn/webauthn-model.js +28 -7
  36. package/package.json +7 -10
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) : 認証情報の作成
@@ -94,6 +97,12 @@ const authenticatorResponse = unpackMakeCredentialResponse(this.authenticator.co
94
97
 
95
98
  WebAuthn の著名なデモサイトである [webauthn.io](https://webauthn.io/) での使用例を示します。[統合テスト](spec/integration/integration.spec.ts) に実際にうごくテストコード例があります。
96
99
 
100
+ webauthn.io の統合テストは外部サイトに依存するため、通常の `pnpm test` ではスキップされます。このテストだけを実行する場合は、下記のコマンドを利用してください。
101
+
102
+ ```sh
103
+ pnpm test:webauthn-io
104
+ ```
105
+
97
106
  ```TypeScript
98
107
  // Origin および WebAuthn API エミュレータを初期化します
99
108
  // ここでは、https://webauthn.io を Origin として利用します
@@ -203,7 +212,7 @@ await page.evaluate(BrowserInjection.HookWebAuthnApis);
203
212
 
204
213
  ## ユニットテストでの WebAuthn API フック
205
214
 
206
- Vitest/Jest などで WebAuthn API を差し替えたい場合は、`src/test-utils/unit-test.ts` の `createPasskeysEmulator` を使うと `PublicKeyCredential` / `navigator.credentials` を簡単にモックできます。
215
+ ユニットテストで WebAuthn API を差し替えたい場合は、`src/test-utils/unit-test.ts` の `createPasskeysEmulator` を使うと `PublicKeyCredential` / `navigator.credentials` を簡単にモックできます。
207
216
 
208
217
  ```TypeScript
209
218
  import { createPasskeysEmulator } from "nid-webauthn-emulator";
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
@@ -95,6 +98,12 @@ const authenticatorResponse = unpackMakeCredentialResponse(this.authenticator.co
95
98
 
96
99
  Here's an example of usage with [webauthn.io](https://webauthn.io/), a well-known WebAuthn demo site. You can find working test code examples in the [integration tests](spec/integration/integration.spec.ts).
97
100
 
101
+ The webauthn.io integration test is skipped by the default `pnpm test` command because it depends on an external site. To run only that test, use:
102
+
103
+ ```sh
104
+ pnpm test:webauthn-io
105
+ ```
106
+
98
107
  ```TypeScript
99
108
  // Initialize the Origin and WebAuthn API emulator
100
109
  // Here we use https://webauthn.io as the Origin
@@ -212,7 +221,7 @@ This ensures that the `WebAuthnEmulator` methods defined earlier with `exposeFun
212
221
 
213
222
  ## Hooking WebAuthn APIs in Unit Tests
214
223
 
215
- If you want to swap out WebAuthn APIs in Vitest/Jest, you can mock `PublicKeyCredential` / `navigator.credentials` using `createPasskeysEmulator` from `src/test-utils/unit-test.ts`.
224
+ If you want to swap out WebAuthn APIs in unit tests, you can mock `PublicKeyCredential` / `navigator.credentials` using `createPasskeysEmulator` from `src/test-utils/unit-test.ts`.
216
225
 
217
226
  ```TypeScript
218
227
  import { createPasskeysEmulator } from "nid-webauthn-emulator";
@@ -3,24 +3,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const globals_1 = require("@jest/globals");
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = require("node:test");
7
8
  const src_1 = require("../../src");
8
9
  const ctap_model_1 = require("../../src/authenticator/ctap-model");
9
10
  const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
10
11
  const credentials_memory_repository_1 = require("../../src/repository/credentials-memory-repository");
11
- (0, globals_1.describe)("Authenticator Emulator Exceptional Test", () => {
12
+ const webauthn_model_1 = require("../../src/webauthn/webauthn-model");
13
+ (0, node_test_1.describe)("Authenticator Emulator Exceptional Test", () => {
12
14
  // Success case has been tested in the webauthn-emulator.spec.ts
13
- (0, globals_1.test)("Unknown command _ CTAP Error", async () => {
15
+ (0, node_test_1.test)("Unknown command _ CTAP Error", async () => {
14
16
  const testRequest = {
15
17
  command: ctap_model_1.CTAP_COMMAND.authenticatorReset,
16
18
  };
17
19
  const authenticator = new src_1.AuthenticatorEmulator();
18
- (0, globals_1.expect)(() => {
20
+ strict_1.default.throws(() => {
19
21
  authenticator.command(testRequest);
20
- }).toThrow("CTAP error: CTAP1_ERR_INVALID_COMMAND (1)");
22
+ }, { message: "CTAP error: CTAP1_ERR_INVALID_COMMAND (1)" });
21
23
  });
22
24
  });
23
- (0, globals_1.describe)("Authenticator Credential Management Tests", () => {
25
+ (0, node_test_1.describe)("Authenticator Credential Management Tests", () => {
24
26
  let authenticator;
25
27
  let repository;
26
28
  // Test data
@@ -41,7 +43,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
41
43
  name: "user3",
42
44
  displayName: "User Three",
43
45
  };
44
- (0, globals_1.beforeEach)(() => {
46
+ (0, node_test_1.beforeEach)(() => {
45
47
  // Create a new repository for each test
46
48
  repository = new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository();
47
49
  // Create authenticator with the repository
@@ -64,8 +66,8 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
64
66
  };
65
67
  authenticator.authenticatorMakeCredential(makeCredentialRequest);
66
68
  }
67
- (0, globals_1.describe)("enumerateCredentialsBegin", () => {
68
- (0, globals_1.test)("should return the total number of credentials for a specific RP", () => {
69
+ (0, node_test_1.describe)("enumerateCredentialsBegin", () => {
70
+ (0, node_test_1.test)("should return the total number of credentials for a specific RP", () => {
69
71
  // Create request for enumerateCredentialsBegin
70
72
  const request = {
71
73
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
@@ -76,19 +78,19 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
76
78
  // Execute the command
77
79
  const response = authenticator.authenticatorCredentialManagement(request);
78
80
  // Verify the response
79
- (0, globals_1.expect)(response.totalCredentials).toBe(2); // We created 2 credentials for rpId1
81
+ strict_1.default.equal(response.totalCredentials, 2); // We created 2 credentials for rpId1
80
82
  });
81
- (0, globals_1.test)("should throw an error if rpId is not provided", () => {
83
+ (0, node_test_1.test)("should throw an error if rpId is not provided", () => {
82
84
  // Create request without rpId
83
85
  const request = {
84
86
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
85
87
  };
86
88
  // Execute the command and expect an error
87
- (0, globals_1.expect)(() => {
89
+ strict_1.default.throws(() => {
88
90
  authenticator.authenticatorCredentialManagement(request);
89
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
91
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
90
92
  });
91
- (0, globals_1.test)("should return 0 credentials for an RP with no credentials", () => {
93
+ (0, node_test_1.test)("should return 0 credentials for an RP with no credentials", () => {
92
94
  // Create request for a non-existent RP
93
95
  const request = {
94
96
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
@@ -99,11 +101,11 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
99
101
  // Execute the command
100
102
  const response = authenticator.authenticatorCredentialManagement(request);
101
103
  // Verify the response
102
- (0, globals_1.expect)(response.totalCredentials).toBe(0);
104
+ strict_1.default.equal(response.totalCredentials, 0);
103
105
  });
104
106
  });
105
- (0, globals_1.describe)("enumerateCredentialsGetNextCredential", () => {
106
- (0, globals_1.test)("should return credentials one by one", () => {
107
+ (0, node_test_1.describe)("enumerateCredentialsGetNextCredential", () => {
108
+ (0, node_test_1.test)("should return credentials one by one", () => {
107
109
  // First, call enumerateCredentialsBegin
108
110
  const beginRequest = {
109
111
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
@@ -118,34 +120,34 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
118
120
  };
119
121
  // Get first credential
120
122
  const response1 = authenticator.authenticatorCredentialManagement(getNextRequest);
121
- (0, globals_1.expect)(response1.user).toBeDefined();
122
- (0, globals_1.expect)(response1.credentialID).toBeDefined();
123
- (0, globals_1.expect)(response1.publicKey).toBeDefined();
123
+ strict_1.default.notEqual(response1.user, undefined);
124
+ strict_1.default.notEqual(response1.credentialID, undefined);
125
+ strict_1.default.notEqual(response1.publicKey, undefined);
124
126
  // Get second credential
125
127
  const response2 = authenticator.authenticatorCredentialManagement(getNextRequest);
126
- (0, globals_1.expect)(response2.user).toBeDefined();
127
- (0, globals_1.expect)(response2.credentialID).toBeDefined();
128
- (0, globals_1.expect)(response2.publicKey).toBeDefined();
128
+ strict_1.default.notEqual(response2.user, undefined);
129
+ strict_1.default.notEqual(response2.credentialID, undefined);
130
+ strict_1.default.notEqual(response2.publicKey, undefined);
129
131
  // Verify we got different credentials
130
132
  if (response1.credentialID && response2.credentialID) {
131
- (0, globals_1.expect)(encode_utils_1.default.encodeBase64Url(response1.credentialID)).not.toBe(encode_utils_1.default.encodeBase64Url(response2.credentialID));
133
+ strict_1.default.notEqual(encode_utils_1.default.encodeBase64Url(response1.credentialID), encode_utils_1.default.encodeBase64Url(response2.credentialID));
132
134
  }
133
135
  // Verify we can't get more credentials
134
- (0, globals_1.expect)(() => {
136
+ strict_1.default.throws(() => {
135
137
  authenticator.authenticatorCredentialManagement(getNextRequest);
136
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
138
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
137
139
  });
138
- (0, globals_1.test)("should throw an error if called without calling enumerateCredentialsBegin first", () => {
140
+ (0, node_test_1.test)("should throw an error if called without calling enumerateCredentialsBegin first", () => {
139
141
  const getNextRequest = {
140
142
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
141
143
  };
142
- (0, globals_1.expect)(() => {
144
+ strict_1.default.throws(() => {
143
145
  authenticator.authenticatorCredentialManagement(getNextRequest);
144
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
146
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
145
147
  });
146
148
  });
147
- (0, globals_1.describe)("updateUserInformation", () => {
148
- (0, globals_1.test)("should update user information for all credentials of a user", () => {
149
+ (0, node_test_1.describe)("updateUserInformation", () => {
150
+ (0, node_test_1.test)("should update user information for all credentials of a user", () => {
149
151
  // Create updated user information
150
152
  const updatedUser = {
151
153
  id: user1.id, // Same ID
@@ -178,33 +180,33 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
178
180
  // Find the credential for user1
179
181
  if (nextResponse.user &&
180
182
  encode_utils_1.default.encodeBase64Url(nextResponse.user.id) === encode_utils_1.default.encodeBase64Url(user1.id)) {
181
- (0, globals_1.expect)(nextResponse.user.name).toBe(updatedUser.name);
182
- (0, globals_1.expect)(nextResponse.user.displayName).toBe(updatedUser.displayName);
183
+ strict_1.default.equal(nextResponse.user.name, updatedUser.name);
184
+ strict_1.default.equal(nextResponse.user.displayName, updatedUser.displayName);
183
185
  }
184
186
  });
185
- (0, globals_1.test)("should throw an error if rpId is not provided", () => {
187
+ (0, node_test_1.test)("should throw an error if rpId is not provided", () => {
186
188
  const request = {
187
189
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
188
190
  subCommandParams: {
189
191
  user: user1,
190
192
  },
191
193
  };
192
- (0, globals_1.expect)(() => {
194
+ strict_1.default.throws(() => {
193
195
  authenticator.authenticatorCredentialManagement(request);
194
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
196
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
195
197
  });
196
- (0, globals_1.test)("should throw an error if user is not provided", () => {
198
+ (0, node_test_1.test)("should throw an error if user is not provided", () => {
197
199
  const request = {
198
200
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
199
201
  subCommandParams: {
200
202
  rpId: rpId1,
201
203
  },
202
204
  };
203
- (0, globals_1.expect)(() => {
205
+ strict_1.default.throws(() => {
204
206
  authenticator.authenticatorCredentialManagement(request);
205
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
207
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
206
208
  });
207
- (0, globals_1.test)("should throw an error if no credentials exist for the user", () => {
209
+ (0, node_test_1.test)("should throw an error if no credentials exist for the user", () => {
208
210
  const nonExistentUser = {
209
211
  id: encode_utils_1.default.strToUint8Array("nonexistent-id"),
210
212
  name: "nonexistent",
@@ -217,13 +219,13 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
217
219
  user: nonExistentUser,
218
220
  },
219
221
  };
220
- (0, globals_1.expect)(() => {
222
+ strict_1.default.throws(() => {
221
223
  authenticator.authenticatorCredentialManagement(request);
222
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
224
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
223
225
  });
224
226
  });
225
- (0, globals_1.describe)("authenticatorCredentialManagement with stateless authenticator", () => {
226
- (0, globals_1.test)("should throw an error if authenticator is stateless", () => {
227
+ (0, node_test_1.describe)("authenticatorCredentialManagement with stateless authenticator", () => {
228
+ (0, node_test_1.test)("should throw an error if authenticator is stateless", () => {
227
229
  // Create a stateless authenticator
228
230
  const statelessAuthenticator = new src_1.AuthenticatorEmulator({
229
231
  stateless: true,
@@ -236,76 +238,222 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
236
238
  },
237
239
  };
238
240
  // Execute the command and expect an error
239
- (0, globals_1.expect)(() => {
241
+ strict_1.default.throws(() => {
240
242
  statelessAuthenticator.authenticatorCredentialManagement(request);
241
- }).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
243
+ }, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
242
244
  });
243
245
  });
244
- (0, globals_1.describe)("Authenticator Credential Management additional error coverage", () => {
245
- (0, globals_1.test)("dispatch default branch throws invalid command", () => {
246
+ (0, node_test_1.describe)("Authenticator Credential Management additional error coverage", () => {
247
+ (0, node_test_1.test)("dispatch default branch throws invalid command", () => {
246
248
  const authenticator = new src_1.AuthenticatorEmulator();
247
249
  // Build a CTAP request with an unknown subCommand to hit default branch
248
250
  const data = encode_utils_1.default.encodeCbor({ "1": -1 });
249
- (0, globals_1.expect)(() => authenticator.command({ command: ctap_model_1.CTAP_COMMAND.authenticatorCredentialManagement, data })).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_COMMAND));
251
+ strict_1.default.throws(() => authenticator.command({ command: ctap_model_1.CTAP_COMMAND.authenticatorCredentialManagement, data }), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_COMMAND));
250
252
  });
251
- (0, globals_1.test)("enumerateCredentialsBegin without repository throws not allowed", () => {
253
+ (0, node_test_1.test)("enumerateCredentialsBegin without repository throws not allowed", () => {
252
254
  const authenticator = new src_1.AuthenticatorEmulator({ stateless: true });
253
255
  const req = (0, ctap_model_1.packCredentialManagementRequest)({
254
256
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
255
257
  subCommandParams: { rpId: "example.com" },
256
258
  });
257
- (0, globals_1.expect)(() => authenticator.command(req)).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
259
+ strict_1.default.throws(() => authenticator.command(req), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
258
260
  });
259
- (0, globals_1.test)("updateUserInformation without repository throws not allowed", () => {
261
+ (0, node_test_1.test)("updateUserInformation without repository throws not allowed", () => {
260
262
  const authenticator = new src_1.AuthenticatorEmulator({ stateless: true });
261
263
  const req = (0, ctap_model_1.packCredentialManagementRequest)({
262
264
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
263
265
  subCommandParams: { rpId: "example.com", user: { id: new Uint8Array([1]), name: "n", displayName: "d" } },
264
266
  });
265
- (0, globals_1.expect)(() => authenticator.command(req)).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
267
+ strict_1.default.throws(() => authenticator.command(req), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
266
268
  });
267
- (0, globals_1.test)("deleteCredential without repository throws not allowed", () => {
269
+ (0, node_test_1.test)("deleteCredential without repository throws not allowed", () => {
268
270
  const authenticator = new src_1.AuthenticatorEmulator({ stateless: true });
269
271
  const req = (0, ctap_model_1.packCredentialManagementRequest)({ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential });
270
- (0, globals_1.expect)(() => authenticator.command(req)).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
272
+ strict_1.default.throws(() => authenticator.command(req), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
271
273
  });
272
- (0, globals_1.test)("deleteCredential without credentialId throws invalid parameter", () => {
274
+ (0, node_test_1.test)("deleteCredential without credentialId throws invalid parameter", () => {
273
275
  const authenticator = new src_1.AuthenticatorEmulator();
274
276
  const req = (0, ctap_model_1.packCredentialManagementRequest)({ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential });
275
- (0, globals_1.expect)(() => authenticator.command(req)).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
277
+ strict_1.default.throws(() => authenticator.command(req), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
276
278
  });
277
- (0, globals_1.test)("deleteCredential for non-existent id throws no credentials", () => {
279
+ (0, node_test_1.test)("deleteCredential for non-existent id throws no credentials", () => {
278
280
  const authenticator = new src_1.AuthenticatorEmulator();
279
281
  const credentialId = encode_utils_1.default.strToUint8Array("non-existent-id");
280
282
  const req = (0, ctap_model_1.packCredentialManagementRequest)({
281
283
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential,
282
284
  subCommandParams: { credentialId },
283
285
  });
284
- (0, globals_1.expect)(() => authenticator.command(req)).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
286
+ strict_1.default.throws(() => authenticator.command(req), new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
285
287
  });
286
- (0, globals_1.test)("private enumerateCredentialsBegin guard throws not allowed when repository missing", () => {
288
+ (0, node_test_1.test)("private enumerateCredentialsBegin guard throws not allowed when repository missing", () => {
287
289
  const authenticator = new src_1.AuthenticatorEmulator({ stateless: true });
288
290
  const target = authenticator.authenticatorEnumerateCredentialsBegin.bind(authenticator);
289
291
  const call = () => target({
290
292
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
291
293
  subCommandParams: { rpId: "x" },
292
294
  });
293
- (0, globals_1.expect)(call).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
295
+ strict_1.default.throws(call, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
294
296
  });
295
- (0, globals_1.test)("private updateUserInformation guard throws not allowed when repository missing", () => {
297
+ (0, node_test_1.test)("private updateUserInformation guard throws not allowed when repository missing", () => {
296
298
  const authenticator = new src_1.AuthenticatorEmulator({ stateless: true });
297
299
  const target = authenticator.authenticatorUpdateUserInformation.bind(authenticator);
298
300
  const call = () => target({
299
301
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
300
302
  subCommandParams: { rpId: "x", user: { id: new Uint8Array([1]), name: "n", displayName: "d" } },
301
303
  });
302
- (0, globals_1.expect)(call).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
304
+ strict_1.default.throws(call, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
303
305
  });
304
- (0, globals_1.test)("private deleteCredential guard throws not allowed when repository missing", () => {
306
+ (0, node_test_1.test)("private deleteCredential guard throws not allowed when repository missing", () => {
305
307
  const authenticator = new src_1.AuthenticatorEmulator({ stateless: true });
306
308
  const target = authenticator.authenticatorDeleteCredential.bind(authenticator);
307
309
  const call = () => target({ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential });
308
- (0, globals_1.expect)(call).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
310
+ strict_1.default.throws(call, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
309
311
  });
310
312
  });
311
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
+ });