@dexterai/vault 0.5.0 → 0.6.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/dist/connect/index.cjs +455 -0
- package/dist/connect/index.d.cts +145 -0
- package/dist/connect/index.d.ts +145 -0
- package/dist/connect/index.js +419 -0
- package/package.json +6 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/connect/index.ts
|
|
31
|
+
var connect_exports = {};
|
|
32
|
+
__export(connect_exports, {
|
|
33
|
+
connectTab: () => connectTab,
|
|
34
|
+
decodeChallengeTo32Bytes: () => decodeChallengeTo32Bytes,
|
|
35
|
+
verifyConnectProof: () => verifyConnectProof
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(connect_exports);
|
|
38
|
+
|
|
39
|
+
// src/connect/verify.ts
|
|
40
|
+
var import_web34 = require("@solana/web3.js");
|
|
41
|
+
var import_node_crypto = require("crypto");
|
|
42
|
+
|
|
43
|
+
// src/instructions/provePasskey.ts
|
|
44
|
+
var import_web32 = require("@solana/web3.js");
|
|
45
|
+
|
|
46
|
+
// src/constants/index.ts
|
|
47
|
+
var import_web3 = require("@solana/web3.js");
|
|
48
|
+
var DEXTER_VAULT_PROGRAM_ID = new import_web3.PublicKey(
|
|
49
|
+
"Hg3wRaydFtJhYrdvYrKECacpJYDsC9Px7yKmpncj2fhc"
|
|
50
|
+
);
|
|
51
|
+
var SWIG_PROGRAM_ID = new import_web3.PublicKey(
|
|
52
|
+
"swigypWHEksbC64pWKwah1WTeh9JXwx8H1rJHLdbQMB"
|
|
53
|
+
);
|
|
54
|
+
var SECP256R1_PROGRAM_ID = new import_web3.PublicKey(
|
|
55
|
+
"Secp256r1SigVerify1111111111111111111111111"
|
|
56
|
+
);
|
|
57
|
+
var ED25519_PROGRAM_ID = new import_web3.PublicKey(
|
|
58
|
+
"Ed25519SigVerify111111111111111111111111111"
|
|
59
|
+
);
|
|
60
|
+
var INSTRUCTIONS_SYSVAR_ID = new import_web3.PublicKey(
|
|
61
|
+
"Sysvar1nstructions1111111111111111111111111"
|
|
62
|
+
);
|
|
63
|
+
var VAULT_SEED_PREFIX = Buffer.from("vault");
|
|
64
|
+
var LOCKED_CLAIM_SEED = Buffer.from("locked-claim");
|
|
65
|
+
var DISCRIMINATORS = Object.freeze({
|
|
66
|
+
initialize_vault: Uint8Array.from([48, 191, 163, 44, 71, 129, 63, 164]),
|
|
67
|
+
set_swig: Uint8Array.from([253, 229, 89, 206, 192, 118, 137, 165]),
|
|
68
|
+
settle_voucher: Uint8Array.from([144, 176, 128, 220, 156, 79, 41, 54]),
|
|
69
|
+
request_withdrawal: Uint8Array.from([251, 85, 121, 205, 56, 201, 12, 177]),
|
|
70
|
+
finalize_withdrawal: Uint8Array.from([178, 87, 206, 68, 201, 186, 164, 232]),
|
|
71
|
+
force_release: Uint8Array.from([122, 190, 243, 252, 54, 202, 208, 234]),
|
|
72
|
+
rotate_passkey: Uint8Array.from([28, 134, 49, 89, 196, 34, 58, 174]),
|
|
73
|
+
rotate_dexter_authority: Uint8Array.from([145, 60, 4, 119, 180, 205, 236, 134]),
|
|
74
|
+
prove_passkey: Uint8Array.from([35, 175, 41, 143, 201, 118, 49, 184]),
|
|
75
|
+
settle_tab_voucher: Uint8Array.from([173, 22, 98, 31, 110, 129, 59, 161]),
|
|
76
|
+
register_session_key: Uint8Array.from([69, 94, 60, 44, 49, 199, 183, 233]),
|
|
77
|
+
revoke_session_key: Uint8Array.from([81, 192, 32, 110, 104, 116, 144, 151]),
|
|
78
|
+
lock_voucher: Uint8Array.from([91, 138, 5, 227, 119, 239, 48, 254]),
|
|
79
|
+
settle_locked_voucher: Uint8Array.from([44, 80, 216, 43, 247, 253, 101, 45]),
|
|
80
|
+
transfer_lock_ownership: Uint8Array.from([193, 13, 131, 134, 95, 25, 229, 157]),
|
|
81
|
+
recover_abandoned_lock: Uint8Array.from([169, 213, 107, 64, 229, 49, 43, 234]),
|
|
82
|
+
open_standby: Uint8Array.from([234, 184, 232, 135, 246, 191, 90, 250]),
|
|
83
|
+
draw_credit: Uint8Array.from([20, 84, 47, 211, 78, 117, 195, 210]),
|
|
84
|
+
repay_credit: Uint8Array.from([38, 113, 240, 182, 109, 179, 154, 245]),
|
|
85
|
+
seize_collateral: Uint8Array.from([40, 250, 7, 243, 168, 184, 116, 154]),
|
|
86
|
+
migrate_v4_to_v5: Uint8Array.from([226, 105, 140, 184, 101, 39, 235, 116])
|
|
87
|
+
});
|
|
88
|
+
var OTS_SESSION_REGISTER_V1_DOMAIN = (() => {
|
|
89
|
+
const buf = new Uint8Array(32);
|
|
90
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V1"), 0);
|
|
91
|
+
return buf;
|
|
92
|
+
})();
|
|
93
|
+
var OTS_SESSION_REGISTER_V2_DOMAIN = (() => {
|
|
94
|
+
const buf = new Uint8Array(32);
|
|
95
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V2"), 0);
|
|
96
|
+
return buf;
|
|
97
|
+
})();
|
|
98
|
+
var OTS_SESSION_REVOKE_V1_DOMAIN = (() => {
|
|
99
|
+
const buf = new Uint8Array(32);
|
|
100
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REVOKE_V1"), 0);
|
|
101
|
+
return buf;
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
// src/instructions/provePasskey.ts
|
|
105
|
+
function encodeBytesVec(buf) {
|
|
106
|
+
const out = Buffer.alloc(4 + buf.length);
|
|
107
|
+
out.writeUInt32LE(buf.length, 0);
|
|
108
|
+
Buffer.from(buf).copy(out, 4);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function encodeFixedBytes(buf, len) {
|
|
112
|
+
if (buf.length !== len) {
|
|
113
|
+
throw new Error(`expected ${len} bytes, got ${buf.length}`);
|
|
114
|
+
}
|
|
115
|
+
return Buffer.from(buf);
|
|
116
|
+
}
|
|
117
|
+
function buildProvePasskeyInstruction(p) {
|
|
118
|
+
const argsBuf = Buffer.concat([
|
|
119
|
+
encodeFixedBytes(p.challenge, 32),
|
|
120
|
+
encodeBytesVec(p.clientDataJSON),
|
|
121
|
+
encodeBytesVec(p.authenticatorData)
|
|
122
|
+
]);
|
|
123
|
+
const data = Buffer.concat([Buffer.from(DISCRIMINATORS.prove_passkey), argsBuf]);
|
|
124
|
+
return new import_web32.TransactionInstruction({
|
|
125
|
+
programId: DEXTER_VAULT_PROGRAM_ID,
|
|
126
|
+
keys: [
|
|
127
|
+
{ pubkey: p.vaultPda, isSigner: false, isWritable: false },
|
|
128
|
+
{ pubkey: import_web32.SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
|
|
129
|
+
],
|
|
130
|
+
data
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/precompile/secp256r1.ts
|
|
135
|
+
var import_web33 = require("@solana/web3.js");
|
|
136
|
+
var SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14;
|
|
137
|
+
var SIGNATURE_SERIALIZED_SIZE = 64;
|
|
138
|
+
var COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33;
|
|
139
|
+
var PRECOMPILE_DATA_START = 2;
|
|
140
|
+
function buildSecp256r1VerifyInstruction(publicKey, signature, message) {
|
|
141
|
+
if (publicKey.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE) {
|
|
142
|
+
throw new Error(`expected ${COMPRESSED_PUBKEY_SERIALIZED_SIZE}-byte pubkey`);
|
|
143
|
+
}
|
|
144
|
+
if (signature.length !== SIGNATURE_SERIALIZED_SIZE) {
|
|
145
|
+
throw new Error(`expected ${SIGNATURE_SERIALIZED_SIZE}-byte signature`);
|
|
146
|
+
}
|
|
147
|
+
const signatureOffset = PRECOMPILE_DATA_START + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
|
148
|
+
const publicKeyOffset = signatureOffset + SIGNATURE_SERIALIZED_SIZE;
|
|
149
|
+
const messageOffset = publicKeyOffset + COMPRESSED_PUBKEY_SERIALIZED_SIZE;
|
|
150
|
+
const messageSize = message.length;
|
|
151
|
+
const totalLen = messageOffset + messageSize;
|
|
152
|
+
const data = Buffer.alloc(totalLen);
|
|
153
|
+
data[0] = 1;
|
|
154
|
+
data[1] = 0;
|
|
155
|
+
data.writeUInt16LE(signatureOffset, PRECOMPILE_DATA_START + 0);
|
|
156
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 2);
|
|
157
|
+
data.writeUInt16LE(publicKeyOffset, PRECOMPILE_DATA_START + 4);
|
|
158
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 6);
|
|
159
|
+
data.writeUInt16LE(messageOffset, PRECOMPILE_DATA_START + 8);
|
|
160
|
+
data.writeUInt16LE(messageSize, PRECOMPILE_DATA_START + 10);
|
|
161
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 12);
|
|
162
|
+
Buffer.from(signature).copy(data, signatureOffset);
|
|
163
|
+
Buffer.from(publicKey).copy(data, publicKeyOffset);
|
|
164
|
+
Buffer.from(message).copy(data, messageOffset);
|
|
165
|
+
return new import_web33.TransactionInstruction({
|
|
166
|
+
keys: [],
|
|
167
|
+
programId: SECP256R1_PROGRAM_ID,
|
|
168
|
+
data
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function buildPrecompileMessage(clientDataJSON, authenticatorData) {
|
|
172
|
+
const subtle = globalThis.crypto?.subtle;
|
|
173
|
+
let clientDataHash;
|
|
174
|
+
if (subtle) {
|
|
175
|
+
const buf = await subtle.digest("SHA-256", clientDataJSON);
|
|
176
|
+
clientDataHash = new Uint8Array(buf);
|
|
177
|
+
} else {
|
|
178
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
179
|
+
clientDataHash = createHash2("sha256").update(clientDataJSON).digest();
|
|
180
|
+
}
|
|
181
|
+
const out = new Uint8Array(authenticatorData.length + 32);
|
|
182
|
+
out.set(authenticatorData, 0);
|
|
183
|
+
out.set(clientDataHash, authenticatorData.length);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/connect/verify.ts
|
|
188
|
+
function decodeChallengeTo32Bytes(challenge) {
|
|
189
|
+
const decoded = tryBase64urlDecode(challenge);
|
|
190
|
+
if (decoded && decoded.length === 32) return decoded;
|
|
191
|
+
return new Uint8Array((0, import_node_crypto.createHash)("sha256").update(challenge, "utf8").digest());
|
|
192
|
+
}
|
|
193
|
+
function tryBase64urlDecode(s) {
|
|
194
|
+
if (!/^[A-Za-z0-9\-_]+={0,2}$/.test(s)) return null;
|
|
195
|
+
try {
|
|
196
|
+
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function verifyConnectProof(args) {
|
|
202
|
+
const { connection, challenge, proof } = args;
|
|
203
|
+
try {
|
|
204
|
+
const challengeBytes = decodeChallengeTo32Bytes(challenge);
|
|
205
|
+
const vaultPda = new import_web34.PublicKey(proof.vault);
|
|
206
|
+
const precompileMessage = await buildPrecompileMessage(
|
|
207
|
+
proof.clientDataJSON,
|
|
208
|
+
proof.authenticatorData
|
|
209
|
+
);
|
|
210
|
+
const ix0 = buildSecp256r1VerifyInstruction(
|
|
211
|
+
proof.passkeyPubkey,
|
|
212
|
+
proof.signature,
|
|
213
|
+
precompileMessage
|
|
214
|
+
);
|
|
215
|
+
const ix1 = buildProvePasskeyInstruction({
|
|
216
|
+
vaultPda,
|
|
217
|
+
challenge: challengeBytes,
|
|
218
|
+
clientDataJSON: proof.clientDataJSON,
|
|
219
|
+
authenticatorData: proof.authenticatorData
|
|
220
|
+
});
|
|
221
|
+
const tx = new import_web34.Transaction();
|
|
222
|
+
tx.add(ix0, ix1);
|
|
223
|
+
tx.feePayer = vaultPda;
|
|
224
|
+
tx.recentBlockhash = import_web34.PublicKey.default.toBase58();
|
|
225
|
+
const simulate = args.simulate ?? ((t) => connection.simulateTransaction(t, void 0, false));
|
|
226
|
+
const res = await simulate(tx);
|
|
227
|
+
const err = res?.value?.err ?? null;
|
|
228
|
+
if (err === null) {
|
|
229
|
+
return { ok: true, vault: vaultPda };
|
|
230
|
+
}
|
|
231
|
+
return { ok: false, reason: `simulation rejected: ${stringifyErr(err)}` };
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function stringifyErr(err) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.stringify(err);
|
|
239
|
+
} catch {
|
|
240
|
+
return String(err);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/signers/browser/index.ts
|
|
245
|
+
var WebAuthnAssertionError = class extends Error {
|
|
246
|
+
code;
|
|
247
|
+
constructor(code, message) {
|
|
248
|
+
super(message);
|
|
249
|
+
this.code = code;
|
|
250
|
+
this.name = "WebAuthnAssertionError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var WebAuthnAssertion = class {
|
|
254
|
+
credentialId;
|
|
255
|
+
publicKeyBase64;
|
|
256
|
+
rpId;
|
|
257
|
+
allowCredentials;
|
|
258
|
+
timeoutMs;
|
|
259
|
+
userVerification;
|
|
260
|
+
constructor(config) {
|
|
261
|
+
if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
|
|
262
|
+
throw new WebAuthnAssertionError(
|
|
263
|
+
"invalid_credential_id",
|
|
264
|
+
"credentialId must be a non-empty Uint8Array"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
this.credentialId = config.credentialId;
|
|
268
|
+
this.publicKeyBase64 = config.publicKeyBase64;
|
|
269
|
+
this.rpId = config.rpId;
|
|
270
|
+
this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
|
|
271
|
+
this.timeoutMs = config.timeoutMs ?? 6e4;
|
|
272
|
+
this.userVerification = config.userVerification ?? "preferred";
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
276
|
+
* three on-chain-ready buffers.
|
|
277
|
+
*
|
|
278
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
279
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
280
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
281
|
+
* does not impose policy here.
|
|
282
|
+
*/
|
|
283
|
+
async assertOver(challenge) {
|
|
284
|
+
ensureBrowser();
|
|
285
|
+
if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
|
|
286
|
+
throw new WebAuthnAssertionError(
|
|
287
|
+
"invalid_challenge",
|
|
288
|
+
"challenge must be a non-empty Uint8Array"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const requestOptions = {
|
|
292
|
+
challenge: toBufferSource(challenge),
|
|
293
|
+
allowCredentials: this.allowCredentials.map((c) => ({
|
|
294
|
+
id: toBufferSource(c.id),
|
|
295
|
+
type: "public-key",
|
|
296
|
+
transports: c.transports
|
|
297
|
+
})),
|
|
298
|
+
timeout: this.timeoutMs,
|
|
299
|
+
userVerification: this.userVerification,
|
|
300
|
+
...this.rpId ? { rpId: this.rpId } : {}
|
|
301
|
+
};
|
|
302
|
+
const credential = await navigator.credentials.get({
|
|
303
|
+
publicKey: requestOptions
|
|
304
|
+
});
|
|
305
|
+
if (!credential) {
|
|
306
|
+
throw new WebAuthnAssertionError(
|
|
307
|
+
"user_cancelled",
|
|
308
|
+
"no assertion returned from authenticator"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (credential.type !== "public-key") {
|
|
312
|
+
throw new WebAuthnAssertionError(
|
|
313
|
+
"credential_invalid",
|
|
314
|
+
`unexpected credential type: ${credential.type}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const assertion = credential.response;
|
|
318
|
+
const derSignature = new Uint8Array(assertion.signature);
|
|
319
|
+
const compactSignature = derSignatureToCompactLowS(derSignature);
|
|
320
|
+
return {
|
|
321
|
+
signature: compactSignature,
|
|
322
|
+
signatureDer: derSignature,
|
|
323
|
+
clientDataJSON: new Uint8Array(assertion.clientDataJSON),
|
|
324
|
+
authenticatorData: new Uint8Array(assertion.authenticatorData)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
329
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
330
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
331
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
332
|
+
* function, two names.
|
|
333
|
+
*/
|
|
334
|
+
sign(challenge) {
|
|
335
|
+
return this.assertOver(challenge);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
function ensureBrowser() {
|
|
339
|
+
if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
|
|
340
|
+
throw new WebAuthnAssertionError(
|
|
341
|
+
"not_browser",
|
|
342
|
+
"WebAuthnAssertion requires a browser environment (navigator.credentials)"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const cred = globalThis.navigator.credentials;
|
|
346
|
+
if (!cred || typeof cred.get !== "function") {
|
|
347
|
+
throw new WebAuthnAssertionError(
|
|
348
|
+
"webauthn_unsupported",
|
|
349
|
+
"this environment does not implement navigator.credentials.get"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function toBufferSource(bytes) {
|
|
354
|
+
const out = new ArrayBuffer(bytes.length);
|
|
355
|
+
new Uint8Array(out).set(bytes);
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
var P256_ORDER = BigInt(
|
|
359
|
+
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
|
|
360
|
+
);
|
|
361
|
+
var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
|
|
362
|
+
function bigintFromBytes(buf) {
|
|
363
|
+
let n = 0n;
|
|
364
|
+
for (const byte of buf) n = n << 8n | BigInt(byte);
|
|
365
|
+
return n;
|
|
366
|
+
}
|
|
367
|
+
function bytesFromBigint(n, length) {
|
|
368
|
+
const out = new Uint8Array(length);
|
|
369
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
370
|
+
out[i] = Number(n & 0xffn);
|
|
371
|
+
n >>= 8n;
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
function derSignatureToCompactLowS(der) {
|
|
376
|
+
let i = 0;
|
|
377
|
+
if (der[i++] !== 48) {
|
|
378
|
+
throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
|
|
379
|
+
}
|
|
380
|
+
i++;
|
|
381
|
+
if (der[i++] !== 2) {
|
|
382
|
+
throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
|
|
383
|
+
}
|
|
384
|
+
const rLen = der[i++];
|
|
385
|
+
if (rLen === void 0) {
|
|
386
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
|
|
387
|
+
}
|
|
388
|
+
let r = der.slice(i, i + rLen);
|
|
389
|
+
i += rLen;
|
|
390
|
+
if (der[i++] !== 2) {
|
|
391
|
+
throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
|
|
392
|
+
}
|
|
393
|
+
const sLen = der[i++];
|
|
394
|
+
if (sLen === void 0) {
|
|
395
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
|
|
396
|
+
}
|
|
397
|
+
let s = der.slice(i, i + sLen);
|
|
398
|
+
i += sLen;
|
|
399
|
+
if (r.length > 32 && r[0] === 0) r = r.slice(1);
|
|
400
|
+
if (s.length > 32 && s[0] === 0) s = s.slice(1);
|
|
401
|
+
if (r.length > 32 || s.length > 32) {
|
|
402
|
+
throw new WebAuthnAssertionError(
|
|
403
|
+
"bad_signature",
|
|
404
|
+
"DER component too large for P-256"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const rPadded = new Uint8Array(32);
|
|
408
|
+
rPadded.set(r, 32 - r.length);
|
|
409
|
+
let sN = bigintFromBytes(s);
|
|
410
|
+
if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
|
|
411
|
+
const sPadded = bytesFromBigint(sN, 32);
|
|
412
|
+
const out = new Uint8Array(64);
|
|
413
|
+
out.set(rPadded, 0);
|
|
414
|
+
out.set(sPadded, 32);
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/connect/ceremony.ts
|
|
419
|
+
var SIWX_LOGIN_PREFIX = "siwx_login";
|
|
420
|
+
async function connectTab(args) {
|
|
421
|
+
const challengeBytes = decodeChallengeTo32Bytes(args.challenge);
|
|
422
|
+
const prefix = new TextEncoder().encode(SIWX_LOGIN_PREFIX);
|
|
423
|
+
const opMessage = new Uint8Array(prefix.length + challengeBytes.length);
|
|
424
|
+
opMessage.set(prefix, 0);
|
|
425
|
+
opMessage.set(challengeBytes, prefix.length);
|
|
426
|
+
const signedDigest = await sha256(opMessage);
|
|
427
|
+
const signer = new WebAuthnAssertion({
|
|
428
|
+
credentialId: args.credentialId,
|
|
429
|
+
...args.rpId ? { rpId: args.rpId } : {}
|
|
430
|
+
});
|
|
431
|
+
const assertion = await signer.assertOver(signedDigest);
|
|
432
|
+
return {
|
|
433
|
+
passkeyPubkey: args.passkeyPubkey,
|
|
434
|
+
vault: args.vault,
|
|
435
|
+
clientDataJSON: assertion.clientDataJSON,
|
|
436
|
+
authenticatorData: assertion.authenticatorData,
|
|
437
|
+
signature: assertion.signature
|
|
438
|
+
// 64-byte compact lowS r||s
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async function sha256(data) {
|
|
442
|
+
const subtle = globalThis.crypto?.subtle;
|
|
443
|
+
if (subtle) {
|
|
444
|
+
const buf = await subtle.digest("SHA-256", data);
|
|
445
|
+
return new Uint8Array(buf);
|
|
446
|
+
}
|
|
447
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
448
|
+
return new Uint8Array(createHash2("sha256").update(data).digest());
|
|
449
|
+
}
|
|
450
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
451
|
+
0 && (module.exports = {
|
|
452
|
+
connectTab,
|
|
453
|
+
decodeChallengeTo32Bytes,
|
|
454
|
+
verifyConnectProof
|
|
455
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { PublicKey, Transaction, Connection } from '@solana/web3.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* verifyConnectProof — the relying-app VERIFIER for "Connect a Tab" step 1.
|
|
5
|
+
*
|
|
6
|
+
* A third-party app uses this to confirm a user controls a named Dexter vault.
|
|
7
|
+
* It reconstructs the read-only two-instruction proof transaction
|
|
8
|
+
* [secp256r1_verify, prove_passkey]
|
|
9
|
+
* from the proof + the challenge it issued + the vault's passkey pubkey, and
|
|
10
|
+
* simulates it against the caller-supplied Connection (Helius mainnet) via the
|
|
11
|
+
* legacy `Transaction` simulate overload — which does NOT verify signatures, so
|
|
12
|
+
* the placeholder blockhash and formal feePayer are never checked.
|
|
13
|
+
* `err === null` → the holder controls the vault.
|
|
14
|
+
* (`connection.simulateTransaction(tx, undefined, false)`: signature is
|
|
15
|
+
* `(transaction, signers?, includeAccounts?)`; the third arg is
|
|
16
|
+
* `includeAccounts`, set false. There is no `sigVerify` param on this
|
|
17
|
+
* overload — `sigVerify` exists only on the VersionedTransaction config.)
|
|
18
|
+
*
|
|
19
|
+
* This is THE canonical method documented in provePasskey.ts: a verifier treats
|
|
20
|
+
* a passing simulate of [secp256r1_verify, prove_passkey] (err === null) as
|
|
21
|
+
* proof of control.
|
|
22
|
+
* It reuses the exact on-chain P-256 semantics rather than re-implementing
|
|
23
|
+
* verification — a forged/wrong-key/wrong-challenge proof makes the on-chain
|
|
24
|
+
* precompile (or prove_passkey's op-message check) reject, and simulate
|
|
25
|
+
* returns a non-null err. The reject path is genuinely simulate-driven; it is
|
|
26
|
+
* NOT a bypassable string compare.
|
|
27
|
+
*
|
|
28
|
+
* The `simulate` step is injectable with a real default (the same
|
|
29
|
+
* injectable-default-real pattern ./tab and ./factoring use for assembleSignV2
|
|
30
|
+
* / readPriorSpent) so the assembly + decision logic is unit-testable offline,
|
|
31
|
+
* while production hits the real chain.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
interface ConnectProof {
|
|
35
|
+
/** 33-byte compressed P-256 passkey pubkey bound to the vault. */
|
|
36
|
+
passkeyPubkey: Uint8Array;
|
|
37
|
+
/** base58 vault PDA the proof claims control of. */
|
|
38
|
+
vault: string;
|
|
39
|
+
/** WebAuthn ceremony outputs (from WebAuthnAssertionResult). */
|
|
40
|
+
clientDataJSON: Uint8Array;
|
|
41
|
+
authenticatorData: Uint8Array;
|
|
42
|
+
/** 64-byte compact lowS r||s P-256 signature. */
|
|
43
|
+
signature: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
interface ConnectVerifyResult {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
vault?: PublicKey;
|
|
48
|
+
reason?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Injectable simulate fn — defaults to the real connection.simulateTransaction.
|
|
51
|
+
* Matches the on-chain verifier method documented in provePasskey.ts.
|
|
52
|
+
*
|
|
53
|
+
* The return shape is intentionally MINIMAL: only `value.err` is consumed by
|
|
54
|
+
* the decision path. The real `simulateTransaction` response is richer —
|
|
55
|
+
* `{ context, value: { err, logs, accounts, unitsConsumed, ... } }`. A future
|
|
56
|
+
* maintainer wanting richer `reason` diagnostics can read `value.logs` (it is
|
|
57
|
+
* present on the real response and on the optional `logs?` below). Keeping the
|
|
58
|
+
* type narrow also keeps the tests' fake simulate trivial. */
|
|
59
|
+
type SimulateFn = (tx: Transaction) => Promise<{
|
|
60
|
+
value: {
|
|
61
|
+
err: unknown;
|
|
62
|
+
logs?: string[] | null;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* CHALLENGE-ENCODING CONTRACT (C2 — the ceremony — MUST match this):
|
|
67
|
+
*
|
|
68
|
+
* The on-chain prove_passkey takes a 32-byte `challenge` (the SIWX nonce/digest);
|
|
69
|
+
* its op-message is "siwx_login" || challenge, and the WebAuthn
|
|
70
|
+
* clientDataJSON.challenge field must base64url-decode to
|
|
71
|
+
* sha256("siwx_login" || challenge).
|
|
72
|
+
*
|
|
73
|
+
* The op-message is exactly utf8("siwx_login") concatenated DIRECTLY with the
|
|
74
|
+
* 32 challenge bytes — no separator, no length prefix, no padding between them
|
|
75
|
+
* (it is rebuilt on-chain by plain byte concatenation; see provePasskey.ts).
|
|
76
|
+
*
|
|
77
|
+
* The relying-app `challenge` STRING that this verifier receives maps to those
|
|
78
|
+
* 32 bytes by this rule:
|
|
79
|
+
* - if it base64url-decodes to EXACTLY 32 bytes, those bytes ARE the challenge;
|
|
80
|
+
* - otherwise, sha256(utf8(challenge)) → 32 bytes.
|
|
81
|
+
* The base64url form is accepted with OR without `=` padding; the canonical
|
|
82
|
+
* issuer form is unpadded `base64url(random 32 bytes)`.
|
|
83
|
+
*
|
|
84
|
+
* So a relying app SHOULD issue `base64url(random 32 bytes)` (the canonical,
|
|
85
|
+
* zero-ambiguity form). The fallback (sha256 of an arbitrary string) keeps any
|
|
86
|
+
* other issuer deterministic. C2 produces the matching ceremony challenge:
|
|
87
|
+
* clientDataJSON.challenge = base64url(sha256("siwx_login" || challengeBytes)).
|
|
88
|
+
*/
|
|
89
|
+
declare function decodeChallengeTo32Bytes(challenge: string): Uint8Array;
|
|
90
|
+
declare function verifyConnectProof(args: {
|
|
91
|
+
connection: Connection;
|
|
92
|
+
/** The challenge the relying app issued (raw string; the SAME one C2 signed). */
|
|
93
|
+
challenge: string;
|
|
94
|
+
proof: ConnectProof;
|
|
95
|
+
/** Default: real connection.simulateTransaction (injectable for tests). */
|
|
96
|
+
simulate?: SimulateFn;
|
|
97
|
+
}): Promise<ConnectVerifyResult>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* connectTab — the browser-side ceremony for "Connect a Tab" step 1 (auth).
|
|
101
|
+
*
|
|
102
|
+
* Runs the WebAuthn passkey assertion and returns a `ConnectProof` that the C1
|
|
103
|
+
* verifier (`verifyConnectProof`) accepts. connectTab + verifyConnectProof must
|
|
104
|
+
* agree byte-for-byte on the challenge contract — the round-trip test is the
|
|
105
|
+
* proof they do.
|
|
106
|
+
*
|
|
107
|
+
* THE CHALLENGE CONTRACT (must match verify.ts / provePasskey.ts / the IDL):
|
|
108
|
+
* - The relying-app `challenge` STRING maps to 32 bytes via the SAME
|
|
109
|
+
* `decodeChallengeTo32Bytes` the verifier uses.
|
|
110
|
+
* - The on-chain prove_passkey op-message is utf8("siwx_login") concatenated
|
|
111
|
+
* DIRECTLY with those 32 challenge bytes (no separator, no length prefix).
|
|
112
|
+
* - The WebAuthn `clientDataJSON.challenge` field must equal
|
|
113
|
+
* base64url(sha256("siwx_login" || challengeBytes)).
|
|
114
|
+
*
|
|
115
|
+
* Since `WebAuthnAssertion.assertOver(X)` causes the browser to write
|
|
116
|
+
* base64url(X) into clientDataJSON.challenge, the bytes we pass to assertOver
|
|
117
|
+
* are sha256("siwx_login" || challengeBytes) — the 32-byte digest, NOT the raw
|
|
118
|
+
* challenge. Then clientDataJSON.challenge == base64url(sha256(opMessage)),
|
|
119
|
+
* exactly what prove_passkey reconstructs and the precompile signature is over.
|
|
120
|
+
*
|
|
121
|
+
* Framework-agnostic: a plain browser function. The C3 button is documented as
|
|
122
|
+
* a snippet, not a shipped React component (React is not a dependency).
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
interface ConnectTabArgs {
|
|
126
|
+
/** The challenge the relying app issued (same string the server will pass to verifyConnectProof). */
|
|
127
|
+
challenge: string;
|
|
128
|
+
/** base58 vault PDA being connected. */
|
|
129
|
+
vault: string;
|
|
130
|
+
/** 33-byte compressed P-256 passkey pubkey bound to the vault. */
|
|
131
|
+
passkeyPubkey: Uint8Array;
|
|
132
|
+
/** Raw WebAuthn credential ID bytes for the vault's passkey. */
|
|
133
|
+
credentialId: Uint8Array;
|
|
134
|
+
/** Optional WebAuthn RP id (defaults to the page's RP). */
|
|
135
|
+
rpId?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Run the passkey assertion and return a verifier-ready ConnectProof.
|
|
139
|
+
*
|
|
140
|
+
* Browser-only (requires navigator.credentials). The returned proof feeds
|
|
141
|
+
* straight into verifyConnectProof with the SAME challenge string.
|
|
142
|
+
*/
|
|
143
|
+
declare function connectTab(args: ConnectTabArgs): Promise<ConnectProof>;
|
|
144
|
+
|
|
145
|
+
export { type ConnectProof, type ConnectTabArgs, type ConnectVerifyResult, type SimulateFn, connectTab, decodeChallengeTo32Bytes, verifyConnectProof };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { PublicKey, Transaction, Connection } from '@solana/web3.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* verifyConnectProof — the relying-app VERIFIER for "Connect a Tab" step 1.
|
|
5
|
+
*
|
|
6
|
+
* A third-party app uses this to confirm a user controls a named Dexter vault.
|
|
7
|
+
* It reconstructs the read-only two-instruction proof transaction
|
|
8
|
+
* [secp256r1_verify, prove_passkey]
|
|
9
|
+
* from the proof + the challenge it issued + the vault's passkey pubkey, and
|
|
10
|
+
* simulates it against the caller-supplied Connection (Helius mainnet) via the
|
|
11
|
+
* legacy `Transaction` simulate overload — which does NOT verify signatures, so
|
|
12
|
+
* the placeholder blockhash and formal feePayer are never checked.
|
|
13
|
+
* `err === null` → the holder controls the vault.
|
|
14
|
+
* (`connection.simulateTransaction(tx, undefined, false)`: signature is
|
|
15
|
+
* `(transaction, signers?, includeAccounts?)`; the third arg is
|
|
16
|
+
* `includeAccounts`, set false. There is no `sigVerify` param on this
|
|
17
|
+
* overload — `sigVerify` exists only on the VersionedTransaction config.)
|
|
18
|
+
*
|
|
19
|
+
* This is THE canonical method documented in provePasskey.ts: a verifier treats
|
|
20
|
+
* a passing simulate of [secp256r1_verify, prove_passkey] (err === null) as
|
|
21
|
+
* proof of control.
|
|
22
|
+
* It reuses the exact on-chain P-256 semantics rather than re-implementing
|
|
23
|
+
* verification — a forged/wrong-key/wrong-challenge proof makes the on-chain
|
|
24
|
+
* precompile (or prove_passkey's op-message check) reject, and simulate
|
|
25
|
+
* returns a non-null err. The reject path is genuinely simulate-driven; it is
|
|
26
|
+
* NOT a bypassable string compare.
|
|
27
|
+
*
|
|
28
|
+
* The `simulate` step is injectable with a real default (the same
|
|
29
|
+
* injectable-default-real pattern ./tab and ./factoring use for assembleSignV2
|
|
30
|
+
* / readPriorSpent) so the assembly + decision logic is unit-testable offline,
|
|
31
|
+
* while production hits the real chain.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
interface ConnectProof {
|
|
35
|
+
/** 33-byte compressed P-256 passkey pubkey bound to the vault. */
|
|
36
|
+
passkeyPubkey: Uint8Array;
|
|
37
|
+
/** base58 vault PDA the proof claims control of. */
|
|
38
|
+
vault: string;
|
|
39
|
+
/** WebAuthn ceremony outputs (from WebAuthnAssertionResult). */
|
|
40
|
+
clientDataJSON: Uint8Array;
|
|
41
|
+
authenticatorData: Uint8Array;
|
|
42
|
+
/** 64-byte compact lowS r||s P-256 signature. */
|
|
43
|
+
signature: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
interface ConnectVerifyResult {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
vault?: PublicKey;
|
|
48
|
+
reason?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Injectable simulate fn — defaults to the real connection.simulateTransaction.
|
|
51
|
+
* Matches the on-chain verifier method documented in provePasskey.ts.
|
|
52
|
+
*
|
|
53
|
+
* The return shape is intentionally MINIMAL: only `value.err` is consumed by
|
|
54
|
+
* the decision path. The real `simulateTransaction` response is richer —
|
|
55
|
+
* `{ context, value: { err, logs, accounts, unitsConsumed, ... } }`. A future
|
|
56
|
+
* maintainer wanting richer `reason` diagnostics can read `value.logs` (it is
|
|
57
|
+
* present on the real response and on the optional `logs?` below). Keeping the
|
|
58
|
+
* type narrow also keeps the tests' fake simulate trivial. */
|
|
59
|
+
type SimulateFn = (tx: Transaction) => Promise<{
|
|
60
|
+
value: {
|
|
61
|
+
err: unknown;
|
|
62
|
+
logs?: string[] | null;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* CHALLENGE-ENCODING CONTRACT (C2 — the ceremony — MUST match this):
|
|
67
|
+
*
|
|
68
|
+
* The on-chain prove_passkey takes a 32-byte `challenge` (the SIWX nonce/digest);
|
|
69
|
+
* its op-message is "siwx_login" || challenge, and the WebAuthn
|
|
70
|
+
* clientDataJSON.challenge field must base64url-decode to
|
|
71
|
+
* sha256("siwx_login" || challenge).
|
|
72
|
+
*
|
|
73
|
+
* The op-message is exactly utf8("siwx_login") concatenated DIRECTLY with the
|
|
74
|
+
* 32 challenge bytes — no separator, no length prefix, no padding between them
|
|
75
|
+
* (it is rebuilt on-chain by plain byte concatenation; see provePasskey.ts).
|
|
76
|
+
*
|
|
77
|
+
* The relying-app `challenge` STRING that this verifier receives maps to those
|
|
78
|
+
* 32 bytes by this rule:
|
|
79
|
+
* - if it base64url-decodes to EXACTLY 32 bytes, those bytes ARE the challenge;
|
|
80
|
+
* - otherwise, sha256(utf8(challenge)) → 32 bytes.
|
|
81
|
+
* The base64url form is accepted with OR without `=` padding; the canonical
|
|
82
|
+
* issuer form is unpadded `base64url(random 32 bytes)`.
|
|
83
|
+
*
|
|
84
|
+
* So a relying app SHOULD issue `base64url(random 32 bytes)` (the canonical,
|
|
85
|
+
* zero-ambiguity form). The fallback (sha256 of an arbitrary string) keeps any
|
|
86
|
+
* other issuer deterministic. C2 produces the matching ceremony challenge:
|
|
87
|
+
* clientDataJSON.challenge = base64url(sha256("siwx_login" || challengeBytes)).
|
|
88
|
+
*/
|
|
89
|
+
declare function decodeChallengeTo32Bytes(challenge: string): Uint8Array;
|
|
90
|
+
declare function verifyConnectProof(args: {
|
|
91
|
+
connection: Connection;
|
|
92
|
+
/** The challenge the relying app issued (raw string; the SAME one C2 signed). */
|
|
93
|
+
challenge: string;
|
|
94
|
+
proof: ConnectProof;
|
|
95
|
+
/** Default: real connection.simulateTransaction (injectable for tests). */
|
|
96
|
+
simulate?: SimulateFn;
|
|
97
|
+
}): Promise<ConnectVerifyResult>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* connectTab — the browser-side ceremony for "Connect a Tab" step 1 (auth).
|
|
101
|
+
*
|
|
102
|
+
* Runs the WebAuthn passkey assertion and returns a `ConnectProof` that the C1
|
|
103
|
+
* verifier (`verifyConnectProof`) accepts. connectTab + verifyConnectProof must
|
|
104
|
+
* agree byte-for-byte on the challenge contract — the round-trip test is the
|
|
105
|
+
* proof they do.
|
|
106
|
+
*
|
|
107
|
+
* THE CHALLENGE CONTRACT (must match verify.ts / provePasskey.ts / the IDL):
|
|
108
|
+
* - The relying-app `challenge` STRING maps to 32 bytes via the SAME
|
|
109
|
+
* `decodeChallengeTo32Bytes` the verifier uses.
|
|
110
|
+
* - The on-chain prove_passkey op-message is utf8("siwx_login") concatenated
|
|
111
|
+
* DIRECTLY with those 32 challenge bytes (no separator, no length prefix).
|
|
112
|
+
* - The WebAuthn `clientDataJSON.challenge` field must equal
|
|
113
|
+
* base64url(sha256("siwx_login" || challengeBytes)).
|
|
114
|
+
*
|
|
115
|
+
* Since `WebAuthnAssertion.assertOver(X)` causes the browser to write
|
|
116
|
+
* base64url(X) into clientDataJSON.challenge, the bytes we pass to assertOver
|
|
117
|
+
* are sha256("siwx_login" || challengeBytes) — the 32-byte digest, NOT the raw
|
|
118
|
+
* challenge. Then clientDataJSON.challenge == base64url(sha256(opMessage)),
|
|
119
|
+
* exactly what prove_passkey reconstructs and the precompile signature is over.
|
|
120
|
+
*
|
|
121
|
+
* Framework-agnostic: a plain browser function. The C3 button is documented as
|
|
122
|
+
* a snippet, not a shipped React component (React is not a dependency).
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
interface ConnectTabArgs {
|
|
126
|
+
/** The challenge the relying app issued (same string the server will pass to verifyConnectProof). */
|
|
127
|
+
challenge: string;
|
|
128
|
+
/** base58 vault PDA being connected. */
|
|
129
|
+
vault: string;
|
|
130
|
+
/** 33-byte compressed P-256 passkey pubkey bound to the vault. */
|
|
131
|
+
passkeyPubkey: Uint8Array;
|
|
132
|
+
/** Raw WebAuthn credential ID bytes for the vault's passkey. */
|
|
133
|
+
credentialId: Uint8Array;
|
|
134
|
+
/** Optional WebAuthn RP id (defaults to the page's RP). */
|
|
135
|
+
rpId?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Run the passkey assertion and return a verifier-ready ConnectProof.
|
|
139
|
+
*
|
|
140
|
+
* Browser-only (requires navigator.credentials). The returned proof feeds
|
|
141
|
+
* straight into verifyConnectProof with the SAME challenge string.
|
|
142
|
+
*/
|
|
143
|
+
declare function connectTab(args: ConnectTabArgs): Promise<ConnectProof>;
|
|
144
|
+
|
|
145
|
+
export { type ConnectProof, type ConnectTabArgs, type ConnectVerifyResult, type SimulateFn, connectTab, decodeChallengeTo32Bytes, verifyConnectProof };
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// src/connect/verify.ts
|
|
2
|
+
import { PublicKey as PublicKey3, Transaction } from "@solana/web3.js";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
|
|
5
|
+
// src/instructions/provePasskey.ts
|
|
6
|
+
import {
|
|
7
|
+
TransactionInstruction,
|
|
8
|
+
SYSVAR_INSTRUCTIONS_PUBKEY
|
|
9
|
+
} from "@solana/web3.js";
|
|
10
|
+
|
|
11
|
+
// src/constants/index.ts
|
|
12
|
+
import { PublicKey } from "@solana/web3.js";
|
|
13
|
+
var DEXTER_VAULT_PROGRAM_ID = new PublicKey(
|
|
14
|
+
"Hg3wRaydFtJhYrdvYrKECacpJYDsC9Px7yKmpncj2fhc"
|
|
15
|
+
);
|
|
16
|
+
var SWIG_PROGRAM_ID = new PublicKey(
|
|
17
|
+
"swigypWHEksbC64pWKwah1WTeh9JXwx8H1rJHLdbQMB"
|
|
18
|
+
);
|
|
19
|
+
var SECP256R1_PROGRAM_ID = new PublicKey(
|
|
20
|
+
"Secp256r1SigVerify1111111111111111111111111"
|
|
21
|
+
);
|
|
22
|
+
var ED25519_PROGRAM_ID = new PublicKey(
|
|
23
|
+
"Ed25519SigVerify111111111111111111111111111"
|
|
24
|
+
);
|
|
25
|
+
var INSTRUCTIONS_SYSVAR_ID = new PublicKey(
|
|
26
|
+
"Sysvar1nstructions1111111111111111111111111"
|
|
27
|
+
);
|
|
28
|
+
var VAULT_SEED_PREFIX = Buffer.from("vault");
|
|
29
|
+
var LOCKED_CLAIM_SEED = Buffer.from("locked-claim");
|
|
30
|
+
var DISCRIMINATORS = Object.freeze({
|
|
31
|
+
initialize_vault: Uint8Array.from([48, 191, 163, 44, 71, 129, 63, 164]),
|
|
32
|
+
set_swig: Uint8Array.from([253, 229, 89, 206, 192, 118, 137, 165]),
|
|
33
|
+
settle_voucher: Uint8Array.from([144, 176, 128, 220, 156, 79, 41, 54]),
|
|
34
|
+
request_withdrawal: Uint8Array.from([251, 85, 121, 205, 56, 201, 12, 177]),
|
|
35
|
+
finalize_withdrawal: Uint8Array.from([178, 87, 206, 68, 201, 186, 164, 232]),
|
|
36
|
+
force_release: Uint8Array.from([122, 190, 243, 252, 54, 202, 208, 234]),
|
|
37
|
+
rotate_passkey: Uint8Array.from([28, 134, 49, 89, 196, 34, 58, 174]),
|
|
38
|
+
rotate_dexter_authority: Uint8Array.from([145, 60, 4, 119, 180, 205, 236, 134]),
|
|
39
|
+
prove_passkey: Uint8Array.from([35, 175, 41, 143, 201, 118, 49, 184]),
|
|
40
|
+
settle_tab_voucher: Uint8Array.from([173, 22, 98, 31, 110, 129, 59, 161]),
|
|
41
|
+
register_session_key: Uint8Array.from([69, 94, 60, 44, 49, 199, 183, 233]),
|
|
42
|
+
revoke_session_key: Uint8Array.from([81, 192, 32, 110, 104, 116, 144, 151]),
|
|
43
|
+
lock_voucher: Uint8Array.from([91, 138, 5, 227, 119, 239, 48, 254]),
|
|
44
|
+
settle_locked_voucher: Uint8Array.from([44, 80, 216, 43, 247, 253, 101, 45]),
|
|
45
|
+
transfer_lock_ownership: Uint8Array.from([193, 13, 131, 134, 95, 25, 229, 157]),
|
|
46
|
+
recover_abandoned_lock: Uint8Array.from([169, 213, 107, 64, 229, 49, 43, 234]),
|
|
47
|
+
open_standby: Uint8Array.from([234, 184, 232, 135, 246, 191, 90, 250]),
|
|
48
|
+
draw_credit: Uint8Array.from([20, 84, 47, 211, 78, 117, 195, 210]),
|
|
49
|
+
repay_credit: Uint8Array.from([38, 113, 240, 182, 109, 179, 154, 245]),
|
|
50
|
+
seize_collateral: Uint8Array.from([40, 250, 7, 243, 168, 184, 116, 154]),
|
|
51
|
+
migrate_v4_to_v5: Uint8Array.from([226, 105, 140, 184, 101, 39, 235, 116])
|
|
52
|
+
});
|
|
53
|
+
var OTS_SESSION_REGISTER_V1_DOMAIN = (() => {
|
|
54
|
+
const buf = new Uint8Array(32);
|
|
55
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V1"), 0);
|
|
56
|
+
return buf;
|
|
57
|
+
})();
|
|
58
|
+
var OTS_SESSION_REGISTER_V2_DOMAIN = (() => {
|
|
59
|
+
const buf = new Uint8Array(32);
|
|
60
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V2"), 0);
|
|
61
|
+
return buf;
|
|
62
|
+
})();
|
|
63
|
+
var OTS_SESSION_REVOKE_V1_DOMAIN = (() => {
|
|
64
|
+
const buf = new Uint8Array(32);
|
|
65
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REVOKE_V1"), 0);
|
|
66
|
+
return buf;
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
// src/instructions/provePasskey.ts
|
|
70
|
+
function encodeBytesVec(buf) {
|
|
71
|
+
const out = Buffer.alloc(4 + buf.length);
|
|
72
|
+
out.writeUInt32LE(buf.length, 0);
|
|
73
|
+
Buffer.from(buf).copy(out, 4);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function encodeFixedBytes(buf, len) {
|
|
77
|
+
if (buf.length !== len) {
|
|
78
|
+
throw new Error(`expected ${len} bytes, got ${buf.length}`);
|
|
79
|
+
}
|
|
80
|
+
return Buffer.from(buf);
|
|
81
|
+
}
|
|
82
|
+
function buildProvePasskeyInstruction(p) {
|
|
83
|
+
const argsBuf = Buffer.concat([
|
|
84
|
+
encodeFixedBytes(p.challenge, 32),
|
|
85
|
+
encodeBytesVec(p.clientDataJSON),
|
|
86
|
+
encodeBytesVec(p.authenticatorData)
|
|
87
|
+
]);
|
|
88
|
+
const data = Buffer.concat([Buffer.from(DISCRIMINATORS.prove_passkey), argsBuf]);
|
|
89
|
+
return new TransactionInstruction({
|
|
90
|
+
programId: DEXTER_VAULT_PROGRAM_ID,
|
|
91
|
+
keys: [
|
|
92
|
+
{ pubkey: p.vaultPda, isSigner: false, isWritable: false },
|
|
93
|
+
{ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
|
|
94
|
+
],
|
|
95
|
+
data
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/precompile/secp256r1.ts
|
|
100
|
+
import { TransactionInstruction as TransactionInstruction2 } from "@solana/web3.js";
|
|
101
|
+
var SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14;
|
|
102
|
+
var SIGNATURE_SERIALIZED_SIZE = 64;
|
|
103
|
+
var COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33;
|
|
104
|
+
var PRECOMPILE_DATA_START = 2;
|
|
105
|
+
function buildSecp256r1VerifyInstruction(publicKey, signature, message) {
|
|
106
|
+
if (publicKey.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE) {
|
|
107
|
+
throw new Error(`expected ${COMPRESSED_PUBKEY_SERIALIZED_SIZE}-byte pubkey`);
|
|
108
|
+
}
|
|
109
|
+
if (signature.length !== SIGNATURE_SERIALIZED_SIZE) {
|
|
110
|
+
throw new Error(`expected ${SIGNATURE_SERIALIZED_SIZE}-byte signature`);
|
|
111
|
+
}
|
|
112
|
+
const signatureOffset = PRECOMPILE_DATA_START + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
|
113
|
+
const publicKeyOffset = signatureOffset + SIGNATURE_SERIALIZED_SIZE;
|
|
114
|
+
const messageOffset = publicKeyOffset + COMPRESSED_PUBKEY_SERIALIZED_SIZE;
|
|
115
|
+
const messageSize = message.length;
|
|
116
|
+
const totalLen = messageOffset + messageSize;
|
|
117
|
+
const data = Buffer.alloc(totalLen);
|
|
118
|
+
data[0] = 1;
|
|
119
|
+
data[1] = 0;
|
|
120
|
+
data.writeUInt16LE(signatureOffset, PRECOMPILE_DATA_START + 0);
|
|
121
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 2);
|
|
122
|
+
data.writeUInt16LE(publicKeyOffset, PRECOMPILE_DATA_START + 4);
|
|
123
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 6);
|
|
124
|
+
data.writeUInt16LE(messageOffset, PRECOMPILE_DATA_START + 8);
|
|
125
|
+
data.writeUInt16LE(messageSize, PRECOMPILE_DATA_START + 10);
|
|
126
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 12);
|
|
127
|
+
Buffer.from(signature).copy(data, signatureOffset);
|
|
128
|
+
Buffer.from(publicKey).copy(data, publicKeyOffset);
|
|
129
|
+
Buffer.from(message).copy(data, messageOffset);
|
|
130
|
+
return new TransactionInstruction2({
|
|
131
|
+
keys: [],
|
|
132
|
+
programId: SECP256R1_PROGRAM_ID,
|
|
133
|
+
data
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function buildPrecompileMessage(clientDataJSON, authenticatorData) {
|
|
137
|
+
const subtle = globalThis.crypto?.subtle;
|
|
138
|
+
let clientDataHash;
|
|
139
|
+
if (subtle) {
|
|
140
|
+
const buf = await subtle.digest("SHA-256", clientDataJSON);
|
|
141
|
+
clientDataHash = new Uint8Array(buf);
|
|
142
|
+
} else {
|
|
143
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
144
|
+
clientDataHash = createHash2("sha256").update(clientDataJSON).digest();
|
|
145
|
+
}
|
|
146
|
+
const out = new Uint8Array(authenticatorData.length + 32);
|
|
147
|
+
out.set(authenticatorData, 0);
|
|
148
|
+
out.set(clientDataHash, authenticatorData.length);
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/connect/verify.ts
|
|
153
|
+
function decodeChallengeTo32Bytes(challenge) {
|
|
154
|
+
const decoded = tryBase64urlDecode(challenge);
|
|
155
|
+
if (decoded && decoded.length === 32) return decoded;
|
|
156
|
+
return new Uint8Array(createHash("sha256").update(challenge, "utf8").digest());
|
|
157
|
+
}
|
|
158
|
+
function tryBase64urlDecode(s) {
|
|
159
|
+
if (!/^[A-Za-z0-9\-_]+={0,2}$/.test(s)) return null;
|
|
160
|
+
try {
|
|
161
|
+
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function verifyConnectProof(args) {
|
|
167
|
+
const { connection, challenge, proof } = args;
|
|
168
|
+
try {
|
|
169
|
+
const challengeBytes = decodeChallengeTo32Bytes(challenge);
|
|
170
|
+
const vaultPda = new PublicKey3(proof.vault);
|
|
171
|
+
const precompileMessage = await buildPrecompileMessage(
|
|
172
|
+
proof.clientDataJSON,
|
|
173
|
+
proof.authenticatorData
|
|
174
|
+
);
|
|
175
|
+
const ix0 = buildSecp256r1VerifyInstruction(
|
|
176
|
+
proof.passkeyPubkey,
|
|
177
|
+
proof.signature,
|
|
178
|
+
precompileMessage
|
|
179
|
+
);
|
|
180
|
+
const ix1 = buildProvePasskeyInstruction({
|
|
181
|
+
vaultPda,
|
|
182
|
+
challenge: challengeBytes,
|
|
183
|
+
clientDataJSON: proof.clientDataJSON,
|
|
184
|
+
authenticatorData: proof.authenticatorData
|
|
185
|
+
});
|
|
186
|
+
const tx = new Transaction();
|
|
187
|
+
tx.add(ix0, ix1);
|
|
188
|
+
tx.feePayer = vaultPda;
|
|
189
|
+
tx.recentBlockhash = PublicKey3.default.toBase58();
|
|
190
|
+
const simulate = args.simulate ?? ((t) => connection.simulateTransaction(t, void 0, false));
|
|
191
|
+
const res = await simulate(tx);
|
|
192
|
+
const err = res?.value?.err ?? null;
|
|
193
|
+
if (err === null) {
|
|
194
|
+
return { ok: true, vault: vaultPda };
|
|
195
|
+
}
|
|
196
|
+
return { ok: false, reason: `simulation rejected: ${stringifyErr(err)}` };
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function stringifyErr(err) {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.stringify(err);
|
|
204
|
+
} catch {
|
|
205
|
+
return String(err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/signers/browser/index.ts
|
|
210
|
+
var WebAuthnAssertionError = class extends Error {
|
|
211
|
+
code;
|
|
212
|
+
constructor(code, message) {
|
|
213
|
+
super(message);
|
|
214
|
+
this.code = code;
|
|
215
|
+
this.name = "WebAuthnAssertionError";
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var WebAuthnAssertion = class {
|
|
219
|
+
credentialId;
|
|
220
|
+
publicKeyBase64;
|
|
221
|
+
rpId;
|
|
222
|
+
allowCredentials;
|
|
223
|
+
timeoutMs;
|
|
224
|
+
userVerification;
|
|
225
|
+
constructor(config) {
|
|
226
|
+
if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
|
|
227
|
+
throw new WebAuthnAssertionError(
|
|
228
|
+
"invalid_credential_id",
|
|
229
|
+
"credentialId must be a non-empty Uint8Array"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
this.credentialId = config.credentialId;
|
|
233
|
+
this.publicKeyBase64 = config.publicKeyBase64;
|
|
234
|
+
this.rpId = config.rpId;
|
|
235
|
+
this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
|
|
236
|
+
this.timeoutMs = config.timeoutMs ?? 6e4;
|
|
237
|
+
this.userVerification = config.userVerification ?? "preferred";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
241
|
+
* three on-chain-ready buffers.
|
|
242
|
+
*
|
|
243
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
244
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
245
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
246
|
+
* does not impose policy here.
|
|
247
|
+
*/
|
|
248
|
+
async assertOver(challenge) {
|
|
249
|
+
ensureBrowser();
|
|
250
|
+
if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
|
|
251
|
+
throw new WebAuthnAssertionError(
|
|
252
|
+
"invalid_challenge",
|
|
253
|
+
"challenge must be a non-empty Uint8Array"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const requestOptions = {
|
|
257
|
+
challenge: toBufferSource(challenge),
|
|
258
|
+
allowCredentials: this.allowCredentials.map((c) => ({
|
|
259
|
+
id: toBufferSource(c.id),
|
|
260
|
+
type: "public-key",
|
|
261
|
+
transports: c.transports
|
|
262
|
+
})),
|
|
263
|
+
timeout: this.timeoutMs,
|
|
264
|
+
userVerification: this.userVerification,
|
|
265
|
+
...this.rpId ? { rpId: this.rpId } : {}
|
|
266
|
+
};
|
|
267
|
+
const credential = await navigator.credentials.get({
|
|
268
|
+
publicKey: requestOptions
|
|
269
|
+
});
|
|
270
|
+
if (!credential) {
|
|
271
|
+
throw new WebAuthnAssertionError(
|
|
272
|
+
"user_cancelled",
|
|
273
|
+
"no assertion returned from authenticator"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (credential.type !== "public-key") {
|
|
277
|
+
throw new WebAuthnAssertionError(
|
|
278
|
+
"credential_invalid",
|
|
279
|
+
`unexpected credential type: ${credential.type}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
const assertion = credential.response;
|
|
283
|
+
const derSignature = new Uint8Array(assertion.signature);
|
|
284
|
+
const compactSignature = derSignatureToCompactLowS(derSignature);
|
|
285
|
+
return {
|
|
286
|
+
signature: compactSignature,
|
|
287
|
+
signatureDer: derSignature,
|
|
288
|
+
clientDataJSON: new Uint8Array(assertion.clientDataJSON),
|
|
289
|
+
authenticatorData: new Uint8Array(assertion.authenticatorData)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
294
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
295
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
296
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
297
|
+
* function, two names.
|
|
298
|
+
*/
|
|
299
|
+
sign(challenge) {
|
|
300
|
+
return this.assertOver(challenge);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
function ensureBrowser() {
|
|
304
|
+
if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
|
|
305
|
+
throw new WebAuthnAssertionError(
|
|
306
|
+
"not_browser",
|
|
307
|
+
"WebAuthnAssertion requires a browser environment (navigator.credentials)"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const cred = globalThis.navigator.credentials;
|
|
311
|
+
if (!cred || typeof cred.get !== "function") {
|
|
312
|
+
throw new WebAuthnAssertionError(
|
|
313
|
+
"webauthn_unsupported",
|
|
314
|
+
"this environment does not implement navigator.credentials.get"
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function toBufferSource(bytes) {
|
|
319
|
+
const out = new ArrayBuffer(bytes.length);
|
|
320
|
+
new Uint8Array(out).set(bytes);
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
var P256_ORDER = BigInt(
|
|
324
|
+
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
|
|
325
|
+
);
|
|
326
|
+
var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
|
|
327
|
+
function bigintFromBytes(buf) {
|
|
328
|
+
let n = 0n;
|
|
329
|
+
for (const byte of buf) n = n << 8n | BigInt(byte);
|
|
330
|
+
return n;
|
|
331
|
+
}
|
|
332
|
+
function bytesFromBigint(n, length) {
|
|
333
|
+
const out = new Uint8Array(length);
|
|
334
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
335
|
+
out[i] = Number(n & 0xffn);
|
|
336
|
+
n >>= 8n;
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
function derSignatureToCompactLowS(der) {
|
|
341
|
+
let i = 0;
|
|
342
|
+
if (der[i++] !== 48) {
|
|
343
|
+
throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
|
|
344
|
+
}
|
|
345
|
+
i++;
|
|
346
|
+
if (der[i++] !== 2) {
|
|
347
|
+
throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
|
|
348
|
+
}
|
|
349
|
+
const rLen = der[i++];
|
|
350
|
+
if (rLen === void 0) {
|
|
351
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
|
|
352
|
+
}
|
|
353
|
+
let r = der.slice(i, i + rLen);
|
|
354
|
+
i += rLen;
|
|
355
|
+
if (der[i++] !== 2) {
|
|
356
|
+
throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
|
|
357
|
+
}
|
|
358
|
+
const sLen = der[i++];
|
|
359
|
+
if (sLen === void 0) {
|
|
360
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
|
|
361
|
+
}
|
|
362
|
+
let s = der.slice(i, i + sLen);
|
|
363
|
+
i += sLen;
|
|
364
|
+
if (r.length > 32 && r[0] === 0) r = r.slice(1);
|
|
365
|
+
if (s.length > 32 && s[0] === 0) s = s.slice(1);
|
|
366
|
+
if (r.length > 32 || s.length > 32) {
|
|
367
|
+
throw new WebAuthnAssertionError(
|
|
368
|
+
"bad_signature",
|
|
369
|
+
"DER component too large for P-256"
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const rPadded = new Uint8Array(32);
|
|
373
|
+
rPadded.set(r, 32 - r.length);
|
|
374
|
+
let sN = bigintFromBytes(s);
|
|
375
|
+
if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
|
|
376
|
+
const sPadded = bytesFromBigint(sN, 32);
|
|
377
|
+
const out = new Uint8Array(64);
|
|
378
|
+
out.set(rPadded, 0);
|
|
379
|
+
out.set(sPadded, 32);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/connect/ceremony.ts
|
|
384
|
+
var SIWX_LOGIN_PREFIX = "siwx_login";
|
|
385
|
+
async function connectTab(args) {
|
|
386
|
+
const challengeBytes = decodeChallengeTo32Bytes(args.challenge);
|
|
387
|
+
const prefix = new TextEncoder().encode(SIWX_LOGIN_PREFIX);
|
|
388
|
+
const opMessage = new Uint8Array(prefix.length + challengeBytes.length);
|
|
389
|
+
opMessage.set(prefix, 0);
|
|
390
|
+
opMessage.set(challengeBytes, prefix.length);
|
|
391
|
+
const signedDigest = await sha256(opMessage);
|
|
392
|
+
const signer = new WebAuthnAssertion({
|
|
393
|
+
credentialId: args.credentialId,
|
|
394
|
+
...args.rpId ? { rpId: args.rpId } : {}
|
|
395
|
+
});
|
|
396
|
+
const assertion = await signer.assertOver(signedDigest);
|
|
397
|
+
return {
|
|
398
|
+
passkeyPubkey: args.passkeyPubkey,
|
|
399
|
+
vault: args.vault,
|
|
400
|
+
clientDataJSON: assertion.clientDataJSON,
|
|
401
|
+
authenticatorData: assertion.authenticatorData,
|
|
402
|
+
signature: assertion.signature
|
|
403
|
+
// 64-byte compact lowS r||s
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function sha256(data) {
|
|
407
|
+
const subtle = globalThis.crypto?.subtle;
|
|
408
|
+
if (subtle) {
|
|
409
|
+
const buf = await subtle.digest("SHA-256", data);
|
|
410
|
+
return new Uint8Array(buf);
|
|
411
|
+
}
|
|
412
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
413
|
+
return new Uint8Array(createHash2("sha256").update(data).digest());
|
|
414
|
+
}
|
|
415
|
+
export {
|
|
416
|
+
connectTab,
|
|
417
|
+
decodeChallengeTo32Bytes,
|
|
418
|
+
verifyConnectProof
|
|
419
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexterai/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Canonical off-chain mirror of the dexter-vault Solana Anchor program — Solana instruction builders, byte-precise message encoders, account decoders, secp256r1/Ed25519 precompile helpers, counterfactual Swig derivation, and signer interfaces. The single source of truth for any TypeScript code that produces bytes the on-chain program will verify.",
|
|
5
5
|
"author": "Dexter",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"import": "./dist/tab/index.js",
|
|
77
77
|
"require": "./dist/tab/index.cjs"
|
|
78
78
|
},
|
|
79
|
+
"./connect": {
|
|
80
|
+
"types": "./dist/connect/index.d.ts",
|
|
81
|
+
"import": "./dist/connect/index.js",
|
|
82
|
+
"require": "./dist/connect/index.cjs"
|
|
83
|
+
},
|
|
79
84
|
"./idl/dexter_vault.json": "./dist/idl/dexter_vault.json"
|
|
80
85
|
},
|
|
81
86
|
"files": [
|