@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/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
|
+
}
|