@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.
Files changed (95) hide show
  1. package/dist/admin.d.ts +38 -0
  2. package/dist/admin.d.ts.map +1 -0
  3. package/dist/admin.js +112 -0
  4. package/dist/admin.js.map +1 -0
  5. package/dist/authkey-session.d.ts +64 -0
  6. package/dist/authkey-session.d.ts.map +1 -0
  7. package/dist/authkey-session.js +164 -0
  8. package/dist/authkey-session.js.map +1 -0
  9. package/dist/bootstrap.d.ts +30 -0
  10. package/dist/bootstrap.d.ts.map +1 -0
  11. package/dist/bootstrap.js +60 -0
  12. package/dist/bootstrap.js.map +1 -0
  13. package/dist/bundler.d.ts +85 -0
  14. package/dist/bundler.d.ts.map +1 -0
  15. package/dist/bundler.js +160 -0
  16. package/dist/bundler.js.map +1 -0
  17. package/dist/delegate.d.ts +57 -0
  18. package/dist/delegate.d.ts.map +1 -0
  19. package/dist/delegate.js +111 -0
  20. package/dist/delegate.js.map +1 -0
  21. package/dist/frostVerify.d.ts +23 -0
  22. package/dist/frostVerify.d.ts.map +1 -0
  23. package/dist/frostVerify.js +69 -0
  24. package/dist/frostVerify.js.map +1 -0
  25. package/dist/index.d.ts +32 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +38 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jwks.d.ts +28 -0
  30. package/dist/jwks.d.ts.map +1 -0
  31. package/dist/jwks.js +81 -0
  32. package/dist/jwks.js.map +1 -0
  33. package/dist/jwt.d.ts +27 -0
  34. package/dist/jwt.d.ts.map +1 -0
  35. package/dist/jwt.js +50 -0
  36. package/dist/jwt.js.map +1 -0
  37. package/dist/keygen.d.ts +26 -0
  38. package/dist/keygen.d.ts.map +1 -0
  39. package/dist/keygen.js +60 -0
  40. package/dist/keygen.js.map +1 -0
  41. package/dist/oauth.d.ts +34 -0
  42. package/dist/oauth.d.ts.map +1 -0
  43. package/dist/oauth.js +119 -0
  44. package/dist/oauth.js.map +1 -0
  45. package/dist/request.d.ts +42 -0
  46. package/dist/request.d.ts.map +1 -0
  47. package/dist/request.js +115 -0
  48. package/dist/request.js.map +1 -0
  49. package/dist/scopedSign.d.ts +82 -0
  50. package/dist/scopedSign.d.ts.map +1 -0
  51. package/dist/scopedSign.js +130 -0
  52. package/dist/scopedSign.js.map +1 -0
  53. package/dist/server-prover.d.ts +29 -0
  54. package/dist/server-prover.d.ts.map +1 -0
  55. package/dist/server-prover.js +54 -0
  56. package/dist/server-prover.js.map +1 -0
  57. package/dist/session.d.ts +14 -0
  58. package/dist/session.d.ts.map +1 -0
  59. package/dist/session.js +29 -0
  60. package/dist/session.js.map +1 -0
  61. package/dist/types.d.ts +56 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +5 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/userop.d.ts +104 -0
  66. package/dist/userop.d.ts.map +1 -0
  67. package/dist/userop.js +212 -0
  68. package/dist/userop.js.map +1 -0
  69. package/dist/x402.d.ts +127 -0
  70. package/dist/x402.d.ts.map +1 -0
  71. package/dist/x402.js +167 -0
  72. package/dist/x402.js.map +1 -0
  73. package/package.json +64 -0
  74. package/src/admin.ts +178 -0
  75. package/src/authkey-session.ts +241 -0
  76. package/src/bootstrap.ts +106 -0
  77. package/src/bundler.ts +256 -0
  78. package/src/delegate.ts +163 -0
  79. package/src/frostVerify.ts +79 -0
  80. package/src/generate-inputs.ts +158 -0
  81. package/src/index.ts +43 -0
  82. package/src/jwks.ts +92 -0
  83. package/src/jwt.ts +74 -0
  84. package/src/keygen.ts +89 -0
  85. package/src/oauth.ts +157 -0
  86. package/src/partial-sha.ts +99 -0
  87. package/src/proof.ts +99 -0
  88. package/src/request.ts +174 -0
  89. package/src/scopedSign.ts +184 -0
  90. package/src/server-prover.ts +76 -0
  91. package/src/session.ts +33 -0
  92. package/src/types.ts +63 -0
  93. package/src/userop.ts +368 -0
  94. package/src/witness.ts +132 -0
  95. package/src/x402.ts +275 -0
