diff --git a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts index 96eb2f096..dd0ed24ea 100644 --- a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts +++ b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts @@ -7,6 +7,7 @@ export const ALLOWED_APIS = { sign_in: ["signin"], sign_up: ["keygen"], sign_in_reshare: ["signin", "reshare"], + sign_in_reshare_ed25519: ["signin", "reshare", "keygen_ed25519"], add_ed25519: ["keygen_ed25519"], }; @@ -14,6 +15,7 @@ export const FINAL_APIS = { sign_in: ["signin"], sign_up: ["keygen"], sign_in_reshare: ["reshare"], + sign_in_reshare_ed25519: ["keygen_ed25519"], add_ed25519: ["keygen_ed25519"], }; diff --git a/backend/oko_api/server/src/middleware/commit_reveal.test.ts b/backend/oko_api/server/src/middleware/commit_reveal.test.ts index fe900c95c..34b35beed 100644 --- a/backend/oko_api/server/src/middleware/commit_reveal.test.ts +++ b/backend/oko_api/server/src/middleware/commit_reveal.test.ts @@ -70,20 +70,12 @@ describe("commit_reveal_middleware_test", () => { app.use(express.json()); // Test routes with middleware - app.post( - "/test/keygen", - commitRevealMiddleware("keygen"), - (_req, res) => { - res.status(200).json({ success: true, data: { message: "keygen ok" } }); - }, - ); - app.post( - "/test/signin", - commitRevealMiddleware("signin"), - (_req, res) => { - res.status(200).json({ success: true, data: { message: "signin ok" } }); - }, - ); + app.post("/test/keygen", commitRevealMiddleware("keygen"), (_req, res) => { + res.status(200).json({ success: true, data: { message: "keygen ok" } }); + }); + app.post("/test/signin", commitRevealMiddleware("signin"), (_req, res) => { + res.status(200).json({ success: true, data: { message: "signin ok" } }); + }); app.locals.db = pool; app.locals.server_keypair = mockServerKeypair; @@ -320,7 +312,9 @@ describe("commit_reveal_middleware_test", () => { // Compute hash with original token const hashRes = sha256(`${authType}${originalIdToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } const idTokenHash = hashRes.data.toHex(); await createSession({ @@ -353,7 +347,9 @@ describe("commit_reveal_middleware_test", () => { // Compute hash with original auth_type const hashRes = sha256(`${originalAuthType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } const idTokenHash = hashRes.data.toHex(); await createSession({ @@ -387,7 +383,9 @@ describe("commit_reveal_middleware_test", () => { const idToken = "test_id_token"; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -416,7 +414,9 @@ describe("commit_reveal_middleware_test", () => { const idToken = "test_id_token"; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -446,11 +446,15 @@ describe("commit_reveal_middleware_test", () => { // Generate client keypair const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -462,10 +466,14 @@ describe("commit_reveal_middleware_test", () => { // Sign wrong message const wrongMessage = "wrong_message"; const signRes = signMessage(wrongMessage, clientKeypair.privateKey); - if (!signRes.success) throw new Error("Failed to sign message"); + if (!signRes.success) { + throw new Error("Failed to sign message"); + } const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); - if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + if (!sigBytesRes.success) { + throw new Error("Failed to convert signature"); + } const response = await request(app) .post("/test/keygen") @@ -496,7 +504,9 @@ describe("commit_reveal_middleware_test", () => { const wrongKeypair = wrongKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Create session with client keypair await createSession({ @@ -510,10 +520,14 @@ describe("commit_reveal_middleware_test", () => { const nodePubkeyHex = mockServerKeypair.publicKey.toHex(); const message = `${nodePubkeyHex}${sessionId}${authType}${idToken}sign_upkeygen`; const signRes = signMessage(message, wrongKeypair.privateKey); - if (!signRes.success) throw new Error("Failed to sign message"); + if (!signRes.success) { + throw new Error("Failed to sign message"); + } const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); - if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + if (!sigBytesRes.success) { + throw new Error("Failed to convert signature"); + } const response = await request(app) .post("/test/keygen") @@ -537,11 +551,15 @@ describe("commit_reveal_middleware_test", () => { // Compute hash with google as auth_type (default) const hashRes = sha256(`google${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Generate client keypair const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; await createSession({ @@ -555,10 +573,14 @@ describe("commit_reveal_middleware_test", () => { const nodePubkeyHex = mockServerKeypair.publicKey.toHex(); const message = `${nodePubkeyHex}${sessionId}google${idToken}sign_upkeygen`; const signRes = signMessage(message, clientKeypair.privateKey); - if (!signRes.success) throw new Error("Failed to sign message"); + if (!signRes.success) { + throw new Error("Failed to sign message"); + } const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); - if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + if (!sigBytesRes.success) { + throw new Error("Failed to convert signature"); + } // Send without auth_type - should default to google const response = await request(app) @@ -608,17 +630,39 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { app.post("/test/signin", commitRevealMiddleware("signin"), (_req, res) => { res.status(200).json({ success: true, data: { message: "signin ok" } }); }); - app.post("/test/reshare", commitRevealMiddleware("reshare"), (_req, res) => { - res.status(200).json({ success: true, data: { message: "reshare ok" } }); - }); - app.post("/test/keygen_ed25519", commitRevealMiddleware("keygen_ed25519"), (_req, res) => { - res.status(200).json({ success: true, data: { message: "keygen_ed25519 ok" } }); - }); + app.post( + "/test/reshare", + commitRevealMiddleware("reshare"), + (_req, res) => { + res + .status(200) + .json({ success: true, data: { message: "reshare ok" } }); + }, + ); + app.post( + "/test/keygen_ed25519", + commitRevealMiddleware("keygen_ed25519"), + (_req, res) => { + res + .status(200) + .json({ success: true, data: { message: "keygen_ed25519 ok" } }); + }, + ); // Route that fails - app.post("/test/keygen_fail", commitRevealMiddleware("keygen"), (_req, res) => { - res.status(500).json({ success: false, code: "INTERNAL_ERROR", msg: "Simulated failure" }); - }); + app.post( + "/test/keygen_fail", + commitRevealMiddleware("keygen"), + (_req, res) => { + res + .status(500) + .json({ + success: false, + code: "INTERNAL_ERROR", + msg: "Simulated failure", + }); + }, + ); app.locals.db = pool; app.locals.server_keypair = mockServerKeypair; @@ -665,7 +709,10 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { return result.rows[0]?.state ?? null; } - async function getApiCallCount(sessionId: string, apiName: string): Promise { + async function getApiCallCount( + sessionId: string, + apiName: string, + ): Promise { const result = await pool.query( `SELECT COUNT(*) as count FROM "commit_reveal_api_calls" WHERE session_id = $1 AND api_name = $2`, [sessionId, apiName], @@ -684,10 +731,14 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const nodePubkeyHex = mockServerKeypair.publicKey.toHex(); const message = `${nodePubkeyHex}${sessionId}${authType}${idToken}${operationType}${apiName}`; const signRes = signMessage(message, clientKeypair.privateKey); - if (!signRes.success) throw new Error("Failed to sign message"); + if (!signRes.success) { + throw new Error("Failed to sign message"); + } const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); - if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + if (!sigBytesRes.success) { + throw new Error("Failed to convert signature"); + } return sigBytesRes.data.toHex(); } @@ -699,11 +750,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -747,11 +802,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -792,11 +851,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -857,11 +920,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -905,11 +972,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // sign_in_reshare allows signin (non-final) then reshare (final) await createSession({ @@ -954,11 +1025,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -1004,11 +1079,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -1046,11 +1125,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -1088,11 +1171,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -1130,11 +1217,15 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } await createSession({ session_id: sessionId, @@ -1165,5 +1256,161 @@ describe("commit_reveal_middleware_replay_and_session_test", () => { expect(response.body.success).toBe(true); expect(response.body.data.message).toBe("reshare ok"); }); + + it("sign_in_reshare_ed25519: should allow signin, reshare, keygen_ed25519 in sequence", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + const clientKeypairRes = generateEddsaKeypair(); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } + const clientKeypair = clientKeypairRes.data; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } + + await createSession({ + session_id: sessionId, + operation_type: "sign_in_reshare_ed25519", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + // 1. signin (non-final) + const signinSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "signin", + ); + + const signinResponse = await request(app) + .post("/test/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signinSignature, + auth_type: authType, + }) + .expect(200); + + expect(signinResponse.body.success).toBe(true); + + // Wait for async handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should still be COMMITTED + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + // 2. reshare (non-final) + const reshareSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "reshare", + ); + + const reshareResponse = await request(app) + .post("/test/reshare") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: reshareSignature, + auth_type: authType, + }) + .expect(200); + + expect(reshareResponse.body.success).toBe(true); + + // Wait for async handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should still be COMMITTED + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + // 3. keygen_ed25519 (final) + const keygenSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "keygen_ed25519", + ); + + const keygenResponse = await request(app) + .post("/test/keygen_ed25519") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: keygenSignature, + auth_type: authType, + }) + .expect(200); + + expect(keygenResponse.body.success).toBe(true); + + // Wait for async handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should now be COMPLETED + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + }); + + it("sign_in_reshare_ed25519: should reject keygen (not allowed)", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + const clientKeypairRes = generateEddsaKeypair(); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } + const clientKeypair = clientKeypairRes.data; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } + + await createSession({ + session_id: sessionId, + operation_type: "sign_in_reshare_ed25519", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "keygen", + ); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("keygen"); + expect(response.body.msg).toContain("sign_in_reshare_ed25519"); + }); }); }); diff --git a/backend/oko_api/server/src/middleware/commit_reveal.ts b/backend/oko_api/server/src/middleware/commit_reveal.ts index 31ba5ddb2..667099c25 100644 --- a/backend/oko_api/server/src/middleware/commit_reveal.ts +++ b/backend/oko_api/server/src/middleware/commit_reveal.ts @@ -1,20 +1,17 @@ import type { Request, Response, NextFunction } from "express"; import { Bytes } from "@oko-wallet/bytes"; import { verifySignature } from "@oko-wallet/crypto-js/node/ecdhe"; -import { sha256 } from "@oko-wallet/crypto-js"; +import { sha256, buildRevealMessage } from "@oko-wallet/crypto-js"; import { getCommitRevealSessionBySessionId, createCommitRevealApiCall, updateCommitRevealSessionState, hasCommitRevealApiBeenCalled, } from "@oko-wallet/oko-pg-interface/commit_reveal"; -import type { ApiName, CommitRevealSession } from "@oko-wallet/oko-types/commit_reveal"; +import type { ApiName } from "@oko-wallet/oko-types/commit_reveal"; import { ErrorCodeMap } from "@oko-wallet/oko-api-error-codes"; -import { - isApiAllowed, - isFinalApi, -} from "@oko-wallet-api/commit_reveal"; +import { isApiAllowed, isFinalApi } from "@oko-wallet-api/commit_reveal"; import type { ServerState } from "@oko-wallet/oko-api-server-state"; const DEFAULT_AUTH_TYPE = "google"; @@ -171,12 +168,12 @@ export function commitRevealMiddleware(apiName: ApiName) { // Verify signature: message = node_pubkey + session_id + auth_type + id_token + operation_type + api_name const nodePubkeyHex = state.server_keypair.publicKey.toHex(); - const message = makeSigMessage({ + const message = buildRevealMessage({ nodePubkeyHex, - cr_session_id, + sessionId: cr_session_id, authType, idToken, - session, + operationType: session.operation_type, apiName, }); const rBytes = Bytes.fromUint8Array( @@ -259,31 +256,3 @@ export function commitRevealMiddleware(apiName: ApiName) { next(); }; } - -export interface SigMessageArgs { - nodePubkeyHex: string; - cr_session_id: string; - authType: string; - idToken: string; - session: CommitRevealSession; - apiName: ApiName; -} - -// message = node_pubkey + session_id + auth_type + id_token + operation_type + api_name -function makeSigMessage({ - nodePubkeyHex, - cr_session_id, - authType, - idToken, - session, - apiName, -}: SigMessageArgs) { - return ( - nodePubkeyHex + - cr_session_id + - authType + - idToken + - session.operation_type + - apiName - ); -} diff --git a/backend/oko_api/server/src/routes/tss_v2/commit.test.ts b/backend/oko_api/server/src/routes/tss_v2/commit.test.ts index 0c2a2779d..5fb531673 100644 --- a/backend/oko_api/server/src/routes/tss_v2/commit.test.ts +++ b/backend/oko_api/server/src/routes/tss_v2/commit.test.ts @@ -152,6 +152,21 @@ describe("commit_route_test", () => { expect(response.body.data).toBeDefined(); }); + it("should successfully create session with sign_in_reshare_ed25519 operation", async () => { + const body = { + ...createValidBody(), + operation_type: "sign_in_reshare_ed25519", + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeDefined(); + }); + it("should create multiple sessions with different session_ids", async () => { const body1 = createValidBody(); const body2 = createValidBody(); diff --git a/backend/oko_api/server/src/routes/tss_v2/e2e.test.ts b/backend/oko_api/server/src/routes/tss_v2/e2e.test.ts index 04ab69133..7e0e88e45 100644 --- a/backend/oko_api/server/src/routes/tss_v2/e2e.test.ts +++ b/backend/oko_api/server/src/routes/tss_v2/e2e.test.ts @@ -143,10 +143,14 @@ describe("tss_v2_commit_reveal_e2e_test", () => { const nodePubkeyHex = mockServerKeypair.publicKey.toHex(); const message = `${nodePubkeyHex}${sessionId}${authType}${idToken}${operationType}${apiName}`; const signRes = signMessage(message, clientKeypair.privateKey); - if (!signRes.success) throw new Error("Failed to sign message"); + if (!signRes.success) { + throw new Error("Failed to sign message"); + } const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); - if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + if (!sigBytesRes.success) { + throw new Error("Failed to convert signature"); + } return sigBytesRes.data.toHex(); } @@ -159,7 +163,10 @@ describe("tss_v2_commit_reveal_e2e_test", () => { return result.rows[0]?.state ?? null; } - async function getApiCallCount(sessionId: string, apiName: string): Promise { + async function getApiCallCount( + sessionId: string, + apiName: string, + ): Promise { const result = await pool.query( `SELECT COUNT(*) as count FROM "commit_reveal_api_calls" WHERE session_id = $1 AND api_name = $2`, [sessionId, apiName], @@ -175,12 +182,16 @@ describe("tss_v2_commit_reveal_e2e_test", () => { // Generate client keypair const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; // Compute id_token_hash const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } const idTokenHash = hashRes.data.toHex(); // Step 1: Commit @@ -195,7 +206,9 @@ describe("tss_v2_commit_reveal_e2e_test", () => { .expect(200); expect(commitResponse.body.success).toBe(true); - expect(commitResponse.body.data.node_pubkey).toBe(mockServerKeypair.publicKey.toHex()); + expect(commitResponse.body.data.node_pubkey).toBe( + mockServerKeypair.publicKey.toHex(), + ); expect(commitResponse.body.data.node_signature).toHaveLength(128); // Verify session is in COMMITTED state @@ -241,11 +254,15 @@ describe("tss_v2_commit_reveal_e2e_test", () => { const idToken = "test_id_token_replay"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit await request(app) @@ -309,11 +326,15 @@ describe("tss_v2_commit_reveal_e2e_test", () => { const idToken = "test_id_token_for_signin"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit const commitResponse = await request(app) @@ -365,11 +386,15 @@ describe("tss_v2_commit_reveal_e2e_test", () => { const idToken = "test_id_token_for_reshare"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit with sign_in_reshare operation await request(app) @@ -448,11 +473,15 @@ describe("tss_v2_commit_reveal_e2e_test", () => { const idToken = "test_id_token_for_ed25519"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit await request(app) @@ -496,6 +525,172 @@ describe("tss_v2_commit_reveal_e2e_test", () => { expect(await getApiCallCount(sessionId, "keygen_ed25519")).toBe(1); }); }); + + describe("sign_in_reshare_ed25519 flow (commit -> signin -> reshare -> keygen_ed25519)", () => { + it("should complete full sign_in_reshare_ed25519 flow", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_for_reshare_ed25519"; + + const clientKeypairRes = generateEddsaKeypair(); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } + const clientKeypair = clientKeypairRes.data; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } + + // Commit with sign_in_reshare_ed25519 operation + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_in_reshare_ed25519", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Step 1: Signin (non-final for sign_in_reshare_ed25519) + const signinSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "signin", + ); + + await request(app) + .post("/tss/v2/user/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signinSignature, + auth_type: authType, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should still be COMMITTED (signin is not final) + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + // Step 2: Reshare (non-final for sign_in_reshare_ed25519) + const reshareSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "reshare", + ); + + await request(app) + .post("/tss/v2/user/reshare") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: reshareSignature, + auth_type: authType, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should still be COMMITTED (reshare is not final for sign_in_reshare_ed25519) + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + // Step 3: Keygen ed25519 (final for sign_in_reshare_ed25519) + const keygenEd25519Signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "keygen_ed25519", + ); + + const keygenResponse = await request(app) + .post("/tss/v2/keygen_ed25519") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: keygenEd25519Signature, + auth_type: authType, + }) + .expect(200); + + expect(keygenResponse.body.success).toBe(true); + expect(keygenResponse.body.data.wallet_id).toBeDefined(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should now be COMPLETED + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + + // All three API calls should be recorded + expect(await getApiCallCount(sessionId, "signin")).toBe(1); + expect(await getApiCallCount(sessionId, "reshare")).toBe(1); + expect(await getApiCallCount(sessionId, "keygen_ed25519")).toBe(1); + }); + + it("should reject keygen (not keygen_ed25519) for sign_in_reshare_ed25519", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_reject_keygen"; + + const clientKeypairRes = generateEddsaKeypair(); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } + const clientKeypair = clientKeypairRes.data; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } + + // Commit with sign_in_reshare_ed25519 operation + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_in_reshare_ed25519", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Try to call keygen (not allowed for sign_in_reshare_ed25519) + const keygenSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare_ed25519", + "keygen", + ); + + const response = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: keygenSignature, + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("keygen"); + expect(response.body.msg).toContain("sign_in_reshare_ed25519"); + }); + }); }); describe("tss_v2_e2e_error_scenarios", () => { @@ -525,13 +720,21 @@ describe("tss_v2_e2e_error_scenarios", () => { app.post("/tss/v2/commit", commitRevealCommit); - app.post("/tss/v2/keygen", commitRevealMiddleware("keygen"), (_req, res) => { - res.status(200).json({ success: true, data: { message: "keygen ok" } }); - }); + app.post( + "/tss/v2/keygen", + commitRevealMiddleware("keygen"), + (_req, res) => { + res.status(200).json({ success: true, data: { message: "keygen ok" } }); + }, + ); - app.post("/tss/v2/user/signin", commitRevealMiddleware("signin"), (_req, res) => { - res.status(200).json({ success: true, data: { message: "signin ok" } }); - }); + app.post( + "/tss/v2/user/signin", + commitRevealMiddleware("signin"), + (_req, res) => { + res.status(200).json({ success: true, data: { message: "signin ok" } }); + }, + ); app.locals.db = pool; app.locals.server_keypair = mockServerKeypair; @@ -557,10 +760,14 @@ describe("tss_v2_e2e_error_scenarios", () => { const nodePubkeyHex = mockServerKeypair.publicKey.toHex(); const message = `${nodePubkeyHex}${sessionId}${authType}${idToken}${operationType}${apiName}`; const signRes = signMessage(message, clientKeypair.privateKey); - if (!signRes.success) throw new Error("Failed to sign message"); + if (!signRes.success) { + throw new Error("Failed to sign message"); + } const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); - if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + if (!sigBytesRes.success) { + throw new Error("Failed to convert signature"); + } return sigBytesRes.data.toHex(); } @@ -572,11 +779,15 @@ describe("tss_v2_e2e_error_scenarios", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit await request(app) @@ -619,11 +830,15 @@ describe("tss_v2_e2e_error_scenarios", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit await request(app) @@ -690,11 +905,15 @@ describe("tss_v2_e2e_error_scenarios", () => { const idToken = "test_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; const hashRes = sha256(`${authType}${idToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit with sign_in operation await request(app) @@ -740,12 +959,16 @@ describe("tss_v2_e2e_error_scenarios", () => { const wrongIdToken = "wrong_id_token"; const clientKeypairRes = generateEddsaKeypair(); - if (!clientKeypairRes.success) throw new Error("Failed to generate keypair"); + if (!clientKeypairRes.success) { + throw new Error("Failed to generate keypair"); + } const clientKeypair = clientKeypairRes.data; // Hash with original token const hashRes = sha256(`${authType}${originalIdToken}`); - if (!hashRes.success) throw new Error("Failed to compute hash"); + if (!hashRes.success) { + throw new Error("Failed to compute hash"); + } // Commit with original token hash await request(app) diff --git a/backend/openapi/src/tss/commit_reveal.ts b/backend/openapi/src/tss/commit_reveal.ts index 0109d0fde..562ee9292 100644 --- a/backend/openapi/src/tss/commit_reveal.ts +++ b/backend/openapi/src/tss/commit_reveal.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { registry } from "@oko-wallet/oko-api-openapi"; export const OperationTypeSchema = z - .enum(["sign_in", "sign_up", "sign_in_reshare", "add_ed25519"]) + .enum(["sign_in", "sign_up", "sign_in_reshare", "sign_in_reshare_ed25519", "add_ed25519"]) .describe("Operation type for commit-reveal session"); // POST /tss/v2/commit-reveal/commit diff --git a/common/oko_types/src/commit_reveal/index.ts b/common/oko_types/src/commit_reveal/index.ts index 8a9015342..54033731e 100644 --- a/common/oko_types/src/commit_reveal/index.ts +++ b/common/oko_types/src/commit_reveal/index.ts @@ -2,6 +2,7 @@ export type OperationType = | "sign_in" | "sign_up" | "sign_in_reshare" + | "sign_in_reshare_ed25519" | "add_ed25519"; export type ApiName = "signin" | "keygen" | "reshare" | "keygen_ed25519"; @@ -33,3 +34,8 @@ export interface CreateSessionParams { id_token_hash: string; expires_at: Date; } + +export interface CommitRevealParams { + cr_session_id: string; + cr_signature: string; +} diff --git a/crypto/crypto_js/common/commit_reveal.ts b/crypto/crypto_js/common/commit_reveal.ts new file mode 100644 index 000000000..ee8f6354b --- /dev/null +++ b/crypto/crypto_js/common/commit_reveal.ts @@ -0,0 +1,25 @@ +export interface CommitRevealSignMessageArgs { + nodePubkeyHex: string; + sessionId: string; + authType: string; + idToken: string; + operationType: string; + apiName: string; +} + +/** + * Create the message to be signed for commit-reveal signature verification. + * message = node_pubkey + session_id + auth_type + id_token + operation_type + api_name + */ +export function buildRevealMessage({ + nodePubkeyHex, + sessionId, + authType, + idToken, + operationType, + apiName, +}: CommitRevealSignMessageArgs): string { + return ( + nodePubkeyHex + sessionId + authType + idToken + operationType + apiName + ); +} diff --git a/crypto/crypto_js/common/index.ts b/crypto/crypto_js/common/index.ts index ef8044397..a76ef4c0d 100644 --- a/crypto/crypto_js/common/index.ts +++ b/crypto/crypto_js/common/index.ts @@ -1,2 +1,3 @@ export * from "./hash"; export * from "./bcrypt"; +export * from "./commit_reveal"; diff --git a/crypto/tecdsa/api_lib/src/index.ts b/crypto/tecdsa/api_lib/src/index.ts index 02cc87ae7..f93d0cde0 100644 --- a/crypto/tecdsa/api_lib/src/index.ts +++ b/crypto/tecdsa/api_lib/src/index.ts @@ -57,6 +57,7 @@ import { type OkoApiErrorResponse, type OkoApiResponse, } from "@oko-wallet/oko-types/api_response"; +import type { CommitRevealParams } from "@oko-wallet/oko-types/commit_reveal"; /* NOTE - The error type returned by the middleware is not compatible with OkoApiErrorResponse. So we use a separate function to handle it. @@ -200,11 +201,13 @@ export async function reqKeygenV2( endpoint: string, payload: KeygenRequestBodyV2, authToken: string, + commitReveal: CommitRevealParams, ) { + const body = { ...payload, ...commitReveal }; const resp: OkoApiResponse = await makePostRequest( endpoint, "keygen", - payload, + body, undefined, authToken, ); diff --git a/embed/oko_attached/src/crypto/commit_reveal/index.ts b/embed/oko_attached/src/crypto/commit_reveal/index.ts index 8cbd70dc4..0c5978506 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/index.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/index.ts @@ -1,7 +1,4 @@ -/** - * Commit-Reveal Session Module - */ - export * from "./types"; export * from "./utils"; export * from "./session"; +export * from "./signature"; diff --git a/embed/oko_attached/src/crypto/commit_reveal/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index f4dfa1c81..3cf6b62da 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -1,327 +1,198 @@ -/** - * Commit-Reveal Session Management - */ - -import type { Bytes32 } from "@oko-wallet/bytes"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { OperationType } from "@oko-wallet/oko-types/commit_reveal"; +import type { OperationType as KsnOperationType } from "@oko-wallet/ksn-interface/commit_reveal"; import type { Result } from "@oko-wallet/stdlib-js"; -import type { - CommitRevealSession, - CommitRevealSessionState, - CreateSessionOptions, - NodeStatus, - EncryptedToken, -} from "./types"; -import type { ClientEcdheKeypair } from "./utils"; +import type { ClientCommitRevealSession, KsnCommitTarget } from "./types"; import { generateSessionId, generateClientKeypair, - calculateTokenHash, - calculateExpiresAt, - isSessionExpired, + computeIdTokenHash, + SESSION_TIMEOUT_MS, } from "./utils"; - -// ============================================================================ -// Session Creation -// ============================================================================ - -export interface CreateSessionResult { - session: CommitRevealSession; - keypair: ClientEcdheKeypair; -} - -/** - * Create a new commit-reveal session. - */ -export function createSession( - options: CreateSessionOptions, -): Result { - const sessionId = generateSessionId(); - - const keypairResult = generateClientKeypair(); - if (!keypairResult.success) { - return { success: false, err: keypairResult.err }; +import { commitToOkoApi } from "@oko-wallet-attached/requests/oko_api"; +import { commitToKsNode } from "@oko-wallet-attached/requests/ks_node_v2"; + +export function createCommitRevealSession( + operationType: OperationType, + authType: AuthType, + idToken: string, +): Result { + const keypairRes = generateClientKeypair(); + if (!keypairRes.success) { + return { success: false, err: keypairRes.err }; } - const tokenHashResult = calculateTokenHash( - options.oauth_token, - options.sdk_version, - ); - if (!tokenHashResult.success) { - return { success: false, err: tokenHashResult.err }; + const hashRes = computeIdTokenHash(authType, idToken); + if (!hashRes.success) { + return { success: false, err: hashRes.err }; } const now = new Date(); - - const session: CommitRevealSession = { - session_id: sessionId, - session_type: "OAUTH_COMMIT_REVEAL", - client_public_key: keypairResult.data.publicKey, - sdk_version: options.sdk_version, - user_email: options.user_email, - public_key: options.wallet_public_key, - token_hash: tokenHashResult.data, - state: "INITIALIZED", - created_at: now, - updated_at: now, - expires_at: calculateExpiresAt(now), - commit_phase: { - nodes_committed: [], - total_nodes: options.node_urls.length, - encrypted_tokens: {}, - node_public_keys: {}, - }, - reveal_phase: { - nodes_revealed: [], - total_nodes: options.node_urls.length, - }, - operation_type: options.operation_type, - }; - return { success: true, - data: { session, keypair: keypairResult.data }, - }; -} - -// ============================================================================ -// State Management -// ============================================================================ - -/** - * Update session state. - */ -export function updateState( - session: CommitRevealSession, - newState: CommitRevealSessionState, -): CommitRevealSession { - return { - ...session, - state: newState, - updated_at: new Date(), - }; -} - -/** - * Check if state transition is valid. - */ -export function canTransitionTo( - session: CommitRevealSession, - targetState: CommitRevealSessionState, -): boolean { - if (isSessionExpired(session.expires_at)) { - return targetState === "TIMEOUT"; - } - - const transitions: Record< - CommitRevealSessionState, - CommitRevealSessionState[] - > = { - INITIALIZED: ["COMMIT_PHASE", "FAILED", "TIMEOUT"], - COMMIT_PHASE: ["COMMITTED", "FAILED", "TIMEOUT"], - COMMITTED: ["REVEAL_PHASE", "FAILED", "TIMEOUT", "ROLLED_BACK"], - REVEAL_PHASE: ["COMPLETED", "FAILED", "TIMEOUT", "ROLLED_BACK"], - COMPLETED: [], - FAILED: ["ROLLED_BACK"], - TIMEOUT: ["ROLLED_BACK"], - ROLLED_BACK: [], - }; - - return transitions[session.state]?.includes(targetState) ?? false; -} - -// ============================================================================ -// Commit Phase -// ============================================================================ - -/** - * Record Oko Server's public key after init. - */ -export function setOkoServerPublicKey( - session: CommitRevealSession, - publicKey: Bytes32, -): CommitRevealSession { - return { - ...session, - oko_server_public_key: publicKey, - state: "COMMIT_PHASE", - updated_at: new Date(), - }; -} - -/** - * Record successful node commit. - */ -export function recordNodeCommit( - session: CommitRevealSession, - nodeUrl: string, - nodeName: string, - nodePublicKey: Bytes32, -): CommitRevealSession { - const status: NodeStatus = { - node_name: nodeName, - node_url: nodeUrl, - status: "SUCCESS", - timestamp: new Date(), - }; - - return { - ...session, - updated_at: new Date(), - commit_phase: { - ...session.commit_phase, - nodes_committed: [...session.commit_phase.nodes_committed, status], - node_public_keys: { - ...session.commit_phase.node_public_keys, - [nodeUrl]: nodePublicKey, - }, + data: { + session_id: generateSessionId(), + operation_type: operationType, + client_keypair: keypairRes.data, + id_token_hash: hashRes.data, + auth_type: authType, + id_token: idToken, + ksn_node_pubkeys: {}, + ksn_operation_types: {}, + created_at: now, + expires_at: new Date(now.getTime() + SESSION_TIMEOUT_MS), }, }; } -/** - * Record failed node commit. - */ -export function recordNodeCommitFailure( - session: CommitRevealSession, - nodeUrl: string, - nodeName: string, - errorMessage: string, -): CommitRevealSession { - const status: NodeStatus = { - node_name: nodeName, - node_url: nodeUrl, - status: "FAILED", - error_message: errorMessage, - timestamp: new Date(), - }; - - return { - ...session, - updated_at: new Date(), - commit_phase: { - ...session.commit_phase, - nodes_committed: [...session.commit_phase.nodes_committed, status], - }, - }; +export function setOkoApiNodePubkey( + session: ClientCommitRevealSession, + nodePubkey: string, +): ClientCommitRevealSession { + return { ...session, oko_api_node_pubkey: nodePubkey }; } -/** - * Store encrypted token for a node. - */ -export function storeEncryptedToken( - session: CommitRevealSession, +export function setKsnNodePubkey( + session: ClientCommitRevealSession, nodeUrl: string, - encryptedToken: EncryptedToken, -): CommitRevealSession { + nodePubkey: string, + operationType: KsnOperationType, +): ClientCommitRevealSession { return { ...session, - updated_at: new Date(), - commit_phase: { - ...session.commit_phase, - encrypted_tokens: { - ...session.commit_phase.encrypted_tokens, - [nodeUrl]: encryptedToken, - }, + ksn_node_pubkeys: { ...session.ksn_node_pubkeys, [nodeUrl]: nodePubkey }, + ksn_operation_types: { + ...session.ksn_operation_types, + [nodeUrl]: operationType, }, }; } -// ============================================================================ -// Reveal Phase -// ============================================================================ - -/** - * Record successful node reveal. - */ -export function recordNodeReveal( - session: CommitRevealSession, - nodeUrl: string, - nodeName: string, -): CommitRevealSession { - const status: NodeStatus = { - node_name: nodeName, - node_url: nodeUrl, - status: "SUCCESS", - timestamp: new Date(), - }; - - return { - ...session, - updated_at: new Date(), - reveal_phase: { - ...session.reveal_phase, - nodes_revealed: [...session.reveal_phase.nodes_revealed, status], - }, - }; +export interface CommitAllResult { + session: ClientCommitRevealSession; + okoApiCommitted: boolean; + ksnCommittedNodes: string[]; } /** - * Record failed node reveal. + * Commit to oko_api and KSN nodes. + * Creates a commit-reveal session and sends commit requests. + * + * @param ksnThreshold - Number of KSN nodes that must successfully commit. + * - For sign_in: pass the MPC threshold (e.g., 2) + * - For register/reshare: pass targets.length (all nodes must succeed) + * + * Shuffles nodes and tries threshold first, retries with backup on failure. */ -export function recordNodeRevealFailure( - session: CommitRevealSession, - nodeUrl: string, - nodeName: string, - errorMessage: string, -): CommitRevealSession { - const status: NodeStatus = { - node_name: nodeName, - node_url: nodeUrl, - status: "FAILED", - error_message: errorMessage, - timestamp: new Date(), - }; - - return { - ...session, - updated_at: new Date(), - reveal_phase: { - ...session.reveal_phase, - nodes_revealed: [...session.reveal_phase.nodes_revealed, status], - }, - }; -} - -// ============================================================================ -// Status Helpers -// ============================================================================ - -export function getSuccessfulCommitCount(session: CommitRevealSession): number { - return session.commit_phase.nodes_committed.filter( - (n) => n.status === "SUCCESS", - ).length; -} +export async function commitAll( + operationType: OperationType, + authType: AuthType, + idToken: string, + ksnCommitTargets: KsnCommitTarget[], + ksnThreshold: number, +): Promise> { + // 1. Create session + const sessionRes = createCommitRevealSession( + operationType, + authType, + idToken, + ); + if (!sessionRes.success) { + return { success: false, err: sessionRes.err }; + } + let session = sessionRes.data; -export function getSuccessfulRevealCount(session: CommitRevealSession): number { - return session.reveal_phase.nodes_revealed.filter( - (n) => n.status === "SUCCESS", - ).length; -} + const clientPubkeyHex = session.client_keypair.publicKey.toHex(); -export function isCommitPhaseComplete(session: CommitRevealSession): boolean { - return ( - session.commit_phase.nodes_committed.length === - session.commit_phase.total_nodes + // 2. Commit to oko_api + const okoApiResult = await commitToOkoApi( + session.session_id, + operationType, + clientPubkeyHex, + session.id_token_hash, ); -} -export function isRevealPhaseComplete(session: CommitRevealSession): boolean { - return ( - session.reveal_phase.nodes_revealed.length === - session.reveal_phase.total_nodes - ); -} + if (!okoApiResult.success || !okoApiResult.data.success) { + return { success: false, err: "Failed to commit to oko_api" }; + } + session = setOkoApiNodePubkey(session, okoApiResult.data.data.node_pubkey); + + // 3. Commit to KSN nodes + // Shuffle nodes + const shuffledTargets = [...ksnCommitTargets]; + for (let i = shuffledTargets.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledTargets[i], shuffledTargets[j]] = [ + shuffledTargets[j], + shuffledTargets[i], + ]; + } -export function meetsThreshold( - session: CommitRevealSession, - threshold: number, -): boolean { - if (session.state === "COMMIT_PHASE" || session.state === "COMMITTED") { - return getSuccessfulCommitCount(session) >= threshold; + const committedNodes: string[] = []; + let targetsToTry = shuffledTargets.slice(0, ksnThreshold); + let backupTargets = shuffledTargets.slice(ksnThreshold); + + while (committedNodes.length < ksnThreshold && targetsToTry.length > 0) { + const results = await Promise.allSettled( + targetsToTry.map((target) => + commitToKsNode( + target.nodeUrl, + session.session_id, + target.operationType, + clientPubkeyHex, + session.id_token_hash, + ).then((res) => ({ + nodeUrl: target.nodeUrl, + operationType: target.operationType, + res, + })), + ), + ); + + const failedTargets: KsnCommitTarget[] = []; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const target = targetsToTry[i]; + + if (result.status === "fulfilled" && result.value.res.success) { + session = setKsnNodePubkey( + session, + result.value.nodeUrl, + result.value.res.data.node_pubkey, + result.value.operationType, + ); + committedNodes.push(result.value.nodeUrl); + } else { + failedTargets.push(target); + } + } + + if (committedNodes.length >= ksnThreshold) { + break; + } + + // Try backup nodes for failed ones + targetsToTry = []; + for (let i = 0; i < failedTargets.length && backupTargets.length > 0; i++) { + targetsToTry.push(backupTargets.shift()!); + } } - if (session.state === "REVEAL_PHASE" || session.state === "COMPLETED") { - return getSuccessfulRevealCount(session) >= threshold; + + if (committedNodes.length < ksnThreshold) { + return { + success: false, + err: `Insufficient KSN commits: got ${committedNodes.length}, need ${ksnThreshold}`, + }; } - return false; + + return { + success: true, + data: { + session, + okoApiCommitted: true, + ksnCommittedNodes: committedNodes, + }, + }; } diff --git a/embed/oko_attached/src/crypto/commit_reveal/signature.ts b/embed/oko_attached/src/crypto/commit_reveal/signature.ts new file mode 100644 index 000000000..da07d7789 --- /dev/null +++ b/embed/oko_attached/src/crypto/commit_reveal/signature.ts @@ -0,0 +1,49 @@ +import type { ApiName as OkoApiName } from "@oko-wallet/oko-types/commit_reveal"; +import type { ApiName as KsnApiName } from "@oko-wallet/ksn-interface/commit_reveal"; +import type { Result } from "@oko-wallet/stdlib-js"; + +import type { ClientCommitRevealSession } from "./types"; +import { createRevealSignature } from "./utils"; + +export function createOkoApiSignature( + session: ClientCommitRevealSession, + apiName: OkoApiName, +): Result { + if (!session.oko_api_node_pubkey) { + return { success: false, err: "oko_api node pubkey not set" }; + } + return createRevealSignature( + session.client_keypair.privateKey, + session.oko_api_node_pubkey, + session.session_id, + session.auth_type, + session.id_token, + session.operation_type, + apiName, + ); +} + +export function createKsnSignature( + session: ClientCommitRevealSession, + nodeUrl: string, + apiName: KsnApiName, +): Result { + const nodePubkey = session.ksn_node_pubkeys[nodeUrl]; + const operationType = session.ksn_operation_types[nodeUrl]; + if (!nodePubkey) { + return { success: false, err: `KSN node pubkey not found for ${nodeUrl}` }; + } + if (!operationType) { + return { success: false, err: `KSN operation type not found for ${nodeUrl}` }; + } + // Use node-specific operation type for signature + return createRevealSignature( + session.client_keypair.privateKey, + nodePubkey, + session.session_id, + session.auth_type, + session.id_token, + operationType, + apiName, + ); +} diff --git a/embed/oko_attached/src/crypto/commit_reveal/types.ts b/embed/oko_attached/src/crypto/commit_reveal/types.ts index e71739f0a..464cd3657 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/types.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/types.ts @@ -1,144 +1,26 @@ -/** - * Commit-Reveal Session Types - */ +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { OperationType } from "@oko-wallet/oko-types/commit_reveal"; +import type { OperationType as KsnOperationType } from "@oko-wallet/ksn-interface/commit_reveal"; +import type { Bytes } from "@oko-wallet/bytes"; -import type { Bytes32 } from "@oko-wallet/bytes"; - -// ============================================================================ -// Session Types -// ============================================================================ - -export type SessionType = "OAUTH_COMMIT_REVEAL"; - -export type CommitRevealSessionState = - | "INITIALIZED" - | "COMMIT_PHASE" - | "COMMITTED" - | "REVEAL_PHASE" - | "COMPLETED" - | "FAILED" - | "TIMEOUT" - | "ROLLED_BACK"; - -export type OperationType = "signin" | "register" | "reshare"; - -export type RollbackReason = - | "TIMEOUT" - | "COMMIT_FAILED" - | "REVEAL_FAILED" - | "USER_CANCELLED" - | "NETWORK_ERROR" - | "VALIDATION_ERROR"; - -export type NodeOperationStatus = "PENDING" | "SUCCESS" | "FAILED"; - -// ============================================================================ -// Data Structures -// ============================================================================ - -export interface NodeStatus { - node_name: string; - node_url: string; - status: NodeOperationStatus; - error_message?: string; - timestamp: Date; -} - -export interface EncryptedToken { - ciphertext: string; - nonce: string; - tag: string; -} - -// ============================================================================ -// Session -// ============================================================================ - -export interface CommitRevealSession { +export interface ClientCommitRevealSession { session_id: string; - session_type: SessionType; - client_public_key: Bytes32; - oko_server_public_key?: Bytes32; - sdk_version: string; - - user_email: string; - public_key: string; // wallet public key (hex) - - token_hash: string; - - state: CommitRevealSessionState; - created_at: Date; - updated_at: Date; - expires_at: Date; - - commit_phase: { - nodes_committed: NodeStatus[]; - total_nodes: number; - encrypted_tokens: Record; - node_public_keys: Record; - }; - - reveal_phase: { - nodes_revealed: NodeStatus[]; - total_nodes: number; - }; - - operation_type: OperationType; - rollback_reason?: RollbackReason; -} - -// ============================================================================ -// API Types -// ============================================================================ - -export interface InitSessionRequest { - session_id: string; - session_type: SessionType; - client_public_key: string; // hex - public_key: string; // wallet public key hex - token_hash: string; - sdk_version: string; - operation_type: OperationType; - node_urls: string[]; -} - -export interface InitSessionResponse { - success: boolean; - data: { - session_id: string; - expires_at: string; - state: CommitRevealSessionState; - oko_server_public_key: string; // hex - }; -} - -export interface CommitRequest { - session_id: string; - client_public_key: string; - public_key: string; - token_hash: string; - sdk_version: string; operation_type: OperationType; -} - -export interface CommitResponse { - success: boolean; - data: { - session_id: string; - node_public_key: string; - state: "COMMITTED"; + client_keypair: { + privateKey: Bytes<32>; + publicKey: Bytes<32>; }; + id_token_hash: string; + auth_type: AuthType; + id_token: string; + oko_api_node_pubkey?: string; + ksn_node_pubkeys: Record; + ksn_operation_types: Record; + created_at: Date; + expires_at: Date; } -// ============================================================================ -// Session Creation -// ============================================================================ - -export interface CreateSessionOptions { - oauth_token: string; - user_email: string; - wallet_public_key: string; - operation_type: OperationType; - node_urls: string[]; - sdk_version: string; +export interface KsnCommitTarget { + nodeUrl: string; + operationType: KsnOperationType; } diff --git a/embed/oko_attached/src/crypto/commit_reveal/utils.ts b/embed/oko_attached/src/crypto/commit_reveal/utils.ts index fed12033d..c704436f9 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/utils.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/utils.ts @@ -1,127 +1,59 @@ -/** - * Commit-Reveal Session Utilities - * - * - Session ID generation (UUID v7) - * - ECDHE keypair generation (ed25519/x25519) - * - Token hash calculation - * - Encryption key derivation - */ - -import { v7 as uuidv7 } from "uuid"; -import type { Bytes32 } from "@oko-wallet/bytes"; +import { v4 as uuidv4 } from "uuid"; +import { sha256, buildRevealMessage } from "@oko-wallet/crypto-js"; import { generateEddsaKeypair, - deriveSessionKey, - type EddsaKeypair, - type EcdheSessionKey, + signMessage, + convertEddsaSignatureToBytes, } from "@oko-wallet/crypto-js/browser"; -import { sha256 } from "@oko-wallet/crypto-js"; +import type { Bytes } from "@oko-wallet/bytes"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { Result } from "@oko-wallet/stdlib-js"; -/** Session timeout: 5 minutes */ export const SESSION_TIMEOUT_MS = 5 * 60 * 1000; -// ============================================================================ -// Session ID -// ============================================================================ - -/** - * Generate session ID using UUID v7 (timestamp-ordered). - */ export function generateSessionId(): string { - return uuidv7(); + return uuidv4(); } -// ============================================================================ -// ECDHE Keypair -// ============================================================================ - -export type ClientEcdheKeypair = EddsaKeypair; - -/** - * Generate ECDHE keypair for commit-reveal session. - * Uses ed25519 curve via @oko-wallet/crypto-js. - */ -export function generateClientKeypair(): Result { +export function generateClientKeypair() { return generateEddsaKeypair(); } -// ============================================================================ -// Version Prefix -// ============================================================================ - -/** - * Extract major version from semver string. - * "1.2.3" -> "1" - */ -export function extractMajorVersion(sdkVersion: string): string { - const parts = sdkVersion.split("."); - return parts[0] ?? "0"; -} - -/** - * Generate version prefix for key derivation. - * "1.2.3" -> "oko-v1-" - */ -export function generateVersionPrefix(sdkVersion: string): string { - const major = extractMajorVersion(sdkVersion); - return `oko-v${major}-`; -} - -// ============================================================================ -// Token Hash -// ============================================================================ - -/** - * Calculate token hash for commitment. - * Formula: SHA256("oko-v{major}-" + oauth_token) - */ -export function calculateTokenHash( - oauthToken: string, - sdkVersion: string, +export function computeIdTokenHash( + authType: AuthType, + idToken: string, ): Result { - const prefix = generateVersionPrefix(sdkVersion); - const dataToHash = prefix + oauthToken; - - const hashResult = sha256(dataToHash); - if (!hashResult.success) { - return { success: false, err: hashResult.err }; + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) { + return { success: false, err: hashRes.err }; } - - return { success: true, data: hashResult.data.toHex() }; -} - -// ============================================================================ -// Encryption Key Derivation -// ============================================================================ - -/** - * Derive encryption key from ECDHE shared secret. - * Formula: SHA256("oko-v{major}-" + shared_secret) - */ -export function deriveEncryptionKey( - clientPrivateKey: Bytes32, - counterPartyPublicKey: Bytes32, - sdkVersion: string, -): Result { - const prefix = generateVersionPrefix(sdkVersion); - return deriveSessionKey(clientPrivateKey, counterPartyPublicKey, prefix); -} - -// ============================================================================ -// Session Expiration -// ============================================================================ - -/** - * Calculate session expiration time (now + 5 minutes). - */ -export function calculateExpiresAt(createdAt: Date = new Date()): Date { - return new Date(createdAt.getTime() + SESSION_TIMEOUT_MS); + return { success: true, data: hashRes.data.toHex() }; } -/** - * Check if session has expired. - */ -export function isSessionExpired(expiresAt: Date): boolean { - return new Date() > expiresAt; +export function createRevealSignature( + clientPrivateKey: Bytes<32>, + nodePubkey: string, + sessionId: string, + authType: AuthType, + idToken: string, + operationType: string, + apiName: string, +): Result { + const message = buildRevealMessage({ + nodePubkeyHex: nodePubkey, + sessionId, + authType, + idToken, + operationType, + apiName, + }); + const signRes = signMessage(message, clientPrivateKey); + if (!signRes.success) { + return { success: false, err: signRes.err }; + } + const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); + if (!sigBytesRes.success) { + return { success: false, err: sigBytesRes.err }; + } + return { success: true, data: sigBytesRes.data.toHex() }; } diff --git a/embed/oko_attached/src/requests/ks_node_v2.ts b/embed/oko_attached/src/requests/ks_node_v2.ts index eab38fd5f..2045205fd 100644 --- a/embed/oko_attached/src/requests/ks_node_v2.ts +++ b/embed/oko_attached/src/requests/ks_node_v2.ts @@ -1,13 +1,19 @@ import type { - GetKeyShareV2RequestBody, GetKeyShareV2Response, - RegisterKeyShareV2RequestBody, - RegisterEd25519V2RequestBody, - ReshareKeyShareV2RequestBody, - ReshareRegisterV2RequestBody, + GetKeyShareV2WithCRRequestBody, + RegisterKeyShareV2WithCRRequestBody, + RegisterEd25519V2WithCRRequestBody, + ReshareKeyShareV2WithCRRequestBody, + ReshareRegisterV2WithCRRequestBody, } from "@oko-wallet/ksn-interface/key_share"; +import type { + CommitRequestBody, + CommitResponseData, +} from "@oko-wallet/ksn-interface/commit_reveal"; +import type { OperationType } from "@oko-wallet/ksn-interface/commit_reveal"; import type { NodeStatusInfo } from "@oko-wallet/oko-types/tss"; import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { CommitRevealParams } from "@oko-wallet/oko-types/commit_reveal"; import type { Result } from "@oko-wallet/stdlib-js"; import type { KSNodeApiResponse } from "@oko-wallet/ksn-interface/response"; @@ -42,6 +48,9 @@ export async function requestKeySharesV2( secp256k1?: string; // public key hex ed25519?: string; // public key hex }, + getCommitRevealParams?: ( + nodeEndpoint: string, + ) => CommitRevealParams | undefined, ): Promise> { const shuffledNodes = [...allNodes]; for (let i = shuffledNodes.length - 1; i > 0; i -= 1) { @@ -56,7 +65,14 @@ export async function requestKeySharesV2( while (succeeded.length < threshold && nodesToTry.length > 0) { const results = await Promise.allSettled( nodesToTry.map((node) => - requestKeyShareFromNodeV2(idToken, node, authType, wallets), + requestKeyShareFromNodeV2( + idToken, + node, + authType, + wallets, + 2, + getCommitRevealParams?.(node.endpoint), + ), ), ); @@ -123,13 +139,18 @@ async function requestKeyShareFromNodeV2( ed25519?: string; }, maxRetries: number = 2, + commitReveal?: CommitRevealParams, ): Promise> { - const body: GetKeyShareV2RequestBody = { + const body: GetKeyShareV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { secp256k1: wallets.secp256k1 }), ...(wallets.ed25519 && { ed25519: wallets.ed25519 }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; let attempt = 0; @@ -220,8 +241,9 @@ export async function registerKeySharesV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, + commitReveal?: CommitRevealParams, ): Promise> { - const body: RegisterKeyShareV2RequestBody = { + const body: RegisterKeyShareV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { @@ -237,6 +259,10 @@ export async function registerKeySharesV2( }, }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -289,11 +315,16 @@ export async function registerKeyShareEd25519V2( authType: AuthType, publicKey: string, share: string, + commitReveal?: CommitRevealParams, ): Promise> { - const body: RegisterEd25519V2RequestBody = { + const body: RegisterEd25519V2WithCRRequestBody = { auth_type: authType, public_key: publicKey, share, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -351,8 +382,9 @@ export async function reshareKeySharesV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, + commitReveal?: CommitRevealParams, ): Promise> { - const body: ReshareKeyShareV2RequestBody = { + const body: ReshareKeyShareV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { @@ -368,6 +400,10 @@ export async function reshareKeySharesV2( }, }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -415,8 +451,9 @@ export async function reshareRegisterV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, + commitReveal?: CommitRevealParams, ): Promise> { - const body: ReshareRegisterV2RequestBody = { + const body: ReshareRegisterV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { @@ -432,6 +469,10 @@ export async function reshareRegisterV2( }, }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -470,3 +511,54 @@ export async function reshareRegisterV2( }; } } + +/** + * Commit to a KS node for commit-reveal scheme. + */ +export async function commitToKsNode( + nodeEndpoint: string, + sessionId: string, + operationType: OperationType, + clientEphemeralPubkey: string, + idTokenHash: string, +): Promise> { + const body: CommitRequestBody = { + session_id: sessionId, + operation_type: operationType, + client_ephemeral_pubkey: clientEphemeralPubkey, + id_token_hash: idTokenHash, + }; + + try { + const response = await fetch(`${nodeEndpoint}/keyshare/v2/commit`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + return { + success: false, + err: `Failed to commit: status(${response.status}) in ${nodeEndpoint}`, + }; + } + + const data = + (await response.json()) as KSNodeApiResponse; + if (data.success === false) { + return { + success: false, + err: `Failed to commit: ${data.code || "UNKNOWN_ERROR"} in ${nodeEndpoint}`, + }; + } + + return { success: true, data: data.data }; + } catch (e) { + return { + success: false, + err: `Failed to commit in ${nodeEndpoint}: ${String(e)}`, + }; + } +} diff --git a/embed/oko_attached/src/requests/oko_api.ts b/embed/oko_attached/src/requests/oko_api.ts index 0fcef0ebd..778ff5426 100644 --- a/embed/oko_attached/src/requests/oko_api.ts +++ b/embed/oko_attached/src/requests/oko_api.ts @@ -1,4 +1,12 @@ import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; +import type { + OperationType, + CommitRevealParams, +} from "@oko-wallet/oko-types/commit_reveal"; +import type { + CommitRequestBody, + CommitResponseData, +} from "@oko-wallet/oko-api-openapi/tss"; import type { Result } from "@oko-wallet/stdlib-js"; import type { FetchError } from "./types"; @@ -14,7 +22,7 @@ export async function makeOkoApiRequest( args: T, baseUrl: string = TSS_V1_ENDPOINT, ): Promise, FetchError>> { - let resp; + let resp: Response; try { resp = await fetch(`${baseUrl}/${path}`, { method: "POST", @@ -47,8 +55,11 @@ export async function makeAuthorizedOkoApiRequest( idToken: string, args: T, baseUrl: string = TSS_V1_ENDPOINT, + commitReveal?: CommitRevealParams, ): Promise, FetchError>> { - let resp; + const body = commitReveal ? { ...args, ...commitReveal } : args; + + let resp: Response; try { resp = await fetch(`${baseUrl}/${path}`, { method: "POST", @@ -56,7 +67,7 @@ export async function makeAuthorizedOkoApiRequest( Authorization: `Bearer ${idToken}`, "Content-Type": "application/json", }, - body: JSON.stringify(args), + body: JSON.stringify(body), }); } catch (err: any) { return { success: false, err: err }; @@ -76,3 +87,21 @@ export async function makeAuthorizedOkoApiRequest( return { success: false, err: err }; } } + +export async function commitToOkoApi( + sessionId: string, + operationType: OperationType, + clientEphemeralPubkey: string, + idTokenHash: string, +): Promise, FetchError>> { + return makeOkoApiRequest( + "commit", + { + session_id: sessionId, + operation_type: operationType, + client_ephemeral_pubkey: clientEphemeralPubkey, + id_token_hash: idTokenHash, + }, + TSS_V2_ENDPOINT, + ); +} diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts index 9639c1f3d..c10ac3047 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts @@ -16,6 +16,7 @@ import { TSS_V2_ENDPOINT, SOCIAL_LOGIN_V2_ENDPOINT, } from "@oko-wallet-attached/requests/oko_api"; +import type { CommitRevealParams } from "@oko-wallet/oko-types/commit_reveal"; import { combineUserShares } from "@oko-wallet-attached/crypto/combine"; import type { UserSignInResultV2 } from "@oko-wallet-attached/window_msgs/types"; import type { FetchError } from "@oko-wallet-attached/requests/types"; @@ -28,6 +29,12 @@ import { reshareKeySharesV2, reshareRegisterV2, } from "@oko-wallet-attached/requests/ks_node_v2"; +import { + commitAll, + createOkoApiSignature, + createKsnSignature, + type KsnCommitTarget, +} from "@oko-wallet-attached/crypto/commit_reveal"; import type { ReshareRequestV2 } from "@oko-wallet/oko-types/user"; import { decodeKeyShareStringToPoint256, @@ -109,20 +116,60 @@ export async function handleNewUserV2( } const secp256k1UserKeyShares = splitUserKeySharesRes.data; + // 4. Commit to oko_api and KSN nodes + const ksnCommitTargets: KsnCommitTarget[] = keyshareNodeMeta.nodes.map( + (node) => ({ + nodeUrl: node.endpoint, + operationType: "sign_up" as const, + }), + ); + const commitRes = await commitAll( + "sign_up", + authType, + idToken, + ksnCommitTargets, + ksnCommitTargets.length, // all nodes for sign_up + ); + if (!commitRes.success) { + return { + success: false, + err: { type: "sign_in_request_fail", error: commitRes.err }, + }; + } + const { session } = commitRes.data; + // 5. Send key shares by both curves to ks nodes using V2 API const registerKeySharesResults: Result[] = await Promise.all( - secp256k1UserKeyShares.map((keyShareByNode, index) => - registerKeySharesV2(keyShareByNode.node.endpoint, idToken, authType, { - secp256k1: { - public_key: secp256k1Keygen1.public_key.toHex(), - share: encodePoint256ToKeyShareString(keyShareByNode.share), - }, - ed25519: { - public_key: ed25519Keygen1.public_key.toHex(), - share: teddsaKeyShareToHex(ed25519UserKeyShares[index].share), + secp256k1UserKeyShares.map((keyShareByNode, index) => { + const ksnSigRes = createKsnSignature( + session, + keyShareByNode.node.endpoint, + "register", + ); + if (!ksnSigRes.success) { + return Promise.resolve({ success: false, err: ksnSigRes.err } as const); + } + const commitRevealParams: CommitRevealParams = { + cr_session_id: session.session_id, + cr_signature: ksnSigRes.data, + }; + return registerKeySharesV2( + keyShareByNode.node.endpoint, + idToken, + authType, + { + secp256k1: { + public_key: secp256k1Keygen1.public_key.toHex(), + share: encodePoint256ToKeyShareString(keyShareByNode.share), + }, + ed25519: { + public_key: ed25519Keygen1.public_key.toHex(), + share: teddsaKeyShareToHex(ed25519UserKeyShares[index].share), + }, }, - }), - ), + commitRevealParams, + ); + }), ); const registerErrResults = registerKeySharesResults.filter( (result) => result.success === false, @@ -138,6 +185,13 @@ export async function handleNewUserV2( } // 6. Call V2 keygen API with both curve types + const okoApiSigRes = createOkoApiSignature(session, "keygen"); + if (!okoApiSigRes.success) { + return { + success: false, + err: { type: "sign_in_request_fail", error: okoApiSigRes.err }, + }; + } const reqKeygenV2Res = await reqKeygenV2( TSS_V2_ENDPOINT, { @@ -156,6 +210,10 @@ export async function handleNewUserV2( }, }, idToken, + { + cr_session_id: session.session_id, + cr_signature: okoApiSigRes.data, + }, ); if (reqKeygenV2Res.success === false) { return { @@ -209,14 +267,45 @@ export async function handleExistingUserV2( keyshareNodeMetaEd25519: KeyShareNodeMetaWithNodeStatusInfo, authType: AuthType, ): Promise> { - // 1. Sign in to API server - const signInResult = await signInV2(idToken, authType); + // 1. Commit to oko_api and KSN nodes + const ksnCommitTargets: KsnCommitTarget[] = + keyshareNodeMetaSecp256k1.nodes.map((node) => ({ + nodeUrl: node.endpoint, + operationType: "sign_in" as const, + })); + const commitRes = await commitAll( + "sign_in", + authType, + idToken, + ksnCommitTargets, + keyshareNodeMetaSecp256k1.threshold, // threshold for sign_in + ); + if (!commitRes.success) { + return { + success: false, + err: { type: "sign_in_request_fail", error: commitRes.err }, + }; + } + const { session } = commitRes.data; + + // 2. Sign in to API server + const okoApiSigRes = createOkoApiSignature(session, "signin"); + if (!okoApiSigRes.success) { + return { + success: false, + err: { type: "sign_in_request_fail", error: okoApiSigRes.err }, + }; + } + const signInResult = await signInV2(idToken, authType, { + cr_session_id: session.session_id, + cr_signature: okoApiSigRes.data, + }); if (!signInResult.success) { return { success: false, err: signInResult.err }; } const signInResp = signInResult.data; - // 2. Request secp256k1 and ed25519 shares from KS nodes using V2 API + // 3. Request secp256k1 and ed25519 shares from KS nodes using V2 API const requestSharesRes = await requestKeySharesV2( idToken, keyshareNodeMetaSecp256k1.nodes, @@ -226,6 +315,8 @@ export async function handleExistingUserV2( secp256k1: signInResp.user.public_key_secp256k1, ed25519: signInResp.user.public_key_ed25519, }, + (nodeEndpoint) => + createKsnCommitRevealParams(session, nodeEndpoint, "get_key_shares"), ); if (!requestSharesRes.success) { const error = requestSharesRes.err; @@ -1085,6 +1176,24 @@ interface SignInRequestV2 { auth_type: AuthType; } +/** + * Create commit-reveal params for KSN API calls. + */ +function createKsnCommitRevealParams( + session: Parameters[0], + nodeEndpoint: string, + apiName: Parameters[2], +): CommitRevealParams | undefined { + const ksnSigRes = createKsnSignature(session, nodeEndpoint, apiName); + if (!ksnSigRes.success) { + return undefined; + } + return { + cr_session_id: session.session_id, + cr_signature: ksnSigRes.data, + }; +} + /** * Sign in to API server and return user data. * Used by handlers that need to authenticate before requesting shares. @@ -1092,11 +1201,20 @@ interface SignInRequestV2 { async function signInV2( idToken: string, authType: AuthType, -): Promise> { + commitReveal?: CommitRevealParams, +): Promise< + Result +> { const signInRes = await makeAuthorizedOkoApiRequest< SignInRequestV2, SignInResponseV2 - >("user/signin", idToken, { auth_type: authType }, TSS_V2_ENDPOINT); + >( + "user/signin", + idToken, + { auth_type: authType }, + TSS_V2_ENDPOINT, + commitReveal, + ); if (!signInRes.success) { console.error("[attached] sign in failed, err: %s", signInRes.err); diff --git a/key_share_node/ksn_interface/src/commit_reveal.ts b/key_share_node/ksn_interface/src/commit_reveal.ts index 227ff1873..988b4f1ee 100644 --- a/key_share_node/ksn_interface/src/commit_reveal.ts +++ b/key_share_node/ksn_interface/src/commit_reveal.ts @@ -2,6 +2,7 @@ export type OperationType = | "sign_in" | "sign_up" | "sign_in_reshare" + | "sign_in_reshare_ed25519" | "register_reshare" | "add_ed25519"; @@ -39,3 +40,15 @@ export interface CreateSessionParams { id_token_hash: string; expires_at: Date; } + +export interface CommitRequestBody { + session_id: string; + operation_type: OperationType; + client_ephemeral_pubkey: string; + id_token_hash: string; +} + +export interface CommitResponseData { + node_pubkey: string; + node_signature: string; +} diff --git a/key_share_node/ksn_interface/src/key_share.ts b/key_share_node/ksn_interface/src/key_share.ts index ed02f5eb9..af301e2bd 100644 --- a/key_share_node/ksn_interface/src/key_share.ts +++ b/key_share_node/ksn_interface/src/key_share.ts @@ -311,3 +311,46 @@ export interface ReshareRegisterV2RequestBody { // --- Internal Helper Types --- export type CheckWalletResult = { exists: boolean } | { error: string }; + +// ============================================================================ +// v2 API Types with Commit-Reveal +// ============================================================================ + +/** + * Commit-reveal fields for KSN v2 API requests. + * Optional fields to support commit-reveal authentication scheme. + */ +export interface CommitRevealFields { + cr_session_id?: string; + cr_signature?: string; +} + +/** + * GET /v2/keyshare request body with commit-reveal fields + */ +export type GetKeyShareV2WithCRRequestBody = GetKeyShareV2RequestBody & + CommitRevealFields; + +/** + * POST /v2/keyshare/register request body with commit-reveal fields + */ +export type RegisterKeyShareV2WithCRRequestBody = + RegisterKeyShareV2RequestBody & CommitRevealFields; + +/** + * POST /v2/keyshare/register/ed25519 request body with commit-reveal fields + */ +export type RegisterEd25519V2WithCRRequestBody = RegisterEd25519V2RequestBody & + CommitRevealFields; + +/** + * POST /v2/keyshare/reshare request body with commit-reveal fields + */ +export type ReshareKeyShareV2WithCRRequestBody = ReshareKeyShareV2RequestBody & + CommitRevealFields; + +/** + * POST /v2/keyshare/reshare/register request body with commit-reveal fields + */ +export type ReshareRegisterV2WithCRRequestBody = ReshareRegisterV2RequestBody & + CommitRevealFields; diff --git a/key_share_node/server/src/commit_reveal/allowed_apis.ts b/key_share_node/server/src/commit_reveal/allowed_apis.ts index 1684eeb8d..b3fcfe534 100644 --- a/key_share_node/server/src/commit_reveal/allowed_apis.ts +++ b/key_share_node/server/src/commit_reveal/allowed_apis.ts @@ -7,14 +7,16 @@ export const ALLOWED_APIS = { sign_in: ["get_key_shares"], sign_up: ["register"], sign_in_reshare: ["get_key_shares", "reshare"], + sign_in_reshare_ed25519: ["get_key_shares", "reshare", "register_ed25519"], register_reshare: ["get_key_shares", "reshare_register"], add_ed25519: ["register_ed25519", "get_key_shares"], }; -export const FINAL_APIS = { +export const FINAL_APIS: Record = { sign_in: ["get_key_shares"], sign_up: ["register"], sign_in_reshare: ["reshare"], + sign_in_reshare_ed25519: ["register_ed25519"], register_reshare: ["reshare_register"], add_ed25519: ["get_key_shares"], }; @@ -23,12 +25,12 @@ export function isApiAllowed( operationType: OperationType, apiName: ApiName, ): boolean { - return ALLOWED_APIS[operationType].includes(apiName); + return (ALLOWED_APIS[operationType]).includes(apiName); } export function isFinalApi( operationType: OperationType, apiName: ApiName, ): boolean { - return FINAL_APIS[operationType].includes(apiName); + return (FINAL_APIS[operationType]).includes(apiName); } diff --git a/key_share_node/server/src/middlewares/commit_reveal.test.ts b/key_share_node/server/src/middlewares/commit_reveal.test.ts index d4ddfb6c0..f05187efc 100644 --- a/key_share_node/server/src/middlewares/commit_reveal.test.ts +++ b/key_share_node/server/src/middlewares/commit_reveal.test.ts @@ -352,6 +352,31 @@ describe("commit_reveal_middleware_test", () => { expect(response.body.success).toBe(true); }); + it("should pass middleware with valid signature for sign_in_reshare_ed25519 operation", async () => { + const ctx = createTestContext({ + operationType: "sign_in_reshare_ed25519", + apiName: "register_ed25519", + }); + await createSession(pool, ctx); + + const signature = createRevealSignature( + ctx, + mockServerKeypair.publicKey.toHex(), + ); + + const response = await request(app) + .post("/test/register_ed25519") + .set("Authorization", `Bearer ${ctx.idToken}`) + .send({ + cr_session_id: ctx.sessionId, + cr_signature: signature, + auth_type: ctx.authType, + }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + it("should default auth_type to google if not provided", async () => { const ctx = createTestContext({ operationType: "sign_in", @@ -1162,6 +1187,78 @@ describe("commit_reveal_middleware_test", () => { expect(sessionRes.data?.state).toBe("COMPLETED"); }); + it("should NOT update session to COMPLETED when non-final API is called for sign_in_reshare_ed25519", async () => { + const ctx = createTestContext({ + operationType: "sign_in_reshare_ed25519", + apiName: "get_key_shares", // NOT final API for sign_in_reshare_ed25519 (final is register_ed25519) + }); + await createSession(pool, ctx); + + const signature = createRevealSignature( + ctx, + mockServerKeypair.publicKey.toHex(), + ); + + await request(app) + .post("/test/get_key_shares") + .set("Authorization", `Bearer ${ctx.idToken}`) + .send({ + cr_session_id: ctx.sessionId, + cr_signature: signature, + auth_type: ctx.authType, + }) + .expect(200); + + // Wait for res.on('finish') to complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify session is still COMMITTED (not COMPLETED) + const sessionRes = await getCommitRevealSessionBySessionId( + pool, + ctx.sessionId, + ); + if (!sessionRes.success) { + throw new Error(`Failed to get session: ${sessionRes.err}`); + } + expect(sessionRes.data?.state).toBe("COMMITTED"); + }); + + it("should update session to COMPLETED when final API is called for sign_in_reshare_ed25519", async () => { + const ctx = createTestContext({ + operationType: "sign_in_reshare_ed25519", + apiName: "register_ed25519", // final API for sign_in_reshare_ed25519 + }); + await createSession(pool, ctx); + + const signature = createRevealSignature( + ctx, + mockServerKeypair.publicKey.toHex(), + ); + + await request(app) + .post("/test/register_ed25519") + .set("Authorization", `Bearer ${ctx.idToken}`) + .send({ + cr_session_id: ctx.sessionId, + cr_signature: signature, + auth_type: ctx.authType, + }) + .expect(200); + + // Wait for res.on('finish') to complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify session is COMPLETED + const sessionRes = await getCommitRevealSessionBySessionId( + pool, + ctx.sessionId, + ); + if (!sessionRes.success) { + throw new Error(`Failed to get session: ${sessionRes.err}`); + } + expect(sessionRes.data?.state).toBe("COMPLETED"); + }); + it("should NOT update session to COMPLETED when API fails", async () => { const ctx = createTestContext({ operationType: "sign_in", diff --git a/key_share_node/server/src/middlewares/commit_reveal.ts b/key_share_node/server/src/middlewares/commit_reveal.ts index b3b379e42..78c6b6be5 100644 --- a/key_share_node/server/src/middlewares/commit_reveal.ts +++ b/key_share_node/server/src/middlewares/commit_reveal.ts @@ -1,17 +1,14 @@ import type { Request, Response, NextFunction } from "express"; import { Bytes } from "@oko-wallet/bytes"; import { verifySignature } from "@oko-wallet/crypto-js/node/ecdhe"; -import { sha256 } from "@oko-wallet/crypto-js"; +import { sha256, buildRevealMessage } from "@oko-wallet/crypto-js"; import { getCommitRevealSessionBySessionId, createCommitRevealApiCall, updateCommitRevealSessionState, hasCommitRevealApiBeenCalled, } from "@oko-wallet/ksn-pg-interface/commit_reveal"; -import type { - ApiName, - CommitRevealSession, -} from "@oko-wallet/ksn-interface/commit_reveal"; +import type { ApiName } from "@oko-wallet/ksn-interface/commit_reveal"; import { ErrorCodeMap } from "@oko-wallet-ksn-server/error"; import { isApiAllowed, isFinalApi } from "@oko-wallet-ksn-server/commit_reveal"; @@ -171,12 +168,12 @@ export function commitRevealMiddleware(apiName: ApiName) { } const nodePubkeyHex = state.serverKeypair.publicKey.toHex(); - const message = makeSigMessage({ + const message = buildRevealMessage({ nodePubkeyHex, - cr_session_id, + sessionId: cr_session_id, authType, idToken, - session, + operationType: session.operation_type, apiName, }); const rBytes = Bytes.fromUint8Array( @@ -259,31 +256,3 @@ export function commitRevealMiddleware(apiName: ApiName) { next(); }; } - -export interface SigMessageArgs { - nodePubkeyHex: string; - cr_session_id: string; - authType: string; - idToken: string; - session: CommitRevealSession; - apiName: ApiName; -} - -// message = node_pubkey + session_id + auth_type + id_token + operation_type + api_name -function makeSigMessage({ - nodePubkeyHex, - cr_session_id, - authType, - idToken, - session, - apiName, -}: SigMessageArgs) { - return ( - nodePubkeyHex + - cr_session_id + - authType + - idToken + - session.operation_type + - apiName - ); -} diff --git a/key_share_node/server/src/openapi/schema/commit_reveal.ts b/key_share_node/server/src/openapi/schema/commit_reveal.ts index efcb2ccc1..3e8c24617 100644 --- a/key_share_node/server/src/openapi/schema/commit_reveal.ts +++ b/key_share_node/server/src/openapi/schema/commit_reveal.ts @@ -7,6 +7,7 @@ export const operationTypeSchema = z "sign_in", "sign_up", "sign_in_reshare", + "sign_in_reshare_ed25519", "register_reshare", "add_ed25519", ]) diff --git a/key_share_node/server/src/routes/key_share_v2/commit.test.ts b/key_share_node/server/src/routes/key_share_v2/commit.test.ts index bd7b502b8..f6189ad2c 100644 --- a/key_share_node/server/src/routes/key_share_v2/commit.test.ts +++ b/key_share_node/server/src/routes/key_share_v2/commit.test.ts @@ -168,6 +168,21 @@ describe("commit_route_test", () => { expect(response.body.data).toBeDefined(); }); + it("should successfully create session with sign_in_reshare_ed25519 operation", async () => { + const body = { + ...createValidBody(), + operation_type: "sign_in_reshare_ed25519", + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeDefined(); + }); + it("should create multiple sessions with different session_ids", async () => { const body1 = createValidBody(); const body2 = createValidBody(); diff --git a/key_share_node/server/src/routes/key_share_v2/e2e.test.ts b/key_share_node/server/src/routes/key_share_v2/e2e.test.ts index bb080e905..57e32d390 100644 --- a/key_share_node/server/src/routes/key_share_v2/e2e.test.ts +++ b/key_share_node/server/src/routes/key_share_v2/e2e.test.ts @@ -46,6 +46,8 @@ const TEST_SECP256K1_PK = "028812785B3F855F677594A6FEB76CA3FD39F2CA36AC5A8454A1417C4232AC566D"; const TEST_ED25519_PK = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; +const TEST_ED25519_PK_2 = + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"; const TEST_ENC_SECRET = "test_enc_secret"; // Generate random 64-byte share (hex) @@ -643,6 +645,231 @@ describe("key_share_v2_commit_reveal_e2e_test", () => { }); }); + describe("sign_in_reshare_ed25519 flow (commit → get_key_shares → reshare → register_ed25519)", () => { + it("should complete sign_in_reshare_ed25519 flow with all three API calls", async () => { + // First, register a user + const signUpCtx = createE2ETestContext({ operationType: "sign_up" }); + const signUpIdTokenHash = computeIdTokenHash( + signUpCtx.authType, + signUpCtx.idToken, + ); + + await request(app) + .post("/keyshare/v2/commit") + .send({ + session_id: signUpCtx.sessionId, + operation_type: signUpCtx.operationType, + client_ephemeral_pubkey: signUpCtx.clientKeypair.publicKey.toHex(), + id_token_hash: signUpIdTokenHash, + }) + .expect(200); + + const registerSignature = createRevealSignature( + signUpCtx, + mockServerKeypair.publicKey.toHex(), + "register", + ); + + const secp256k1Share = generateRandomShare(); + const ed25519Share = generateRandomShare(); + + await request(app) + .post("/keyshare/v2/register") + .set("Authorization", `Bearer ${signUpCtx.idToken}`) + .set("x-mock-user-id", signUpCtx.userIdentifier) + .send({ + cr_session_id: signUpCtx.sessionId, + cr_signature: registerSignature, + auth_type: signUpCtx.authType, + wallets: { + secp256k1: { + public_key: TEST_SECP256K1_PK, + share: secp256k1Share, + }, + ed25519: { + public_key: TEST_ED25519_PK, + share: ed25519Share, + }, + }, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Now sign_in_reshare_ed25519 flow + const reshareEd25519Ctx = createE2ETestContext({ + operationType: "sign_in_reshare_ed25519", + userIdentifier: signUpCtx.userIdentifier, + }); + const reshareEd25519IdTokenHash = computeIdTokenHash( + reshareEd25519Ctx.authType, + reshareEd25519Ctx.idToken, + ); + + // Commit for sign_in_reshare_ed25519 + await request(app) + .post("/keyshare/v2/commit") + .send({ + session_id: reshareEd25519Ctx.sessionId, + operation_type: reshareEd25519Ctx.operationType, + client_ephemeral_pubkey: + reshareEd25519Ctx.clientKeypair.publicKey.toHex(), + id_token_hash: reshareEd25519IdTokenHash, + }) + .expect(200); + + // Step 1: get_key_shares (non-final API) + const getSignature = createRevealSignature( + reshareEd25519Ctx, + mockServerKeypair.publicKey.toHex(), + "get_key_shares", + ); + + await request(app) + .post("/keyshare/v2") + .set("Authorization", `Bearer ${reshareEd25519Ctx.idToken}`) + .set("x-mock-user-id", reshareEd25519Ctx.userIdentifier) + .send({ + cr_session_id: reshareEd25519Ctx.sessionId, + cr_signature: getSignature, + auth_type: reshareEd25519Ctx.authType, + wallets: { + secp256k1: TEST_SECP256K1_PK, + }, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify session is still COMMITTED + let sessionRes = await getCommitRevealSessionBySessionId( + pool, + reshareEd25519Ctx.sessionId, + ); + expect(sessionRes.success).toBe(true); + if (sessionRes.success) { + expect(sessionRes.data?.state).toBe("COMMITTED"); + } + + // Step 2: reshare (non-final API for sign_in_reshare_ed25519) + const reshareSignature = createRevealSignature( + reshareEd25519Ctx, + mockServerKeypair.publicKey.toHex(), + "reshare", + ); + + await request(app) + .post("/keyshare/v2/reshare") + .set("Authorization", `Bearer ${reshareEd25519Ctx.idToken}`) + .set("x-mock-user-id", reshareEd25519Ctx.userIdentifier) + .send({ + cr_session_id: reshareEd25519Ctx.sessionId, + cr_signature: reshareSignature, + auth_type: reshareEd25519Ctx.authType, + wallets: { + secp256k1: { + public_key: TEST_SECP256K1_PK, + share: secp256k1Share, + }, + }, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify session is still COMMITTED (reshare is not final for sign_in_reshare_ed25519) + sessionRes = await getCommitRevealSessionBySessionId( + pool, + reshareEd25519Ctx.sessionId, + ); + expect(sessionRes.success).toBe(true); + if (sessionRes.success) { + expect(sessionRes.data?.state).toBe("COMMITTED"); + } + + // Step 3: register_ed25519 (final API for sign_in_reshare_ed25519) + // Use a different ed25519 public key to avoid DUPLICATE_PUBLIC_KEY error + const registerEd25519Signature = createRevealSignature( + reshareEd25519Ctx, + mockServerKeypair.publicKey.toHex(), + "register_ed25519", + ); + + const newEd25519Share = generateRandomShare(); + + await request(app) + .post("/keyshare/v2/register/ed25519") + .set("Authorization", `Bearer ${reshareEd25519Ctx.idToken}`) + .set("x-mock-user-id", reshareEd25519Ctx.userIdentifier) + .send({ + cr_session_id: reshareEd25519Ctx.sessionId, + cr_signature: registerEd25519Signature, + auth_type: reshareEd25519Ctx.authType, + public_key: TEST_ED25519_PK_2, + share: newEd25519Share, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify session is now COMPLETED + sessionRes = await getCommitRevealSessionBySessionId( + pool, + reshareEd25519Ctx.sessionId, + ); + expect(sessionRes.success).toBe(true); + if (sessionRes.success) { + expect(sessionRes.data?.state).toBe("COMPLETED"); + } + }); + + it("should reject register (not register_ed25519) for sign_in_reshare_ed25519", async () => { + const ctx = createE2ETestContext({ + operationType: "sign_in_reshare_ed25519", + }); + const idTokenHash = computeIdTokenHash(ctx.authType, ctx.idToken); + + // Commit with sign_in_reshare_ed25519 operation + await request(app) + .post("/keyshare/v2/commit") + .send({ + session_id: ctx.sessionId, + operation_type: ctx.operationType, + client_ephemeral_pubkey: ctx.clientKeypair.publicKey.toHex(), + id_token_hash: idTokenHash, + }) + .expect(200); + + // Try to call register (not allowed for sign_in_reshare_ed25519) + const registerSignature = createRevealSignature( + ctx, + mockServerKeypair.publicKey.toHex(), + "register", + ); + + const response = await request(app) + .post("/keyshare/v2/register") + .set("Authorization", `Bearer ${ctx.idToken}`) + .set("x-mock-user-id", ctx.userIdentifier) + .send({ + cr_session_id: ctx.sessionId, + cr_signature: registerSignature, + auth_type: ctx.authType, + wallets: { + secp256k1: { + public_key: TEST_SECP256K1_PK, + share: generateRandomShare(), + }, + }, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("not allowed"); + }); + }); + describe("add_ed25519 flow (commit → register_ed25519 → get_key_shares)", () => { it("should reject adding same ed25519 public key twice", async () => { // First, sign up with both wallets