@oleary-labs/signet-sdk 0.1.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/admin.d.ts +38 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +112 -0
- package/dist/admin.js.map +1 -0
- package/dist/authkey-session.d.ts +64 -0
- package/dist/authkey-session.d.ts.map +1 -0
- package/dist/authkey-session.js +164 -0
- package/dist/authkey-session.js.map +1 -0
- package/dist/bootstrap.d.ts +30 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +60 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundler.d.ts +85 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +160 -0
- package/dist/bundler.js.map +1 -0
- package/dist/delegate.d.ts +57 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +111 -0
- package/dist/delegate.js.map +1 -0
- package/dist/frostVerify.d.ts +23 -0
- package/dist/frostVerify.d.ts.map +1 -0
- package/dist/frostVerify.js +69 -0
- package/dist/frostVerify.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/jwks.d.ts +28 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +81 -0
- package/dist/jwks.js.map +1 -0
- package/dist/jwt.d.ts +27 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +50 -0
- package/dist/jwt.js.map +1 -0
- package/dist/keygen.d.ts +26 -0
- package/dist/keygen.d.ts.map +1 -0
- package/dist/keygen.js +60 -0
- package/dist/keygen.js.map +1 -0
- package/dist/oauth.d.ts +34 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +119 -0
- package/dist/oauth.js.map +1 -0
- package/dist/request.d.ts +42 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +115 -0
- package/dist/request.js.map +1 -0
- package/dist/scopedSign.d.ts +82 -0
- package/dist/scopedSign.d.ts.map +1 -0
- package/dist/scopedSign.js +130 -0
- package/dist/scopedSign.js.map +1 -0
- package/dist/server-prover.d.ts +29 -0
- package/dist/server-prover.d.ts.map +1 -0
- package/dist/server-prover.js +54 -0
- package/dist/server-prover.js.map +1 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/userop.d.ts +104 -0
- package/dist/userop.d.ts.map +1 -0
- package/dist/userop.js +212 -0
- package/dist/userop.js.map +1 -0
- package/dist/x402.d.ts +127 -0
- package/dist/x402.d.ts.map +1 -0
- package/dist/x402.js +167 -0
- package/dist/x402.js.map +1 -0
- package/package.json +64 -0
- package/src/admin.ts +178 -0
- package/src/authkey-session.ts +241 -0
- package/src/bootstrap.ts +106 -0
- package/src/bundler.ts +256 -0
- package/src/delegate.ts +163 -0
- package/src/frostVerify.ts +79 -0
- package/src/generate-inputs.ts +158 -0
- package/src/index.ts +43 -0
- package/src/jwks.ts +92 -0
- package/src/jwt.ts +74 -0
- package/src/keygen.ts +89 -0
- package/src/oauth.ts +157 -0
- package/src/partial-sha.ts +99 -0
- package/src/proof.ts +99 -0
- package/src/request.ts +174 -0
- package/src/scopedSign.ts +184 -0
- package/src/server-prover.ts +76 -0
- package/src/session.ts +33 -0
- package/src/types.ts +63 -0
- package/src/userop.ts +368 -0
- package/src/witness.ts +132 -0
- package/src/x402.ts +275 -0
package/src/userop.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signet UserOperation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic orchestration of the full ERC-4337 flow:
|
|
5
|
+
* build → (paymaster stub) → estimate → (paymaster real) → hash → FROST sign → submit → confirm
|
|
6
|
+
*
|
|
7
|
+
* The caller provides an already-encoded callData (the app-specific part);
|
|
8
|
+
* everything else is generic Signet protocol logic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type Address,
|
|
13
|
+
type Hex,
|
|
14
|
+
encodeFunctionData,
|
|
15
|
+
encodeAbiParameters,
|
|
16
|
+
keccak256,
|
|
17
|
+
concat,
|
|
18
|
+
createPublicClient,
|
|
19
|
+
http,
|
|
20
|
+
} from "viem";
|
|
21
|
+
import type { SessionKeypair, IdTokenClaims } from "./types";
|
|
22
|
+
import { signSignRequest } from "./request";
|
|
23
|
+
import {
|
|
24
|
+
sendUserOp as bundlerSendUserOp,
|
|
25
|
+
getUserOpReceipt as bundlerGetUserOpReceipt,
|
|
26
|
+
estimateUserOpGas,
|
|
27
|
+
getPaymasterStubData,
|
|
28
|
+
getPaymasterData,
|
|
29
|
+
applyPaymasterSponsorship,
|
|
30
|
+
type PaymasterContext,
|
|
31
|
+
} from "./bundler";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Types
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ERC-4337 PackedUserOperation.
|
|
39
|
+
*
|
|
40
|
+
* This is the format expected by the EntryPoint and validated
|
|
41
|
+
* by SignetAccount.validateUserOp. The signature field carries
|
|
42
|
+
* a 65-byte FROST Schnorr signature (Rx || z || v).
|
|
43
|
+
*/
|
|
44
|
+
export interface PackedUserOperation {
|
|
45
|
+
sender: Address;
|
|
46
|
+
nonce: bigint;
|
|
47
|
+
initCode: Hex;
|
|
48
|
+
callData: Hex;
|
|
49
|
+
accountGasLimits: Hex;
|
|
50
|
+
preVerificationGas: bigint;
|
|
51
|
+
gasFees: Hex;
|
|
52
|
+
paymasterAndData: Hex;
|
|
53
|
+
signature: Hex;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type UserOpStatus =
|
|
57
|
+
| "building"
|
|
58
|
+
| "sponsoring-stub"
|
|
59
|
+
| "estimating"
|
|
60
|
+
| "sponsoring"
|
|
61
|
+
| "signing"
|
|
62
|
+
| "submitting"
|
|
63
|
+
| "confirming";
|
|
64
|
+
|
|
65
|
+
export interface SignetWriteConfig {
|
|
66
|
+
rpcUrl: string;
|
|
67
|
+
chainId: number;
|
|
68
|
+
entryPointAddress: Address;
|
|
69
|
+
bundlerProxyUrl: string;
|
|
70
|
+
nodeProxyUrl: string;
|
|
71
|
+
bootstrapGroup: Address;
|
|
72
|
+
bootstrapNodes: string[];
|
|
73
|
+
accountFactoryAddress: Address;
|
|
74
|
+
accountFactoryAbi: readonly Record<string, unknown>[];
|
|
75
|
+
usePaymaster: boolean;
|
|
76
|
+
paymasterContext?: PaymasterContext;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SignetWriteParams {
|
|
80
|
+
account: Address;
|
|
81
|
+
groupPublicKey: Hex;
|
|
82
|
+
dest: Address;
|
|
83
|
+
value?: bigint;
|
|
84
|
+
callData: Hex;
|
|
85
|
+
sessionKeypair: SessionKeypair;
|
|
86
|
+
claims: IdTokenClaims;
|
|
87
|
+
onStatus?: (status: UserOpStatus) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SignetWriteResult {
|
|
91
|
+
userOpHash: Hex;
|
|
92
|
+
transactionHash: Hex;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Main pipeline
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Submit a UserOperation through the full Signet pipeline.
|
|
101
|
+
*
|
|
102
|
+
* Ordering is strict — see CLAUDE.md "Write flow ordering" for rationale:
|
|
103
|
+
* 1. Build unsigned UserOp (with initCode if account not deployed)
|
|
104
|
+
* 2. (if paymaster) Attach stub paymasterAndData for gas estimation
|
|
105
|
+
* 3. Estimate gas via bundler
|
|
106
|
+
* 4. (if paymaster) Replace stub with real signed paymaster blob
|
|
107
|
+
* 5. Compute UserOp hash
|
|
108
|
+
* 6. FROST threshold sign via bootstrap group
|
|
109
|
+
* 7. Submit to bundler
|
|
110
|
+
* 8. Poll for receipt
|
|
111
|
+
*/
|
|
112
|
+
export async function submitUserOp(
|
|
113
|
+
config: SignetWriteConfig,
|
|
114
|
+
params: SignetWriteParams
|
|
115
|
+
): Promise<SignetWriteResult> {
|
|
116
|
+
const { onStatus } = params;
|
|
117
|
+
|
|
118
|
+
// 1. Build the UserOperation
|
|
119
|
+
onStatus?.("building");
|
|
120
|
+
const nonce = await fetchNonce(config.rpcUrl, config.entryPointAddress, params.account);
|
|
121
|
+
const initCode = await buildInitCode(config, params.account, params.groupPublicKey);
|
|
122
|
+
|
|
123
|
+
let userOp = buildUserOp({
|
|
124
|
+
sender: params.account,
|
|
125
|
+
nonce,
|
|
126
|
+
initCode,
|
|
127
|
+
dest: params.dest,
|
|
128
|
+
value: params.value,
|
|
129
|
+
callData: params.callData,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 2. (optional) Attach paymaster stub before gas estimation
|
|
133
|
+
if (config.usePaymaster) {
|
|
134
|
+
onStatus?.("sponsoring-stub");
|
|
135
|
+
const stub = await getPaymasterStubData(
|
|
136
|
+
config.bundlerProxyUrl, config.entryPointAddress, config.chainId, userOp,
|
|
137
|
+
config.paymasterContext,
|
|
138
|
+
);
|
|
139
|
+
userOp = applyPaymasterSponsorship(userOp, stub);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 3. Estimate gas
|
|
143
|
+
onStatus?.("estimating");
|
|
144
|
+
const gasEstimate = await estimateUserOpGas(
|
|
145
|
+
config.bundlerProxyUrl, config.entryPointAddress, userOp
|
|
146
|
+
);
|
|
147
|
+
userOp.accountGasLimits =
|
|
148
|
+
`0x${BigInt(gasEstimate.verificationGasLimit).toString(16).padStart(32, "0")}${BigInt(gasEstimate.callGasLimit).toString(16).padStart(32, "0")}` as Hex;
|
|
149
|
+
userOp.preVerificationGas = BigInt(gasEstimate.preVerificationGas);
|
|
150
|
+
|
|
151
|
+
// 4. (optional) Replace stub with real signed paymaster blob
|
|
152
|
+
if (config.usePaymaster) {
|
|
153
|
+
onStatus?.("sponsoring");
|
|
154
|
+
const real = await getPaymasterData(
|
|
155
|
+
config.bundlerProxyUrl, config.entryPointAddress, config.chainId, userOp,
|
|
156
|
+
config.paymasterContext,
|
|
157
|
+
);
|
|
158
|
+
userOp = applyPaymasterSponsorship(userOp, real);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 5. Hash + 6. FROST threshold sign
|
|
162
|
+
onStatus?.("signing");
|
|
163
|
+
const opHash = getUserOpHash(userOp, config.entryPointAddress, config.chainId);
|
|
164
|
+
const messageHash = hexToBytes(opHash);
|
|
165
|
+
|
|
166
|
+
const signReq = await signSignRequest(
|
|
167
|
+
params.sessionKeypair,
|
|
168
|
+
params.claims,
|
|
169
|
+
config.bootstrapGroup,
|
|
170
|
+
messageHash,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const signRes = await fetch(config.nodeProxyUrl, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
"x-node-url": config.bootstrapNodes[0],
|
|
178
|
+
"x-node-path": "/v1/sign",
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify(signReq),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!signRes.ok) {
|
|
184
|
+
const body = await signRes.text();
|
|
185
|
+
throw new Error(`Threshold signing failed: ${signRes.status} — ${body}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { ethereum_signature } = await signRes.json();
|
|
189
|
+
userOp.signature = ethereum_signature as Hex;
|
|
190
|
+
|
|
191
|
+
// 7. Submit to bundler
|
|
192
|
+
onStatus?.("submitting");
|
|
193
|
+
const { userOpHash } = await bundlerSendUserOp(
|
|
194
|
+
config.bundlerProxyUrl, config.entryPointAddress, userOp
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// 8. Poll for receipt
|
|
198
|
+
onStatus?.("confirming");
|
|
199
|
+
let receipt = null;
|
|
200
|
+
for (let i = 0; i < 60; i++) {
|
|
201
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
202
|
+
receipt = await bundlerGetUserOpReceipt(config.bundlerProxyUrl, userOpHash);
|
|
203
|
+
if (receipt) break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!receipt) throw new Error("Transaction confirmation timed out");
|
|
207
|
+
if (!receipt.success) throw new Error("UserOperation reverted on-chain");
|
|
208
|
+
|
|
209
|
+
return { userOpHash, transactionHash: receipt.transactionHash };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Utilities
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build an unsigned UserOperation for a SignetAccount.execute call.
|
|
218
|
+
*/
|
|
219
|
+
export function buildUserOp(params: {
|
|
220
|
+
sender: Address;
|
|
221
|
+
nonce: bigint;
|
|
222
|
+
initCode?: Hex;
|
|
223
|
+
dest: Address;
|
|
224
|
+
value?: bigint;
|
|
225
|
+
callData: Hex;
|
|
226
|
+
}): PackedUserOperation {
|
|
227
|
+
const executeCallData = encodeFunctionData({
|
|
228
|
+
abi: [
|
|
229
|
+
{
|
|
230
|
+
name: "execute",
|
|
231
|
+
type: "function",
|
|
232
|
+
inputs: [
|
|
233
|
+
{ name: "dest", type: "address" },
|
|
234
|
+
{ name: "value", type: "uint256" },
|
|
235
|
+
{ name: "data", type: "bytes" },
|
|
236
|
+
],
|
|
237
|
+
outputs: [],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
functionName: "execute",
|
|
241
|
+
args: [params.dest, params.value ?? 0n, params.callData],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
sender: params.sender,
|
|
246
|
+
nonce: params.nonce,
|
|
247
|
+
initCode: params.initCode ?? "0x",
|
|
248
|
+
callData: executeCallData,
|
|
249
|
+
accountGasLimits: "0x000000000000000000000000000f4240000000000000000000000000001e8480",
|
|
250
|
+
preVerificationGas: 50000n,
|
|
251
|
+
gasFees: "0x000000000000000000000000000000010000000000000000000000003b9aca00",
|
|
252
|
+
paymasterAndData: "0x",
|
|
253
|
+
signature: "0x",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Compute the UserOperation hash for signing.
|
|
259
|
+
*
|
|
260
|
+
* Matches EntryPoint v0.7 packed format:
|
|
261
|
+
* keccak256(abi.encode(keccak256(packedFields), entryPoint, chainId))
|
|
262
|
+
*/
|
|
263
|
+
export function getUserOpHash(
|
|
264
|
+
userOp: PackedUserOperation,
|
|
265
|
+
entryPoint: Address,
|
|
266
|
+
chainId: number
|
|
267
|
+
): Hex {
|
|
268
|
+
const packedHash = keccak256(
|
|
269
|
+
encodeAbiParameters(
|
|
270
|
+
[
|
|
271
|
+
{ type: "address" },
|
|
272
|
+
{ type: "uint256" },
|
|
273
|
+
{ type: "bytes32" },
|
|
274
|
+
{ type: "bytes32" },
|
|
275
|
+
{ type: "bytes32" },
|
|
276
|
+
{ type: "uint256" },
|
|
277
|
+
{ type: "bytes32" },
|
|
278
|
+
{ type: "bytes32" },
|
|
279
|
+
],
|
|
280
|
+
[
|
|
281
|
+
userOp.sender,
|
|
282
|
+
userOp.nonce,
|
|
283
|
+
keccak256(userOp.initCode),
|
|
284
|
+
keccak256(userOp.callData),
|
|
285
|
+
userOp.accountGasLimits as Hex,
|
|
286
|
+
userOp.preVerificationGas,
|
|
287
|
+
userOp.gasFees as Hex,
|
|
288
|
+
keccak256(userOp.paymasterAndData),
|
|
289
|
+
]
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return keccak256(
|
|
294
|
+
encodeAbiParameters(
|
|
295
|
+
[{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
|
|
296
|
+
[packedHash, entryPoint, BigInt(chainId)]
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Fetch the current nonce for an account from the EntryPoint.
|
|
303
|
+
*/
|
|
304
|
+
export async function fetchNonce(
|
|
305
|
+
rpcUrl: string,
|
|
306
|
+
entryPointAddress: Address,
|
|
307
|
+
account: Address
|
|
308
|
+
): Promise<bigint> {
|
|
309
|
+
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
310
|
+
return client.readContract({
|
|
311
|
+
address: entryPointAddress,
|
|
312
|
+
abi: [{
|
|
313
|
+
name: "getNonce",
|
|
314
|
+
type: "function",
|
|
315
|
+
inputs: [
|
|
316
|
+
{ name: "sender", type: "address" },
|
|
317
|
+
{ name: "key", type: "uint192" },
|
|
318
|
+
],
|
|
319
|
+
outputs: [{ type: "uint256" }],
|
|
320
|
+
stateMutability: "view",
|
|
321
|
+
}],
|
|
322
|
+
functionName: "getNonce",
|
|
323
|
+
args: [account, 0n],
|
|
324
|
+
}) as Promise<bigint>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if a SignetAccount is deployed at the given address.
|
|
329
|
+
*/
|
|
330
|
+
export async function isAccountDeployed(
|
|
331
|
+
rpcUrl: string,
|
|
332
|
+
account: Address
|
|
333
|
+
): Promise<boolean> {
|
|
334
|
+
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
335
|
+
const code = await client.getCode({ address: account });
|
|
336
|
+
return !!code && code !== "0x";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Build initCode for deploying a SignetAccount via the account factory.
|
|
341
|
+
* Returns "0x" if the account is already deployed.
|
|
342
|
+
*/
|
|
343
|
+
export async function buildInitCode(
|
|
344
|
+
config: Pick<SignetWriteConfig, "rpcUrl" | "accountFactoryAddress" | "accountFactoryAbi" | "entryPointAddress">,
|
|
345
|
+
account: Address,
|
|
346
|
+
groupPublicKey: Hex
|
|
347
|
+
): Promise<Hex> {
|
|
348
|
+
const deployed = await isAccountDeployed(config.rpcUrl, account);
|
|
349
|
+
if (deployed) return "0x";
|
|
350
|
+
|
|
351
|
+
const factoryCallData = encodeFunctionData({
|
|
352
|
+
abi: config.accountFactoryAbi,
|
|
353
|
+
functionName: "createAccount",
|
|
354
|
+
args: [config.entryPointAddress, groupPublicKey, 0n],
|
|
355
|
+
} as Parameters<typeof encodeFunctionData>[0]);
|
|
356
|
+
|
|
357
|
+
return concat([config.accountFactoryAddress, factoryCallData]);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Internal helpers
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
function hexToBytes(hex: Hex): Uint8Array {
|
|
365
|
+
return new Uint8Array(
|
|
366
|
+
(hex.slice(2).match(/.{2}/g) ?? []).map((b) => parseInt(b, 16))
|
|
367
|
+
);
|
|
368
|
+
}
|
package/src/witness.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZK proof witness construction for the jwt_auth noir circuit.
|
|
3
|
+
*
|
|
4
|
+
* Uses generateInputs from noir-jwt for the core RSA/JWT witness,
|
|
5
|
+
* then adds the claim assertions and session_pub binding that our
|
|
6
|
+
* circuit requires.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { generateInputs } from "./generate-inputs";
|
|
10
|
+
import type { IdTokenClaims } from "./types";
|
|
11
|
+
|
|
12
|
+
/** Full witness for the jwt_auth circuit (Prover.toml format). */
|
|
13
|
+
export interface FullCircuitWitness {
|
|
14
|
+
// From generateInputs (RSA + JWT data)
|
|
15
|
+
data: { storage: number[]; len: number };
|
|
16
|
+
base64_decode_offset: number;
|
|
17
|
+
pubkey_modulus_limbs: string[];
|
|
18
|
+
redc_params_limbs: string[];
|
|
19
|
+
signature_limbs: string[];
|
|
20
|
+
|
|
21
|
+
// Claim assertions (public inputs)
|
|
22
|
+
expected_iss: { storage: number[]; len: number };
|
|
23
|
+
expected_sub: { storage: number[]; len: number };
|
|
24
|
+
expected_exp: number;
|
|
25
|
+
expected_aud: { storage: number[]; len: number };
|
|
26
|
+
expected_azp: { storage: number[]; len: number };
|
|
27
|
+
|
|
28
|
+
// Session binding (public input)
|
|
29
|
+
_session_pub: number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build full circuit witness from a JWT, JWKS key, and session public key.
|
|
34
|
+
*
|
|
35
|
+
* @param jwt — raw JWT string
|
|
36
|
+
* @param jwksKey — the RSA public key from Google JWKS (as JsonWebKey)
|
|
37
|
+
* @param claims — decoded JWT claims
|
|
38
|
+
* @param sessionPubBytes — 33-byte compressed secp256k1 session public key
|
|
39
|
+
*/
|
|
40
|
+
export async function buildFullWitness(
|
|
41
|
+
jwt: string,
|
|
42
|
+
jwksKey: JsonWebKey,
|
|
43
|
+
claims: IdTokenClaims,
|
|
44
|
+
sessionPubBytes: number[]
|
|
45
|
+
): Promise<FullCircuitWitness> {
|
|
46
|
+
// Generate core JWT/RSA inputs using noir-jwt library
|
|
47
|
+
const inputs = await generateInputs({
|
|
48
|
+
jwt,
|
|
49
|
+
pubkey: jwksKey,
|
|
50
|
+
maxSignedDataLength: 1024,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!inputs.data) {
|
|
54
|
+
throw new Error("Expected full data mode (no partial SHA)");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
// Core JWT/RSA witness
|
|
59
|
+
data: inputs.data,
|
|
60
|
+
base64_decode_offset: inputs.base64_decode_offset,
|
|
61
|
+
pubkey_modulus_limbs: inputs.pubkey_modulus_limbs,
|
|
62
|
+
redc_params_limbs: inputs.redc_params_limbs,
|
|
63
|
+
signature_limbs: inputs.signature_limbs,
|
|
64
|
+
|
|
65
|
+
// Claim assertions
|
|
66
|
+
expected_iss: toBoundedVec(claims.iss, 128),
|
|
67
|
+
expected_sub: toBoundedVec(claims.sub, 128),
|
|
68
|
+
expected_exp: claims.exp,
|
|
69
|
+
expected_aud: toBoundedVec(claims.aud, 128),
|
|
70
|
+
expected_azp: toBoundedVec(claims.azp, 128),
|
|
71
|
+
|
|
72
|
+
// Session binding
|
|
73
|
+
_session_pub: sessionPubBytes,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Serialize a FullCircuitWitness to Prover.toml format for nargo.
|
|
79
|
+
*/
|
|
80
|
+
export function witnessToProverToml(w: FullCircuitWitness): string {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
|
|
83
|
+
// Bare keys must come before [table] sections in TOML
|
|
84
|
+
lines.push(`base64_decode_offset = ${w.base64_decode_offset}`);
|
|
85
|
+
lines.push(`expected_exp = ${w.expected_exp}`);
|
|
86
|
+
lines.push(
|
|
87
|
+
`redc_params_limbs = [${w.redc_params_limbs.map((l) => `"${l}"`).join(", ")}]`
|
|
88
|
+
);
|
|
89
|
+
lines.push(
|
|
90
|
+
`signature_limbs = [${w.signature_limbs.map((l) => `"${l}"`).join(", ")}]`
|
|
91
|
+
);
|
|
92
|
+
lines.push(
|
|
93
|
+
`pubkey_modulus_limbs = [${w.pubkey_modulus_limbs.map((l) => `"${l}"`).join(", ")}]`
|
|
94
|
+
);
|
|
95
|
+
lines.push(`_session_pub = [${w._session_pub.join(", ")}]`);
|
|
96
|
+
lines.push("");
|
|
97
|
+
|
|
98
|
+
// BoundedVec tables
|
|
99
|
+
lines.push("[data]");
|
|
100
|
+
lines.push(`storage = [${w.data.storage.join(", ")}]`);
|
|
101
|
+
lines.push(`len = ${w.data.len}`);
|
|
102
|
+
lines.push("");
|
|
103
|
+
|
|
104
|
+
writeBoundedVecToml(lines, "expected_iss", w.expected_iss);
|
|
105
|
+
writeBoundedVecToml(lines, "expected_sub", w.expected_sub);
|
|
106
|
+
writeBoundedVecToml(lines, "expected_aud", w.expected_aud);
|
|
107
|
+
writeBoundedVecToml(lines, "expected_azp", w.expected_azp);
|
|
108
|
+
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toBoundedVec(
|
|
113
|
+
value: string,
|
|
114
|
+
maxLen: number
|
|
115
|
+
): { storage: number[]; len: number } {
|
|
116
|
+
const storage = new Array(maxLen).fill(0);
|
|
117
|
+
for (let i = 0; i < value.length; i++) {
|
|
118
|
+
storage[i] = value.charCodeAt(i);
|
|
119
|
+
}
|
|
120
|
+
return { storage, len: value.length };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writeBoundedVecToml(
|
|
124
|
+
lines: string[],
|
|
125
|
+
name: string,
|
|
126
|
+
vec: { storage: number[]; len: number }
|
|
127
|
+
): void {
|
|
128
|
+
lines.push(`[${name}]`);
|
|
129
|
+
lines.push(`storage = [${vec.storage.join(", ")}]`);
|
|
130
|
+
lines.push(`len = ${vec.len}`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|