package/src/jwks.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * JWKS (JSON Web Key Set) fetcher.
3
+ *
4
+ * Fetches RSA public keys from any OIDC-compliant issuer and extracts
5
+ * the modulus needed for the ZK proof witness. Supports Google, Clerk,
6
+ * and any issuer with a standard JWKS endpoint.
7
+ */
8
+
9
+ import type { JWKSKey } from "./types";
10
+
11
+ const GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
12
+
13
+ // Cache per issuer URI
14
+ const cache = new Map<string, { keys: JWKSKey[]; expiry: number }>();
15
+ const CACHE_TTL = 3600_000; // 1 hour
16
+
17
+ /**
18
+ * Derive the JWKS URI from a JWT issuer.
19
+ * Google uses a non-standard path; all other OIDC issuers use /.well-known/jwks.json.
20
+ */
21
+ function jwksUriForIssuer(issuer: string): string {
22
+ if (issuer === "https://accounts.google.com") {
23
+ return GOOGLE_JWKS_URI;
24
+ }
25
+ // Standard OIDC convention
26
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
27
+ return `${base}/.well-known/jwks.json`;
28
+ }
29
+
30
+ /**
31
+ * Fetch JWKS keys for an issuer.
32
+ */
33
+ export async function fetchJWKS(issuer?: string): Promise<JWKSKey[]> {
34
+ const uri = jwksUriForIssuer(issuer ?? "https://accounts.google.com");
35
+ const cached = cache.get(uri);
36
+ if (cached && Date.now() < cached.expiry) {
37
+ return cached.keys;
38
+ }
39
+
40
+ const res = await fetch(uri);
41
+ if (!res.ok) {
42
+ throw new Error(`Failed to fetch JWKS from ${uri}: ${res.status}`);
43
+ }
44
+
45
+ const data = await res.json();
46
+ const keys = data.keys as JWKSKey[];
47
+ cache.set(uri, { keys, expiry: Date.now() + CACHE_TTL });
48
+ return keys;
49
+ }
50
+
51
+ /** @deprecated Use fetchJWKS() instead */
52
+ export const fetchGoogleJWKS = () => fetchJWKS("https://accounts.google.com");
53
+
54
+ /**
55
+ * Find the RSA key matching a JWT's kid.
56
+ * If issuer is provided, fetches from that issuer's JWKS endpoint.
57
+ */
58
+ export async function getJWKSKeyForKid(kid: string, issuer?: string): Promise<JWKSKey> {
59
+ const keys = await fetchJWKS(issuer);
60
+ const key = keys.find((k) => k.kid === kid);
61
+ if (!key) {
62
+ throw new Error(`No JWKS key found for kid: ${kid}`);
63
+ }
64
+ if (key.kty !== "RSA") {
65
+ throw new Error(`Expected RSA key, got ${key.kty}`);
66
+ }
67
+ return key;
68
+ }
69
+
70
+ /**
71
+ * Decode a base64url-encoded JWKS modulus to a BigInt.
72
+ */
73
+ export function decodeModulus(base64url: string): bigint {
74
+ const binary = atob(base64url.replace(/-/g, "+").replace(/_/g, "/"));
75
+ let hex = "";
76
+ for (let i = 0; i < binary.length; i++) {
77
+ hex += binary.charCodeAt(i).toString(16).padStart(2, "0");
78
+ }
79
+ return BigInt("0x" + hex);
80
+ }
81
+
82
+ /**
83
+ * Decode a base64url-encoded JWKS modulus to raw bytes.
84
+ */
85
+ export function decodeModulusBytes(base64url: string): Uint8Array {
86
+ const binary = atob(base64url.replace(/-/g, "+").replace(/_/g, "/"));
87
+ const bytes = new Uint8Array(binary.length);
88
+ for (let i = 0; i < binary.length; i++) {
89
+ bytes[i] = binary.charCodeAt(i);
90
+ }
91
+ return bytes;
92
+ }
package/src/jwt.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * JWT parsing utilities for ZK proof witness construction.
3
+ *
4
+ * Extracts the components needed by the noir jwt_auth circuit:
5
+ * - Signed data (header.payload as raw bytes)
6
+ * - RSA signature (decoded from base64url)
7
+ * - Key ID (kid) from the header for JWKS lookup
8
+ */
9
+
10
+ import type { IdTokenClaims } from "./types";
11
+
12
+ /** Parsed JWT components needed for proof generation. */
13
+ export interface ParsedJWT {
14
+ /** Raw signed data: base64(header) + "." + base64(payload) as bytes */
15
+ signedData: Uint8Array;
16
+ /** Base64-decoded RSA signature bytes */
17
+ signatureBytes: Uint8Array;
18
+ /** Key ID from the JWT header — used to find the right JWKS key */
19
+ kid: string;
20
+ /** Decoded payload claims */
21
+ claims: IdTokenClaims;
22
+ /** Offset where base64-encoded payload starts (header length + 1 for the dot) */
23
+ base64DecodeOffset: number;
24
+ }
25
+
26
+ /**
27
+ * Parse a JWT into its components for ZK proof generation.
28
+ */
29
+ export function parseJWT(jwt: string): ParsedJWT {
30
+ const parts = jwt.split(".");
31
+ if (parts.length !== 3) {
32
+ throw new Error("Invalid JWT: expected 3 parts");
33
+ }
34
+
35
+ const [headerB64, payloadB64, signatureB64] = parts;
36
+
37
+ // Decode header to get kid
38
+ const header = JSON.parse(base64urlDecode(headerB64));
39
+ if (header.alg !== "RS256") {
40
+ throw new Error(`Unsupported JWT algorithm: ${header.alg}`);
41
+ }
42
+
43
+ // Signed data is the raw ASCII bytes of "header.payload"
44
+ const signedDataStr = `${headerB64}.${payloadB64}`;
45
+ const signedData = new TextEncoder().encode(signedDataStr);
46
+
47
+ // Decode the RSA signature
48
+ const signatureBytes = base64urlDecodeBytes(signatureB64);
49
+
50
+ // Decode claims
51
+ const claims = JSON.parse(base64urlDecode(payloadB64)) as IdTokenClaims;
52
+
53
+ return {
54
+ signedData,
55
+ signatureBytes,
56
+ kid: header.kid,
57
+ claims,
58
+ base64DecodeOffset: headerB64.length + 1,
59
+ };
60
+ }
61
+
62
+ function base64urlDecode(s: string): string {
63
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/");
64
+ return atob(padded);
65
+ }
66
+
67
+ function base64urlDecodeBytes(s: string): Uint8Array {
68
+ const decoded = base64urlDecode(s);
69
+ const bytes = new Uint8Array(decoded.length);
70
+ for (let i = 0; i < decoded.length; i++) {
71
+ bytes[i] = decoded.charCodeAt(i);
72
+ }
73
+ return bytes;
74
+ }
package/src/keygen.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Distributed key generation via bootstrap nodes.
3
+ *
4
+ * After auth, call keygen to create a key shard for the session identity.
5
+ * 409 (key already exists) is treated as success — the key was already created
6
+ * in a previous session.
7
+ */
8
+
9
+ import type { SessionKeypair, IdTokenClaims } from "./types";
10
+ import { signKeygenRequest } from "./request";
11
+
12
+ export interface KeygenConfig {
13
+ nodeUrls: string[];
14
+ groupId: string;
15
+ /** Proxy endpoint for CORS */
16
+ proxyEndpoint?: string;
17
+ }
18
+
19
+ export interface KeygenResult {
20
+ keyId: string;
21
+ ethereumAddress: string;
22
+ groupPublicKey: string;
23
+ alreadyExisted: boolean;
24
+ }
25
+
26
+ /**
27
+ * Trigger keygen on a bootstrap node.
28
+ * If the key already exists (409), returns success with alreadyExisted=true.
29
+ */
30
+ export async function keygen(
31
+ config: KeygenConfig,
32
+ keypair: SessionKeypair,
33
+ claims: IdTokenClaims,
34
+ keySuffix?: string,
35
+ identity?: string,
36
+ curve?: string,
37
+ scope?: string,
38
+ ): Promise<KeygenResult> {
39
+ const req = await signKeygenRequest(keypair, claims, config.groupId, keySuffix, identity);
40
+
41
+ // Try the first node (keygen only needs to be initiated on one node)
42
+ const nodeUrl = config.nodeUrls[0];
43
+ const url = config.proxyEndpoint
44
+ ? config.proxyEndpoint
45
+ : `${nodeUrl}/v1/keygen`;
46
+
47
+ const headers: Record<string, string> = {
48
+ "Content-Type": "application/json",
49
+ };
50
+ if (config.proxyEndpoint) {
51
+ headers["x-node-url"] = nodeUrl;
52
+ headers["x-node-path"] = "/v1/keygen";
53
+ }
54
+
55
+ // Add optional curve and scope to the request body
56
+ const body: Record<string, unknown> = { ...req };
57
+ if (curve) body.curve = curve;
58
+ if (scope) body.scope = scope;
59
+
60
+ const res = await fetch(url, {
61
+ method: "POST",
62
+ headers,
63
+ body: JSON.stringify(body),
64
+ });
65
+
66
+ if (res.status === 409) {
67
+ // Key already exists — node now returns full key info on 409
68
+ const data = await res.json();
69
+ return {
70
+ keyId: data.key_id ?? `${claims.iss}:${claims.sub}${keySuffix ? `:${keySuffix}` : ""}`,
71
+ ethereumAddress: data.ethereum_address ?? "",
72
+ groupPublicKey: data.public_key ?? "",
73
+ alreadyExisted: true,
74
+ };
75
+ }
76
+
77
+ if (!res.ok) {
78
+ const body = await res.text();
79
+ throw new Error(`Keygen failed: ${res.status} — ${body}`);
80
+ }
81
+
82
+ const data = await res.json();
83
+ return {
84
+ keyId: data.key_id,
85
+ ethereumAddress: data.ethereum_address,
86
+ groupPublicKey: data.public_key,
87
+ alreadyExisted: false,
88
+ };
89
+ }
package/src/oauth.ts ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Google OAuth PKCE flow.
3
+ *
4
+ * Framework-agnostic — uses browser APIs only (sessionStorage, crypto, location).
5
+ * The token exchange itself is delegated to a caller-provided endpoint
6
+ * so the client_secret stays server-side.
7
+ */
8
+
9
+ import type { IdTokenClaims } from "./types";
10
+
11
+ function base64urlEncode(buf: ArrayBuffer | Uint8Array): string {
12
+ return btoa(String.fromCharCode(...new Uint8Array(buf)))
13
+ .replace(/\+/g, "-")
14
+ .replace(/\//g, "_")
15
+ .replace(/=/g, "");
16
+ }
17
+
18
+ function generateVerifier(): string {
19
+ const buf = new Uint8Array(32);
20
+ crypto.getRandomValues(buf);
21
+ return base64urlEncode(buf);
22
+ }
23
+
24
+ async function generateChallenge(verifier: string): Promise<string> {
25
+ const data = new TextEncoder().encode(verifier);
26
+ const digest = await crypto.subtle.digest("SHA-256", data);
27
+ return base64urlEncode(digest);
28
+ }
29
+
30
+ export interface OAuthConfig {
31
+ clientId: string;
32
+ callbackPath?: string; // defaults to "/auth/callback"
33
+ }
34
+
35
+ /**
36
+ * Kick off the Google OAuth PKCE flow.
37
+ * Stores PKCE state in sessionStorage and redirects to Google.
38
+ */
39
+ export async function startGoogleOAuth(
40
+ config: OAuthConfig,
41
+ returnTo?: string
42
+ ): Promise<void> {
43
+ if (!config.clientId) {
44
+ throw new Error("Google Client ID not configured");
45
+ }
46
+
47
+ const verifier = generateVerifier();
48
+ const challenge = await generateChallenge(verifier);
49
+ const state = base64urlEncode(
50
+ crypto.getRandomValues(new Uint8Array(16))
51
+ );
52
+
53
+ sessionStorage.setItem("signet_oauth_verifier", verifier);
54
+ sessionStorage.setItem("signet_oauth_state", state);
55
+ sessionStorage.setItem(
56
+ "signet_oauth_return_to",
57
+ returnTo ?? window.location.pathname
58
+ );
59
+
60
+ const callbackPath = config.callbackPath ?? "/auth/callback";
61
+ const redirectUri = `${window.location.origin}${callbackPath}`;
62
+
63
+ const params = new URLSearchParams({
64
+ client_id: config.clientId,
65
+ redirect_uri: redirectUri,
66
+ response_type: "code",
67
+ scope: "openid profile email",
68
+ code_challenge: challenge,
69
+ code_challenge_method: "S256",
70
+ state,
71
+ });
72
+
73
+ window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
74
+ }
75
+
76
+ /**
77
+ * Handle the OAuth callback: validate state, exchange code for tokens.
78
+ *
79
+ * @param tokenEndpoint — URL of the server-side token exchange endpoint
80
+ * @returns The raw JWT id_token string, or throws on error.
81
+ */
82
+ export async function handleOAuthCallback(
83
+ tokenEndpoint: string
84
+ ): Promise<string> {
85
+ const params = new URLSearchParams(window.location.search);
86
+ const code = params.get("code");
87
+ const state = params.get("state");
88
+ const error = params.get("error");
89
+
90
+ if (error) {
91
+ throw new Error(
92
+ `Google OAuth error: ${error} — ${params.get("error_description") ?? ""}`
93
+ );
94
+ }
95
+
96
+ if (!code) {
97
+ throw new Error("No authorization code received");
98
+ }
99
+
100
+ const savedState = sessionStorage.getItem("signet_oauth_state");
101
+ if (state !== savedState) {
102
+ throw new Error("State mismatch — possible CSRF attack");
103
+ }
104
+
105
+ const verifier = sessionStorage.getItem("signet_oauth_verifier");
106
+ const callbackUri = window.location.origin + window.location.pathname;
107
+
108
+ // Clean up PKCE state
109
+ sessionStorage.removeItem("signet_oauth_state");
110
+ sessionStorage.removeItem("signet_oauth_verifier");
111
+
112
+ const res = await fetch(tokenEndpoint, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({
116
+ code,
117
+ code_verifier: verifier,
118
+ redirect_uri: callbackUri,
119
+ }),
120
+ });
121
+
122
+ const data = await res.json();
123
+
124
+ if (data.error) {
125
+ throw new Error(
126
+ `Token exchange failed: ${data.error} — ${data.error_description ?? ""}`
127
+ );
128
+ }
129
+
130
+ return data.id_token;
131
+ }
132
+
133
+ /**
134
+ * Decode a JWT payload without verification.
135
+ * Signature verification happens via ZK proof.
136
+ */
137
+ export function decodeIdToken(jwt: string): IdTokenClaims {
138
+ const payload = jwt.split(".")[1];
139
+ const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
140
+ const claims = JSON.parse(decoded);
141
+ // Preserve original aud/azp presence for ZK proof public input matching.
142
+ // If the JWT doesn't have aud or azp, keep them empty so the prover
143
+ // and verifier agree on the same (empty) public inputs.
144
+ if (claims.aud === undefined) claims.aud = "";
145
+ if (claims.azp === undefined) claims.azp = "";
146
+ if (claims.email === undefined) claims.email = "";
147
+ return claims;
148
+ }
149
+
150
+ /**
151
+ * Get the return-to path stored before the OAuth redirect.
152
+ */
153
+ export function getOAuthReturnTo(): string {
154
+ const returnTo = sessionStorage.getItem("signet_oauth_return_to") ?? "/";
155
+ sessionStorage.removeItem("signet_oauth_return_to");
156
+ return returnTo;
157
+ }
@@ -0,0 +1,99 @@
1
+ // Returns the intermediate SHA256 hash of the data
2
+ export async function generatePartialSHA256(data: Uint8Array, hashUntilIndex: number) {
3
+ if (typeof data === 'string') {
4
+ const encoder = new TextEncoder();
5
+ data = encoder.encode(data); // Convert string to Uint8Array
6
+ }
7
+
8
+ const blockSize = 64; // 512 bits
9
+ const blockIndex = Math.floor(hashUntilIndex / blockSize);
10
+ const H = new Uint32Array([
11
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
12
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
13
+ ]);
14
+
15
+ for (let i = 0; i < blockIndex; i++) {
16
+ if (i * blockSize >= data.length) {
17
+ throw new Error('Block index out of range.');
18
+ }
19
+
20
+ const block = new Uint8Array(blockSize);
21
+ block.set(data.slice(i * blockSize, (i + 1) * blockSize));
22
+ sha256Block(H, block);
23
+ }
24
+
25
+ // Get the intermediate digest (this is **not** the final hash)
26
+ return {
27
+ partialHash: H,
28
+ remainingData: data.slice(blockIndex * blockSize)
29
+ }
30
+ }
31
+
32
+ /**
33
+ * SHA-256 constants (first 32 bits of fractional parts of cube roots of primes)
34
+ */
35
+ const K = [
36
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
37
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
38
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
39
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
40
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
41
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
42
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
43
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
44
+ ];
45
+
46
+ /**
47
+ * Rotate right function (SHA-256 bitwise operations)
48
+ */
49
+ function rotr(n: number, x: number) {
50
+ return (x >>> n) | (x << (32 - n));
51
+ }
52
+
53
+ /**
54
+ * SHA-256 Compression Function (Processes 64-byte blocks)
55
+ */
56
+ function sha256Block(H: Uint32Array, block: Uint8Array) {
57
+ let w = new Uint32Array(64);
58
+ let a = H[0], b = H[1], c = H[2], d = H[3];
59
+ let e = H[4], f = H[5], g = H[6], h = H[7];
60
+
61
+ // Convert block into 32-bit words
62
+ for (let i = 0; i < 16; i++) {
63
+ w[i] = (block[i * 4] << 24) | (block[i * 4 + 1] << 16) | (block[i * 4 + 2] << 8) | block[i * 4 + 3];
64
+ }
65
+ for (let i = 16; i < 64; i++) {
66
+ const s0 = rotr(7, w[i - 15]) ^ rotr(18, w[i - 15]) ^ (w[i - 15] >>> 3);
67
+ const s1 = rotr(17, w[i - 2]) ^ rotr(19, w[i - 2]) ^ (w[i - 2] >>> 10);
68
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
69
+ }
70
+
71
+ // Main compression loop
72
+ for (let i = 0; i < 64; i++) {
73
+ const S1 = rotr(6, e) ^ rotr(11, e) ^ rotr(25, e);
74
+ const ch = (e & f) ^ (~e & g);
75
+ const temp1 = (h + S1 + ch + K[i] + w[i]) >>> 0;
76
+ const S0 = rotr(2, a) ^ rotr(13, a) ^ rotr(22, a);
77
+ const maj = (a & b) ^ (a & c) ^ (b & c);
78
+ const temp2 = (S0 + maj) >>> 0;
79
+
80
+ h = g;
81
+ g = f;
82
+ f = e;
83
+ e = (d + temp1) >>> 0;
84
+ d = c;
85
+ c = b;
86
+ b = a;
87
+ a = (temp1 + temp2) >>> 0;
88
+ }
89
+
90
+ // Update intermediate hash values
91
+ H[0] = (H[0] + a) >>> 0;
92
+ H[1] = (H[1] + b) >>> 0;
93
+ H[2] = (H[2] + c) >>> 0;
94
+ H[3] = (H[3] + d) >>> 0;
95
+ H[4] = (H[4] + e) >>> 0;
96
+ H[5] = (H[5] + f) >>> 0;
97
+ H[6] = (H[6] + g) >>> 0;
98
+ H[7] = (H[7] + h) >>> 0;
99
+ }
package/src/proof.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Client-side ZK proof generation for JWT authentication.
3
+ *
4
+ * Runs entirely in the browser:
5
+ * 1. Parse JWT + fetch JWKS → build circuit witness
6
+ * 2. @noir-lang/noir_js → generate ACIR witness from compiled circuit
7
+ * 3. @aztec/bb.js → generate UltraHonk proof via WASM
8
+ *
9
+ * Expected time: ~2-7 seconds in a modern browser.
10
+ */
11
+
12
+ import { Noir } from "@noir-lang/noir_js";
13
+ import { UltraHonkBackend } from "@aztec/bb.js";
14
+ import { jwt as jwtArtifacts, assertBbJsVersion } from "@oleary-labs/signet-circuits";
15
+ import { decodeIdToken } from "./oauth";
16
+ import { getJWKSKeyForKid, decodeModulusBytes } from "./jwks";
17
+ import { buildFullWitness } from "./witness";
18
+ import { hexToBytes } from "./session";
19
+ import type { IdTokenClaims } from "./types";
20
+
21
+ /** Proof generation result. */
22
+ export interface ProofResult {
23
+ proof: Uint8Array;
24
+ publicInputs: string[];
25
+ claims: IdTokenClaims;
26
+ }
27
+
28
+ // Circuit artifact from @signet/circuits — embedded at build time.
29
+ const circuit = jwtArtifacts.circuit;
30
+
31
+ /**
32
+ * Generate a ZK proof that a JWT is valid — entirely client-side.
33
+ *
34
+ * @param jwt — raw JWT string (header.payload.signature)
35
+ * @param sessionPubHex — 33-byte compressed secp256k1 session public key (hex)
36
+ * @returns proof bytes, public inputs, and decoded claims
37
+ */
38
+ export async function generateJWTProof(
39
+ jwt: string,
40
+ sessionPubHex: string
41
+ ): Promise<ProofResult> {
42
+ // 1. Parse JWT and decode claims
43
+ const parts = jwt.split(".");
44
+ const headerB64 = parts[0];
45
+ const header = JSON.parse(
46
+ atob(headerB64.replace(/-/g, "+").replace(/_/g, "/"))
47
+ );
48
+ const claims = decodeIdToken(jwt);
49
+
50
+ // 2. Fetch the RSA key from the issuer's JWKS
51
+ const jwksKey = await getJWKSKeyForKid(header.kid, claims.iss);
52
+ const jsonWebKey: JsonWebKey = {
53
+ kty: jwksKey.kty,
54
+ n: jwksKey.n,
55
+ e: jwksKey.e,
56
+ alg: jwksKey.alg,
57
+ };
58
+
59
+ // 3. Build full circuit witness
60
+ const sessionPubBytes = Array.from(hexToBytes(sessionPubHex));
61
+ const witness = await buildFullWitness(jwt, jsonWebKey, claims, sessionPubBytes);
62
+
63
+ // 4. Version check — fail fast if bb.js doesn't match the circuit artifacts.
64
+ await assertBbJsVersion();
65
+
66
+ // 5. Generate ACIR witness from compiled circuit
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ const noir = new Noir(circuit as any);
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const { witness: acirWitness } = await noir.execute(witness as any);
71
+
72
+ // 6. Generate UltraHonk proof via bb.js WASM
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ const backend = new UltraHonkBackend((circuit as any).bytecode);
75
+ const proofData = await backend.generateProof(acirWitness);
76
+
77
+ await backend.destroy();
78
+
79
+ return {
80
+ proof: proofData.proof,
81
+ publicInputs: proofData.publicInputs,
82
+ claims,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Get the RSA modulus bytes for a JWT (for the node auth request).
88
+ */
89
+ export async function getJWTModulusBytes(jwt: string): Promise<Uint8Array> {
90
+ const parts = jwt.split(".");
91
+ const header = JSON.parse(
92
+ atob(parts[0].replace(/-/g, "+").replace(/_/g, "/"))
93
+ );
94
+ const claims = JSON.parse(
95
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
96
+ );
97
+ const jwksKey = await getJWKSKeyForKid(header.kid, claims.iss);
98
+ return decodeModulusBytes(jwksKey.n);
99
+ }