@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/lib/signer/secp256k1.cjs
CHANGED
|
@@ -5,14 +5,12 @@ const require_protocols_signer = require('../protocols/signer.cjs');
|
|
|
5
5
|
let _ocap_util = require("@ocap/util");
|
|
6
6
|
let randombytes = require("randombytes");
|
|
7
7
|
randombytes = require_rolldown_runtime.__toESM(randombytes);
|
|
8
|
-
let
|
|
9
|
-
|
|
8
|
+
let _noble_curves_secp256k1 = require("@noble/curves/secp256k1");
|
|
9
|
+
let _noble_curves_abstract_utils = require("@noble/curves/abstract/utils");
|
|
10
10
|
|
|
11
11
|
//#region src/signer/secp256k1.ts
|
|
12
|
-
const EC = elliptic.default.ec;
|
|
13
|
-
const secp256k1 = new EC("secp256k1");
|
|
14
12
|
/**
|
|
15
|
-
* Signer implementation for secp256k1, based on `
|
|
13
|
+
* Signer implementation for secp256k1, based on `@noble/curves`
|
|
16
14
|
*
|
|
17
15
|
* @class Secp256k1Signer
|
|
18
16
|
*/
|
|
@@ -24,8 +22,7 @@ var Secp256k1Signer = class extends require_protocols_signer.default {
|
|
|
24
22
|
}
|
|
25
23
|
isValidSK(sk) {
|
|
26
24
|
if (sk.byteLength !== 32) return false;
|
|
27
|
-
|
|
28
|
-
return bn.cmp(secp256k1.curve.n) < 0 && !bn.isZero();
|
|
25
|
+
return _noble_curves_secp256k1.secp256k1.utils.isValidPrivateKey(sk);
|
|
29
26
|
}
|
|
30
27
|
/**
|
|
31
28
|
* @public
|
|
@@ -53,7 +50,7 @@ var Secp256k1Signer = class extends require_protocols_signer.default {
|
|
|
53
50
|
*/
|
|
54
51
|
getPublicKey(sk, encoding = "hex") {
|
|
55
52
|
if (!this.isValidSK((0, _ocap_util.toUint8Array)(sk))) throw new Error("Invalid secret key");
|
|
56
|
-
let pk = secp256k1.
|
|
53
|
+
let pk = (0, _noble_curves_abstract_utils.bytesToHex)(_noble_curves_secp256k1.secp256k1.getPublicKey((0, _ocap_util.toUint8Array)(sk), this.pkCompressed));
|
|
57
54
|
if (this.pkHasFormatPrefix === false) pk = pk.slice(2);
|
|
58
55
|
return require_encode.encode(`0x${pk}`, encoding);
|
|
59
56
|
}
|
|
@@ -62,30 +59,28 @@ var Secp256k1Signer = class extends require_protocols_signer.default {
|
|
|
62
59
|
*/
|
|
63
60
|
sign(message, sk, encoding = "hex") {
|
|
64
61
|
const msg = (0, _ocap_util.toUint8Array)(message);
|
|
65
|
-
return require_encode.encode(`0x${secp256k1.
|
|
62
|
+
return require_encode.encode(`0x${_noble_curves_secp256k1.secp256k1.sign(msg, (0, _ocap_util.toUint8Array)(sk)).toDERHex()}`, encoding);
|
|
66
63
|
}
|
|
67
64
|
/**
|
|
68
65
|
* Verify if a signature is valid
|
|
69
66
|
*/
|
|
70
67
|
verify(message, signature, pk) {
|
|
71
68
|
const msg = (0, _ocap_util.toUint8Array)(message);
|
|
72
|
-
const sigHex = (0, _ocap_util.
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
69
|
+
const sigHex = (0, _ocap_util.toHex)(signature).replace(/^0x/i, "");
|
|
70
|
+
let pkBytes = (0, _ocap_util.toUint8Array)(pk);
|
|
71
|
+
if (this.pkHasFormatPrefix === false && pkBytes[0] !== 4 && pkBytes.byteLength === 64) {
|
|
72
|
+
const prefixed = new Uint8Array(65);
|
|
73
|
+
prefixed[0] = 4;
|
|
74
|
+
prefixed.set(pkBytes, 1);
|
|
75
|
+
pkBytes = prefixed;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const sig = _noble_curves_secp256k1.secp256k1.Signature.fromDER(sigHex);
|
|
79
|
+
if (sig.hasHighS()) return false;
|
|
80
|
+
return _noble_curves_secp256k1.secp256k1.verify(sig, msg, pkBytes);
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
85
83
|
}
|
|
86
|
-
let pkBuffer = (0, _ocap_util.toBuffer)(pk);
|
|
87
|
-
if (this.pkHasFormatPrefix === false && pkBuffer[0] !== 4 && pkBuffer.byteLength === 64) pkBuffer = Buffer.concat([Uint8Array.from([4]), Uint8Array.from(pkBuffer)]);
|
|
88
|
-
return secp256k1.keyFromPublic(pkBuffer).verify((0, _ocap_util.stripHexPrefix)(msg), sigHex);
|
|
89
84
|
}
|
|
90
85
|
};
|
|
91
86
|
var secp256k1_default = new Secp256k1Signer();
|
|
@@ -4,7 +4,7 @@ import { BytesType, EncodingType, KeyPairType } from "@ocap/util";
|
|
|
4
4
|
//#region src/signer/secp256k1.d.ts
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Signer implementation for secp256k1, based on `
|
|
7
|
+
* Signer implementation for secp256k1, based on `@noble/curves`
|
|
8
8
|
*
|
|
9
9
|
* @class Secp256k1Signer
|
|
10
10
|
*/
|
package/lib/webauthn.cjs
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let _noble_hashes_sha2_js = require("@noble/hashes/sha2.js");
|
|
3
|
+
let _noble_curves_secp256k1 = require("@noble/curves/secp256k1");
|
|
4
|
+
let _noble_curves_p256 = require("@noble/curves/p256");
|
|
5
|
+
let _noble_curves_p384 = require("@noble/curves/p384");
|
|
6
|
+
let _noble_curves_p521 = require("@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__ */ require_rolldown_runtime.__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 (0, _noble_hashes_sha2_js.sha256)(input);
|
|
225
|
+
if ([
|
|
226
|
+
COSEALG.ES384,
|
|
227
|
+
COSEALG.PS384,
|
|
228
|
+
COSEALG.RS384
|
|
229
|
+
].includes(algorithm)) return (0, _noble_hashes_sha2_js.sha384)(input);
|
|
230
|
+
if ([
|
|
231
|
+
COSEALG.ES512,
|
|
232
|
+
COSEALG.PS512,
|
|
233
|
+
COSEALG.RS512,
|
|
234
|
+
COSEALG.EdDSA
|
|
235
|
+
].includes(algorithm)) return (0, _noble_hashes_sha2_js.sha512)(input);
|
|
236
|
+
return (0, _noble_hashes_sha2_js.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: _noble_curves_p256.p256,
|
|
298
|
+
hash: _noble_hashes_sha2_js.sha256,
|
|
299
|
+
componentLen: 32
|
|
300
|
+
};
|
|
301
|
+
case COSECRV.P384: return {
|
|
302
|
+
curve: _noble_curves_p384.p384,
|
|
303
|
+
hash: _noble_hashes_sha2_js.sha384,
|
|
304
|
+
componentLen: 48
|
|
305
|
+
};
|
|
306
|
+
case COSECRV.P521: return {
|
|
307
|
+
curve: _noble_curves_p521.p521,
|
|
308
|
+
hash: _noble_hashes_sha2_js.sha512,
|
|
309
|
+
componentLen: 66
|
|
310
|
+
};
|
|
311
|
+
case COSECRV.SECP256K1: return {
|
|
312
|
+
curve: _noble_curves_secp256k1.secp256k1,
|
|
313
|
+
hash: _noble_hashes_sha2_js.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 = (0, _noble_hashes_sha2_js.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
|
+
exports.COSEALG = COSEALG;
|
|
513
|
+
exports.COSECRV = COSECRV;
|
|
514
|
+
exports.COSEKEYS = COSEKEYS;
|
|
515
|
+
exports.COSEKTY = COSEKTY;
|
|
516
|
+
exports.cose = cose;
|
|
517
|
+
exports.decodeClientDataJSON = decodeClientDataJSON;
|
|
518
|
+
exports.generateAuthenticationOptions = generateAuthenticationOptions;
|
|
519
|
+
exports.generateChallenge = generateChallenge;
|
|
520
|
+
exports.generateRegistrationOptions = generateRegistrationOptions;
|
|
521
|
+
exports.isoBase64URL = isoBase64URL;
|
|
522
|
+
exports.isoCBOR = isoCBOR;
|
|
523
|
+
exports.isoUint8Array = isoUint8Array;
|
|
524
|
+
exports.parseAuthenticatorData = parseAuthenticatorData;
|
|
525
|
+
exports.toHash = toHash;
|
|
526
|
+
exports.verifyRegistrationResponse = verifyRegistrationResponse;
|
|
527
|
+
exports.verifySignature = verifySignature;
|
|
528
|
+
Object.defineProperty(exports, 'webauthn_exports', {
|
|
529
|
+
enumerable: true,
|
|
530
|
+
get: function () {
|
|
531
|
+
return webauthn_exports;
|
|
532
|
+
}
|
|
533
|
+
});
|