From 429dab68e19c5b11e2f059ecfc9ce80cca7dd0a4 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Thu, 29 Jan 2026 19:02:15 +0900 Subject: [PATCH 01/15] merge OKO-575 --- .../server/src/commit_reveal/allowed_apis.ts | 32 + .../src/middleware/commit_reveal.test.ts | 1169 +++++++++++++++++ .../server/src/middleware/commit_reveal.ts | 252 ++++ .../server/src/routes/tss_v2/commit.test.ts | 410 ++++++ .../server/src/routes/tss_v2/commit.ts | 173 +++ .../server/src/routes/tss_v2/e2e.test.ts | 786 +++++++++++ .../oko_api/server/src/routes/tss_v2/index.ts | 15 +- backend/oko_api_error_codes/src/index.ts | 5 + backend/openapi/src/tss/commit_reveal.ts | 106 ++ backend/openapi/src/tss/index.ts | 1 + backend/openapi/src/tss/keygen_ed25519.ts | 13 +- backend/openapi/src/tss/request.ts | 77 +- common/oko_types/src/api_response/index.ts | 5 + common/oko_types/src/commit_reveal/index.ts | 11 +- .../ksn_interface/src/commit_reveal.ts | 7 + .../server/src/commit_reveal/allowed_apis.ts | 24 +- .../server/src/middlewares/commit_reveal.ts | 2 +- 17 files changed, 3030 insertions(+), 58 deletions(-) create mode 100644 backend/oko_api/server/src/commit_reveal/allowed_apis.ts create mode 100644 backend/oko_api/server/src/middleware/commit_reveal.test.ts create mode 100644 backend/oko_api/server/src/middleware/commit_reveal.ts create mode 100644 backend/oko_api/server/src/routes/tss_v2/commit.test.ts create mode 100644 backend/oko_api/server/src/routes/tss_v2/commit.ts create mode 100644 backend/oko_api/server/src/routes/tss_v2/e2e.test.ts create mode 100644 backend/openapi/src/tss/commit_reveal.ts diff --git a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts new file mode 100644 index 000000000..da7d3f9a2 --- /dev/null +++ b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts @@ -0,0 +1,32 @@ +import type { + OperationType, + ApiName, +} from "@oko-wallet/oko-types/commit_reveal"; + +export const ALLOWED_APIS: Record = { + sign_in: ["signin"], + sign_up: ["keygen"], + sign_in_reshare: ["signin", "reshare"], + add_ed25519: ["keygen_ed25519"], +}; + +export const FINAL_APIS: Record = { + sign_in: ["signin"], + sign_up: ["keygen"], + sign_in_reshare: ["reshare"], + add_ed25519: ["keygen_ed25519"], +}; + +export function isApiAllowed( + operationType: OperationType, + apiName: ApiName, +): boolean { + return (ALLOWED_APIS[operationType]).includes(apiName); +} + +export function isFinalApi( + operationType: OperationType, + apiName: ApiName, +): boolean { + return (FINAL_APIS[operationType]).includes(apiName); +} diff --git a/backend/oko_api/server/src/middleware/commit_reveal.test.ts b/backend/oko_api/server/src/middleware/commit_reveal.test.ts new file mode 100644 index 000000000..f70ec3c26 --- /dev/null +++ b/backend/oko_api/server/src/middleware/commit_reveal.test.ts @@ -0,0 +1,1169 @@ +import request from "supertest"; +import express from "express"; +import type { Pool } from "pg"; +import { Bytes } from "@oko-wallet/bytes"; +import { randomBytes } from "node:crypto"; +import { v4 as uuidv4 } from "uuid"; +import { createPgConn } from "@oko-wallet/postgres-lib"; +import winston from "winston"; +import { sha256 } from "@oko-wallet/crypto-js"; +import { + generateEddsaKeypair, + signMessage, + convertEddsaSignatureToBytes, +} from "@oko-wallet/crypto-js/node/ecdhe"; + +import { testPgConfig } from "@oko-wallet-api/database/test_config"; +import { resetPgDatabase } from "@oko-wallet-api/testing/database"; +import { commitRevealMiddleware } from "./commit_reveal"; + +// Mock keypair for testing +const privateKeyRes = Bytes.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001", + 32, +); +const publicKeyRes = Bytes.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000002", + 32, +); +if (!privateKeyRes.success || !publicKeyRes.success) { + throw new Error("Failed to create mock keypair"); +} +const mockServerKeypair = { + privateKey: privateKeyRes.data, + publicKey: publicKeyRes.data, +}; + +const testLogger = winston.createLogger({ + level: "error", + silent: true, + transports: [new winston.transports.Console()], +}); + +function generateRandomHex(bytes: number): string { + return randomBytes(bytes).toString("hex"); +} + +describe("commit_reveal_middleware_basic_validation_test", () => { + let pool: Pool; + let app: express.Application; + + beforeAll(async () => { + const config = testPgConfig; + const createPostgresRes = await createPgConn({ + database: config.database, + host: config.host, + password: config.password, + user: config.user, + port: config.port, + ssl: config.ssl, + }); + + if (createPostgresRes.success === false) { + console.error(createPostgresRes.err); + throw new Error("Failed to create postgres database"); + } + + pool = createPostgresRes.data; + + app = express(); + 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.locals.db = pool; + app.locals.server_keypair = mockServerKeypair; + app.locals.logger = testLogger; + }); + + beforeEach(async () => { + await resetPgDatabase(pool); + }); + + afterAll(async () => { + await pool.end(); + }); + + // Helper to create a session directly in DB + async function createSession(params: { + session_id: string; + operation_type: string; + client_ephemeral_pubkey: string; + id_token_hash: string; + state?: string; + expires_at?: Date; + }) { + const expiresAt = params.expires_at ?? new Date(Date.now() + 5 * 60 * 1000); + const state = params.state ?? "COMMITTED"; + + await pool.query( + `INSERT INTO "commit_reveal_sessions" (session_id, operation_type, client_ephemeral_pubkey, id_token_hash, state, expires_at) VALUES ($1, $2, $3, $4, $5, $6)`, + [ + params.session_id, + params.operation_type, + Buffer.from(params.client_ephemeral_pubkey, "hex"), + params.id_token_hash, + state, + expiresAt, + ], + ); + } + + describe("missing required fields", () => { + it("should return 400 when cr_session_id is missing", async () => { + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_signature: generateRandomHex(64), + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("cr_session_id"); + }); + + it("should return 400 when cr_signature is missing", async () => { + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: uuidv4(), + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("cr_signature"); + }); + + it("should return 401 when Authorization header is missing", async () => { + const sessionId = uuidv4(); + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + }); + + const response = await request(app) + .post("/test/keygen") + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + }) + .expect(401); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("UNAUTHORIZED"); + }); + }); + + describe("session validation", () => { + it("should return 404 when session does not exist", async () => { + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: uuidv4(), + cr_signature: generateRandomHex(64), + }) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_NOT_FOUND"); + }); + + it("should return 400 when session is not in COMMITTED state", async () => { + const sessionId = uuidv4(); + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + state: "COMPLETED", + }); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("COMMITTED"); + }); + + it("should return 410 when session has expired", async () => { + const sessionId = uuidv4(); + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + expires_at: new Date(Date.now() - 1000), // expired 1 second ago + }); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + }) + .expect(410); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_EXPIRED"); + }); + }); + + describe("operation-API validation", () => { + it("should return 400 when API is not allowed for operation", async () => { + const sessionId = uuidv4(); + // Create session with sign_in operation + await createSession({ + session_id: sessionId, + operation_type: "sign_in", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + }); + + // Try to call keygen API (not allowed for sign_in) + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + }) + .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"); + }); + + it("should return 400 when sign_in operation tries to call keygen", async () => { + const sessionId = uuidv4(); + await createSession({ + session_id: sessionId, + operation_type: "sign_in", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + }); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + }); + + it("should return 400 when sign_up operation tries to call signin", async () => { + const sessionId = uuidv4(); + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + }); + + const response = await request(app) + .post("/test/signin") + .set("Authorization", "Bearer test_token") + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("signin"); + expect(response.body.msg).toContain("sign_up"); + }); + }); + + describe("id_token_hash validation", () => { + it("should return 400 when id_token does not match committed hash", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const originalIdToken = "original_id_token"; + const wrongIdToken = "wrong_id_token"; + + // Compute hash with original token + const hashRes = sha256(`${authType}${originalIdToken}`); + if (!hashRes.success) throw new Error("Failed to compute hash"); + const idTokenHash = hashRes.data.toHex(); + + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: idTokenHash, + }); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${wrongIdToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("id_token_hash mismatch"); + }); + + it("should return 400 when auth_type does not match committed hash", async () => { + const sessionId = uuidv4(); + const originalAuthType = "google"; + const wrongAuthType = "auth0"; + const idToken = "test_id_token"; + + // Compute hash with original auth_type + const hashRes = sha256(`${originalAuthType}${idToken}`); + if (!hashRes.success) throw new Error("Failed to compute hash"); + const idTokenHash = hashRes.data.toHex(); + + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: idTokenHash, + }); + + // Send with wrong auth_type + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(64), + auth_type: wrongAuthType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("id_token_hash mismatch"); + }); + }); + + describe("signature validation", () => { + it("should return 400 when signature format is invalid hex", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) throw new Error("Failed to compute hash"); + + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: hashRes.data.toHex(), + }); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: "invalid_hex_signature_not_valid", + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_SIGNATURE"); + }); + + it("should return 400 when signature length is wrong", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) throw new Error("Failed to compute hash"); + + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: hashRes.data.toHex(), + }); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: generateRandomHex(32), // 32 bytes instead of 64 + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_SIGNATURE"); + }); + + it("should return 400 when signature verification fails (wrong message)", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + // Generate client keypair + 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_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + // Sign wrong message + const wrongMessage = "wrong_message"; + const signRes = signMessage(wrongMessage, clientKeypair.privateKey); + 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"); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: sigBytesRes.data.toHex(), + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_SIGNATURE"); + }); + + it("should return 400 when signed with different keypair", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + // Generate two different keypairs + const clientKeypairRes = generateEddsaKeypair(); + const wrongKeypairRes = generateEddsaKeypair(); + if (!clientKeypairRes.success || !wrongKeypairRes.success) { + throw new Error("Failed to generate keypairs"); + } + const clientKeypair = clientKeypairRes.data; + const wrongKeypair = wrongKeypairRes.data; + + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) throw new Error("Failed to compute hash"); + + // Create session with client keypair + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + // Create correct message but sign with wrong keypair + 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"); + + const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); + if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: sigBytesRes.data.toHex(), + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_SIGNATURE"); + }); + }); + + describe("auth_type handling", () => { + it("should use google as default when auth_type is not provided", async () => { + const sessionId = uuidv4(); + const idToken = "test_id_token"; + + // Compute hash with google as auth_type (default) + const hashRes = sha256(`google${idToken}`); + 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"); + const clientKeypair = clientKeypairRes.data; + + await createSession({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + // Create correct message with google as auth_type + 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"); + + const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); + if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + + // Send without auth_type - should default to google + const response = await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: sigBytesRes.data.toHex(), + // auth_type not provided + }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); +}); + +describe("commit_reveal_middleware_replay_and_session_test", () => { + let pool: Pool; + let app: express.Application; + + beforeAll(async () => { + const config = testPgConfig; + const createPostgresRes = await createPgConn({ + database: config.database, + host: config.host, + password: config.password, + user: config.user, + port: config.port, + ssl: config.ssl, + }); + + if (createPostgresRes.success === false) { + console.error(createPostgresRes.err); + throw new Error("Failed to create postgres database"); + } + + pool = createPostgresRes.data; + + app = express(); + app.use(express.json()); + + // Final API routes (keygen for sign_up, signin for sign_in) + 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/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.locals.db = pool; + app.locals.server_keypair = mockServerKeypair; + app.locals.logger = testLogger; + }); + + beforeEach(async () => { + await resetPgDatabase(pool); + }); + + afterAll(async () => { + await pool.end(); + }); + + async function createSession(params: { + session_id: string; + operation_type: string; + client_ephemeral_pubkey: string; + id_token_hash: string; + state?: string; + expires_at?: Date; + }) { + const expiresAt = params.expires_at ?? new Date(Date.now() + 5 * 60 * 1000); + const state = params.state ?? "COMMITTED"; + + await pool.query( + `INSERT INTO "commit_reveal_sessions" (session_id, operation_type, client_ephemeral_pubkey, id_token_hash, state, expires_at) VALUES ($1, $2, $3, $4, $5, $6)`, + [ + params.session_id, + params.operation_type, + Buffer.from(params.client_ephemeral_pubkey, "hex"), + params.id_token_hash, + state, + expiresAt, + ], + ); + } + + async function getSessionState(sessionId: string): Promise { + const result = await pool.query( + `SELECT state FROM "commit_reveal_sessions" WHERE session_id = $1`, + [sessionId], + ); + return result.rows[0]?.state ?? null; + } + + 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], + ); + return parseInt(result.rows[0].count, 10); + } + + function createValidSignature( + clientKeypair: { privateKey: Bytes<32>; publicKey: Bytes<32> }, + sessionId: string, + authType: string, + idToken: string, + operationType: string, + apiName: string, + ): string { + 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"); + + const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); + if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + + return sigBytesRes.data.toHex(); + } + + describe("replay attack prevention", () => { + it("should record api_call on successful response", 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_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + // Before call: no api_call record + expect(await getApiCallCount(sessionId, "keygen")).toBe(0); + + await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // After call: api_call recorded + expect(await getApiCallCount(sessionId, "keygen")).toBe(1); + }); + + it("should not record api_call on failed response (retry 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_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + await request(app) + .post("/test/keygen_fail") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(500); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // api_call should not be recorded on failure + expect(await getApiCallCount(sessionId, "keygen")).toBe(0); + }); + + it("should return 409 when same API called twice with same session", 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", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in", + "signin", + ); + + // First call succeeds + await request(app) + .post("/test/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Reset session state to COMMITTED to allow second call attempt + await pool.query( + `UPDATE "commit_reveal_sessions" SET state = 'COMMITTED' WHERE session_id = $1`, + [sessionId], + ); + + // Second call with same signature should fail + const response = await request(app) + .post("/test/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("API_ALREADY_CALLED"); + }); + }); + + describe("final API and session completion", () => { + it("should change session to COMPLETED on final API success", 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_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + // Before: COMMITTED + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + await request(app) + .post("/test/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // After: COMPLETED (keygen is final for sign_up) + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + }); + + it("should keep session COMMITTED on non-final API success", 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"); + + // sign_in_reshare allows signin (non-final) then reshare (final) + await createSession({ + session_id: sessionId, + operation_type: "sign_in_reshare", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare", + "signin", + ); + + // Before: COMMITTED + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + await request(app) + .post("/test/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // After: still COMMITTED (signin is not final for sign_in_reshare) + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + }); + + it("should not update session to COMPLETED on API failure", 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_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + // Before: COMMITTED + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + await request(app) + .post("/test/keygen_fail") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(500); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // After: still COMMITTED (API failed) + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + }); + }); + + describe("route integration tests", () => { + it("keygen route: should pass with sign_up operation", 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_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "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(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.message).toBe("keygen ok"); + }); + + it("keygen_ed25519 route: should pass with add_ed25519 operation", 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: "add_ed25519", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "add_ed25519", + "keygen_ed25519", + ); + + const response = await request(app) + .post("/test/keygen_ed25519") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.message).toBe("keygen_ed25519 ok"); + }); + + it("signin route: should pass with sign_in operation", 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", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in", + "signin", + ); + + const response = await request(app) + .post("/test/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.message).toBe("signin ok"); + }); + + it("reshare route: should pass with sign_in_reshare operation", 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", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare", + "reshare", + ); + + const response = await request(app) + .post("/test/reshare") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.message).toBe("reshare ok"); + }); + }); +}); diff --git a/backend/oko_api/server/src/middleware/commit_reveal.ts b/backend/oko_api/server/src/middleware/commit_reveal.ts new file mode 100644 index 000000000..beb27daf5 --- /dev/null +++ b/backend/oko_api/server/src/middleware/commit_reveal.ts @@ -0,0 +1,252 @@ +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 { + getCommitRevealSessionBySessionId, + createCommitRevealApiCall, + updateCommitRevealSessionState, + hasCommitRevealApiBeenCalled, +} from "@oko-wallet/oko-pg-interface/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/allowed_apis"; +import type { ServerState } from "@oko-wallet/oko-api-server-state"; + +export interface CommitRevealBody { + cr_session_id: string; + cr_signature: string; // 128 chars hex (64 bytes) + auth_type?: string; +} + +export function commitRevealMiddleware(apiName: ApiName) { + return async (req: Request, res: Response, next: NextFunction) => { + const state = req.app.locals as ServerState; + const body = req.body as CommitRevealBody; + const { cr_session_id, cr_signature } = body; + + if (!cr_session_id || !cr_signature) { + res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: "cr_session_id and cr_signature are required", + }); + return; + } + + const sessionResult = await getCommitRevealSessionBySessionId( + state.db, + cr_session_id, + ); + if (!sessionResult.success) { + res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to get session: ${sessionResult.err}`, + }); + return; + } + + const session = sessionResult.data; + if (!session) { + res.status(ErrorCodeMap.SESSION_NOT_FOUND).json({ + success: false, + code: "SESSION_NOT_FOUND", + msg: "Session not found", + }); + return; + } + + if (session.state !== "COMMITTED") { + res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: `Session is not in COMMITTED state: ${session.state}`, + }); + return; + } + + if (new Date() > session.expires_at) { + res.status(ErrorCodeMap.SESSION_EXPIRED).json({ + success: false, + code: "SESSION_EXPIRED", + msg: "Session has expired", + }); + return; + } + + if (!isApiAllowed(session.operation_type, apiName)) { + res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: `API "${apiName}" is not allowed for operation "${session.operation_type}"`, + }); + return; + } + + // Check if this API has already been called for this session (replay attack prevention) + const apiCalledResult = await hasCommitRevealApiBeenCalled( + state.db, + cr_session_id, + apiName, + ); + if (!apiCalledResult.success) { + res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to check API call status: ${apiCalledResult.err}`, + }); + return; + } + if (apiCalledResult.data) { + res.status(ErrorCodeMap.API_ALREADY_CALLED).json({ + success: false, + code: "API_ALREADY_CALLED", + msg: `API "${apiName}" has already been called for this session`, + }); + return; + } + + const signatureRes = Bytes.fromHexString(cr_signature, 64); + if (!signatureRes.success) { + res.status(ErrorCodeMap.INVALID_SIGNATURE).json({ + success: false, + code: "INVALID_SIGNATURE", + msg: `Invalid signature format: ${signatureRes.err}`, + }); + return; + } + + const clientPubkeyRes = Bytes.fromUint8Array( + session.client_ephemeral_pubkey, + 32, + ); + if (!clientPubkeyRes.success) { + res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to parse client pubkey: ${clientPubkeyRes.err}`, + }); + return; + } + + // Get auth_type and id_token from request + const authType = body.auth_type ?? "google"; + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: "Authorization header with Bearer token required", + }); + return; + } + const idToken = authHeader.substring(7).trim(); + + // Verify id_token_hash matches committed hash + const computedHashRes = sha256(`${authType}${idToken}`); + if (!computedHashRes.success) { + res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to compute id_token_hash: ${computedHashRes.err}`, + }); + return; + } + const computedHash = computedHashRes.data.toHex(); + if (computedHash !== session.id_token_hash) { + res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: "id_token_hash mismatch: token does not match committed session", + }); + return; + } + + // Verify signature: message = node_pubkey + session_id + auth_type + id_token + operation_type + api_name + const nodePubkeyHex = state.server_keypair.publicKey.toHex(); + const message = `${nodePubkeyHex}${cr_session_id}${authType}${idToken}${session.operation_type}${apiName}`; + const rBytes = Bytes.fromUint8Array( + signatureRes.data.toUint8Array().slice(0, 32), + 32, + ); + const sBytes = Bytes.fromUint8Array( + signatureRes.data.toUint8Array().slice(32, 64), + 32, + ); + if (!rBytes.success || !sBytes.success) { + res.status(ErrorCodeMap.INVALID_SIGNATURE).json({ + success: false, + code: "INVALID_SIGNATURE", + msg: "Failed to parse signature components", + }); + return; + } + + const verifyResult = verifySignature( + message, + { r: rBytes.data, s: sBytes.data }, + clientPubkeyRes.data, + ); + if (!verifyResult.success) { + res.status(ErrorCodeMap.INVALID_SIGNATURE).json({ + success: false, + code: "INVALID_SIGNATURE", + msg: `Signature verification failed: ${verifyResult.err}`, + }); + return; + } + if (!verifyResult.data) { + res.status(ErrorCodeMap.INVALID_SIGNATURE).json({ + success: false, + code: "INVALID_SIGNATURE", + msg: "Invalid signature", + }); + return; + } + + res.locals.cr_session_id = cr_session_id; + + // Record API call and update session state on successful response + res.on("finish", async () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + const client = await state.db.connect(); + try { + await client.query("BEGIN"); + + await createCommitRevealApiCall( + client, + cr_session_id, + apiName, + signatureRes.data.toUint8Array(), + ); + + if (isFinalApi(session.operation_type, apiName)) { + await updateCommitRevealSessionState( + client, + cr_session_id, + "COMPLETED", + ); + } + + await client.query("COMMIT"); + } catch (err) { + await client.query("ROLLBACK"); + state.logger.error( + "Failed to record API call for session %s: %s", + cr_session_id, + err, + ); + } finally { + client.release(); + } + } + }); + + next(); + }; +} 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 new file mode 100644 index 000000000..fa6c3e4a4 --- /dev/null +++ b/backend/oko_api/server/src/routes/tss_v2/commit.test.ts @@ -0,0 +1,410 @@ +import request from "supertest"; +import express from "express"; +import type { Pool } from "pg"; +import { Bytes } from "@oko-wallet/bytes"; +import { randomBytes } from "node:crypto"; +import { v4 as uuidv4 } from "uuid"; +import { createPgConn } from "@oko-wallet/postgres-lib"; +import winston from "winston"; + +import { testPgConfig } from "@oko-wallet-api/database/test_config"; +import { resetPgDatabase } from "@oko-wallet-api/testing/database"; +import { commitRevealCommit } from "./commit"; + +// Mock keypair for testing +const privateKeyRes = Bytes.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001", + 32, +); +const publicKeyRes = Bytes.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000002", + 32, +); +if (!privateKeyRes.success || !publicKeyRes.success) { + throw new Error("Failed to create mock keypair"); +} +const mockServerKeypair = { + privateKey: privateKeyRes.data, + publicKey: publicKeyRes.data, +}; + +const testLogger = winston.createLogger({ + level: "error", + silent: true, + transports: [new winston.transports.Console()], +}); + +function generateRandomHex(bytes: number): string { + return randomBytes(bytes).toString("hex"); +} + +describe("commit_reveal_commit_success_test", () => { + let pool: Pool; + let app: express.Application; + + beforeAll(async () => { + const config = testPgConfig; + const createPostgresRes = await createPgConn({ + database: config.database, + host: config.host, + password: config.password, + user: config.user, + port: config.port, + ssl: config.ssl, + }); + + if (createPostgresRes.success === false) { + console.error(createPostgresRes.err); + throw new Error("Failed to create postgres database"); + } + + pool = createPostgresRes.data; + + app = express(); + app.use(express.json()); + + app.post("/tss/v2/commit", commitRevealCommit); + + app.locals.db = pool; + app.locals.server_keypair = mockServerKeypair; + app.locals.logger = testLogger; + }); + + beforeEach(async () => { + await resetPgDatabase(pool); + }); + + afterAll(async () => { + await pool.end(); + }); + + const testEndpoint = "/tss/v2/commit"; + + const createValidBody = () => ({ + session_id: uuidv4(), + operation_type: "sign_in", + client_ephemeral_pubkey: generateRandomHex(32), + id_token_hash: generateRandomHex(32), + }); + + describe("success cases", () => { + it("should successfully create session with sign_in operation", async () => { + const body = createValidBody(); + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeDefined(); + expect(response.body.data.node_pubkey).toBe( + mockServerKeypair.publicKey.toHex(), + ); + expect(response.body.data.node_signature).toBeDefined(); + expect(response.body.data.node_signature).toHaveLength(128); // 64 bytes hex + }); + + it("should successfully create session with sign_up operation", async () => { + const body = { + ...createValidBody(), + operation_type: "sign_up", + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeDefined(); + expect(response.body.data.node_pubkey).toBeDefined(); + expect(response.body.data.node_signature).toBeDefined(); + }); + + it("should successfully create session with sign_in_reshare operation", async () => { + const body = { + ...createValidBody(), + operation_type: "sign_in_reshare", + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeDefined(); + }); + + it("should successfully create session with add_ed25519 operation", async () => { + const body = { + ...createValidBody(), + operation_type: "add_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(); + const body3 = createValidBody(); + + const response1 = await request(app) + .post(testEndpoint) + .send(body1) + .expect(200); + const response2 = await request(app) + .post(testEndpoint) + .send(body2) + .expect(200); + const response3 = await request(app) + .post(testEndpoint) + .send(body3) + .expect(200); + + expect(response1.body.success).toBe(true); + expect(response2.body.success).toBe(true); + expect(response3.body.success).toBe(true); + }); + }); + + describe("session verification", () => { + it("should create session in COMMITTED state", async () => { + const body = createValidBody(); + + await request(app).post(testEndpoint).send(body).expect(200); + + // Verify session was created in DB + const result = await pool.query( + 'SELECT * FROM "commit_reveal_sessions" WHERE session_id = $1', + [body.session_id], + ); + + expect(result.rows.length).toBe(1); + expect(result.rows[0].state).toBe("COMMITTED"); + expect(result.rows[0].operation_type).toBe(body.operation_type); + expect(result.rows[0].id_token_hash).toBe(body.id_token_hash); + }); + + it("should set expires_at to approximately 5 minutes from now", async () => { + const body = createValidBody(); + const beforeRequest = new Date(); + + await request(app).post(testEndpoint).send(body).expect(200); + + const afterRequest = new Date(); + + const result = await pool.query( + 'SELECT * FROM "commit_reveal_sessions" WHERE session_id = $1', + [body.session_id], + ); + + expect(result.rows.length).toBe(1); + + const expiresAt = new Date(result.rows[0].expires_at); + const expectedMinExpiresAt = new Date( + beforeRequest.getTime() + 5 * 60 * 1000 - 1000, + ); + const expectedMaxExpiresAt = new Date( + afterRequest.getTime() + 5 * 60 * 1000 + 1000, + ); + + expect(expiresAt.getTime()).toBeGreaterThanOrEqual( + expectedMinExpiresAt.getTime(), + ); + expect(expiresAt.getTime()).toBeLessThanOrEqual( + expectedMaxExpiresAt.getTime(), + ); + }); + }); + + describe("duplicate key errors", () => { + it("should return 409 when session_id already exists", async () => { + const sessionId = uuidv4(); + const body1 = { + ...createValidBody(), + session_id: sessionId, + }; + const body2 = { + ...createValidBody(), + session_id: sessionId, + }; + + await request(app).post(testEndpoint).send(body1).expect(200); + + const response = await request(app) + .post(testEndpoint) + .send(body2) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_ALREADY_EXISTS"); + }); + + it("should return 409 when client_ephemeral_pubkey already exists", async () => { + const pubkey = generateRandomHex(32); + const body1 = { + ...createValidBody(), + client_ephemeral_pubkey: pubkey, + }; + const body2 = { + ...createValidBody(), + client_ephemeral_pubkey: pubkey, + }; + + await request(app).post(testEndpoint).send(body1).expect(200); + + const response = await request(app) + .post(testEndpoint) + .send(body2) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_ALREADY_EXISTS"); + }); + + it("should return 409 when id_token_hash already exists", async () => { + const idTokenHash = generateRandomHex(32); + const body1 = { + ...createValidBody(), + id_token_hash: idTokenHash, + }; + const body2 = { + ...createValidBody(), + id_token_hash: idTokenHash, + }; + + await request(app).post(testEndpoint).send(body1).expect(200); + + const response = await request(app) + .post(testEndpoint) + .send(body2) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_ALREADY_EXISTS"); + }); + }); + + describe("invalid input errors", () => { + it("should return 400 when client_ephemeral_pubkey is invalid hex", async () => { + const body = { + ...createValidBody(), + client_ephemeral_pubkey: "invalid_hex_string", + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("client_ephemeral_pubkey"); + }); + + it("should return 400 when client_ephemeral_pubkey is wrong length", async () => { + const body = { + ...createValidBody(), + client_ephemeral_pubkey: generateRandomHex(16), // 16 bytes instead of 32 + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("client_ephemeral_pubkey"); + }); + + it("should return 400 when id_token_hash is invalid hex", async () => { + const body = { + ...createValidBody(), + id_token_hash: "invalid_hex_string", + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("id_token_hash"); + }); + + it("should return 400 when id_token_hash is wrong length", async () => { + const body = { + ...createValidBody(), + id_token_hash: generateRandomHex(16), // 16 bytes instead of 32 + }; + + const response = await request(app) + .post(testEndpoint) + .send(body) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_REQUEST"); + expect(response.body.msg).toContain("id_token_hash"); + }); + + it("should return 500 when session_id is missing", async () => { + const body = createValidBody(); + const { session_id, ...bodyWithoutSessionId } = body; + + const response = await request(app) + .post(testEndpoint) + .send(bodyWithoutSessionId) + .expect(500); + + expect(response.body.success).toBe(false); + }); + + it("should return 500 when operation_type is missing", async () => { + const body = createValidBody(); + const { operation_type, ...bodyWithoutOperationType } = body; + + const response = await request(app) + .post(testEndpoint) + .send(bodyWithoutOperationType) + .expect(500); + + expect(response.body.success).toBe(false); + }); + + it("should return 400 when client_ephemeral_pubkey is missing", async () => { + const body = createValidBody(); + const { client_ephemeral_pubkey, ...bodyWithoutPubkey } = body; + + const response = await request(app) + .post(testEndpoint) + .send(bodyWithoutPubkey) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it("should return 400 when id_token_hash is missing", async () => { + const body = createValidBody(); + const { id_token_hash, ...bodyWithoutHash } = body; + + const response = await request(app) + .post(testEndpoint) + .send(bodyWithoutHash) + .expect(400); + + expect(response.body.success).toBe(false); + }); + }); +}); diff --git a/backend/oko_api/server/src/routes/tss_v2/commit.ts b/backend/oko_api/server/src/routes/tss_v2/commit.ts new file mode 100644 index 000000000..4b39fd136 --- /dev/null +++ b/backend/oko_api/server/src/routes/tss_v2/commit.ts @@ -0,0 +1,173 @@ +import type { Request, Response } from "express"; +import { Bytes } from "@oko-wallet/bytes"; +import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; +import { + signMessage, + convertEddsaSignatureToBytes, +} from "@oko-wallet/crypto-js/node/ecdhe"; +import { createCommitRevealSession } from "@oko-wallet/oko-pg-interface/commit_reveal"; +import type { ServerState } from "@oko-wallet/oko-api-server-state"; + +import { registry } from "@oko-wallet/oko-api-openapi"; +import { ErrorResponseSchema } from "@oko-wallet/oko-api-openapi/common"; +import { + CommitRequestBodySchema, + CommitSuccessResponseSchema, + type CommitRequestBody, + type CommitResponseData, +} from "@oko-wallet/oko-api-openapi/tss"; + +const SESSION_EXPIRY_MINUTES = 5; + +registry.registerPath({ + method: "post", + path: "/tss/v2/commit", + tags: ["TSS"], + summary: "Create a commit-reveal session", + description: + "Creates a new commit-reveal session for frontrunning prevention. Returns node's public key and signature.", + request: { + body: { + required: true, + content: { + "application/json": { + schema: CommitRequestBodySchema, + }, + }, + }, + }, + responses: { + 200: { + description: "Successfully created session", + content: { + "application/json": { + schema: CommitSuccessResponseSchema, + }, + }, + }, + 400: { + description: "Bad request - invalid input", + content: { + "application/json": { + schema: ErrorResponseSchema, + examples: { + INVALID_REQUEST: { + value: { + success: false, + code: "INVALID_REQUEST", + msg: "Invalid client_ephemeral_pubkey format", + }, + }, + }, + }, + }, + }, + 409: { + description: "Conflict - session already exists", + content: { + "application/json": { + schema: ErrorResponseSchema, + examples: { + SESSION_ALREADY_EXISTS: { + value: { + success: false, + code: "SESSION_ALREADY_EXISTS", + msg: "Session with this id_token_hash already exists", + }, + }, + }, + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + }, + }, +}); + +export async function commitRevealCommit( + req: Request, + res: Response>, +) { + const state = req.app.locals as ServerState; + const body = req.body; + + // Validate client_ephemeral_pubkey (32 bytes hex) + const clientPubkeyRes = Bytes.fromHexString(body.client_ephemeral_pubkey, 32); + if (!clientPubkeyRes.success) { + return res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: `Invalid client_ephemeral_pubkey: ${clientPubkeyRes.err}`, + }); + } + + // Validate id_token_hash (32 bytes hex) + const idTokenHashRes = Bytes.fromHexString(body.id_token_hash, 32); + if (!idTokenHashRes.success) { + return res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: `Invalid id_token_hash: ${idTokenHashRes.err}`, + }); + } + + const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MINUTES * 60 * 1000); + + const createResult = await createCommitRevealSession(state.db, { + session_id: body.session_id, + operation_type: body.operation_type, + client_ephemeral_pubkey: clientPubkeyRes.data.toUint8Array(), + id_token_hash: body.id_token_hash, + expires_at: expiresAt, + }); + + if (!createResult.success) { + // Check for duplicate key errors + if (createResult.err.includes("duplicate key")) { + return res.status(409).json({ + success: false, + code: "SESSION_ALREADY_EXISTS", + msg: "Session with this session_id, client_ephemeral_pubkey, or id_token_hash already exists", + }); + } + return res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to create session: ${createResult.err}`, + }); + } + + // Sign the node's public key with the node's private key + const nodePubkeyHex = state.server_keypair.publicKey.toHex(); + const signResult = signMessage(nodePubkeyHex, state.server_keypair.privateKey); + if (!signResult.success) { + return res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to sign node public key: ${signResult.err}`, + }); + } + + const signatureBytesRes = convertEddsaSignatureToBytes(signResult.data); + if (!signatureBytesRes.success) { + return res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: `Failed to convert signature: ${signatureBytesRes.err}`, + }); + } + + return res.status(200).json({ + success: true, + data: { + node_pubkey: nodePubkeyHex, + node_signature: signatureBytesRes.data.toHex(), + }, + }); +} 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 new file mode 100644 index 000000000..81a032086 --- /dev/null +++ b/backend/oko_api/server/src/routes/tss_v2/e2e.test.ts @@ -0,0 +1,786 @@ +import request from "supertest"; +import express from "express"; +import type { Pool } from "pg"; +import { Bytes } from "@oko-wallet/bytes"; +import { randomBytes } from "node:crypto"; +import { v4 as uuidv4 } from "uuid"; +import { createPgConn } from "@oko-wallet/postgres-lib"; +import winston from "winston"; +import { sha256 } from "@oko-wallet/crypto-js"; +import { + generateEddsaKeypair, + signMessage, + convertEddsaSignatureToBytes, +} from "@oko-wallet/crypto-js/node/ecdhe"; + +import { testPgConfig } from "@oko-wallet-api/database/test_config"; +import { resetPgDatabase } from "@oko-wallet-api/testing/database"; +import { commitRevealCommit } from "./commit"; +import { commitRevealMiddleware } from "@oko-wallet-api/middleware/commit_reveal"; + +// Mock keypair for testing +const privateKeyRes = Bytes.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001", + 32, +); +const publicKeyRes = Bytes.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000002", + 32, +); +if (!privateKeyRes.success || !publicKeyRes.success) { + throw new Error("Failed to create mock keypair"); +} +const mockServerKeypair = { + privateKey: privateKeyRes.data, + publicKey: publicKeyRes.data, +}; + +const testLogger = winston.createLogger({ + level: "error", + silent: true, + transports: [new winston.transports.Console()], +}); + +function generateRandomHex(bytes: number): string { + return randomBytes(bytes).toString("hex"); +} + +describe("tss_v2_e2e_success_flows", () => { + let pool: Pool; + let app: express.Application; + + beforeAll(async () => { + const config = testPgConfig; + const createPostgresRes = await createPgConn({ + database: config.database, + host: config.host, + password: config.password, + user: config.user, + port: config.port, + ssl: config.ssl, + }); + + if (createPostgresRes.success === false) { + console.error(createPostgresRes.err); + throw new Error("Failed to create postgres database"); + } + + pool = createPostgresRes.data; + + app = express(); + app.use(express.json()); + + // Commit endpoint (no middleware) + app.post("/tss/v2/commit", commitRevealCommit); + + // Protected endpoints with commit-reveal middleware + // Mock handlers that simulate successful API calls + app.post( + "/tss/v2/keygen", + commitRevealMiddleware("keygen"), + (_req, res) => { + res.status(200).json({ + success: true, + data: { wallet_id: uuidv4(), public_key: generateRandomHex(33) }, + }); + }, + ); + + app.post( + "/tss/v2/keygen_ed25519", + commitRevealMiddleware("keygen_ed25519"), + (_req, res) => { + res.status(200).json({ + success: true, + data: { wallet_id: uuidv4(), public_key: generateRandomHex(32) }, + }); + }, + ); + + app.post( + "/tss/v2/user/signin", + commitRevealMiddleware("signin"), + (_req, res) => { + res.status(200).json({ + success: true, + data: { token: "mock_jwt_token", user_id: uuidv4() }, + }); + }, + ); + + app.post( + "/tss/v2/user/reshare", + commitRevealMiddleware("reshare"), + (_req, res) => { + res.status(200).json({ + success: true, + data: { reshare_complete: true }, + }); + }, + ); + + app.locals.db = pool; + app.locals.server_keypair = mockServerKeypair; + app.locals.logger = testLogger; + }); + + beforeEach(async () => { + await resetPgDatabase(pool); + }); + + afterAll(async () => { + await pool.end(); + }); + + function createValidSignature( + clientKeypair: { privateKey: Bytes<32>; publicKey: Bytes<32> }, + sessionId: string, + authType: string, + idToken: string, + operationType: string, + apiName: string, + ): string { + 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"); + + const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); + if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + + return sigBytesRes.data.toHex(); + } + + async function getSessionState(sessionId: string): Promise { + const result = await pool.query( + `SELECT state FROM "commit_reveal_sessions" WHERE session_id = $1`, + [sessionId], + ); + return result.rows[0]?.state ?? null; + } + + 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], + ); + return parseInt(result.rows[0].count, 10); + } + + describe("sign_up flow (commit -> keygen)", () => { + it("should complete full sign_up flow: commit -> keygen -> verify data", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_for_signup"; + + // Generate client keypair + const clientKeypairRes = generateEddsaKeypair(); + 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"); + const idTokenHash = hashRes.data.toHex(); + + // Step 1: Commit + const commitResponse = await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: idTokenHash, + }) + .expect(200); + + expect(commitResponse.body.success).toBe(true); + expect(commitResponse.body.data.node_pubkey).toBe(mockServerKeypair.publicKey.toHex()); + expect(commitResponse.body.data.node_signature).toHaveLength(128); + + // Verify session is in COMMITTED state + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + // Step 2: Keygen with commit-reveal signature + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + const keygenResponse = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + expect(keygenResponse.body.success).toBe(true); + expect(keygenResponse.body.data.wallet_id).toBeDefined(); + expect(keygenResponse.body.data.public_key).toBeDefined(); + + // Wait for async finish handler + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify session is now COMPLETED + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + + // Verify API call was recorded + expect(await getApiCallCount(sessionId, "keygen")).toBe(1); + }); + + it("should reject replay attack on keygen API", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_replay"; + + 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 + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + // First keygen call - success + await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Reset session state to COMMITTED to allow second attempt + await pool.query( + `UPDATE "commit_reveal_sessions" SET state = 'COMMITTED' WHERE session_id = $1`, + [sessionId], + ); + + // Second keygen call - should be rejected (replay attack) + const replayResponse = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(409); + + expect(replayResponse.body.success).toBe(false); + expect(replayResponse.body.code).toBe("API_ALREADY_CALLED"); + }); + }); + + describe("sign_in flow (commit -> signin)", () => { + it("should complete full sign_in flow", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_for_signin"; + + 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 + const commitResponse = await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_in", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + expect(commitResponse.body.success).toBe(true); + + // Signin + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in", + "signin", + ); + + const signinResponse = await request(app) + .post("/tss/v2/user/signin") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + expect(signinResponse.body.success).toBe(true); + expect(signinResponse.body.data.token).toBeDefined(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should be COMPLETED (signin is final for sign_in) + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + }); + }); + + describe("sign_in_reshare flow (commit -> signin -> reshare)", () => { + it("should complete full sign_in_reshare flow", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_for_reshare"; + + 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 operation + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_in_reshare", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Step 1: Signin (non-final for sign_in_reshare) + const signinSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare", + "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 for sign_in_reshare) + expect(await getSessionState(sessionId)).toBe("COMMITTED"); + + // Step 2: Reshare (final for sign_in_reshare) + const reshareSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in_reshare", + "reshare", + ); + + const reshareResponse = 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); + + expect(reshareResponse.body.success).toBe(true); + expect(reshareResponse.body.data.reshare_complete).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should now be COMPLETED + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + + // Both API calls should be recorded + expect(await getApiCallCount(sessionId, "signin")).toBe(1); + expect(await getApiCallCount(sessionId, "reshare")).toBe(1); + }); + }); + + describe("add_ed25519 flow (commit -> keygen_ed25519)", () => { + it("should complete full add_ed25519 flow", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token_for_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 + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "add_ed25519", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Keygen ed25519 + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "add_ed25519", + "keygen_ed25519", + ); + + const keygenResponse = await request(app) + .post("/tss/v2/keygen_ed25519") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(200); + + expect(keygenResponse.body.success).toBe(true); + expect(keygenResponse.body.data.wallet_id).toBeDefined(); + expect(keygenResponse.body.data.public_key).toBeDefined(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Session should be COMPLETED + expect(await getSessionState(sessionId)).toBe("COMPLETED"); + expect(await getApiCallCount(sessionId, "keygen_ed25519")).toBe(1); + }); + }); +}); + +describe("tss_v2_e2e_error_scenarios", () => { + let pool: Pool; + let app: express.Application; + + beforeAll(async () => { + const config = testPgConfig; + const createPostgresRes = await createPgConn({ + database: config.database, + host: config.host, + password: config.password, + user: config.user, + port: config.port, + ssl: config.ssl, + }); + + if (createPostgresRes.success === false) { + console.error(createPostgresRes.err); + throw new Error("Failed to create postgres database"); + } + + pool = createPostgresRes.data; + + app = express(); + app.use(express.json()); + + 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/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; + app.locals.logger = testLogger; + }); + + beforeEach(async () => { + await resetPgDatabase(pool); + }); + + afterAll(async () => { + await pool.end(); + }); + + function createValidSignature( + clientKeypair: { privateKey: Bytes<32>; publicKey: Bytes<32> }, + sessionId: string, + authType: string, + idToken: string, + operationType: string, + apiName: string, + ): string { + 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"); + + const sigBytesRes = convertEddsaSignatureToBytes(signRes.data); + if (!sigBytesRes.success) throw new Error("Failed to convert signature"); + + return sigBytesRes.data.toHex(); + } + + describe("error scenarios", () => { + it("should reject invalid signature", 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"); + + // Commit + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Try keygen with wrong signature (signed with wrong message) + const wrongSignature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "wrong_api", // wrong api name in signature + ); + + const response = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: wrongSignature, + auth_type: authType, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("INVALID_SIGNATURE"); + }); + + it("should reject expired session", 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"); + + // Commit + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Manually expire the session + await pool.query( + `UPDATE "commit_reveal_sessions" SET expires_at = $1 WHERE session_id = $2`, + [new Date(Date.now() - 1000), sessionId], + ); + + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_up", + "keygen", + ); + + const response = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: sessionId, + cr_signature: signature, + auth_type: authType, + }) + .expect(410); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_EXPIRED"); + }); + + it("should reject non-existent session", async () => { + const nonExistentSessionId = uuidv4(); + const authType = "google"; + const idToken = "test_id_token"; + + const response = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${idToken}`) + .send({ + cr_session_id: nonExistentSessionId, + cr_signature: generateRandomHex(64), + auth_type: authType, + }) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.code).toBe("SESSION_NOT_FOUND"); + }); + + it("should reject wrong operation type for API", 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"); + + // Commit with sign_in operation + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_in", // sign_in operation + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Try to call keygen (not allowed for sign_in) + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + idToken, + "sign_in", + "keygen", + ); + + const response = await request(app) + .post("/tss/v2/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"); + }); + + it("should reject id_token_hash mismatch", async () => { + const sessionId = uuidv4(); + const authType = "google"; + const originalIdToken = "original_id_token"; + const wrongIdToken = "wrong_id_token"; + + const clientKeypairRes = generateEddsaKeypair(); + 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"); + + // Commit with original token hash + await request(app) + .post("/tss/v2/commit") + .send({ + session_id: sessionId, + operation_type: "sign_up", + client_ephemeral_pubkey: clientKeypair.publicKey.toHex(), + id_token_hash: hashRes.data.toHex(), + }) + .expect(200); + + // Try keygen with wrong id_token (different from committed hash) + const signature = createValidSignature( + clientKeypair, + sessionId, + authType, + wrongIdToken, // wrong token + "sign_up", + "keygen", + ); + + const response = await request(app) + .post("/tss/v2/keygen") + .set("Authorization", `Bearer ${wrongIdToken}`) // wrong token in header + .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("id_token_hash mismatch"); + }); + }); +}); diff --git a/backend/oko_api/server/src/routes/tss_v2/index.ts b/backend/oko_api/server/src/routes/tss_v2/index.ts index 767a5b144..0a15e20ec 100644 --- a/backend/oko_api/server/src/routes/tss_v2/index.ts +++ b/backend/oko_api/server/src/routes/tss_v2/index.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import { oauthMiddleware } from "@oko-wallet-api/middleware/auth/oauth"; import { tssActivateMiddleware } from "@oko-wallet-api/middleware/auth/tss_activate"; +import { commitRevealMiddleware } from "@oko-wallet-api/middleware/commit_reveal"; import { keygenV2 } from "./keygen"; import { userJwtMiddlewareV2 } from "@oko-wallet-api/middleware/auth/keplr_auth"; import { presignStep1 } from "./presign_step_1"; @@ -27,15 +28,25 @@ import { keygenEd25519 } from "./keygen_ed25519"; import { userSignInV2 } from "./user_signin"; import { userReshareV2 } from "./user_reshare"; import { userCheckEmailV2 } from "./user_check_email"; +import { commitRevealCommit } from "./commit"; export function makeTSSRouterV2() { const router = Router(); - router.post("/keygen", oauthMiddleware, tssActivateMiddleware, keygenV2); + router.post("/commit", commitRevealCommit); + + router.post( + "/keygen", + oauthMiddleware, + commitRevealMiddleware("keygen"), + tssActivateMiddleware, + keygenV2, + ); router.post( "/keygen_ed25519", oauthMiddleware, + commitRevealMiddleware("keygen_ed25519"), tssActivateMiddleware, keygenEd25519, ); @@ -151,6 +162,7 @@ export function makeTSSRouterV2() { router.post( "/user/signin", oauthMiddleware, + commitRevealMiddleware("signin"), tssActivateMiddleware, userSignInV2, ); @@ -158,6 +170,7 @@ export function makeTSSRouterV2() { router.post( "/user/reshare", oauthMiddleware, + commitRevealMiddleware("reshare"), tssActivateMiddleware, userReshareV2, ); diff --git a/backend/oko_api_error_codes/src/index.ts b/backend/oko_api_error_codes/src/index.ts index eeb4a74f8..c19fbe4e1 100644 --- a/backend/oko_api_error_codes/src/index.ts +++ b/backend/oko_api_error_codes/src/index.ts @@ -42,5 +42,10 @@ export const ErrorCodeMap: Record = { INVALID_PUBLIC_KEY: 400, INVALID_WALLET_TYPE: 400, REFERRAL_NOT_FOUND: 404, + SESSION_ALREADY_EXISTS: 409, + SESSION_NOT_FOUND: 404, + SESSION_EXPIRED: 410, + API_ALREADY_CALLED: 409, + INVALID_SIGNATURE: 400, UNKNOWN_ERROR: 500, }; diff --git a/backend/openapi/src/tss/commit_reveal.ts b/backend/openapi/src/tss/commit_reveal.ts new file mode 100644 index 000000000..0109d0fde --- /dev/null +++ b/backend/openapi/src/tss/commit_reveal.ts @@ -0,0 +1,106 @@ +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"]) + .describe("Operation type for commit-reveal session"); + +// POST /tss/v2/commit-reveal/commit + +export const CommitRequestBodySchema = registry.register( + "CommitRevealCommitRequestBody", + z + .object({ + session_id: z + .string() + .uuid() + .describe("Client-generated session ID (UUIDv4)") + .openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }), + operation_type: OperationTypeSchema, + client_ephemeral_pubkey: z + .string() + .length(64) + .describe("Client ephemeral public key (32 bytes hex)") + .openapi({ + example: + "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd", + }), + id_token_hash: z + .string() + .length(64) + .describe("SHA-256 hash of (auth_type | id_token) (32 bytes hex)") + .openapi({ + example: + "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + }), + }) + .openapi("CommitRevealCommitRequestBody", { + description: "Request payload for creating a commit-reveal session.", + }), +); + +export const CommitResponseDataSchema = registry.register( + "CommitRevealCommitResponseData", + z + .object({ + node_pubkey: z + .string() + .length(64) + .describe("Node's public key (32 bytes hex)") + .openapi({ + example: + "b2c3d4e5f67890123456789012345678901234567890123456789012345678ef", + }), + node_signature: z + .string() + .length(128) + .describe("Node's signature on node_pubkey (64 bytes hex)") + .openapi({ + example: + "c3d4e5f6789012345678901234567890123456789012345678901234567890abc3d4e5f6789012345678901234567890123456789012345678901234567890ab", + }), + }) + .openapi("CommitRevealCommitResponseData", { + description: "Response data containing node's public key and signature.", + }), +); + +export const CommitSuccessResponseSchema = registry.register( + "CommitRevealCommitSuccessResponse", + z + .object({ + success: z.literal(true), + data: CommitResponseDataSchema, + }) + .openapi("CommitRevealCommitSuccessResponse", { + description: "Success response for commit request.", + }), +); + +export type CommitRequestBody = z.infer; +export type CommitResponseData = z.infer; + +// Commit-reveal request fields (used by protected endpoints) + +export const CommitRevealRequestFieldsSchema = z.object({ + cr_session_id: z + .string() + .uuid() + .describe("Commit-reveal session ID from commit API") + .openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }), + cr_signature: z + .string() + .length(128) + .describe( + "Client signature: sign(node_pubkey + session_id + auth_type + id_token + operation_type + api_name) (64 bytes hex)", + ) + .openapi({ + example: + "c3d4e5f6789012345678901234567890123456789012345678901234567890abc3d4e5f6789012345678901234567890123456789012345678901234567890ab", + }), + auth_type: z + .string() + .optional() + .describe("Authentication type (defaults to 'google')") + .openapi({ example: "google" }), +}); diff --git a/backend/openapi/src/tss/index.ts b/backend/openapi/src/tss/index.ts index 7ca982f1d..8ef92a18b 100644 --- a/backend/openapi/src/tss/index.ts +++ b/backend/openapi/src/tss/index.ts @@ -1,3 +1,4 @@ +export * from "./commit_reveal"; export * from "./keygen_ed25519"; export * from "./presign"; export * from "./request"; diff --git a/backend/openapi/src/tss/keygen_ed25519.ts b/backend/openapi/src/tss/keygen_ed25519.ts index aeacb79d4..30650cbaf 100644 --- a/backend/openapi/src/tss/keygen_ed25519.ts +++ b/backend/openapi/src/tss/keygen_ed25519.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { registry } from "../registry"; +import { CommitRevealRequestFieldsSchema } from "./commit_reveal"; const TeddsaKeygenOutputSchema = registry.register( "TeddsaKeygenOutput", @@ -22,9 +23,11 @@ const TeddsaKeygenOutputSchema = registry.register( export const KeygenEd25519RequestSchema = registry.register( "TssKeygenEd25519Request", - z.object({ - keygen_2: TeddsaKeygenOutputSchema.openapi({ - description: "Server's keygen output from centralized key generation", - }), - }), + z + .object({ + keygen_2: TeddsaKeygenOutputSchema.openapi({ + description: "Server's keygen output from centralized key generation", + }), + }) + .merge(CommitRevealRequestFieldsSchema), ); diff --git a/backend/openapi/src/tss/request.ts b/backend/openapi/src/tss/request.ts index d28f0a34f..f64e91df1 100644 --- a/backend/openapi/src/tss/request.ts +++ b/backend/openapi/src/tss/request.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { registry } from "../registry"; +import { CommitRevealRequestFieldsSchema } from "./commit_reveal"; const OAuthTypeSchema = z.enum(["google", "auth0"]).openapi({ description: "OAuth provider type", @@ -9,9 +10,11 @@ const OAuthTypeSchema = z.enum(["google", "auth0"]).openapi({ export const SignInRequestSchema = registry.register( "TssUserSignInRequest", - z.object({ - auth_type: OAuthTypeSchema, - }), + z + .object({ + auth_type: OAuthTypeSchema.optional(), + }) + .merge(CommitRevealRequestFieldsSchema), ); export const KeygenRequestSchema = registry.register( @@ -73,23 +76,25 @@ const TeddsaKeygenOutputSchema = z.object({ export const KeygenRequestV2Schema = registry.register( "TssKeygenRequestV2", - z.object({ - keygen_2_secp256k1: z - .object({ - private_share: z.string().openapi({ - description: "Private key share for secp256k1 TSS", - }), - public_key: z.string().openapi({ - description: "secp256k1 public key in hex format", + z + .object({ + keygen_2_secp256k1: z + .object({ + private_share: z.string().openapi({ + description: "Private key share for secp256k1 TSS", + }), + public_key: z.string().openapi({ + description: "secp256k1 public key in hex format", + }), + }) + .openapi({ + description: "Keygen stage 2 payload for secp256k1", }), - }) - .openapi({ - description: "Keygen stage 2 payload for secp256k1", + keygen_2_ed25519: TeddsaKeygenOutputSchema.openapi({ + description: "Server's keygen output for ed25519", }), - keygen_2_ed25519: TeddsaKeygenOutputSchema.openapi({ - description: "Server's keygen output for ed25519", - }), - }), + }) + .merge(CommitRevealRequestFieldsSchema), ); const ReshareWalletInfoSchema = z.object({ @@ -112,21 +117,23 @@ const ReshareWalletInfoSchema = z.object({ export const ReshareRequestV2Schema = registry.register( "TssUserReshareRequestV2", - z.object({ - wallets: z - .object({ - secp256k1: ReshareWalletInfoSchema.openapi({ - description: "secp256k1 wallet reshare info", - }).optional(), - ed25519: ReshareWalletInfoSchema.openapi({ - description: "ed25519 wallet reshare info", - }).optional(), - }) - .refine((data) => data.secp256k1 || data.ed25519, { - message: "At least one of secp256k1 or ed25519 must be provided", - }) - .openapi({ - description: "Wallet reshare info per curve type", - }), - }), + z + .object({ + wallets: z + .object({ + secp256k1: ReshareWalletInfoSchema.openapi({ + description: "secp256k1 wallet reshare info", + }).optional(), + ed25519: ReshareWalletInfoSchema.openapi({ + description: "ed25519 wallet reshare info", + }).optional(), + }) + .refine((data) => data.secp256k1 || data.ed25519, { + message: "At least one of secp256k1 or ed25519 must be provided", + }) + .openapi({ + description: "Wallet reshare info per curve type", + }), + }) + .merge(CommitRevealRequestFieldsSchema), ); diff --git a/common/oko_types/src/api_response/index.ts b/common/oko_types/src/api_response/index.ts index 4b4a2161c..72b56a09f 100644 --- a/common/oko_types/src/api_response/index.ts +++ b/common/oko_types/src/api_response/index.ts @@ -53,4 +53,9 @@ export type ErrorCode = | "INVALID_PUBLIC_KEY" | "INVALID_WALLET_TYPE" | "REFERRAL_NOT_FOUND" + | "SESSION_ALREADY_EXISTS" + | "SESSION_NOT_FOUND" + | "SESSION_EXPIRED" + | "API_ALREADY_CALLED" + | "INVALID_SIGNATURE" | "UNKNOWN_ERROR"; diff --git a/common/oko_types/src/commit_reveal/index.ts b/common/oko_types/src/commit_reveal/index.ts index 460526e21..8a9015342 100644 --- a/common/oko_types/src/commit_reveal/index.ts +++ b/common/oko_types/src/commit_reveal/index.ts @@ -1,5 +1,12 @@ -export type OperationType = "sign_in" | "sign_up" | "reshare"; -export type SessionState = "COMMITTED" | "COMPLETED" | "EXPIRED"; +export type OperationType = + | "sign_in" + | "sign_up" + | "sign_in_reshare" + | "add_ed25519"; + +export type ApiName = "signin" | "keygen" | "reshare" | "keygen_ed25519"; + +export type SessionState = "COMMITTED" | "COMPLETED"; export interface CommitRevealSession { session_id: string; diff --git a/key_share_node/ksn_interface/src/commit_reveal.ts b/key_share_node/ksn_interface/src/commit_reveal.ts index c0277599e..227ff1873 100644 --- a/key_share_node/ksn_interface/src/commit_reveal.ts +++ b/key_share_node/ksn_interface/src/commit_reveal.ts @@ -5,6 +5,13 @@ export type OperationType = | "register_reshare" | "add_ed25519"; +export type ApiName = + | "get_key_shares" + | "register" + | "reshare" + | "reshare_register" + | "register_ed25519"; + export type SessionState = "COMMITTED" | "COMPLETED"; export interface CommitRevealSession { 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 7f0861976..2cbc712e0 100644 --- a/key_share_node/server/src/commit_reveal/allowed_apis.ts +++ b/key_share_node/server/src/commit_reveal/allowed_apis.ts @@ -1,13 +1,9 @@ -import type { OperationType } from "@oko-wallet/ksn-interface/commit_reveal"; +import type { + OperationType, + ApiName, +} from "@oko-wallet/ksn-interface/commit_reveal"; -export type ApiName = - | "get_key_shares" - | "register" - | "reshare" - | "reshare_register" - | "register_ed25519"; - -export const ALLOWED_APIS = { +export const ALLOWED_APIS: Record = { sign_in: ["get_key_shares"], sign_up: ["register"], sign_in_reshare: ["get_key_shares", "reshare"], @@ -15,7 +11,7 @@ export const ALLOWED_APIS = { 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"], @@ -25,14 +21,14 @@ export const FINAL_APIS = { export function isApiAllowed( operationType: OperationType, - apiName: string, + apiName: ApiName, ): boolean { - return ALLOWED_APIS[operationType].includes(apiName); + return (ALLOWED_APIS[operationType]).includes(apiName); } export function isFinalApi( operationType: OperationType, - apiName: string, + 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.ts b/key_share_node/server/src/middlewares/commit_reveal.ts index 01fc66c21..4a92bc6de 100644 --- a/key_share_node/server/src/middlewares/commit_reveal.ts +++ b/key_share_node/server/src/middlewares/commit_reveal.ts @@ -8,12 +8,12 @@ import { updateCommitRevealSessionState, hasCommitRevealApiBeenCalled, } from "@oko-wallet/ksn-pg-interface/commit_reveal"; +import type { ApiName } from "@oko-wallet/ksn-interface/commit_reveal"; import { ErrorCodeMap } from "@oko-wallet-ksn-server/error"; import { isApiAllowed, isFinalApi, - type ApiName, } from "@oko-wallet-ksn-server/commit_reveal"; import type { ServerState } from "@oko-wallet-ksn-server/state"; import { logger } from "@oko-wallet-ksn-server/logger"; From 09847ef9f49fd47f2345b765637bfec17a17d13f Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Thu, 29 Jan 2026 19:23:32 +0900 Subject: [PATCH 02/15] attached: add commit reveal types --- .../src/crypto/commit_reveal/session.ts | 328 +----------------- .../src/crypto/commit_reveal/types.ts | 153 +------- .../src/crypto/commit_reveal/utils.ts | 128 +------ 3 files changed, 16 insertions(+), 593 deletions(-) diff --git a/embed/oko_attached/src/crypto/commit_reveal/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index f4dfa1c81..5521fb93b 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -1,327 +1 @@ -/** - * Commit-Reveal Session Management - */ - -import type { Bytes32 } from "@oko-wallet/bytes"; -import type { Result } from "@oko-wallet/stdlib-js"; - -import type { - CommitRevealSession, - CommitRevealSessionState, - CreateSessionOptions, - NodeStatus, - EncryptedToken, -} from "./types"; -import type { ClientEcdheKeypair } from "./utils"; -import { - generateSessionId, - generateClientKeypair, - calculateTokenHash, - calculateExpiresAt, - isSessionExpired, -} 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 }; - } - - const tokenHashResult = calculateTokenHash( - options.oauth_token, - options.sdk_version, - ); - if (!tokenHashResult.success) { - return { success: false, err: tokenHashResult.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, - }, - }, - }; -} - -/** - * 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], - }, - }; -} - -/** - * Store encrypted token for a node. - */ -export function storeEncryptedToken( - session: CommitRevealSession, - nodeUrl: string, - encryptedToken: EncryptedToken, -): CommitRevealSession { - return { - ...session, - updated_at: new Date(), - commit_phase: { - ...session.commit_phase, - encrypted_tokens: { - ...session.commit_phase.encrypted_tokens, - [nodeUrl]: encryptedToken, - }, - }, - }; -} - -// ============================================================================ -// 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], - }, - }; -} - -/** - * Record failed node reveal. - */ -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 function getSuccessfulRevealCount(session: CommitRevealSession): number { - return session.reveal_phase.nodes_revealed.filter( - (n) => n.status === "SUCCESS", - ).length; -} - -export function isCommitPhaseComplete(session: CommitRevealSession): boolean { - return ( - session.commit_phase.nodes_committed.length === - session.commit_phase.total_nodes - ); -} - -export function isRevealPhaseComplete(session: CommitRevealSession): boolean { - return ( - session.reveal_phase.nodes_revealed.length === - session.reveal_phase.total_nodes - ); -} - -export function meetsThreshold( - session: CommitRevealSession, - threshold: number, -): boolean { - if (session.state === "COMMIT_PHASE" || session.state === "COMMITTED") { - return getSuccessfulCommitCount(session) >= threshold; - } - if (session.state === "REVEAL_PHASE" || session.state === "COMPLETED") { - return getSuccessfulRevealCount(session) >= threshold; - } - return false; -} +// @TODO \ No newline at end of file diff --git a/embed/oko_attached/src/crypto/commit_reveal/types.ts b/embed/oko_attached/src/crypto/commit_reveal/types.ts index e71739f0a..62f8beb0b 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/types.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/types.ts @@ -1,144 +1,19 @@ -/** - * 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 { 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 + client_keypair: { + privateKey: Bytes<32>; + publicKey: Bytes<32>; }; -} - -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"; - }; -} - -// ============================================================================ -// Session Creation -// ============================================================================ - -export interface CreateSessionOptions { - oauth_token: string; - user_email: string; - wallet_public_key: string; - operation_type: OperationType; - node_urls: string[]; - sdk_version: string; + id_token_hash: string; + auth_type: AuthType; + id_token: string; + oko_api_node_pubkey?: string; + ksn_node_pubkeys: Record; + created_at: Date; + expires_at: Date; } diff --git a/embed/oko_attached/src/crypto/commit_reveal/utils.ts b/embed/oko_attached/src/crypto/commit_reveal/utils.ts index fed12033d..5521fb93b 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/utils.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/utils.ts @@ -1,127 +1 @@ -/** - * 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 { - generateEddsaKeypair, - deriveSessionKey, - type EddsaKeypair, - type EcdheSessionKey, -} from "@oko-wallet/crypto-js/browser"; -import { sha256 } from "@oko-wallet/crypto-js"; -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(); -} - -// ============================================================================ -// 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 { - 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, -): Result { - const prefix = generateVersionPrefix(sdkVersion); - const dataToHash = prefix + oauthToken; - - const hashResult = sha256(dataToHash); - if (!hashResult.success) { - return { success: false, err: hashResult.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); -} - -/** - * Check if session has expired. - */ -export function isSessionExpired(expiresAt: Date): boolean { - return new Date() > expiresAt; -} +// @TODO \ No newline at end of file From 2b878a4e18220cc06dd258ff79d4f77608e8f511 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Thu, 29 Jan 2026 21:34:29 +0900 Subject: [PATCH 03/15] attached: add commit reveal utils --- .../src/crypto/commit_reveal/utils.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/embed/oko_attached/src/crypto/commit_reveal/utils.ts b/embed/oko_attached/src/crypto/commit_reveal/utils.ts index 5521fb93b..2537d29b7 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/utils.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/utils.ts @@ -1 +1,52 @@ -// @TODO \ No newline at end of file +import { v4 as uuidv4 } from "uuid"; +import { sha256 } from "@oko-wallet/crypto-js"; +import { + generateEddsaKeypair, + signMessage, + convertEddsaSignatureToBytes, +} from "@oko-wallet/crypto-js/browser"; +import type { Bytes } from "@oko-wallet/bytes"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { Result } from "@oko-wallet/stdlib-js"; + +export const SESSION_TIMEOUT_MS = 5 * 60 * 1000; + +export function generateSessionId(): string { + return uuidv4(); +} + +export function generateClientKeypair() { + return generateEddsaKeypair(); +} + +export function computeIdTokenHash( + authType: AuthType, + idToken: string, +): Result { + const hashRes = sha256(`${authType}${idToken}`); + if (!hashRes.success) { + return { success: false, err: hashRes.err }; + } + return { success: true, data: hashRes.data.toHex() }; +} + +export function createRevealSignature( + clientPrivateKey: Bytes<32>, + nodePubkey: string, + sessionId: string, + authType: AuthType, + idToken: string, + operationType: string, + apiName: string, +): Result { + const message = `${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() }; +} From a9561c9e839a3d24124cb8ce2a354459395fd729 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 12:35:21 +0900 Subject: [PATCH 04/15] o --- .../server/src/middleware/commit_reveal.ts | 43 +++---------------- crypto/crypto_js/common/commit_reveal.ts | 25 +++++++++++ crypto/crypto_js/common/index.ts | 1 + .../server/src/middlewares/commit_reveal.ts | 41 +++--------------- 4 files changed, 37 insertions(+), 73 deletions(-) create mode 100644 crypto/crypto_js/common/commit_reveal.ts diff --git a/backend/oko_api/server/src/middleware/commit_reveal.ts b/backend/oko_api/server/src/middleware/commit_reveal.ts index 31ba5ddb2..22b3110a2 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, makeCommitRevealSignMessage } 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 = makeCommitRevealSignMessage({ 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/crypto/crypto_js/common/commit_reveal.ts b/crypto/crypto_js/common/commit_reveal.ts new file mode 100644 index 000000000..6de8b60d3 --- /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 makeCommitRevealSignMessage({ + 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/key_share_node/server/src/middlewares/commit_reveal.ts b/key_share_node/server/src/middlewares/commit_reveal.ts index b3b379e42..8006d83f0 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, makeCommitRevealSignMessage } 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 = makeCommitRevealSignMessage({ 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 - ); -} From f076d87ca4ca045558d57940f5d2c94922ff3af7 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 12:51:21 +0900 Subject: [PATCH 05/15] attached: add commit-reveal session management functions --- .../src/crypto/commit_reveal/session.ts | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/embed/oko_attached/src/crypto/commit_reveal/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index 5521fb93b..ef8f7300c 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -1 +1,61 @@ -// @TODO \ No newline at end of file +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { OperationType } from "@oko-wallet/oko-types/commit_reveal"; +import type { Result } from "@oko-wallet/stdlib-js"; + +import type { ClientCommitRevealSession } from "./types"; +import { + generateSessionId, + generateClientKeypair, + computeIdTokenHash, + SESSION_TIMEOUT_MS, +} from "./utils"; + +export function createCommitRevealSession( + operationType: OperationType, + authType: AuthType, + idToken: string, +): Result { + const keypairRes = generateClientKeypair(); + if (!keypairRes.success) { + return { success: false, err: keypairRes.err }; + } + + const hashRes = computeIdTokenHash(authType, idToken); + if (!hashRes.success) { + return { success: false, err: hashRes.err }; + } + + const now = new Date(); + return { + success: true, + 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: {}, + created_at: now, + expires_at: new Date(now.getTime() + SESSION_TIMEOUT_MS), + }, + }; +} + +export function setOkoApiNodePubkey( + session: ClientCommitRevealSession, + nodePubkey: string, +): ClientCommitRevealSession { + return { ...session, oko_api_node_pubkey: nodePubkey }; +} + +export function setKsnNodePubkey( + session: ClientCommitRevealSession, + nodeUrl: string, + nodePubkey: string, +): ClientCommitRevealSession { + return { + ...session, + ksn_node_pubkeys: { ...session.ksn_node_pubkeys, [nodeUrl]: nodePubkey }, + }; +} From e455b2ab5a1db273250ad2b4b2769fa96628685d Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 13:07:01 +0900 Subject: [PATCH 06/15] o --- .../server/src/middleware/commit_reveal.ts | 4 +- crypto/crypto_js/common/commit_reveal.ts | 2 +- .../src/crypto/commit_reveal/index.ts | 5 +-- .../src/crypto/commit_reveal/signature.ts | 44 +++++++++++++++++++ .../src/crypto/commit_reveal/utils.ts | 11 ++++- .../server/src/middlewares/commit_reveal.ts | 4 +- 6 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 embed/oko_attached/src/crypto/commit_reveal/signature.ts diff --git a/backend/oko_api/server/src/middleware/commit_reveal.ts b/backend/oko_api/server/src/middleware/commit_reveal.ts index 22b3110a2..667099c25 100644 --- a/backend/oko_api/server/src/middleware/commit_reveal.ts +++ b/backend/oko_api/server/src/middleware/commit_reveal.ts @@ -1,7 +1,7 @@ import type { Request, Response, NextFunction } from "express"; import { Bytes } from "@oko-wallet/bytes"; import { verifySignature } from "@oko-wallet/crypto-js/node/ecdhe"; -import { sha256, makeCommitRevealSignMessage } from "@oko-wallet/crypto-js"; +import { sha256, buildRevealMessage } from "@oko-wallet/crypto-js"; import { getCommitRevealSessionBySessionId, createCommitRevealApiCall, @@ -168,7 +168,7 @@ 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 = makeCommitRevealSignMessage({ + const message = buildRevealMessage({ nodePubkeyHex, sessionId: cr_session_id, authType, diff --git a/crypto/crypto_js/common/commit_reveal.ts b/crypto/crypto_js/common/commit_reveal.ts index 6de8b60d3..ee8f6354b 100644 --- a/crypto/crypto_js/common/commit_reveal.ts +++ b/crypto/crypto_js/common/commit_reveal.ts @@ -11,7 +11,7 @@ export interface CommitRevealSignMessageArgs { * 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 makeCommitRevealSignMessage({ +export function buildRevealMessage({ nodePubkeyHex, sessionId, authType, 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/signature.ts b/embed/oko_attached/src/crypto/commit_reveal/signature.ts new file mode 100644 index 000000000..dbecbbe4e --- /dev/null +++ b/embed/oko_attached/src/crypto/commit_reveal/signature.ts @@ -0,0 +1,44 @@ +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]; + if (!nodePubkey) { + return { success: false, err: `KSN node pubkey not found for ${nodeUrl}` }; + } + return createRevealSignature( + session.client_keypair.privateKey, + nodePubkey, + session.session_id, + session.auth_type, + session.id_token, + session.operation_type, + apiName, + ); +} diff --git a/embed/oko_attached/src/crypto/commit_reveal/utils.ts b/embed/oko_attached/src/crypto/commit_reveal/utils.ts index 2537d29b7..c704436f9 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/utils.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/utils.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { sha256 } from "@oko-wallet/crypto-js"; +import { sha256, buildRevealMessage } from "@oko-wallet/crypto-js"; import { generateEddsaKeypair, signMessage, @@ -39,7 +39,14 @@ export function createRevealSignature( operationType: string, apiName: string, ): Result { - const message = `${nodePubkey}${sessionId}${authType}${idToken}${operationType}${apiName}`; + const message = buildRevealMessage({ + nodePubkeyHex: nodePubkey, + sessionId, + authType, + idToken, + operationType, + apiName, + }); const signRes = signMessage(message, clientPrivateKey); if (!signRes.success) { return { success: false, err: signRes.err }; diff --git a/key_share_node/server/src/middlewares/commit_reveal.ts b/key_share_node/server/src/middlewares/commit_reveal.ts index 8006d83f0..78c6b6be5 100644 --- a/key_share_node/server/src/middlewares/commit_reveal.ts +++ b/key_share_node/server/src/middlewares/commit_reveal.ts @@ -1,7 +1,7 @@ import type { Request, Response, NextFunction } from "express"; import { Bytes } from "@oko-wallet/bytes"; import { verifySignature } from "@oko-wallet/crypto-js/node/ecdhe"; -import { sha256, makeCommitRevealSignMessage } from "@oko-wallet/crypto-js"; +import { sha256, buildRevealMessage } from "@oko-wallet/crypto-js"; import { getCommitRevealSessionBySessionId, createCommitRevealApiCall, @@ -168,7 +168,7 @@ export function commitRevealMiddleware(apiName: ApiName) { } const nodePubkeyHex = state.serverKeypair.publicKey.toHex(); - const message = makeCommitRevealSignMessage({ + const message = buildRevealMessage({ nodePubkeyHex, sessionId: cr_session_id, authType, From 271de693fc0a8d096ee26daf7aa409324e4e519c Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 14:03:11 +0900 Subject: [PATCH 07/15] o --- embed/oko_attached/src/requests/ks_node_v2.ts | 55 +++++++++++++++++++ embed/oko_attached/src/requests/oko_api.ts | 23 ++++++++ .../ksn_interface/src/commit_reveal.ts | 12 ++++ 3 files changed, 90 insertions(+) diff --git a/embed/oko_attached/src/requests/ks_node_v2.ts b/embed/oko_attached/src/requests/ks_node_v2.ts index eab38fd5f..89080a405 100644 --- a/embed/oko_attached/src/requests/ks_node_v2.ts +++ b/embed/oko_attached/src/requests/ks_node_v2.ts @@ -6,6 +6,11 @@ import type { ReshareKeyShareV2RequestBody, ReshareRegisterV2RequestBody, } 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 { Result } from "@oko-wallet/stdlib-js"; @@ -470,3 +475,53 @@ 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..768bf3e0c 100644 --- a/embed/oko_attached/src/requests/oko_api.ts +++ b/embed/oko_attached/src/requests/oko_api.ts @@ -1,4 +1,9 @@ import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; +import type { OperationType } 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"; @@ -76,3 +81,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/key_share_node/ksn_interface/src/commit_reveal.ts b/key_share_node/ksn_interface/src/commit_reveal.ts index 227ff1873..ab0058019 100644 --- a/key_share_node/ksn_interface/src/commit_reveal.ts +++ b/key_share_node/ksn_interface/src/commit_reveal.ts @@ -39,3 +39,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; +} From b175346a0b3aac04daad528ec122e740b3a58f5c Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 14:25:54 +0900 Subject: [PATCH 08/15] attached: add parallel commits --- .../src/crypto/commit_reveal/session.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/embed/oko_attached/src/crypto/commit_reveal/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index ef8f7300c..d2bd2a806 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -9,6 +9,8 @@ import { computeIdTokenHash, SESSION_TIMEOUT_MS, } from "./utils"; +import { commitToOkoApi } from "@oko-wallet-attached/requests/oko_api"; +import { commitToKsNode } from "@oko-wallet-attached/requests/ks_node_v2"; export function createCommitRevealSession( operationType: OperationType, @@ -59,3 +61,88 @@ export function setKsnNodePubkey( ksn_node_pubkeys: { ...session.ksn_node_pubkeys, [nodeUrl]: nodePubkey }, }; } + +export interface CommitAllResult { + session: ClientCommitRevealSession; + okoApiCommitted: boolean; + ksnCommittedNodes: string[]; +} + +/** + * Commit to oko_api and KSN nodes in parallel. + * Creates a commit-reveal session and sends commit requests to all nodes. + */ +export async function commitAll( + operationType: OperationType, + authType: AuthType, + idToken: string, + ksnNodeUrls: string[], +): Promise> { + // 1. Create session + const sessionRes = createCommitRevealSession(operationType, authType, idToken); + if (!sessionRes.success) { + return { success: false, err: sessionRes.err }; + } + let session = sessionRes.data; + + const clientPubkeyHex = session.client_keypair.publicKey.toHex(); + + // 2. Commit to oko_api and KSN nodes in parallel + const [okoApiResult, ...ksnResults] = await Promise.allSettled([ + commitToOkoApi( + session.session_id, + operationType, + clientPubkeyHex, + session.id_token_hash, + ), + ...ksnNodeUrls.map((nodeUrl) => + commitToKsNode( + nodeUrl, + session.session_id, + operationType, + clientPubkeyHex, + session.id_token_hash, + ).then((res) => ({ nodeUrl, res })), + ), + ]); + + // 3. Process oko_api result + let okoApiCommitted = false; + if (okoApiResult.status === "fulfilled" && okoApiResult.value.success) { + const apiResponse = okoApiResult.value.data; + if (apiResponse.success) { + session = setOkoApiNodePubkey(session, apiResponse.data.node_pubkey); + okoApiCommitted = true; + } + } + + // 4. Process KSN results + const ksnCommittedNodes: string[] = []; + for (const result of ksnResults) { + if (result.status === "fulfilled") { + const { nodeUrl, res } = result.value; + if (res.success) { + session = setKsnNodePubkey(session, nodeUrl, res.data.node_pubkey); + ksnCommittedNodes.push(nodeUrl); + } + } + } + + // 5. Check if we have enough commits + if (!okoApiCommitted) { + return { success: false, err: "Failed to commit to oko_api" }; + } + + if (ksnCommittedNodes.length === 0) { + return { success: false, err: "Failed to commit to any KSN node" }; + } + + return { + success: true, + data: { + session, + okoApiCommitted, + ksnCommittedNodes, + }, + }; +} From c6dee031d7f5bff2851b2c0c2e02aad84cc56495 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 15:27:05 +0900 Subject: [PATCH 09/15] oko_api: refactor commit operation type --- .../server/src/commit_reveal/allowed_apis.ts | 2 + .../src/middleware/commit_reveal.test.ts | 369 +++++++++++++++--- .../server/src/routes/tss_v2/commit.test.ts | 15 + .../server/src/routes/tss_v2/e2e.test.ts | 283 ++++++++++++-- backend/openapi/src/tss/commit_reveal.ts | 2 +- common/oko_types/src/commit_reveal/index.ts | 1 + .../ksn_interface/src/commit_reveal.ts | 1 + .../server/src/commit_reveal/allowed_apis.ts | 2 + 8 files changed, 583 insertions(+), 92 deletions(-) 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/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..db973ddb3 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"; diff --git a/key_share_node/ksn_interface/src/commit_reveal.ts b/key_share_node/ksn_interface/src/commit_reveal.ts index ab0058019..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"; 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 0439eed4b..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,6 +7,7 @@ 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"], }; @@ -15,6 +16,7 @@ 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"], }; From 2b542d429ce70f7b6a56e617954cc925dab74930 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 15:34:48 +0900 Subject: [PATCH 10/15] ks_node: refactor operation types --- .../src/middlewares/commit_reveal.test.ts | 97 ++++++++ .../src/openapi/schema/commit_reveal.ts | 1 + .../src/routes/key_share_v2/commit.test.ts | 15 ++ .../src/routes/key_share_v2/e2e.test.ts | 227 ++++++++++++++++++ 4 files changed, 340 insertions(+) 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/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 From 3a736ec0ecde328538dabebb63ca1c7eaf17829b Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 15:46:14 +0900 Subject: [PATCH 11/15] o --- .../src/crypto/commit_reveal/session.ts | 22 ++++++++++++------- .../src/crypto/commit_reveal/signature.ts | 7 +++++- .../src/crypto/commit_reveal/types.ts | 7 ++++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/embed/oko_attached/src/crypto/commit_reveal/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index d2bd2a806..d5e4cc86b 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -1,8 +1,9 @@ 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 { ClientCommitRevealSession } from "./types"; +import type { ClientCommitRevealSession, KsnCommitTarget } from "./types"; import { generateSessionId, generateClientKeypair, @@ -38,6 +39,7 @@ export function createCommitRevealSession( auth_type: authType, id_token: idToken, ksn_node_pubkeys: {}, + ksn_operation_types: {}, created_at: now, expires_at: new Date(now.getTime() + SESSION_TIMEOUT_MS), }, @@ -55,10 +57,12 @@ export function setKsnNodePubkey( session: ClientCommitRevealSession, nodeUrl: string, nodePubkey: string, + operationType: KsnOperationType, ): ClientCommitRevealSession { return { ...session, ksn_node_pubkeys: { ...session.ksn_node_pubkeys, [nodeUrl]: nodePubkey }, + ksn_operation_types: { ...session.ksn_operation_types, [nodeUrl]: operationType }, }; } @@ -71,12 +75,14 @@ export interface CommitAllResult { /** * Commit to oko_api and KSN nodes in parallel. * Creates a commit-reveal session and sends commit requests to all nodes. + * Supports per-node KSN operation types for reshare scenarios where ACTIVE and new nodes + * use different operation types. */ export async function commitAll( operationType: OperationType, authType: AuthType, idToken: string, - ksnNodeUrls: string[], + ksnCommitTargets: KsnCommitTarget[], ): Promise> { // 1. Create session const sessionRes = createCommitRevealSession(operationType, authType, idToken); @@ -95,14 +101,14 @@ export async function commitAll( clientPubkeyHex, session.id_token_hash, ), - ...ksnNodeUrls.map((nodeUrl) => + ...ksnCommitTargets.map((target) => commitToKsNode( - nodeUrl, + target.nodeUrl, session.session_id, - operationType, + target.operationType, clientPubkeyHex, session.id_token_hash, - ).then((res) => ({ nodeUrl, res })), + ).then((res) => ({ nodeUrl: target.nodeUrl, operationType: target.operationType, res })), ), ]); @@ -120,9 +126,9 @@ export async function commitAll( const ksnCommittedNodes: string[] = []; for (const result of ksnResults) { if (result.status === "fulfilled") { - const { nodeUrl, res } = result.value; + const { nodeUrl, operationType: ksnOpType, res } = result.value; if (res.success) { - session = setKsnNodePubkey(session, nodeUrl, res.data.node_pubkey); + session = setKsnNodePubkey(session, nodeUrl, res.data.node_pubkey, ksnOpType); ksnCommittedNodes.push(nodeUrl); } } diff --git a/embed/oko_attached/src/crypto/commit_reveal/signature.ts b/embed/oko_attached/src/crypto/commit_reveal/signature.ts index dbecbbe4e..da07d7789 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/signature.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/signature.ts @@ -29,16 +29,21 @@ export function createKsnSignature( 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, - session.operation_type, + 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 62f8beb0b..464cd3657 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/types.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/types.ts @@ -1,5 +1,6 @@ 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"; export interface ClientCommitRevealSession { @@ -14,6 +15,12 @@ export interface ClientCommitRevealSession { id_token: string; oko_api_node_pubkey?: string; ksn_node_pubkeys: Record; + ksn_operation_types: Record; created_at: Date; expires_at: Date; } + +export interface KsnCommitTarget { + nodeUrl: string; + operationType: KsnOperationType; +} From f68a05ab7a563a9decfd5dac9cf36851e358520d Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 16:02:20 +0900 Subject: [PATCH 12/15] o --- embed/oko_attached/src/requests/ks_node_v2.ts | 66 +++++++++++++++---- embed/oko_attached/src/requests/oko_api.ts | 12 +++- key_share_node/ksn_interface/src/key_share.ts | 43 ++++++++++++ 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/embed/oko_attached/src/requests/ks_node_v2.ts b/embed/oko_attached/src/requests/ks_node_v2.ts index 89080a405..22541cb6b 100644 --- a/embed/oko_attached/src/requests/ks_node_v2.ts +++ b/embed/oko_attached/src/requests/ks_node_v2.ts @@ -1,10 +1,10 @@ import type { - GetKeyShareV2RequestBody, GetKeyShareV2Response, - RegisterKeyShareV2RequestBody, - RegisterEd25519V2RequestBody, - ReshareKeyShareV2RequestBody, - ReshareRegisterV2RequestBody, + GetKeyShareV2WithCRRequestBody, + RegisterKeyShareV2WithCRRequestBody, + RegisterEd25519V2WithCRRequestBody, + ReshareKeyShareV2WithCRRequestBody, + ReshareRegisterV2WithCRRequestBody, } from "@oko-wallet/ksn-interface/key_share"; import type { CommitRequestBody, @@ -16,6 +16,12 @@ import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { Result } from "@oko-wallet/stdlib-js"; import type { KSNodeApiResponse } from "@oko-wallet/ksn-interface/response"; +export interface KsnCommitRevealParams { + cr_session_id: string; + cr_signature: string; + auth_type: AuthType; +} + export interface RequestKeySharesV2Result { secp256k1?: string; // share hex string ed25519?: string; // share hex string @@ -47,6 +53,9 @@ export async function requestKeySharesV2( secp256k1?: string; // public key hex ed25519?: string; // public key hex }, + getCommitRevealParams?: ( + nodeEndpoint: string, + ) => KsnCommitRevealParams | undefined, ): Promise> { const shuffledNodes = [...allNodes]; for (let i = shuffledNodes.length - 1; i > 0; i -= 1) { @@ -61,7 +70,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), + ), ), ); @@ -128,13 +144,18 @@ async function requestKeyShareFromNodeV2( ed25519?: string; }, maxRetries: number = 2, + commitReveal?: KsnCommitRevealParams, ): 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; @@ -225,8 +246,9 @@ export async function registerKeySharesV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, + commitReveal?: KsnCommitRevealParams, ): Promise> { - const body: RegisterKeyShareV2RequestBody = { + const body: RegisterKeyShareV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { @@ -242,6 +264,10 @@ export async function registerKeySharesV2( }, }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -294,11 +320,16 @@ export async function registerKeyShareEd25519V2( authType: AuthType, publicKey: string, share: string, + commitReveal?: KsnCommitRevealParams, ): 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 { @@ -356,8 +387,9 @@ export async function reshareKeySharesV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, + commitReveal?: KsnCommitRevealParams, ): Promise> { - const body: ReshareKeyShareV2RequestBody = { + const body: ReshareKeyShareV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { @@ -373,6 +405,10 @@ export async function reshareKeySharesV2( }, }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -420,8 +456,9 @@ export async function reshareRegisterV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, + commitReveal?: KsnCommitRevealParams, ): Promise> { - const body: ReshareRegisterV2RequestBody = { + const body: ReshareRegisterV2WithCRRequestBody = { auth_type: authType, wallets: { ...(wallets.secp256k1 && { @@ -437,6 +474,10 @@ export async function reshareRegisterV2( }, }), }, + ...(commitReveal && { + cr_session_id: commitReveal.cr_session_id, + cr_signature: commitReveal.cr_signature, + }), }; try { @@ -509,7 +550,8 @@ export async function commitToKsNode( }; } - const data = (await response.json()) as KSNodeApiResponse; + const data = + (await response.json()) as KSNodeApiResponse; if (data.success === false) { return { success: false, diff --git a/embed/oko_attached/src/requests/oko_api.ts b/embed/oko_attached/src/requests/oko_api.ts index 768bf3e0c..59d5b2d7b 100644 --- a/embed/oko_attached/src/requests/oko_api.ts +++ b/embed/oko_attached/src/requests/oko_api.ts @@ -5,10 +5,17 @@ import type { CommitResponseData, } from "@oko-wallet/oko-api-openapi/tss"; import type { Result } from "@oko-wallet/stdlib-js"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { FetchError } from "./types"; import { OKO_API_ENDPOINT } from "./endpoints"; +export interface CommitRevealParams { + cr_session_id: string; + cr_signature: string; + auth_type: AuthType; +} + export const TSS_V1_ENDPOINT = `${OKO_API_ENDPOINT}/tss/v1`; export const TSS_V2_ENDPOINT = `${OKO_API_ENDPOINT}/tss/v2`; export const SOCIAL_LOGIN_V1_ENDPOINT = `${OKO_API_ENDPOINT}/social-login/v1`; @@ -52,7 +59,10 @@ export async function makeAuthorizedOkoApiRequest( idToken: string, args: T, baseUrl: string = TSS_V1_ENDPOINT, + commitReveal?: CommitRevealParams, ): Promise, FetchError>> { + const body = commitReveal ? { ...args, ...commitReveal } : args; + let resp; try { resp = await fetch(`${baseUrl}/${path}`, { @@ -61,7 +71,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 }; 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; From 9cd6024aa298b7b641597de0889afae7cf79d647 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 16:19:11 +0900 Subject: [PATCH 13/15] o --- .../src/crypto/commit_reveal/session.ts | 128 +++++++++++------- 1 file changed, 81 insertions(+), 47 deletions(-) diff --git a/embed/oko_attached/src/crypto/commit_reveal/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index d5e4cc86b..412276bc5 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -73,16 +73,21 @@ export interface CommitAllResult { } /** - * Commit to oko_api and KSN nodes in parallel. - * Creates a commit-reveal session and sends commit requests to all nodes. - * Supports per-node KSN operation types for reshare scenarios where ACTIVE and new nodes - * use different operation types. + * 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 async function commitAll( operationType: OperationType, authType: AuthType, idToken: string, ksnCommitTargets: KsnCommitTarget[], + ksnThreshold: number, ): Promise> { // 1. Create session const sessionRes = createCommitRevealSession(operationType, authType, idToken); @@ -93,62 +98,91 @@ export async function commitAll( const clientPubkeyHex = session.client_keypair.publicKey.toHex(); - // 2. Commit to oko_api and KSN nodes in parallel - const [okoApiResult, ...ksnResults] = await Promise.allSettled([ - commitToOkoApi( - session.session_id, - operationType, - clientPubkeyHex, - session.id_token_hash, - ), - ...ksnCommitTargets.map((target) => - commitToKsNode( - target.nodeUrl, - session.session_id, - target.operationType, - clientPubkeyHex, - session.id_token_hash, - ).then((res) => ({ nodeUrl: target.nodeUrl, operationType: target.operationType, res })), - ), - ]); - - // 3. Process oko_api result - let okoApiCommitted = false; - if (okoApiResult.status === "fulfilled" && okoApiResult.value.success) { - const apiResponse = okoApiResult.value.data; - if (apiResponse.success) { - session = setOkoApiNodePubkey(session, apiResponse.data.node_pubkey); - okoApiCommitted = true; - } + // 2. Commit to oko_api + const okoApiResult = await commitToOkoApi( + session.session_id, + operationType, + clientPubkeyHex, + session.id_token_hash, + ); + + 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]]; + } + + 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, + })), + ), + ); - // 4. Process KSN results - const ksnCommittedNodes: string[] = []; - for (const result of ksnResults) { - if (result.status === "fulfilled") { - const { nodeUrl, operationType: ksnOpType, res } = result.value; - if (res.success) { - session = setKsnNodePubkey(session, nodeUrl, res.data.node_pubkey, ksnOpType); - ksnCommittedNodes.push(nodeUrl); + 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); } } - } - // 5. Check if we have enough commits - if (!okoApiCommitted) { - return { success: false, err: "Failed to commit to oko_api" }; + 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 (ksnCommittedNodes.length === 0) { - return { success: false, err: "Failed to commit to any KSN node" }; + if (committedNodes.length < ksnThreshold) { + return { + success: false, + err: `Insufficient KSN commits: got ${committedNodes.length}, need ${ksnThreshold}`, + }; } return { success: true, data: { session, - okoApiCommitted, - ksnCommittedNodes, + okoApiCommitted: true, + ksnCommittedNodes: committedNodes, }, }; } From 4a64fa3cf981eef7cd7e88bf71bc6f113f8828f6 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 17:44:05 +0900 Subject: [PATCH 14/15] attached: apply commit-reveal to sign up flow --- common/oko_types/src/commit_reveal/index.ts | 5 ++ crypto/tecdsa/api_lib/src/index.ts | 5 +- .../src/crypto/commit_reveal/session.ts | 16 +++- .../window_msgs/oauth_info_pass/user_v2.ts | 81 ++++++++++++++++--- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/common/oko_types/src/commit_reveal/index.ts b/common/oko_types/src/commit_reveal/index.ts index db973ddb3..54033731e 100644 --- a/common/oko_types/src/commit_reveal/index.ts +++ b/common/oko_types/src/commit_reveal/index.ts @@ -34,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/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/session.ts b/embed/oko_attached/src/crypto/commit_reveal/session.ts index 412276bc5..3cf6b62da 100644 --- a/embed/oko_attached/src/crypto/commit_reveal/session.ts +++ b/embed/oko_attached/src/crypto/commit_reveal/session.ts @@ -62,7 +62,10 @@ export function setKsnNodePubkey( return { ...session, ksn_node_pubkeys: { ...session.ksn_node_pubkeys, [nodeUrl]: nodePubkey }, - ksn_operation_types: { ...session.ksn_operation_types, [nodeUrl]: operationType }, + ksn_operation_types: { + ...session.ksn_operation_types, + [nodeUrl]: operationType, + }, }; } @@ -90,7 +93,11 @@ export async function commitAll( ksnThreshold: number, ): Promise> { // 1. Create session - const sessionRes = createCommitRevealSession(operationType, authType, idToken); + const sessionRes = createCommitRevealSession( + operationType, + authType, + idToken, + ); if (!sessionRes.success) { return { success: false, err: sessionRes.err }; } @@ -116,7 +123,10 @@ export async function commitAll( 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]]; + [shuffledTargets[i], shuffledTargets[j]] = [ + shuffledTargets[j], + shuffledTargets[i], + ]; } const committedNodes: string[] = []; 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..db2006e0a 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 @@ -27,7 +27,14 @@ import { registerKeyShareEd25519V2, reshareKeySharesV2, reshareRegisterV2, + type KsnCommitRevealParams, } 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,61 @@ 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: KsnCommitRevealParams = { + cr_session_id: session.session_id, + cr_signature: ksnSigRes.data, + auth_type: authType, + }; + 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 +186,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 +211,10 @@ export async function handleNewUserV2( }, }, idToken, + { + cr_session_id: session.session_id, + cr_signature: okoApiSigRes.data, + }, ); if (reqKeygenV2Res.success === false) { return { From bd5a6fff4ac830dfeefddc4e0460e759ad44f4b9 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 30 Jan 2026 20:31:46 +0900 Subject: [PATCH 15/15] attached: apply commit-reveal to sign in flow --- embed/oko_attached/src/requests/ks_node_v2.ts | 19 ++--- embed/oko_attached/src/requests/oko_api.ts | 16 ++-- .../window_msgs/oauth_info_pass/user_v2.ts | 75 +++++++++++++++++-- 3 files changed, 80 insertions(+), 30 deletions(-) diff --git a/embed/oko_attached/src/requests/ks_node_v2.ts b/embed/oko_attached/src/requests/ks_node_v2.ts index 22541cb6b..2045205fd 100644 --- a/embed/oko_attached/src/requests/ks_node_v2.ts +++ b/embed/oko_attached/src/requests/ks_node_v2.ts @@ -13,15 +13,10 @@ import type { 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"; -export interface KsnCommitRevealParams { - cr_session_id: string; - cr_signature: string; - auth_type: AuthType; -} - export interface RequestKeySharesV2Result { secp256k1?: string; // share hex string ed25519?: string; // share hex string @@ -55,7 +50,7 @@ export async function requestKeySharesV2( }, getCommitRevealParams?: ( nodeEndpoint: string, - ) => KsnCommitRevealParams | undefined, + ) => CommitRevealParams | undefined, ): Promise> { const shuffledNodes = [...allNodes]; for (let i = shuffledNodes.length - 1; i > 0; i -= 1) { @@ -144,7 +139,7 @@ async function requestKeyShareFromNodeV2( ed25519?: string; }, maxRetries: number = 2, - commitReveal?: KsnCommitRevealParams, + commitReveal?: CommitRevealParams, ): Promise> { const body: GetKeyShareV2WithCRRequestBody = { auth_type: authType, @@ -246,7 +241,7 @@ export async function registerKeySharesV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, - commitReveal?: KsnCommitRevealParams, + commitReveal?: CommitRevealParams, ): Promise> { const body: RegisterKeyShareV2WithCRRequestBody = { auth_type: authType, @@ -320,7 +315,7 @@ export async function registerKeyShareEd25519V2( authType: AuthType, publicKey: string, share: string, - commitReveal?: KsnCommitRevealParams, + commitReveal?: CommitRevealParams, ): Promise> { const body: RegisterEd25519V2WithCRRequestBody = { auth_type: authType, @@ -387,7 +382,7 @@ export async function reshareKeySharesV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, - commitReveal?: KsnCommitRevealParams, + commitReveal?: CommitRevealParams, ): Promise> { const body: ReshareKeyShareV2WithCRRequestBody = { auth_type: authType, @@ -456,7 +451,7 @@ export async function reshareRegisterV2( secp256k1?: { public_key: string; share: string }; ed25519?: { public_key: string; share: string }; }, - commitReveal?: KsnCommitRevealParams, + commitReveal?: CommitRevealParams, ): Promise> { const body: ReshareRegisterV2WithCRRequestBody = { auth_type: authType, diff --git a/embed/oko_attached/src/requests/oko_api.ts b/embed/oko_attached/src/requests/oko_api.ts index 59d5b2d7b..778ff5426 100644 --- a/embed/oko_attached/src/requests/oko_api.ts +++ b/embed/oko_attached/src/requests/oko_api.ts @@ -1,21 +1,17 @@ import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; -import type { OperationType } from "@oko-wallet/oko-types/commit_reveal"; +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 { AuthType } from "@oko-wallet/oko-types/auth"; import type { FetchError } from "./types"; import { OKO_API_ENDPOINT } from "./endpoints"; -export interface CommitRevealParams { - cr_session_id: string; - cr_signature: string; - auth_type: AuthType; -} - export const TSS_V1_ENDPOINT = `${OKO_API_ENDPOINT}/tss/v1`; export const TSS_V2_ENDPOINT = `${OKO_API_ENDPOINT}/tss/v2`; export const SOCIAL_LOGIN_V1_ENDPOINT = `${OKO_API_ENDPOINT}/social-login/v1`; @@ -26,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", @@ -63,7 +59,7 @@ export async function makeAuthorizedOkoApiRequest( ): Promise, FetchError>> { const body = commitReveal ? { ...args, ...commitReveal } : args; - let resp; + let resp: Response; try { resp = await fetch(`${baseUrl}/${path}`, { method: "POST", 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 db2006e0a..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"; @@ -27,7 +28,6 @@ import { registerKeyShareEd25519V2, reshareKeySharesV2, reshareRegisterV2, - type KsnCommitRevealParams, } from "@oko-wallet-attached/requests/ks_node_v2"; import { commitAll, @@ -149,10 +149,9 @@ export async function handleNewUserV2( if (!ksnSigRes.success) { return Promise.resolve({ success: false, err: ksnSigRes.err } as const); } - const commitRevealParams: KsnCommitRevealParams = { + const commitRevealParams: CommitRevealParams = { cr_session_id: session.session_id, cr_signature: ksnSigRes.data, - auth_type: authType, }; return registerKeySharesV2( keyShareByNode.node.endpoint, @@ -268,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, @@ -285,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; @@ -1144,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. @@ -1151,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);