@magland/mochi 0.3.1 → 0.4.0
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.md +7 -6
- package/dist/agefile.js +47 -0
- package/dist/agescript.js +343 -0
- package/dist/api/admin.js +66 -28
- package/dist/api/auth.js +3 -2
- package/dist/api/backup.js +227 -192
- package/dist/api/contents.js +46 -16
- package/dist/api/repos.js +60 -4
- package/dist/api/write.js +138 -8
- package/dist/api.js +34 -1
- package/dist/browse.js +171 -33
- package/dist/build-info.json +2 -2
- package/dist/ci/api.js +138 -4
- package/dist/ci/artifacts.js +82 -2
- package/dist/ci/engine.js +30 -17
- package/dist/ci/expr.js +14 -0
- package/dist/ci/runners.js +60 -1
- package/dist/ci/runs.js +21 -3
- package/dist/ci/trigger.js +6 -4
- package/dist/ci/views.js +24 -3
- package/dist/ci/web.js +48 -0
- package/dist/cli/admin-cmd.js +40 -3
- package/dist/cli/backup-cmd.js +138 -120
- package/dist/cli/gitrun.js +50 -0
- package/dist/cli/pr-cmd.js +155 -0
- package/dist/cli/repo-cmd.js +134 -6
- package/dist/cli/sync-cmd.js +128 -0
- package/dist/cli/target.js +7 -3
- package/dist/config.js +10 -0
- package/dist/credentials.js +9 -8
- package/dist/deploy-cli.js +214 -109
- package/dist/deploy-runner-cli.js +125 -49
- package/dist/domains.js +259 -0
- package/dist/egress.js +23 -6
- package/dist/forms.js +703 -32
- package/dist/git.js +124 -13
- package/dist/githttp.js +80 -23
- package/dist/githubauth.js +185 -0
- package/dist/icons.js +14 -0
- package/dist/import-cli.js +40 -25
- package/dist/index.js +110 -4
- package/dist/issueweb.js +10 -6
- package/dist/lfs.js +9 -2
- package/dist/limit.js +2 -2
- package/dist/logincodes.js +31 -0
- package/dist/maintenance.js +297 -0
- package/dist/markdown.js +192 -3
- package/dist/mdedit.js +29 -0
- package/dist/migrate.js +48 -0
- package/dist/naming.js +25 -0
- package/dist/onetime.js +112 -0
- package/dist/ops.js +293 -16
- package/dist/pagescript.js +355 -24
- package/dist/perms.js +1 -0
- package/dist/profile.js +2 -0
- package/dist/pulls.js +21 -1
- package/dist/pullweb.js +25 -6
- package/dist/releases.js +10 -1
- package/dist/runner/actions.js +12 -1
- package/dist/runner/client.js +40 -2
- package/dist/runner/docker.js +40 -28
- package/dist/runner/download.js +77 -0
- package/dist/runner/externals.js +12 -1
- package/dist/runner/job.js +26 -11
- package/dist/runner/steps.js +42 -7
- package/dist/runner-cli.js +89 -0
- package/dist/scan.js +58 -2
- package/dist/server.js +107 -26
- package/dist/session.js +82 -40
- package/dist/site.js +110 -27
- package/dist/sitepublish.js +177 -0
- package/dist/sitesettings.js +420 -0
- package/dist/siteshost.js +116 -28
- package/dist/source.js +20 -0
- package/dist/style.js +162 -3
- package/dist/topics.js +165 -0
- package/dist/vault.js +399 -6
- package/dist/vendor-age.js +15 -0
- package/dist/views.js +242 -57
- package/dist/web.js +11 -1
- package/dist/webauthn.js +347 -0
- package/dist/webops.js +1263 -33
- package/package.json +4 -4
package/dist/webauthn.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SUPPORTED_ALGS = exports.COSE_RS256 = exports.COSE_EDDSA = exports.COSE_ES256 = exports.WebAuthnError = void 0;
|
|
37
|
+
exports.b64url = b64url;
|
|
38
|
+
exports.fromB64url = fromB64url;
|
|
39
|
+
exports.mintChallenge = mintChallenge;
|
|
40
|
+
exports.claimedChallenge = claimedChallenge;
|
|
41
|
+
exports.verifyRegistration = verifyRegistration;
|
|
42
|
+
exports.verifyAssertion = verifyAssertion;
|
|
43
|
+
const crypto = __importStar(require("crypto"));
|
|
44
|
+
// WebAuthn (passkey) verification, on node's own crypto and nothing else.
|
|
45
|
+
//
|
|
46
|
+
// A passkey is a public-key credential the browser keeps: registration hands
|
|
47
|
+
// the vault a public key, and signing in proves possession of the private one
|
|
48
|
+
// by signing a server-minted challenge. What a server has to implement is
|
|
49
|
+
// small and precisely specified -- parse the authenticator's CBOR, check the
|
|
50
|
+
// challenge, the origin, and the RP ID hash, and verify one signature -- so it
|
|
51
|
+
// is implemented here rather than through a dependency, the way the multipart
|
|
52
|
+
// parser and the cookie parser are. The subset is deliberate:
|
|
53
|
+
//
|
|
54
|
+
// - Attestation is not verified. Attestation says which make of
|
|
55
|
+
// authenticator minted the key, which matters to a bank with a hardware
|
|
56
|
+
// policy and not to a vault whose user is registering a key on their own
|
|
57
|
+
// account. Browsers send fmt "none" unless asked; whatever arrives, only
|
|
58
|
+
// authData is read.
|
|
59
|
+
// - Three algorithms: ES256 (-7) and EdDSA (-8), which are what passkeys
|
|
60
|
+
// are minted with today, and RS256 (-257) for older platform
|
|
61
|
+
// authenticators. The COSE key is converted to a DER SPKI once, at
|
|
62
|
+
// registration, so signing in is a single crypto.verify.
|
|
63
|
+
//
|
|
64
|
+
// Nothing here touches the vault or the request: callers hand in the expected
|
|
65
|
+
// challenge, origin, and RP ID, and get back parsed facts or a thrown
|
|
66
|
+
// WebAuthnError whose message is safe to show.
|
|
67
|
+
class WebAuthnError extends Error {
|
|
68
|
+
}
|
|
69
|
+
exports.WebAuthnError = WebAuthnError;
|
|
70
|
+
// COSE algorithm identifiers, the only ones offered at registration.
|
|
71
|
+
exports.COSE_ES256 = -7;
|
|
72
|
+
exports.COSE_EDDSA = -8;
|
|
73
|
+
exports.COSE_RS256 = -257;
|
|
74
|
+
exports.SUPPORTED_ALGS = [exports.COSE_ES256, exports.COSE_EDDSA, exports.COSE_RS256];
|
|
75
|
+
function b64url(buf) {
|
|
76
|
+
return buf.toString('base64url');
|
|
77
|
+
}
|
|
78
|
+
function fromB64url(s) {
|
|
79
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s))
|
|
80
|
+
throw new WebAuthnError('not base64url');
|
|
81
|
+
return Buffer.from(s, 'base64url');
|
|
82
|
+
}
|
|
83
|
+
/** A fresh challenge, base64url, as the client APIs expect to echo it back. */
|
|
84
|
+
function mintChallenge() {
|
|
85
|
+
return b64url(crypto.randomBytes(32));
|
|
86
|
+
}
|
|
87
|
+
function cborItem(buf, at) {
|
|
88
|
+
if (at >= buf.length)
|
|
89
|
+
throw new WebAuthnError('truncated CBOR');
|
|
90
|
+
const initial = buf[at];
|
|
91
|
+
const major = initial >> 5;
|
|
92
|
+
const info = initial & 0x1f;
|
|
93
|
+
let length = 0;
|
|
94
|
+
let next = at + 1;
|
|
95
|
+
if (info < 24) {
|
|
96
|
+
length = info;
|
|
97
|
+
}
|
|
98
|
+
else if (info === 24 || info === 25 || info === 26) {
|
|
99
|
+
const bytes = info === 24 ? 1 : info === 25 ? 2 : 4;
|
|
100
|
+
if (next + bytes > buf.length)
|
|
101
|
+
throw new WebAuthnError('truncated CBOR');
|
|
102
|
+
length = info === 24 ? buf[next] : info === 25 ? buf.readUInt16BE(next) : buf.readUInt32BE(next);
|
|
103
|
+
next += bytes;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// 27 is a 64-bit length and 31 an indefinite one; neither appears in the
|
|
107
|
+
// structures this parses, and a credential id or key that claimed to need
|
|
108
|
+
// one would not be worth believing.
|
|
109
|
+
throw new WebAuthnError('unsupported CBOR length');
|
|
110
|
+
}
|
|
111
|
+
switch (major) {
|
|
112
|
+
case 0:
|
|
113
|
+
return { value: length, next };
|
|
114
|
+
case 1:
|
|
115
|
+
return { value: -1 - length, next };
|
|
116
|
+
case 2: {
|
|
117
|
+
if (next + length > buf.length)
|
|
118
|
+
throw new WebAuthnError('truncated CBOR');
|
|
119
|
+
return { value: buf.subarray(next, next + length), next: next + length };
|
|
120
|
+
}
|
|
121
|
+
case 3: {
|
|
122
|
+
if (next + length > buf.length)
|
|
123
|
+
throw new WebAuthnError('truncated CBOR');
|
|
124
|
+
return { value: buf.subarray(next, next + length).toString('utf8'), next: next + length };
|
|
125
|
+
}
|
|
126
|
+
case 4: {
|
|
127
|
+
const arr = [];
|
|
128
|
+
for (let i = 0; i < length; i++) {
|
|
129
|
+
const item = cborItem(buf, next);
|
|
130
|
+
arr.push(item.value);
|
|
131
|
+
next = item.next;
|
|
132
|
+
}
|
|
133
|
+
return { value: arr, next };
|
|
134
|
+
}
|
|
135
|
+
case 5: {
|
|
136
|
+
const map = new Map();
|
|
137
|
+
for (let i = 0; i < length; i++) {
|
|
138
|
+
const key = cborItem(buf, next);
|
|
139
|
+
const val = cborItem(buf, key.next);
|
|
140
|
+
map.set(key.value, val.value);
|
|
141
|
+
next = val.next;
|
|
142
|
+
}
|
|
143
|
+
return { value: map, next };
|
|
144
|
+
}
|
|
145
|
+
case 7: {
|
|
146
|
+
if (info === 20)
|
|
147
|
+
return { value: false, next };
|
|
148
|
+
if (info === 21)
|
|
149
|
+
return { value: true, next };
|
|
150
|
+
if (info === 22)
|
|
151
|
+
return { value: null, next };
|
|
152
|
+
throw new WebAuthnError('unsupported CBOR simple value');
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
throw new WebAuthnError('unsupported CBOR type');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function cborDecode(buf) {
|
|
159
|
+
return cborItem(buf, 0).value;
|
|
160
|
+
}
|
|
161
|
+
// ---- COSE keys ----
|
|
162
|
+
function coseField(key, label) {
|
|
163
|
+
const v = key.get(label);
|
|
164
|
+
if (v === undefined)
|
|
165
|
+
throw new WebAuthnError('COSE key is missing a field');
|
|
166
|
+
return v;
|
|
167
|
+
}
|
|
168
|
+
function coseBytes(key, label) {
|
|
169
|
+
const v = coseField(key, label);
|
|
170
|
+
if (!Buffer.isBuffer(v))
|
|
171
|
+
throw new WebAuthnError('COSE key field is not bytes');
|
|
172
|
+
return v;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A COSE public key as a node KeyObject, via JWK import so no ASN.1 is built
|
|
176
|
+
* by hand. Returns the key with the algorithm it signs under, checked against
|
|
177
|
+
* the key's own type: a mismatched pair is refused here rather than trusted
|
|
178
|
+
* into vault.json.
|
|
179
|
+
*/
|
|
180
|
+
function coseToKey(cose) {
|
|
181
|
+
const kty = coseField(cose, 1);
|
|
182
|
+
const alg = coseField(cose, 3);
|
|
183
|
+
if (typeof alg !== 'number' || !exports.SUPPORTED_ALGS.includes(alg)) {
|
|
184
|
+
throw new WebAuthnError('the authenticator chose an algorithm this vault does not accept');
|
|
185
|
+
}
|
|
186
|
+
if (kty === 2 && alg === exports.COSE_ES256) {
|
|
187
|
+
if (coseField(cose, -1) !== 1)
|
|
188
|
+
throw new WebAuthnError('ES256 key is not on P-256');
|
|
189
|
+
const x = coseBytes(cose, -2);
|
|
190
|
+
const y = coseBytes(cose, -3);
|
|
191
|
+
if (x.length !== 32 || y.length !== 32)
|
|
192
|
+
throw new WebAuthnError('P-256 coordinates have the wrong length');
|
|
193
|
+
const jwk = { kty: 'EC', crv: 'P-256', x: b64url(x), y: b64url(y) };
|
|
194
|
+
return { key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), alg };
|
|
195
|
+
}
|
|
196
|
+
if (kty === 1 && alg === exports.COSE_EDDSA) {
|
|
197
|
+
if (coseField(cose, -1) !== 6)
|
|
198
|
+
throw new WebAuthnError('EdDSA key is not Ed25519');
|
|
199
|
+
const jwk = { kty: 'OKP', crv: 'Ed25519', x: b64url(coseBytes(cose, -2)) };
|
|
200
|
+
return { key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), alg };
|
|
201
|
+
}
|
|
202
|
+
if (kty === 3 && alg === exports.COSE_RS256) {
|
|
203
|
+
const jwk = { kty: 'RSA', n: b64url(coseBytes(cose, -1)), e: b64url(coseBytes(cose, -2)) };
|
|
204
|
+
return { key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), alg };
|
|
205
|
+
}
|
|
206
|
+
throw new WebAuthnError('unsupported COSE key type');
|
|
207
|
+
}
|
|
208
|
+
// ---- authenticator data ----
|
|
209
|
+
const FLAG_UP = 0x01;
|
|
210
|
+
const FLAG_UV = 0x04;
|
|
211
|
+
const FLAG_AT = 0x40;
|
|
212
|
+
function parseAuthData(data) {
|
|
213
|
+
if (data.length < 37)
|
|
214
|
+
throw new WebAuthnError('authenticator data is too short');
|
|
215
|
+
const flags = data[32];
|
|
216
|
+
const counter = data.readUInt32BE(33);
|
|
217
|
+
let credential = null;
|
|
218
|
+
if (flags & FLAG_AT) {
|
|
219
|
+
if (data.length < 55)
|
|
220
|
+
throw new WebAuthnError('attested credential data is too short');
|
|
221
|
+
const idLen = data.readUInt16BE(53);
|
|
222
|
+
if (55 + idLen > data.length)
|
|
223
|
+
throw new WebAuthnError('credential id is truncated');
|
|
224
|
+
const id = data.subarray(55, 55 + idLen);
|
|
225
|
+
const cose = cborItem(data, 55 + idLen).value;
|
|
226
|
+
if (!(cose instanceof Map))
|
|
227
|
+
throw new WebAuthnError('credential public key is not a COSE map');
|
|
228
|
+
credential = { id, cose };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
rpIdHash: data.subarray(0, 32),
|
|
232
|
+
userPresent: (flags & FLAG_UP) !== 0,
|
|
233
|
+
userVerified: (flags & FLAG_UV) !== 0,
|
|
234
|
+
counter,
|
|
235
|
+
credential,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// ---- client data ----
|
|
239
|
+
function checkClientData(clientDataJSON, expect) {
|
|
240
|
+
let parsed;
|
|
241
|
+
try {
|
|
242
|
+
parsed = JSON.parse(clientDataJSON.toString('utf8'));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw new WebAuthnError('clientDataJSON is not JSON');
|
|
246
|
+
}
|
|
247
|
+
const c = (typeof parsed === 'object' && parsed !== null ? parsed : {});
|
|
248
|
+
if (c.type !== expect.type)
|
|
249
|
+
throw new WebAuthnError('wrong WebAuthn ceremony type');
|
|
250
|
+
if (typeof c.challenge !== 'string' || c.challenge !== expect.challenge) {
|
|
251
|
+
throw new WebAuthnError('the challenge does not match');
|
|
252
|
+
}
|
|
253
|
+
if (typeof c.origin !== 'string' || c.origin !== expect.origin) {
|
|
254
|
+
throw new WebAuthnError(`the origin does not match this vault (${String(c.origin)})`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The challenge a client data blob claims to answer, read before anything is
|
|
259
|
+
* verified. It is how the server finds the pending challenge to check the rest
|
|
260
|
+
* against; nothing is trusted from it until checkClientData has run.
|
|
261
|
+
*/
|
|
262
|
+
function claimedChallenge(clientDataJSON) {
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(clientDataJSON.toString('utf8'));
|
|
265
|
+
return typeof parsed.challenge === 'string' ? parsed.challenge : null;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function rpIdHashOk(authData, rpId) {
|
|
272
|
+
const expected = crypto.createHash('sha256').update(rpId).digest();
|
|
273
|
+
return authData.rpIdHash.length === expected.length && crypto.timingSafeEqual(authData.rpIdHash, expected);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Verify a navigator.credentials.create() response. The challenge must be one
|
|
277
|
+
* this server minted (the caller's business to have checked, via a one-time
|
|
278
|
+
* store); everything else is checked here.
|
|
279
|
+
*/
|
|
280
|
+
function verifyRegistration(input) {
|
|
281
|
+
checkClientData(input.clientDataJSON, {
|
|
282
|
+
type: 'webauthn.create',
|
|
283
|
+
challenge: input.challenge,
|
|
284
|
+
origin: input.origin,
|
|
285
|
+
});
|
|
286
|
+
const att = cborDecode(input.attestationObject);
|
|
287
|
+
if (!(att instanceof Map))
|
|
288
|
+
throw new WebAuthnError('attestation object is not a CBOR map');
|
|
289
|
+
const rawAuthData = att.get('authData');
|
|
290
|
+
if (!Buffer.isBuffer(rawAuthData))
|
|
291
|
+
throw new WebAuthnError('attestation object has no authData');
|
|
292
|
+
const authData = parseAuthData(rawAuthData);
|
|
293
|
+
if (!rpIdHashOk(authData, input.rpId)) {
|
|
294
|
+
throw new WebAuthnError('the credential was made for a different hostname');
|
|
295
|
+
}
|
|
296
|
+
if (!authData.userPresent)
|
|
297
|
+
throw new WebAuthnError('the authenticator did not report user presence');
|
|
298
|
+
if (!authData.credential)
|
|
299
|
+
throw new WebAuthnError('no credential in the registration');
|
|
300
|
+
if (authData.credential.id.length < 8 || authData.credential.id.length > 1023) {
|
|
301
|
+
throw new WebAuthnError('credential id has an unreasonable length');
|
|
302
|
+
}
|
|
303
|
+
const { key, alg } = coseToKey(authData.credential.cose);
|
|
304
|
+
return {
|
|
305
|
+
id: b64url(authData.credential.id),
|
|
306
|
+
publicKey: b64url(key.export({ type: 'spki', format: 'der' })),
|
|
307
|
+
alg,
|
|
308
|
+
counter: authData.counter,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Verify a navigator.credentials.get() response against a stored public key.
|
|
313
|
+
* The signature covers authenticatorData plus the hash of clientDataJSON,
|
|
314
|
+
* which is what binds the challenge and the origin to the key.
|
|
315
|
+
*/
|
|
316
|
+
function verifyAssertion(input) {
|
|
317
|
+
checkClientData(input.clientDataJSON, {
|
|
318
|
+
type: 'webauthn.get',
|
|
319
|
+
challenge: input.challenge,
|
|
320
|
+
origin: input.origin,
|
|
321
|
+
});
|
|
322
|
+
const authData = parseAuthData(input.authenticatorData);
|
|
323
|
+
if (!rpIdHashOk(authData, input.rpId)) {
|
|
324
|
+
throw new WebAuthnError('the assertion was made for a different hostname');
|
|
325
|
+
}
|
|
326
|
+
if (!authData.userPresent)
|
|
327
|
+
throw new WebAuthnError('the authenticator did not report user presence');
|
|
328
|
+
let key;
|
|
329
|
+
try {
|
|
330
|
+
key = crypto.createPublicKey({ key: fromB64url(input.publicKey), format: 'der', type: 'spki' });
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
throw new WebAuthnError('the stored public key could not be read');
|
|
334
|
+
}
|
|
335
|
+
const signed = Buffer.concat([
|
|
336
|
+
input.authenticatorData,
|
|
337
|
+
crypto.createHash('sha256').update(input.clientDataJSON).digest(),
|
|
338
|
+
]);
|
|
339
|
+
// ES256 and RS256 hash before signing and their WebAuthn signatures are
|
|
340
|
+
// ASN.1 DER, which is node's default; Ed25519 signs the message itself.
|
|
341
|
+
const ok = input.alg === exports.COSE_EDDSA
|
|
342
|
+
? crypto.verify(null, signed, key, input.signature)
|
|
343
|
+
: crypto.verify('sha256', signed, key, input.signature);
|
|
344
|
+
if (!ok)
|
|
345
|
+
throw new WebAuthnError('the signature does not verify');
|
|
346
|
+
return { counter: authData.counter, userVerified: authData.userVerified };
|
|
347
|
+
}
|