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.
- package/README.ja.md +10 -1
- package/README.md +10 -1
- package/dist/spec/authenticator/authenticator-emulator.spec.js +213 -65
- package/dist/spec/authenticator/ctap-model.spec.js +58 -53
- package/dist/spec/integration/integration.spec.js +7 -4
- package/dist/spec/integration/passkeys-api-client.d.ts +0 -1
- package/dist/spec/integration/webauthn-io.d.ts +0 -1
- package/dist/spec/libs/browser-injection.spec.js +55 -7
- package/dist/spec/libs/cbor.spec.js +57 -35
- package/dist/spec/libs/cose-key.spec.js +46 -40
- package/dist/spec/repository/credential-repository.spec.js +13 -12
- package/dist/spec/test-utils/unit-test.spec.js +28 -27
- package/dist/spec/webauthn/cose-key.spec.js +18 -17
- package/dist/spec/webauthn/webauthn-emulator.spec.js +376 -137
- package/dist/spec/webauthn/webauthn-model-json.spec.js +87 -42
- package/dist/spec/webauthn/webauthn-model.spec.js +59 -17
- package/dist/spec/webauthn/webauthn-test-server.d.ts +0 -1
- package/dist/spec/webauthn/webauthn-test-server.js +2 -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/repository/credentials-repository.d.ts +0 -1
- package/dist/src/test-utils/unit-test.d.ts +7 -5
- 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.d.ts +4 -4
- package/dist/src/webauthn/webauthn-emulator.js +100 -3
- package/dist/src/webauthn/webauthn-model-json.d.ts +7 -94
- package/dist/src/webauthn/webauthn-model-json.js +90 -9
- package/dist/src/webauthn/webauthn-model.d.ts +2 -0
- package/dist/src/webauthn/webauthn-model.js +28 -7
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
20
|
+
strict_1.default.throws(() => {
|
|
19
21
|
authenticator.command(testRequest);
|
|
20
|
-
}
|
|
22
|
+
}, { message: "CTAP error: CTAP1_ERR_INVALID_COMMAND (1)" });
|
|
21
23
|
});
|
|
22
24
|
});
|
|
23
|
-
(0,
|
|
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,
|
|
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,
|
|
68
|
-
(0,
|
|
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
|
-
|
|
81
|
+
strict_1.default.equal(response.totalCredentials, 2); // We created 2 credentials for rpId1
|
|
80
82
|
});
|
|
81
|
-
(0,
|
|
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
|
-
|
|
89
|
+
strict_1.default.throws(() => {
|
|
88
90
|
authenticator.authenticatorCredentialManagement(request);
|
|
89
|
-
}
|
|
91
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
90
92
|
});
|
|
91
|
-
(0,
|
|
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
|
-
|
|
104
|
+
strict_1.default.equal(response.totalCredentials, 0);
|
|
103
105
|
});
|
|
104
106
|
});
|
|
105
|
-
(0,
|
|
106
|
-
(0,
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
strict_1.default.throws(() => {
|
|
135
137
|
authenticator.authenticatorCredentialManagement(getNextRequest);
|
|
136
|
-
}
|
|
138
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
|
|
137
139
|
});
|
|
138
|
-
(0,
|
|
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
|
-
|
|
144
|
+
strict_1.default.throws(() => {
|
|
143
145
|
authenticator.authenticatorCredentialManagement(getNextRequest);
|
|
144
|
-
}
|
|
146
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
|
-
(0,
|
|
148
|
-
(0,
|
|
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
|
-
|
|
182
|
-
|
|
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,
|
|
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
|
-
|
|
194
|
+
strict_1.default.throws(() => {
|
|
193
195
|
authenticator.authenticatorCredentialManagement(request);
|
|
194
|
-
}
|
|
196
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
195
197
|
});
|
|
196
|
-
(0,
|
|
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
|
-
|
|
205
|
+
strict_1.default.throws(() => {
|
|
204
206
|
authenticator.authenticatorCredentialManagement(request);
|
|
205
|
-
}
|
|
207
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
206
208
|
});
|
|
207
|
-
(0,
|
|
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
|
-
|
|
222
|
+
strict_1.default.throws(() => {
|
|
221
223
|
authenticator.authenticatorCredentialManagement(request);
|
|
222
|
-
}
|
|
224
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
|
|
223
225
|
});
|
|
224
226
|
});
|
|
225
|
-
(0,
|
|
226
|
-
(0,
|
|
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
|
-
|
|
241
|
+
strict_1.default.throws(() => {
|
|
240
242
|
statelessAuthenticator.authenticatorCredentialManagement(request);
|
|
241
|
-
}
|
|
243
|
+
}, new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
|
|
242
244
|
});
|
|
243
245
|
});
|
|
244
|
-
(0,
|
|
245
|
-
(0,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
(
|
|
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,
|
|
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
|
-
(
|
|
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,
|
|
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
|
-
(
|
|
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
|
+
});
|