@ocap/mcrypto 1.29.22 → 1.29.23
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/esm/_virtual/rolldown_runtime.mjs +18 -0
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +2 -1
- package/esm/signer/ethereum.d.mts +1 -1
- package/esm/signer/ethereum.mjs +25 -9
- package/esm/signer/passkey.d.mts +1 -1
- package/esm/signer/passkey.mjs +2 -2
- package/esm/signer/secp256k1.d.mts +1 -1
- package/esm/signer/secp256k1.mjs +21 -25
- package/esm/webauthn.d.mts +210 -0
- package/esm/webauthn.mjs +512 -0
- package/lib/_virtual/rolldown_runtime.cjs +14 -0
- package/lib/index.cjs +8 -1
- package/lib/index.d.cts +2 -1
- package/lib/signer/ethereum.cjs +25 -11
- package/lib/signer/ethereum.d.cts +1 -1
- package/lib/signer/passkey.cjs +7 -7
- package/lib/signer/passkey.d.cts +1 -1
- package/lib/signer/secp256k1.cjs +20 -25
- package/lib/signer/secp256k1.d.cts +1 -1
- package/lib/webauthn.cjs +533 -0
- package/lib/webauthn.d.cts +210 -0
- package/package.json +5 -9
package/esm/webauthn.mjs
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/rolldown_runtime.mjs";
|
|
2
|
+
import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
4
|
+
import { p256 } from "@noble/curves/p256";
|
|
5
|
+
import { p384 } from "@noble/curves/p384";
|
|
6
|
+
import { p521 } from "@noble/curves/p521";
|
|
7
|
+
|
|
8
|
+
//#region src/webauthn.ts
|
|
9
|
+
/**
|
|
10
|
+
* Lightweight WebAuthn/passkey utilities — replaces @simplewebauthn/server/helpers.
|
|
11
|
+
*
|
|
12
|
+
* Provides COSE constants, CBOR-based credential public key decoding, base64url helpers,
|
|
13
|
+
* authenticator data parsing, and ECDSA signature verification using @noble/curves.
|
|
14
|
+
*
|
|
15
|
+
* Designed for environments sensitive to bundle size (e.g. Cloudflare Workers).
|
|
16
|
+
*/
|
|
17
|
+
var webauthn_exports = /* @__PURE__ */ __exportAll({
|
|
18
|
+
COSEALG: () => COSEALG,
|
|
19
|
+
COSECRV: () => COSECRV,
|
|
20
|
+
COSEKEYS: () => COSEKEYS,
|
|
21
|
+
COSEKTY: () => COSEKTY,
|
|
22
|
+
cose: () => cose,
|
|
23
|
+
decodeClientDataJSON: () => decodeClientDataJSON,
|
|
24
|
+
generateAuthenticationOptions: () => generateAuthenticationOptions,
|
|
25
|
+
generateChallenge: () => generateChallenge,
|
|
26
|
+
generateRegistrationOptions: () => generateRegistrationOptions,
|
|
27
|
+
isoBase64URL: () => isoBase64URL,
|
|
28
|
+
isoCBOR: () => isoCBOR,
|
|
29
|
+
isoUint8Array: () => isoUint8Array,
|
|
30
|
+
parseAuthenticatorData: () => parseAuthenticatorData,
|
|
31
|
+
toHash: () => toHash,
|
|
32
|
+
verifyRegistrationResponse: () => verifyRegistrationResponse,
|
|
33
|
+
verifySignature: () => verifySignature
|
|
34
|
+
});
|
|
35
|
+
const COSEKEYS = {
|
|
36
|
+
kty: 1,
|
|
37
|
+
alg: 3,
|
|
38
|
+
crv: -1,
|
|
39
|
+
x: -2,
|
|
40
|
+
y: -3,
|
|
41
|
+
n: -1,
|
|
42
|
+
e: -2
|
|
43
|
+
};
|
|
44
|
+
const COSEKTY = {
|
|
45
|
+
OKP: 1,
|
|
46
|
+
EC2: 2,
|
|
47
|
+
RSA: 3
|
|
48
|
+
};
|
|
49
|
+
const COSEALG = {
|
|
50
|
+
ES256: -7,
|
|
51
|
+
EdDSA: -8,
|
|
52
|
+
ES384: -35,
|
|
53
|
+
ES512: -36,
|
|
54
|
+
PS256: -37,
|
|
55
|
+
PS384: -38,
|
|
56
|
+
PS512: -39,
|
|
57
|
+
ES256K: -47,
|
|
58
|
+
RS256: -257,
|
|
59
|
+
RS384: -258,
|
|
60
|
+
RS512: -259,
|
|
61
|
+
RS1: -65535
|
|
62
|
+
};
|
|
63
|
+
const COSECRV = {
|
|
64
|
+
P256: 1,
|
|
65
|
+
P384: 2,
|
|
66
|
+
P521: 3,
|
|
67
|
+
ED25519: 6,
|
|
68
|
+
SECP256K1: 8
|
|
69
|
+
};
|
|
70
|
+
const cose = {
|
|
71
|
+
COSEKEYS,
|
|
72
|
+
COSEKTY,
|
|
73
|
+
COSEALG,
|
|
74
|
+
COSECRV
|
|
75
|
+
};
|
|
76
|
+
const isoBase64URL = {
|
|
77
|
+
toBuffer(base64url) {
|
|
78
|
+
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
79
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
80
|
+
const binary = atob(padded);
|
|
81
|
+
const bytes = new Uint8Array(binary.length);
|
|
82
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
83
|
+
return bytes;
|
|
84
|
+
},
|
|
85
|
+
fromBuffer(buffer, to = "base64url") {
|
|
86
|
+
const normalized = new Uint8Array(buffer);
|
|
87
|
+
let binary = "";
|
|
88
|
+
for (let i = 0; i < normalized.length; i++) binary += String.fromCharCode(normalized[i]);
|
|
89
|
+
const base64 = btoa(binary);
|
|
90
|
+
if (to === "base64") return base64;
|
|
91
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
92
|
+
},
|
|
93
|
+
toUTF8String(base64url) {
|
|
94
|
+
const bytes = isoBase64URL.toBuffer(base64url);
|
|
95
|
+
return new TextDecoder().decode(bytes);
|
|
96
|
+
},
|
|
97
|
+
fromUTF8String(utf8String) {
|
|
98
|
+
const bytes = new TextEncoder().encode(utf8String);
|
|
99
|
+
return isoBase64URL.fromBuffer(bytes);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const isoUint8Array = {
|
|
103
|
+
concat(arrays) {
|
|
104
|
+
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
105
|
+
const result = new Uint8Array(totalLength);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const arr of arrays) {
|
|
108
|
+
result.set(arr, offset);
|
|
109
|
+
offset += arr.length;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
},
|
|
113
|
+
areEqual(a, b) {
|
|
114
|
+
if (a.length !== b.length) return false;
|
|
115
|
+
return a.every((v, i) => v === b[i]);
|
|
116
|
+
},
|
|
117
|
+
toDataView(array) {
|
|
118
|
+
return new DataView(array.buffer, array.byteOffset, array.length);
|
|
119
|
+
},
|
|
120
|
+
fromHex(hex) {
|
|
121
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
122
|
+
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = Number.parseInt(hex.substring(i, i + 2), 16);
|
|
123
|
+
return bytes;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function decodeCBORInternal(data, offset) {
|
|
127
|
+
const major = data[offset] >> 5;
|
|
128
|
+
const additional = data[offset] & 31;
|
|
129
|
+
offset++;
|
|
130
|
+
let length;
|
|
131
|
+
if (additional < 24) length = additional;
|
|
132
|
+
else if (additional === 24) length = data[offset++];
|
|
133
|
+
else if (additional === 25) {
|
|
134
|
+
length = data[offset] << 8 | data[offset + 1];
|
|
135
|
+
offset += 2;
|
|
136
|
+
} else if (additional === 26) {
|
|
137
|
+
length = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
|
|
138
|
+
offset += 4;
|
|
139
|
+
} else throw new Error(`CBOR: unsupported additional info ${additional}`);
|
|
140
|
+
switch (major) {
|
|
141
|
+
case 0: return [length, offset];
|
|
142
|
+
case 1: return [-1 - length, offset];
|
|
143
|
+
case 2: return [data.slice(offset, offset + length), offset + length];
|
|
144
|
+
case 3: return [new TextDecoder().decode(data.slice(offset, offset + length)), offset + length];
|
|
145
|
+
case 4: {
|
|
146
|
+
const arr = [];
|
|
147
|
+
for (let i = 0; i < length; i++) {
|
|
148
|
+
const [value, newOffset] = decodeCBORInternal(data, offset);
|
|
149
|
+
arr.push(value);
|
|
150
|
+
offset = newOffset;
|
|
151
|
+
}
|
|
152
|
+
return [arr, offset];
|
|
153
|
+
}
|
|
154
|
+
case 5: {
|
|
155
|
+
const map = /* @__PURE__ */ new Map();
|
|
156
|
+
for (let i = 0; i < length; i++) {
|
|
157
|
+
const [key, keyOffset] = decodeCBORInternal(data, offset);
|
|
158
|
+
const [value, valueOffset] = decodeCBORInternal(data, keyOffset);
|
|
159
|
+
map.set(key, value);
|
|
160
|
+
offset = valueOffset;
|
|
161
|
+
}
|
|
162
|
+
return [map, offset];
|
|
163
|
+
}
|
|
164
|
+
default: throw new Error(`CBOR: unsupported major type ${major}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function encodeCBORValue(value) {
|
|
168
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
169
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
170
|
+
return [...encodeCBORHead(2, bytes.length), ...bytes];
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === "number") {
|
|
173
|
+
if (value >= 0) return encodeCBORHead(0, value);
|
|
174
|
+
return encodeCBORHead(1, -1 - value);
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
const encoded = new TextEncoder().encode(value);
|
|
178
|
+
return [...encodeCBORHead(3, encoded.length), ...encoded];
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
const items = value.flatMap((v) => encodeCBORValue(v));
|
|
182
|
+
return [...encodeCBORHead(4, value.length), ...items];
|
|
183
|
+
}
|
|
184
|
+
if (value instanceof Map) {
|
|
185
|
+
const items = [];
|
|
186
|
+
for (const [k, v] of value) items.push(...encodeCBORValue(k), ...encodeCBORValue(v));
|
|
187
|
+
return [...encodeCBORHead(5, value.size), ...items];
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`CBOR encode: unsupported type ${typeof value}`);
|
|
190
|
+
}
|
|
191
|
+
function encodeCBORHead(major, length) {
|
|
192
|
+
const prefix = major << 5;
|
|
193
|
+
if (length < 24) return [prefix | length];
|
|
194
|
+
if (length < 256) return [prefix | 24, length];
|
|
195
|
+
if (length < 65536) return [
|
|
196
|
+
prefix | 25,
|
|
197
|
+
length >> 8 & 255,
|
|
198
|
+
length & 255
|
|
199
|
+
];
|
|
200
|
+
return [
|
|
201
|
+
prefix | 26,
|
|
202
|
+
length >> 24 & 255,
|
|
203
|
+
length >> 16 & 255,
|
|
204
|
+
length >> 8 & 255,
|
|
205
|
+
length & 255
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
const isoCBOR = {
|
|
209
|
+
decodeFirst(input) {
|
|
210
|
+
const [value] = decodeCBORInternal(new Uint8Array(input), 0);
|
|
211
|
+
return value;
|
|
212
|
+
},
|
|
213
|
+
encode(input) {
|
|
214
|
+
return new Uint8Array(encodeCBORValue(input));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
async function toHash(data, algorithm = COSEALG.ES256) {
|
|
218
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
219
|
+
if ([
|
|
220
|
+
COSEALG.ES256,
|
|
221
|
+
COSEALG.PS256,
|
|
222
|
+
COSEALG.RS256,
|
|
223
|
+
COSEALG.ES256K
|
|
224
|
+
].includes(algorithm)) return sha256(input);
|
|
225
|
+
if ([
|
|
226
|
+
COSEALG.ES384,
|
|
227
|
+
COSEALG.PS384,
|
|
228
|
+
COSEALG.RS384
|
|
229
|
+
].includes(algorithm)) return sha384(input);
|
|
230
|
+
if ([
|
|
231
|
+
COSEALG.ES512,
|
|
232
|
+
COSEALG.PS512,
|
|
233
|
+
COSEALG.RS512,
|
|
234
|
+
COSEALG.EdDSA
|
|
235
|
+
].includes(algorithm)) return sha512(input);
|
|
236
|
+
return sha256(input);
|
|
237
|
+
}
|
|
238
|
+
function decodeClientDataJSON(data) {
|
|
239
|
+
return JSON.parse(isoBase64URL.toUTF8String(data));
|
|
240
|
+
}
|
|
241
|
+
function parseAuthenticatorData(authData) {
|
|
242
|
+
if (authData.byteLength < 37) throw new Error(`Authenticator data was ${authData.byteLength} bytes, expected at least 37 bytes`);
|
|
243
|
+
let pointer = 0;
|
|
244
|
+
const dataView = isoUint8Array.toDataView(authData);
|
|
245
|
+
const rpIdHash = authData.slice(pointer, pointer += 32);
|
|
246
|
+
const flagsBuf = authData.slice(pointer, pointer += 1);
|
|
247
|
+
const flagsInt = flagsBuf[0];
|
|
248
|
+
const flags = {
|
|
249
|
+
up: !!(flagsInt & 1),
|
|
250
|
+
uv: !!(flagsInt & 4),
|
|
251
|
+
be: !!(flagsInt & 8),
|
|
252
|
+
bs: !!(flagsInt & 16),
|
|
253
|
+
at: !!(flagsInt & 64),
|
|
254
|
+
ed: !!(flagsInt & 128),
|
|
255
|
+
flagsInt
|
|
256
|
+
};
|
|
257
|
+
const counterBuf = authData.slice(pointer, pointer + 4);
|
|
258
|
+
const counter = dataView.getUint32(pointer, false);
|
|
259
|
+
pointer += 4;
|
|
260
|
+
let aaguid;
|
|
261
|
+
let credentialID;
|
|
262
|
+
let credentialPublicKey;
|
|
263
|
+
if (flags.at) {
|
|
264
|
+
aaguid = authData.slice(pointer, pointer += 16);
|
|
265
|
+
const credIDLen = dataView.getUint16(pointer);
|
|
266
|
+
pointer += 2;
|
|
267
|
+
credentialID = authData.slice(pointer, pointer += credIDLen);
|
|
268
|
+
const firstDecoded = isoCBOR.decodeFirst(authData.slice(pointer));
|
|
269
|
+
const firstEncoded = isoCBOR.encode(firstDecoded);
|
|
270
|
+
credentialPublicKey = firstEncoded;
|
|
271
|
+
pointer += firstEncoded.byteLength;
|
|
272
|
+
}
|
|
273
|
+
let extensionsData;
|
|
274
|
+
let extensionsDataBuffer;
|
|
275
|
+
if (flags.ed) {
|
|
276
|
+
const firstDecoded = isoCBOR.decodeFirst(authData.slice(pointer));
|
|
277
|
+
extensionsDataBuffer = isoCBOR.encode(firstDecoded);
|
|
278
|
+
extensionsData = firstDecoded;
|
|
279
|
+
pointer += extensionsDataBuffer.byteLength;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
rpIdHash,
|
|
283
|
+
flagsBuf,
|
|
284
|
+
flags,
|
|
285
|
+
counter,
|
|
286
|
+
counterBuf,
|
|
287
|
+
aaguid,
|
|
288
|
+
credentialID,
|
|
289
|
+
credentialPublicKey,
|
|
290
|
+
extensionsData,
|
|
291
|
+
extensionsDataBuffer
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function getCurveForCrv(crv) {
|
|
295
|
+
switch (crv) {
|
|
296
|
+
case COSECRV.P256: return {
|
|
297
|
+
curve: p256,
|
|
298
|
+
hash: sha256,
|
|
299
|
+
componentLen: 32
|
|
300
|
+
};
|
|
301
|
+
case COSECRV.P384: return {
|
|
302
|
+
curve: p384,
|
|
303
|
+
hash: sha384,
|
|
304
|
+
componentLen: 48
|
|
305
|
+
};
|
|
306
|
+
case COSECRV.P521: return {
|
|
307
|
+
curve: p521,
|
|
308
|
+
hash: sha512,
|
|
309
|
+
componentLen: 66
|
|
310
|
+
};
|
|
311
|
+
case COSECRV.SECP256K1: return {
|
|
312
|
+
curve: secp256k1,
|
|
313
|
+
hash: sha256,
|
|
314
|
+
componentLen: 32
|
|
315
|
+
};
|
|
316
|
+
default: throw new Error(`Unsupported COSE curve: ${crv}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function verifySignature(opts) {
|
|
320
|
+
const { signature, data, credentialPublicKey } = opts;
|
|
321
|
+
const coseKey = isoCBOR.decodeFirst(credentialPublicKey);
|
|
322
|
+
const kty = coseKey.get(COSEKEYS.kty);
|
|
323
|
+
if (kty !== COSEKTY.EC2) throw new Error(`Unsupported COSE key type: ${kty} (only EC2 is supported)`);
|
|
324
|
+
const crv = coseKey.get(COSEKEYS.crv);
|
|
325
|
+
const x = coseKey.get(COSEKEYS.x);
|
|
326
|
+
const y = coseKey.get(COSEKEYS.y);
|
|
327
|
+
if (!x || !y) throw new Error("COSE public key missing x or y coordinate");
|
|
328
|
+
const { curve, hash, componentLen } = getCurveForCrv(crv);
|
|
329
|
+
const pubKeyBytes = new Uint8Array(1 + componentLen * 2);
|
|
330
|
+
pubKeyBytes[0] = 4;
|
|
331
|
+
pubKeyBytes.set(x.length <= componentLen ? x : x.slice(x.length - componentLen), 1 + (componentLen - x.length));
|
|
332
|
+
pubKeyBytes.set(y.length <= componentLen ? y : y.slice(y.length - componentLen), 1 + componentLen + (componentLen - y.length));
|
|
333
|
+
const msgHash = hash(data);
|
|
334
|
+
try {
|
|
335
|
+
const sig = curve.Signature.fromDER(signature);
|
|
336
|
+
return curve.verify(sig, msgHash, pubKeyBytes);
|
|
337
|
+
} catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function generateChallenge() {
|
|
342
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
343
|
+
}
|
|
344
|
+
function convertAAGUIDToString(aaguid) {
|
|
345
|
+
const hex = Array.from(aaguid, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
346
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
347
|
+
}
|
|
348
|
+
async function matchExpectedRPID(rpIdHash, expectedRPIDs) {
|
|
349
|
+
for (const rpID of expectedRPIDs) {
|
|
350
|
+
const expectedHash = sha256(new TextEncoder().encode(rpID));
|
|
351
|
+
if (isoUint8Array.areEqual(rpIdHash, expectedHash)) return rpID;
|
|
352
|
+
}
|
|
353
|
+
throw new Error(`Unexpected RP ID hash, expected one of: ${expectedRPIDs.join(", ")}`);
|
|
354
|
+
}
|
|
355
|
+
function parseBackupFlags(flags) {
|
|
356
|
+
const credentialDeviceType = flags.be ? "multiDevice" : "singleDevice";
|
|
357
|
+
const credentialBackedUp = flags.bs;
|
|
358
|
+
if (!flags.be && flags.bs) throw new Error("Credential indicates backup state but not backup eligibility");
|
|
359
|
+
return {
|
|
360
|
+
credentialDeviceType,
|
|
361
|
+
credentialBackedUp
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
async function generateRegistrationOptions(options) {
|
|
365
|
+
const { rpName, rpID, userName, userID, challenge = generateChallenge(), userDisplayName = "", timeout = 6e4, attestationType = "none", excludeCredentials = [], authenticatorSelection = {
|
|
366
|
+
residentKey: "preferred",
|
|
367
|
+
userVerification: "preferred"
|
|
368
|
+
}, extensions, supportedAlgorithmIDs = [
|
|
369
|
+
-8,
|
|
370
|
+
-7,
|
|
371
|
+
-257
|
|
372
|
+
] } = options;
|
|
373
|
+
const pubKeyCredParams = supportedAlgorithmIDs.map((id) => ({
|
|
374
|
+
alg: id,
|
|
375
|
+
type: "public-key"
|
|
376
|
+
}));
|
|
377
|
+
if (authenticatorSelection.residentKey === void 0) {
|
|
378
|
+
if (authenticatorSelection.requireResidentKey) authenticatorSelection.residentKey = "required";
|
|
379
|
+
} else authenticatorSelection.requireResidentKey = authenticatorSelection.residentKey === "required";
|
|
380
|
+
let _challenge = challenge;
|
|
381
|
+
if (typeof _challenge === "string") _challenge = new TextEncoder().encode(_challenge);
|
|
382
|
+
let _userID = userID;
|
|
383
|
+
if (!_userID) _userID = crypto.getRandomValues(new Uint8Array(32));
|
|
384
|
+
return {
|
|
385
|
+
challenge: isoBase64URL.fromBuffer(_challenge),
|
|
386
|
+
rp: {
|
|
387
|
+
name: rpName,
|
|
388
|
+
id: rpID
|
|
389
|
+
},
|
|
390
|
+
user: {
|
|
391
|
+
id: isoBase64URL.fromBuffer(_userID),
|
|
392
|
+
name: userName,
|
|
393
|
+
displayName: userDisplayName
|
|
394
|
+
},
|
|
395
|
+
pubKeyCredParams,
|
|
396
|
+
timeout,
|
|
397
|
+
attestation: attestationType,
|
|
398
|
+
excludeCredentials: excludeCredentials.map((cred) => ({
|
|
399
|
+
...cred,
|
|
400
|
+
id: cred.id.replace(/=+$/, ""),
|
|
401
|
+
type: "public-key"
|
|
402
|
+
})),
|
|
403
|
+
authenticatorSelection,
|
|
404
|
+
extensions: {
|
|
405
|
+
...extensions,
|
|
406
|
+
credProps: true
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function generateAuthenticationOptions(options) {
|
|
411
|
+
const { rpID, allowCredentials, challenge = generateChallenge(), timeout = 6e4, userVerification = "preferred", extensions } = options;
|
|
412
|
+
let _challenge = challenge;
|
|
413
|
+
if (typeof _challenge === "string") _challenge = new TextEncoder().encode(_challenge);
|
|
414
|
+
return {
|
|
415
|
+
rpId: rpID,
|
|
416
|
+
challenge: isoBase64URL.fromBuffer(_challenge),
|
|
417
|
+
allowCredentials: allowCredentials?.map((cred) => ({
|
|
418
|
+
...cred,
|
|
419
|
+
id: cred.id.replace(/=+$/, ""),
|
|
420
|
+
type: "public-key"
|
|
421
|
+
})),
|
|
422
|
+
timeout,
|
|
423
|
+
userVerification,
|
|
424
|
+
extensions
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function verifyRegistrationResponse(options) {
|
|
428
|
+
const { response, expectedChallenge, expectedOrigin, expectedRPID, expectedType, requireUserPresence = true, requireUserVerification = true, supportedAlgorithmIDs = [
|
|
429
|
+
-8,
|
|
430
|
+
-7,
|
|
431
|
+
-36,
|
|
432
|
+
-37,
|
|
433
|
+
-38,
|
|
434
|
+
-39,
|
|
435
|
+
-257,
|
|
436
|
+
-258,
|
|
437
|
+
-259,
|
|
438
|
+
-65535
|
|
439
|
+
] } = options;
|
|
440
|
+
const { id, rawId, type: credentialType, response: attestationResponse } = response;
|
|
441
|
+
if (!id) throw new Error("Missing credential ID");
|
|
442
|
+
if (id !== rawId) throw new Error("Credential ID was not base64url-encoded");
|
|
443
|
+
if (credentialType !== "public-key") throw new Error(`Unexpected credential type ${credentialType}, expected "public-key"`);
|
|
444
|
+
const clientDataJSON = decodeClientDataJSON(attestationResponse.clientDataJSON);
|
|
445
|
+
const { type, origin, challenge } = clientDataJSON;
|
|
446
|
+
if (Array.isArray(expectedType)) {
|
|
447
|
+
if (!expectedType.includes(type)) throw new Error(`Unexpected registration response type "${type}", expected one of: ${expectedType.join(", ")}`);
|
|
448
|
+
} else if (expectedType) {
|
|
449
|
+
if (type !== expectedType) throw new Error(`Unexpected registration response type "${type}", expected "${expectedType}"`);
|
|
450
|
+
} else if (type !== "webauthn.create") throw new Error(`Unexpected registration response type: ${type}`);
|
|
451
|
+
if (typeof expectedChallenge === "function") {
|
|
452
|
+
if (!await expectedChallenge(challenge)) throw new Error(`Custom challenge verifier returned false for registration response challenge "${challenge}"`);
|
|
453
|
+
} else if (challenge !== expectedChallenge) throw new Error(`Unexpected registration response challenge "${challenge}", expected "${expectedChallenge}"`);
|
|
454
|
+
if (Array.isArray(expectedOrigin)) {
|
|
455
|
+
if (!expectedOrigin.includes(origin)) throw new Error(`Unexpected registration response origin "${origin}", expected one of: ${expectedOrigin.join(", ")}`);
|
|
456
|
+
} else if (origin !== expectedOrigin) throw new Error(`Unexpected registration response origin "${origin}", expected "${expectedOrigin}"`);
|
|
457
|
+
const attestationObject = isoBase64URL.toBuffer(attestationResponse.attestationObject);
|
|
458
|
+
const decodedAttestationObject = isoCBOR.decodeFirst(attestationObject);
|
|
459
|
+
const fmt = decodedAttestationObject.get("fmt");
|
|
460
|
+
const authData = decodedAttestationObject.get("authData");
|
|
461
|
+
const attStmt = decodedAttestationObject.get("attStmt");
|
|
462
|
+
const { aaguid, rpIdHash, flags, credentialID, counter, credentialPublicKey, extensionsData } = parseAuthenticatorData(authData);
|
|
463
|
+
let matchedRPID;
|
|
464
|
+
if (expectedRPID) matchedRPID = await matchExpectedRPID(rpIdHash, Array.isArray(expectedRPID) ? expectedRPID : [expectedRPID]);
|
|
465
|
+
if (requireUserPresence && !flags.up) throw new Error("User presence was required, but user was not present");
|
|
466
|
+
if (requireUserVerification && !flags.uv) throw new Error("User verification was required, but user could not be verified");
|
|
467
|
+
if (!credentialID) throw new Error("No credential ID was provided by authenticator");
|
|
468
|
+
if (!credentialPublicKey) throw new Error("No public key was provided by authenticator");
|
|
469
|
+
if (!aaguid) throw new Error("No AAGUID was present during registration");
|
|
470
|
+
const alg = isoCBOR.decodeFirst(credentialPublicKey).get(COSEKEYS.alg);
|
|
471
|
+
if (typeof alg !== "number") throw new Error("Credential public key was missing numeric alg");
|
|
472
|
+
if (!supportedAlgorithmIDs.includes(alg)) throw new Error(`Unexpected public key alg "${alg}", expected one of "${supportedAlgorithmIDs.join(", ")}"`);
|
|
473
|
+
let verified = false;
|
|
474
|
+
if (fmt === "none") {
|
|
475
|
+
if (attStmt.size > 0) throw new Error("None attestation had unexpected attestation statement");
|
|
476
|
+
verified = true;
|
|
477
|
+
} else if (fmt === "packed" && attStmt.size === 2 && attStmt.has("alg") && attStmt.has("sig")) {
|
|
478
|
+
const clientDataHash = await toHash(isoBase64URL.toBuffer(attestationResponse.clientDataJSON));
|
|
479
|
+
const signedData = isoUint8Array.concat([authData, clientDataHash]);
|
|
480
|
+
verified = await verifySignature({
|
|
481
|
+
signature: attStmt.get("sig"),
|
|
482
|
+
data: signedData,
|
|
483
|
+
credentialPublicKey
|
|
484
|
+
});
|
|
485
|
+
} else throw new Error(`Unsupported attestation format: "${fmt}". Only "none" and "packed" (self-attestation) are supported.`);
|
|
486
|
+
if (!verified) return { verified: false };
|
|
487
|
+
const { credentialDeviceType, credentialBackedUp } = parseBackupFlags(flags);
|
|
488
|
+
return {
|
|
489
|
+
verified: true,
|
|
490
|
+
registrationInfo: {
|
|
491
|
+
fmt,
|
|
492
|
+
aaguid: convertAAGUIDToString(aaguid),
|
|
493
|
+
credential: {
|
|
494
|
+
id: isoBase64URL.fromBuffer(credentialID),
|
|
495
|
+
publicKey: credentialPublicKey,
|
|
496
|
+
counter,
|
|
497
|
+
transports: response.response.transports
|
|
498
|
+
},
|
|
499
|
+
credentialType: "public-key",
|
|
500
|
+
attestationObject,
|
|
501
|
+
userVerified: flags.uv,
|
|
502
|
+
credentialDeviceType,
|
|
503
|
+
credentialBackedUp,
|
|
504
|
+
origin: clientDataJSON.origin,
|
|
505
|
+
rpID: matchedRPID,
|
|
506
|
+
authenticatorExtensionResults: extensionsData
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
//#endregion
|
|
512
|
+
export { COSEALG, COSECRV, COSEKEYS, COSEKTY, cose, decodeClientDataJSON, generateAuthenticationOptions, generateChallenge, generateRegistrationOptions, isoBase64URL, isoCBOR, isoUint8Array, parseAuthenticatorData, toHash, verifyRegistrationResponse, verifySignature, webauthn_exports };
|
|
@@ -5,6 +5,19 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __exportAll = (all, symbols) => {
|
|
9
|
+
let target = {};
|
|
10
|
+
for (var name in all) {
|
|
11
|
+
__defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (symbols) {
|
|
17
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
18
|
+
}
|
|
19
|
+
return target;
|
|
20
|
+
};
|
|
8
21
|
var __copyProps = (to, from, except, desc) => {
|
|
9
22
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
23
|
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
@@ -26,4 +39,5 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
39
|
|
|
27
40
|
//#endregion
|
|
28
41
|
|
|
42
|
+
exports.__exportAll = __exportAll;
|
|
29
43
|
exports.__toESM = __toESM;
|
package/lib/index.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const require_hasher_sha3 = require('./hasher/sha3.cjs');
|
|
|
6
6
|
const require_signer_ed25519 = require('./signer/ed25519.cjs');
|
|
7
7
|
const require_signer_secp256k1 = require('./signer/secp256k1.cjs');
|
|
8
8
|
const require_signer_ethereum = require('./signer/ethereum.cjs');
|
|
9
|
+
const require_webauthn = require('./webauthn.cjs');
|
|
9
10
|
const require_signer_passkey = require('./signer/passkey.cjs');
|
|
10
11
|
let randombytes = require("randombytes");
|
|
11
12
|
randombytes = require_rolldown_runtime.__toESM(randombytes);
|
|
@@ -189,4 +190,10 @@ exports.getHasher = getHasher;
|
|
|
189
190
|
exports.getRandomBytes = getRandomBytes;
|
|
190
191
|
exports.getSigner = getSigner;
|
|
191
192
|
exports.toTxHash = toTxHash;
|
|
192
|
-
exports.types = types;
|
|
193
|
+
exports.types = types;
|
|
194
|
+
Object.defineProperty(exports, 'webauthn', {
|
|
195
|
+
enumerable: true,
|
|
196
|
+
get: function () {
|
|
197
|
+
return require_webauthn.webauthn_exports;
|
|
198
|
+
}
|
|
199
|
+
});
|
package/lib/index.d.cts
CHANGED
|
@@ -5,6 +5,7 @@ import { Ed25519Signer } from "./signer/ed25519.cjs";
|
|
|
5
5
|
import { Secp256k1Signer } from "./signer/secp256k1.cjs";
|
|
6
6
|
import { EthereumSigner } from "./signer/ethereum.cjs";
|
|
7
7
|
import { PasskeySigner } from "./signer/passkey.cjs";
|
|
8
|
+
import { webauthn_d_exports } from "./webauthn.cjs";
|
|
8
9
|
import { BytesType, EncodingType, KeyPairType } from "@ocap/util";
|
|
9
10
|
import { LiteralUnion } from "type-fest";
|
|
10
11
|
|
|
@@ -238,4 +239,4 @@ declare function getRandomBytes(length: number, encoding?: 'Uint8Array'): Uint8A
|
|
|
238
239
|
declare function getRandomBytes(length: number, encoding?: EncodingType): BytesType;
|
|
239
240
|
declare const toTxHash: (buf: Buffer | Uint8Array) => string;
|
|
240
241
|
//#endregion
|
|
241
|
-
export { AddressType, HashFnType, HashType, Hasher, KeyType, RoleType, Signer, SignerType, getHasher, getRandomBytes, getSigner, toTxHash, types };
|
|
242
|
+
export { AddressType, HashFnType, HashType, Hasher, KeyType, RoleType, Signer, SignerType, getHasher, getRandomBytes, getSigner, toTxHash, types, webauthn_d_exports as webauthn };
|
package/lib/signer/ethereum.cjs
CHANGED
|
@@ -2,14 +2,20 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
2
2
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
3
3
|
const require_signer_secp256k1 = require('./secp256k1.cjs');
|
|
4
4
|
let _ocap_util = require("@ocap/util");
|
|
5
|
-
let
|
|
6
|
-
|
|
7
|
-
let
|
|
8
|
-
eth_lib_lib_hash = require_rolldown_runtime.__toESM(eth_lib_lib_hash);
|
|
5
|
+
let _noble_hashes_sha3_js = require("@noble/hashes/sha3.js");
|
|
6
|
+
let _noble_hashes_utils_js = require("@noble/hashes/utils.js");
|
|
7
|
+
let _noble_curves_secp256k1 = require("@noble/curves/secp256k1");
|
|
9
8
|
|
|
10
9
|
//#region src/signer/ethereum.ts
|
|
10
|
+
function toChecksumAddress(address) {
|
|
11
|
+
const addr = address.slice(2).toLowerCase();
|
|
12
|
+
const hash = (0, _noble_hashes_utils_js.bytesToHex)((0, _noble_hashes_sha3_js.keccak_256)(Buffer.from(addr)));
|
|
13
|
+
let result = "0x";
|
|
14
|
+
for (let i = 0; i < 40; i++) result += Number.parseInt(hash[i], 16) > 7 ? addr[i].toUpperCase() : addr[i];
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
11
17
|
/**
|
|
12
|
-
* Signer implementation for secp256k1, based on
|
|
18
|
+
* Signer implementation for secp256k1, based on `@noble/curves`, and ethereum compatible
|
|
13
19
|
*
|
|
14
20
|
* @class EthereumSigner
|
|
15
21
|
*/
|
|
@@ -20,17 +26,25 @@ var EthereumSigner = class extends require_signer_secp256k1.Secp256k1Signer {
|
|
|
20
26
|
}
|
|
21
27
|
ethHash(data) {
|
|
22
28
|
const messageBytes = (0, _ocap_util.hexToBytes)((0, _ocap_util.isHexStrict)(data) ? data : (0, _ocap_util.utf8ToHex)(data));
|
|
23
|
-
const messageBuffer = Buffer.from(messageBytes);
|
|
24
29
|
const preamble = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return eth_lib_lib_hash.default.keccak256s(ethMessage);
|
|
30
|
+
const ethMessage = Buffer.concat([Buffer.from(preamble), Buffer.from(messageBytes)]);
|
|
31
|
+
return `0x${(0, _noble_hashes_utils_js.bytesToHex)((0, _noble_hashes_sha3_js.keccak_256)(new Uint8Array(ethMessage)))}`;
|
|
28
32
|
}
|
|
29
33
|
ethSign(data, privateKey) {
|
|
30
|
-
|
|
34
|
+
const msgHash = Buffer.from(data.slice(2), "hex");
|
|
35
|
+
const sk = Buffer.from(privateKey.slice(2), "hex");
|
|
36
|
+
const sig = _noble_curves_secp256k1.secp256k1.sign(msgHash, sk);
|
|
37
|
+
const v = (27 + sig.recovery).toString(16).padStart(2, "0");
|
|
38
|
+
return `0x${sig.toCompactHex()}${v}`;
|
|
31
39
|
}
|
|
32
40
|
ethRecover(data, signature) {
|
|
33
|
-
|
|
41
|
+
const sigHex = signature.slice(2);
|
|
42
|
+
const v = Number.parseInt(sigHex.slice(128, 130), 16);
|
|
43
|
+
const recovery = v < 2 ? v : 1 - v % 2;
|
|
44
|
+
const sig = _noble_curves_secp256k1.secp256k1.Signature.fromCompact(sigHex.slice(0, 128)).addRecoveryBit(recovery);
|
|
45
|
+
const msgHash = Buffer.from(data.slice(2), "hex");
|
|
46
|
+
const pubKeyHex = sig.recoverPublicKey(msgHash).toHex(false).slice(2);
|
|
47
|
+
return toChecksumAddress(`0x${(0, _noble_hashes_utils_js.bytesToHex)((0, _noble_hashes_sha3_js.keccak_256)(Buffer.from(pubKeyHex, "hex"))).slice(-40)}`);
|
|
34
48
|
}
|
|
35
49
|
};
|
|
36
50
|
var ethereum_default = new EthereumSigner();
|
|
@@ -3,7 +3,7 @@ import { Secp256k1Signer } from "./secp256k1.cjs";
|
|
|
3
3
|
//#region src/signer/ethereum.d.ts
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Signer implementation for secp256k1, based on
|
|
6
|
+
* Signer implementation for secp256k1, based on `@noble/curves`, and ethereum compatible
|
|
7
7
|
*
|
|
8
8
|
* @class EthereumSigner
|
|
9
9
|
*/
|
package/lib/signer/passkey.cjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
2
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
3
3
|
const require_protocols_signer = require('../protocols/signer.cjs');
|
|
4
|
+
const require_webauthn = require('../webauthn.cjs');
|
|
4
5
|
let _ocap_util = require("@ocap/util");
|
|
5
|
-
let _simplewebauthn_server_helpers = require("@simplewebauthn/server/helpers");
|
|
6
6
|
|
|
7
7
|
//#region src/signer/passkey.ts
|
|
8
8
|
/**
|
|
9
|
-
* Signer implementation for passkey, based on `@
|
|
9
|
+
* Signer implementation for passkey, based on `@noble/curves`
|
|
10
10
|
* Since passkey supports only verification, we do not need to implement the sign method
|
|
11
11
|
* And passkeys can used multiple algorithms, we do not need to implement the algorithm selection
|
|
12
12
|
*
|
|
@@ -34,11 +34,11 @@ var PasskeySigner = class extends require_protocols_signer.default {
|
|
|
34
34
|
const parsed = JSON.parse(extra);
|
|
35
35
|
if (!parsed.authenticatorData || !parsed.clientDataJSON) throw new Error("extra.authenticatorData or extra.clientDataJSON is required for passkey signature verification");
|
|
36
36
|
const authDataBuffer = (0, _ocap_util.toBuffer)((0, _ocap_util.fromBase64)(parsed.authenticatorData));
|
|
37
|
-
const clientDataHash = await
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
40
|
-
signature:
|
|
41
|
-
data:
|
|
37
|
+
const clientDataHash = await require_webauthn.toHash(require_webauthn.isoBase64URL.toBuffer(parsed.clientDataJSON));
|
|
38
|
+
if (require_webauthn.decodeClientDataJSON(parsed.clientDataJSON).challenge !== (0, _ocap_util.toBase64)(challenge)) throw new Error("challenge mismatch for passkey signature");
|
|
39
|
+
return require_webauthn.verifySignature({
|
|
40
|
+
signature: require_webauthn.isoBase64URL.toBuffer(typeof signature === "string" ? signature : (0, _ocap_util.toBase64)(signature)),
|
|
41
|
+
data: require_webauthn.isoUint8Array.concat([Uint8Array.from(authDataBuffer), clientDataHash]),
|
|
42
42
|
credentialPublicKey: (0, _ocap_util.toUint8Array)(pk)
|
|
43
43
|
});
|
|
44
44
|
}
|
package/lib/signer/passkey.d.cts
CHANGED
|
@@ -4,7 +4,7 @@ import { BytesType, EncodingType, KeyPairType } from "@ocap/util";
|
|
|
4
4
|
//#region src/signer/passkey.d.ts
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Signer implementation for passkey, based on `@
|
|
7
|
+
* Signer implementation for passkey, based on `@noble/curves`
|
|
8
8
|
* Since passkey supports only verification, we do not need to implement the sign method
|
|
9
9
|
* And passkeys can used multiple algorithms, we do not need to implement the algorithm selection
|
|
10
10
|
*
|