diff --git a/Cargo.toml b/Cargo.toml index 619769a9e..f01e5a553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,20 +8,21 @@ members = [ "crypto/teddsa/frost_core", "crypto/teddsa/frost_ed25519_keplr", "crypto/teddsa/frost_rerandomized", + "crypto/teddsa/teddsa_keplr_mock", + "crypto/teddsa/teddsa_wasm_mock/wasm", + "crypto/teddsa/teddsa_keplr_addon_mock/addon", "crypto/teddsa/frost_ed25519_keplr_wasm/wasm", "key_share_node/key_share_node_2/core", "key_share_node/key_share_node_2/pg_interface", "key_share_node/key_share_node_2/server", - ] [workspace.dependencies] -cait_sith_keplr = { version = "=0.0.2-rc.4" } - -# for frost_ed25519 frost_core = { path = "crypto/teddsa/frost_core", version = "2.2.0", default-features = false } +frost_ed25519_keplr = { path = "crypto/teddsa/frost_ed25519_keplr", version = "2.2.0", default-features = false } frost_rerandomized = { path = "crypto/teddsa/frost_rerandomized", version = "2.2.0", default-features = false } +cait_sith_keplr = { version = "=0.0.2-rc.4" } criterion = "0.6" document-features = "0.2.7" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/crypto/teddsa/frost_ed25519_keplr/Cargo.toml b/crypto/teddsa/frost_ed25519_keplr/Cargo.toml index 924cebc98..2c52b05fd 100644 --- a/crypto/teddsa/frost_ed25519_keplr/Cargo.toml +++ b/crypto/teddsa/frost_ed25519_keplr/Cargo.toml @@ -20,25 +20,27 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] +frost_rerandomized = { workspace = true } +frost_core = { workspace = true } + curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } -document-features.workspace = true -frost_core.workspace = true -frost_rerandomized.workspace = true -rand_core.workspace = true +document-features = { workspace = true } +rand_core = { workspace = true } sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] -criterion.workspace = true frost_core = { workspace = true, features = ["test-impl"] } frost_rerandomized = { workspace = true, features = ["test-impl"] } + +criterion = { workspace = true } ed25519-dalek = "2.1.0" -insta.workspace = true -hex.workspace = true -lazy_static.workspace = true -proptest.workspace = true -rand.workspace = true -rand_chacha.workspace = true -serde_json.workspace = true +insta = { workspace = true } +hex = { workspace = true } +lazy_static = { workspace = true } +proptest = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } +serde_json = { workspace = true } [features] default = ["serialization", "cheater-detection", "std"] diff --git a/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/Cargo.toml b/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/Cargo.toml index 1c7df2145..86fa6f631 100644 --- a/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/Cargo.toml +++ b/crypto/teddsa/frost_ed25519_keplr_wasm/wasm/Cargo.toml @@ -20,7 +20,10 @@ strip = true #default = ["wee_alloc"] [dependencies] -frost_ed25519_keplr = { path = "../../frost_ed25519_keplr", features = ["serde"] } +frost_ed25519_keplr = { path = "../../frost_ed25519_keplr", features = [ + "serde", +] } + wasm-bindgen = "0.2.100" curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } serde = { version = "1.0", features = ["derive"] } diff --git a/crypto/teddsa/teddsa_hooks_mock/package.json b/crypto/teddsa/teddsa_hooks_mock/package.json new file mode 100644 index 000000000..58121f44f --- /dev/null +++ b/crypto/teddsa/teddsa_hooks_mock/package.json @@ -0,0 +1,15 @@ +{ + "name": "@oko-wallet/teddsa-hooks-mock", + "main": "./src/index.ts", + "version": "0.1.0", + "private": true, + "dependencies": { + "@oko-wallet/bytes": "^0.0.3-alpha.62", + "@oko-wallet/stdlib-js": "^0.0.2-rc.42", + "@oko-wallet/teddsa-wasm-mock": "workspace:*" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.8.3" + } +} diff --git a/crypto/teddsa/teddsa_hooks_mock/src/index.ts b/crypto/teddsa/teddsa_hooks_mock/src/index.ts new file mode 100644 index 000000000..7c89555ba --- /dev/null +++ b/crypto/teddsa/teddsa_hooks_mock/src/index.ts @@ -0,0 +1,3 @@ +export * from "./keygen"; +export * from "./sign"; +export * from "./types"; diff --git a/crypto/teddsa/teddsa_hooks_mock/src/keygen.ts b/crypto/teddsa/teddsa_hooks_mock/src/keygen.ts new file mode 100644 index 000000000..dbbb46e26 --- /dev/null +++ b/crypto/teddsa/teddsa_hooks_mock/src/keygen.ts @@ -0,0 +1,93 @@ +import { wasmModule } from "@oko-wallet/teddsa-wasm-mock"; +import { Bytes } from "@oko-wallet/bytes"; +import type { Bytes32 } from "@oko-wallet/bytes"; +import type { Result } from "@oko-wallet/stdlib-js"; + +import type { + TeddsaKeygenResult, + TeddsaKeygenOutputBytes, + TeddsaCentralizedKeygenOutput, +} from "./types"; + +/** + * Import an existing Ed25519 secret key and split it into threshold shares. + * + * @param secretKey - 32-byte Ed25519 secret key + * @returns Result containing keygen shares for both participants + */ +export async function importExternalSecretKeyEd25519( + secretKey: Bytes32, +): Promise> { + try { + const keygenOutput: TeddsaCentralizedKeygenOutput = + wasmModule.cli_keygen_import_ed25519([...secretKey.toUint8Array()]); + + return processKeygenOutput(keygenOutput); + } catch (error: any) { + return { + success: false, + err: String(error), + }; + } +} + +/** + * Generate a new 2-of-2 threshold Ed25519 key using centralized key generation. + * + * @returns Result containing keygen shares for both participants + */ +export async function runTeddsaKeygen(): Promise< + Result +> { + try { + const keygenOutput: TeddsaCentralizedKeygenOutput = + wasmModule.cli_keygen_centralized_ed25519(); + + return processKeygenOutput(keygenOutput); + } catch (error: any) { + return { + success: false, + err: String(error), + }; + } +} + +/** + * Process raw WASM keygen output into typed result + */ +function processKeygenOutput( + keygenOutput: TeddsaCentralizedKeygenOutput, +): Result { + const [keygen_1_raw, keygen_2_raw] = keygenOutput.keygen_outputs; + + // Convert public key to Bytes32 + const publicKeyBytesRes = Bytes.fromUint8Array( + new Uint8Array(keygenOutput.public_key), + 32, + ); + if (publicKeyBytesRes.success === false) { + return { + success: false, + err: publicKeyBytesRes.err, + }; + } + + const keygen_1: TeddsaKeygenOutputBytes = { + key_package: new Uint8Array(keygen_1_raw.key_package), + public_key_package: new Uint8Array(keygen_1_raw.public_key_package), + identifier: new Uint8Array(keygen_1_raw.identifier), + public_key: publicKeyBytesRes.data, + }; + + const keygen_2: TeddsaKeygenOutputBytes = { + key_package: new Uint8Array(keygen_2_raw.key_package), + public_key_package: new Uint8Array(keygen_2_raw.public_key_package), + identifier: new Uint8Array(keygen_2_raw.identifier), + public_key: publicKeyBytesRes.data, // Same public key for both participants + }; + + return { + success: true, + data: { keygen_1, keygen_2 }, + }; +} diff --git a/crypto/teddsa/teddsa_hooks_mock/src/sign.ts b/crypto/teddsa/teddsa_hooks_mock/src/sign.ts new file mode 100644 index 000000000..e4925c4b2 --- /dev/null +++ b/crypto/teddsa/teddsa_hooks_mock/src/sign.ts @@ -0,0 +1,213 @@ +import { wasmModule } from "@oko-wallet/teddsa-wasm-mock"; +import type { Result } from "@oko-wallet/stdlib-js"; + +import type { + TeddsaSignRound1Output, + TeddsaSignRound2Output, + TeddsaAggregateOutput, + CommitmentEntry, + SignatureShareEntry, + TeddsaKeygenOutputBytes, +} from "./types"; + +export type TeddsaSignError = + | { type: "aborted" } + | { type: "error"; msg: string }; + +/** + * TEdDSA signing round 1: Generate nonces and commitments + * + * @param keyPackage - Participant's serialized KeyPackage + * @returns Nonces (keep secret) and commitments (share with coordinator) + */ +export function teddsaSignRound1( + keyPackage: Uint8Array, +): Result { + try { + const result: TeddsaSignRound1Output = wasmModule.cli_sign_round1_ed25519([ + ...keyPackage, + ]); + return { success: true, data: result }; + } catch (error: any) { + return { success: false, err: String(error) }; + } +} + +/** + * TEdDSA signing round 2: Generate signature share + * + * @param message - Message to sign + * @param keyPackage - Participant's serialized KeyPackage + * @param nonces - Nonces from round 1 + * @param allCommitments - Commitments from all participants + * @returns Signature share + */ +export function teddsaSignRound2( + message: Uint8Array, + keyPackage: Uint8Array, + nonces: Uint8Array, + allCommitments: CommitmentEntry[], +): Result { + try { + const input = { + message: [...message], + key_package: [...keyPackage], + nonces: [...nonces], + all_commitments: allCommitments, + }; + const result: TeddsaSignRound2Output = + wasmModule.cli_sign_round2_ed25519(input); + return { success: true, data: result }; + } catch (error: any) { + return { success: false, err: String(error) }; + } +} + +/** + * Aggregate signature shares into final Ed25519 signature + * + * @param message - Original message that was signed + * @param allCommitments - Commitments from all participants + * @param allSignatureShares - Signature shares from all participants + * @param publicKeyPackage - Serialized PublicKeyPackage + * @returns 64-byte Ed25519 signature + */ +export function teddsaAggregate( + message: Uint8Array, + allCommitments: CommitmentEntry[], + allSignatureShares: SignatureShareEntry[], + publicKeyPackage: Uint8Array, +): Result { + try { + const input = { + message: [...message], + all_commitments: allCommitments, + all_signature_shares: allSignatureShares, + public_key_package: [...publicKeyPackage], + }; + const result: TeddsaAggregateOutput = + wasmModule.cli_aggregate_ed25519(input); + return { success: true, data: new Uint8Array(result.signature) }; + } catch (error: any) { + return { success: false, err: String(error) }; + } +} + +/** + * Verify an Ed25519 signature + * + * @param message - Original message + * @param signature - 64-byte Ed25519 signature + * @param publicKeyPackage - Serialized PublicKeyPackage + * @returns true if signature is valid + */ +export function teddsaVerify( + message: Uint8Array, + signature: Uint8Array, + publicKeyPackage: Uint8Array, +): Result { + try { + const input = { + message: [...message], + signature: [...signature], + public_key_package: [...publicKeyPackage], + }; + const isValid: boolean = wasmModule.cli_verify_ed25519(input); + return { success: true, data: isValid }; + } catch (error: any) { + return { success: false, err: String(error) }; + } +} + +/** + * High-level function to run complete TEdDSA signing protocol locally (for testing) + * + * This function performs all signing rounds locally with both key shares. + * In production, round 1 and round 2 would be distributed between client and server. + * + * @param message - Message to sign + * @param keygen1 - First participant's keygen output + * @param keygen2 - Second participant's keygen output + * @returns 64-byte Ed25519 signature + */ +export async function runTeddsaSignLocal( + message: Uint8Array, + keygen1: TeddsaKeygenOutputBytes, + keygen2: TeddsaKeygenOutputBytes, +): Promise> { + try { + // Round 1: Both participants generate commitments + const round1_1 = teddsaSignRound1(keygen1.key_package); + if (!round1_1.success) { + return { success: false, err: { type: "error", msg: round1_1.err } }; + } + + const round1_2 = teddsaSignRound1(keygen2.key_package); + if (!round1_2.success) { + return { success: false, err: { type: "error", msg: round1_2.err } }; + } + + // Collect all commitments + const allCommitments: CommitmentEntry[] = [ + { + identifier: round1_1.data.identifier, + commitments: round1_1.data.commitments, + }, + { + identifier: round1_2.data.identifier, + commitments: round1_2.data.commitments, + }, + ]; + + // Round 2: Both participants generate signature shares + const round2_1 = teddsaSignRound2( + message, + keygen1.key_package, + new Uint8Array(round1_1.data.nonces), + allCommitments, + ); + if (!round2_1.success) { + return { success: false, err: { type: "error", msg: round2_1.err } }; + } + + const round2_2 = teddsaSignRound2( + message, + keygen2.key_package, + new Uint8Array(round1_2.data.nonces), + allCommitments, + ); + if (!round2_2.success) { + return { success: false, err: { type: "error", msg: round2_2.err } }; + } + + // Aggregate signature shares + const allSignatureShares: SignatureShareEntry[] = [ + { + identifier: round2_1.data.identifier, + signature_share: round2_1.data.signature_share, + }, + { + identifier: round2_2.data.identifier, + signature_share: round2_2.data.signature_share, + }, + ]; + + const aggregateResult = teddsaAggregate( + message, + allCommitments, + allSignatureShares, + keygen1.public_key_package, + ); + + if (!aggregateResult.success) { + return { + success: false, + err: { type: "error", msg: aggregateResult.err }, + }; + } + + return { success: true, data: aggregateResult.data }; + } catch (error: any) { + return { success: false, err: { type: "error", msg: String(error) } }; + } +} diff --git a/crypto/teddsa/teddsa_hooks_mock/src/types.ts b/crypto/teddsa/teddsa_hooks_mock/src/types.ts new file mode 100644 index 000000000..92a5b86b5 --- /dev/null +++ b/crypto/teddsa/teddsa_hooks_mock/src/types.ts @@ -0,0 +1,81 @@ +import type { Bytes32 } from "@oko-wallet/bytes"; + +/** + * TEdDSA keygen output for a single participant + */ +export interface TeddsaKeygenOutputBytes { + /** Serialized KeyPackage (contains private share) */ + key_package: Uint8Array; + /** Serialized PublicKeyPackage (shared among all participants) */ + public_key_package: Uint8Array; + /** Participant identifier */ + identifier: Uint8Array; + /** Ed25519 public key (32 bytes) */ + public_key: Bytes32; +} + +/** + * TEdDSA keygen result containing shares for both participants + */ +export interface TeddsaKeygenResult { + keygen_1: TeddsaKeygenOutputBytes; + keygen_2: TeddsaKeygenOutputBytes; +} + +/** + * Raw keygen output from WASM module + */ +export interface TeddsaCentralizedKeygenOutput { + private_key: number[]; + keygen_outputs: TeddsaKeygenOutput[]; + public_key: number[]; +} + +/** + * Single keygen output from WASM + */ +export interface TeddsaKeygenOutput { + key_package: number[]; + public_key_package: number[]; + identifier: number[]; +} + +/** + * Sign round 1 output from WASM + */ +export interface TeddsaSignRound1Output { + nonces: number[]; + commitments: number[]; + identifier: number[]; +} + +/** + * Sign round 2 output from WASM + */ +export interface TeddsaSignRound2Output { + signature_share: number[]; + identifier: number[]; +} + +/** + * Aggregate output from WASM + */ +export interface TeddsaAggregateOutput { + signature: number[]; +} + +/** + * Commitment entry for signing + */ +export interface CommitmentEntry { + identifier: number[]; + commitments: number[]; +} + +/** + * Signature share entry for aggregation + */ +export interface SignatureShareEntry { + identifier: number[]; + signature_share: number[]; +} diff --git a/crypto/teddsa/teddsa_hooks_mock/tsconfig.json b/crypto/teddsa/teddsa_hooks_mock/tsconfig.json new file mode 100644 index 000000000..8d81c3fe1 --- /dev/null +++ b/crypto/teddsa/teddsa_hooks_mock/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/crypto/teddsa/teddsa_interface_mock/package.json b/crypto/teddsa/teddsa_interface_mock/package.json new file mode 100644 index 000000000..8fbba0bca --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/package.json @@ -0,0 +1,21 @@ +{ + "name": "@oko-wallet/teddsa-interface-mock", + "version": "0.0.1-alpha.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "private": true, + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "del-cli dist", + "build": "yarn clean && tsc && tsc-alias" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "del-cli": "^6.0.0", + "tsc-alias": "^1.8.16", + "typescript": "^5.8.3" + } +} diff --git a/crypto/teddsa/teddsa_interface_mock/src/api/index.ts b/crypto/teddsa/teddsa_interface_mock/src/api/index.ts new file mode 100644 index 000000000..682a70d17 --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./keygen"; +export * from "./sign"; diff --git a/crypto/teddsa/teddsa_interface_mock/src/api/keygen.ts b/crypto/teddsa/teddsa_interface_mock/src/api/keygen.ts new file mode 100644 index 000000000..b94da780e --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/api/keygen.ts @@ -0,0 +1,41 @@ +import type { TeddsaKeygenOutput } from "../keygen"; + +/** + * Request body for TEdDSA keygen initialization + */ +export interface TeddsaKeygenInitRequest { + /** User identifier */ + user_id: string; +} + +/** + * Response from TEdDSA keygen initialization + */ +export interface TeddsaKeygenInitResponse { + /** Session ID for this keygen operation */ + session_id: string; +} + +/** + * Request body for storing client's keygen share on server + */ +export interface TeddsaKeygenStoreRequest { + /** User identifier */ + user_id: string; + /** Session ID from init */ + session_id: string; + /** Server's keygen output (keygen_2) */ + keygen_output: TeddsaKeygenOutput; + /** Ed25519 public key (32 bytes) */ + public_key: number[]; +} + +/** + * Response from storing keygen share + */ +export interface TeddsaKeygenStoreResponse { + /** Whether the operation was successful */ + success: boolean; + /** Ed25519 public key (32 bytes) - confirmed */ + public_key: number[]; +} diff --git a/crypto/teddsa/teddsa_interface_mock/src/api/sign.ts b/crypto/teddsa/teddsa_interface_mock/src/api/sign.ts new file mode 100644 index 000000000..38e79b963 --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/api/sign.ts @@ -0,0 +1,64 @@ +import type { + TeddsaCommitmentEntry, + TeddsaSignatureShareEntry, +} from "../sign"; + +/** + * Request body for TEdDSA sign round 1 + */ +export interface TeddsaSignRound1Request { + /** Session ID for this signing operation */ + session_id: string; + /** Message to sign (as byte array) */ + message: number[]; + /** Client's commitment from round 1 */ + client_commitment: TeddsaCommitmentEntry; +} + +/** + * Response from TEdDSA sign round 1 + */ +export interface TeddsaSignRound1Response { + /** Server's commitment */ + server_commitment: TeddsaCommitmentEntry; +} + +/** + * Request body for TEdDSA sign round 2 + */ +export interface TeddsaSignRound2Request { + /** Session ID for this signing operation */ + session_id: string; + /** Client's signature share */ + client_signature_share: TeddsaSignatureShareEntry; +} + +/** + * Response from TEdDSA sign round 2 + */ +export interface TeddsaSignRound2Response { + /** Server's signature share */ + server_signature_share: TeddsaSignatureShareEntry; +} + +/** + * Request body for TEdDSA signature aggregation (optional, can be done client-side) + */ +export interface TeddsaAggregateRequest { + /** Session ID for this signing operation */ + session_id: string; + /** Message that was signed */ + message: number[]; + /** All commitments from both participants */ + all_commitments: TeddsaCommitmentEntry[]; + /** All signature shares from both participants */ + all_signature_shares: TeddsaSignatureShareEntry[]; +} + +/** + * Response from TEdDSA signature aggregation + */ +export interface TeddsaAggregateResponse { + /** 64-byte Ed25519 signature */ + signature: number[]; +} diff --git a/crypto/teddsa/teddsa_interface_mock/src/index.ts b/crypto/teddsa/teddsa_interface_mock/src/index.ts new file mode 100644 index 000000000..8e5d6b2b1 --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/index.ts @@ -0,0 +1,4 @@ +export * from "./keygen"; +export * from "./sign"; +export * from "./participant"; +export * from "./api"; diff --git a/crypto/teddsa/teddsa_interface_mock/src/keygen.ts b/crypto/teddsa/teddsa_interface_mock/src/keygen.ts new file mode 100644 index 000000000..57a9975fd --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/keygen.ts @@ -0,0 +1,35 @@ +/** + * Output from centralized key generation for a single participant + */ +export interface TeddsaKeygenOutput { + /** Serialized FROST KeyPackage (contains private share) */ + key_package: number[]; + /** Serialized FROST PublicKeyPackage (shared among all participants) */ + public_key_package: number[]; + /** Participant identifier bytes */ + identifier: number[]; +} + +/** + * Output from centralized key generation containing all shares + */ +export interface TeddsaCentralizedKeygenOutput { + /** Original private key (only available during centralized keygen) */ + private_key: number[]; + /** Array of keygen outputs for each participant */ + keygen_outputs: TeddsaKeygenOutput[]; + /** Ed25519 public key (32 bytes) */ + public_key: number[]; +} + +/** + * Client-side keygen state for TEdDSA + */ +export interface TeddsaClientKeygenState { + /** First participant's keygen output (client) */ + keygen_1: TeddsaKeygenOutput | null; + /** Second participant's keygen output (server) */ + keygen_2: TeddsaKeygenOutput | null; + /** Ed25519 public key (32 bytes) */ + public_key: number[] | null; +} diff --git a/crypto/teddsa/teddsa_interface_mock/src/participant.ts b/crypto/teddsa/teddsa_interface_mock/src/participant.ts new file mode 100644 index 000000000..5c15a9c92 --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/participant.ts @@ -0,0 +1,9 @@ +/** + * Participant identifiers for 2-of-2 threshold scheme + */ +export enum Participant { + /** Client participant (keygen_1) */ + P0 = 0, + /** Server participant (keygen_2) */ + P1 = 1, +} diff --git a/crypto/teddsa/teddsa_interface_mock/src/sign.ts b/crypto/teddsa/teddsa_interface_mock/src/sign.ts new file mode 100644 index 000000000..6edb3307c --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/src/sign.ts @@ -0,0 +1,73 @@ +/** + * Output from signing round 1 + */ +export interface TeddsaSignRound1Output { + /** Serialized nonces (keep secret, use in round 2) */ + nonces: number[]; + /** Serialized commitments (send to other participants) */ + commitments: number[]; + /** Participant identifier */ + identifier: number[]; +} + +/** + * Output from signing round 2 + */ +export interface TeddsaSignRound2Output { + /** Participant's signature share */ + signature_share: number[]; + /** Participant identifier */ + identifier: number[]; +} + +/** + * Commitment entry for signing protocol + */ +export interface TeddsaCommitmentEntry { + /** Participant identifier */ + identifier: number[]; + /** Serialized commitments */ + commitments: number[]; +} + +/** + * Signature share entry for aggregation + */ +export interface TeddsaSignatureShareEntry { + /** Participant identifier */ + identifier: number[]; + /** Serialized signature share */ + signature_share: number[]; +} + +/** + * Output from signature aggregation + */ +export interface TeddsaAggregateOutput { + /** 64-byte Ed25519 signature */ + signature: number[]; +} + +/** + * Final Ed25519 signature + */ +export interface TeddsaSignature { + /** 64-byte Ed25519 signature as hex string */ + signature: string; +} + +/** + * Client-side sign state for TEdDSA + */ +export interface TeddsaClientSignState { + /** Message being signed */ + message: Uint8Array | null; + /** Client's nonces from round 1 */ + nonces: number[] | null; + /** Client's commitments from round 1 */ + commitments: number[] | null; + /** All participants' commitments */ + all_commitments: TeddsaCommitmentEntry[] | null; + /** All participants' signature shares */ + all_signature_shares: TeddsaSignatureShareEntry[] | null; +} diff --git a/crypto/teddsa/teddsa_interface_mock/tsconfig.json b/crypto/teddsa/teddsa_interface_mock/tsconfig.json new file mode 100644 index 000000000..8d81c3fe1 --- /dev/null +++ b/crypto/teddsa/teddsa_interface_mock/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/Cargo.toml b/crypto/teddsa/teddsa_keplr_addon_mock/addon/Cargo.toml new file mode 100644 index 000000000..0f1e9e281 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/Cargo.toml @@ -0,0 +1,25 @@ +[package] +edition = "2021" +name = "teddsa_addon" +version = "0.0.1" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix +napi = { version = "2.12.2", default-features = false, features = [ + "napi4", + "serde-json" +] } +napi-derive = "2.12.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +teddsa_keplr_mock = { path = "../../teddsa_keplr_mock" } + +[build-dependencies] +napi-build = "2.0.1" + +[profile.release] +lto = true +strip = "symbols" diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/build.rs b/crypto/teddsa/teddsa_keplr_addon_mock/addon/build.rs new file mode 100644 index 000000000..9fc236788 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/index.d.ts b/crypto/teddsa/teddsa_keplr_addon_mock/addon/index.d.ts new file mode 100644 index 000000000..41421d5da --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/index.d.ts @@ -0,0 +1,67 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +/** + * Generate a 2-of-2 threshold Ed25519 key using centralized key generation. + * + * Returns a JSON object containing: + * - `keygen_outputs`: Array of two key shares + * - `public_key`: The Ed25519 public key (32 bytes) + * - `private_key`: The original private key (for backup purposes only) + */ +export declare function napiKeygenCentralizedEd25519(): any +/** + * Import an existing Ed25519 secret key and split it into threshold shares. + * + * # Arguments + * * `secret_key` - A 32-byte array representing the Ed25519 secret key + */ +export declare function napiKeygenImportEd25519(secretKey: Array): any +/** + * Round 1: Generate signing commitments for a participant. + * + * # Arguments + * * `key_package` - The participant's serialized KeyPackage + * + * Returns a JSON object containing: + * - `nonces`: Serialized nonces (keep secret, use in round 2) + * - `commitments`: Serialized commitments (send to coordinator) + * - `identifier`: Participant identifier + */ +export declare function napiSignRound1Ed25519(keyPackage: Array): any +/** + * Round 2: Generate a signature share for a participant. + * + * # Arguments + * * `message` - Message to sign + * * `key_package` - The participant's serialized KeyPackage + * * `nonces` - Nonces from round 1 + * * `all_commitments` - JSON array of CommitmentEntry objects + */ +export declare function napiSignRound2Ed25519(message: Array, keyPackage: Array, nonces: Array, allCommitments: any): any +/** + * Aggregate signature shares into a final threshold signature. + * + * # Arguments + * * `message` - Message that was signed + * * `all_commitments` - JSON array of CommitmentEntry objects + * * `all_signature_shares` - JSON array of SignatureShareEntry objects + * * `public_key_package` - Serialized PublicKeyPackage + * + * Returns a JSON object containing: + * - `signature`: The 64-byte Ed25519 signature + */ +export declare function napiAggregateEd25519(message: Array, allCommitments: any, allSignatureShares: any, publicKeyPackage: Array): any +/** + * Verify a signature against a public key. + * + * # Arguments + * * `message` - Original message + * * `signature` - 64-byte Ed25519 signature + * * `public_key_package` - Serialized PublicKeyPackage + * + * Returns `true` if the signature is valid, `false` otherwise. + */ +export declare function napiVerifyEd25519(message: Array, signature: Array, publicKeyPackage: Array): boolean diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/index.js b/crypto/teddsa/teddsa_keplr_addon_mock/addon/index.js new file mode 100644 index 000000000..34633d3ff --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/index.js @@ -0,0 +1,320 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'teddsa-addon.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.android-arm64.node') + } else { + nativeBinding = require('teddsa-addon-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'teddsa-addon.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.android-arm-eabi.node') + } else { + nativeBinding = require('teddsa-addon-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.win32-x64-msvc.node') + } else { + nativeBinding = require('teddsa-addon-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.win32-ia32-msvc.node') + } else { + nativeBinding = require('teddsa-addon-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.win32-arm64-msvc.node') + } else { + nativeBinding = require('teddsa-addon-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'teddsa-addon.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.darwin-universal.node') + } else { + nativeBinding = require('teddsa-addon-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'teddsa-addon.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.darwin-x64.node') + } else { + nativeBinding = require('teddsa-addon-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.darwin-arm64.node') + } else { + nativeBinding = require('teddsa-addon-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'teddsa-addon.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.freebsd-x64.node') + } else { + nativeBinding = require('teddsa-addon-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-x64-musl.node') + } else { + nativeBinding = require('teddsa-addon-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-x64-gnu.node') + } else { + nativeBinding = require('teddsa-addon-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-arm64-musl.node') + } else { + nativeBinding = require('teddsa-addon-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-arm64-gnu.node') + } else { + nativeBinding = require('teddsa-addon-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-arm-musleabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-arm-musleabihf.node') + } else { + nativeBinding = require('teddsa-addon-linux-arm-musleabihf') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('teddsa-addon-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + } + break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-riscv64-musl.node') + } else { + nativeBinding = require('teddsa-addon-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-riscv64-gnu.node') + } else { + nativeBinding = require('teddsa-addon-linux-riscv64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 's390x': + localFileExisted = existsSync( + join(__dirname, 'teddsa-addon.linux-s390x-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./teddsa-addon.linux-s390x-gnu.node') + } else { + nativeBinding = require('teddsa-addon-linux-s390x-gnu') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { napiKeygenCentralizedEd25519, napiKeygenImportEd25519, napiSignRound1Ed25519, napiSignRound2Ed25519, napiAggregateEd25519, napiVerifyEd25519 } = nativeBinding + +module.exports.napiKeygenCentralizedEd25519 = napiKeygenCentralizedEd25519 +module.exports.napiKeygenImportEd25519 = napiKeygenImportEd25519 +module.exports.napiSignRound1Ed25519 = napiSignRound1Ed25519 +module.exports.napiSignRound2Ed25519 = napiSignRound2Ed25519 +module.exports.napiAggregateEd25519 = napiAggregateEd25519 +module.exports.napiVerifyEd25519 = napiVerifyEd25519 diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/package.json b/crypto/teddsa/teddsa_keplr_addon_mock/addon/package.json new file mode 100644 index 000000000..5abd6c3ac --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/package.json @@ -0,0 +1,30 @@ +{ + "name": "teddsa-addon", + "version": "0.0.1", + "main": "index.js", + "types": "index.d.ts", + "napi": { + "name": "teddsa-addon", + "triples": {} + }, + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^2.18.4", + "ava": "^6.0.1" + }, + "ava": { + "timeout": "3m" + }, + "engines": { + "node": ">= 10" + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release", + "build:debug": "napi build --platform", + "prepublishOnly": "napi prepublish -t npm", + "test": "ava", + "universal": "napi universal", + "version": "napi version" + } +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/keygen.rs b/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/keygen.rs new file mode 100644 index 000000000..6d79e09b9 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/keygen.rs @@ -0,0 +1,57 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; +use teddsa_keplr_mock::{keygen_centralized, keygen_import}; + +/// Generate a 2-of-2 threshold Ed25519 key using centralized key generation. +/// +/// Returns a JSON object containing: +/// - `keygen_outputs`: Array of two key shares +/// - `public_key`: The Ed25519 public key (32 bytes) +/// - `private_key`: The original private key (for backup purposes only) +#[napi] +pub fn napi_keygen_centralized_ed25519() -> Result { + let output = keygen_centralized().map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("keygen_centralized error: {:?}", e), + ) + })?; + + serde_json::to_value(output).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("serialization error: {:?}", e), + ) + }) +} + +/// Import an existing Ed25519 secret key and split it into threshold shares. +/// +/// # Arguments +/// * `secret_key` - A 32-byte array representing the Ed25519 secret key +#[napi] +pub fn napi_keygen_import_ed25519(secret_key: Vec) -> Result { + if secret_key.len() != 32 { + return Err(napi::Error::new( + napi::Status::InvalidArg, + "secret_key must be exactly 32 bytes", + )); + } + + let mut secret_arr = [0u8; 32]; + secret_arr.copy_from_slice(&secret_key); + + let output = keygen_import(secret_arr).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("keygen_import error: {:?}", e), + ) + })?; + + serde_json::to_value(output).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("serialization error: {:?}", e), + ) + }) +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/lib.rs b/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/lib.rs new file mode 100644 index 000000000..fdca68826 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/lib.rs @@ -0,0 +1,5 @@ +mod keygen; +mod sign; + +pub use keygen::*; +pub use sign::*; diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/sign.rs b/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/sign.rs new file mode 100644 index 000000000..401aeda1a --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/addon/src/sign.rs @@ -0,0 +1,170 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; +use serde::{Deserialize, Serialize}; +use teddsa_keplr_mock::{aggregate, sign_round1, sign_round2, verify}; + +/// Commitment entry for signing +#[derive(Serialize, Deserialize)] +pub struct CommitmentEntry { + pub identifier: Vec, + pub commitments: Vec, +} + +/// Signature share entry for aggregation +#[derive(Serialize, Deserialize)] +pub struct SignatureShareEntry { + pub identifier: Vec, + pub signature_share: Vec, +} + +/// Round 1: Generate signing commitments for a participant. +/// +/// # Arguments +/// * `key_package` - The participant's serialized KeyPackage +/// +/// Returns a JSON object containing: +/// - `nonces`: Serialized nonces (keep secret, use in round 2) +/// - `commitments`: Serialized commitments (send to coordinator) +/// - `identifier`: Participant identifier +#[napi] +pub fn napi_sign_round1_ed25519(key_package: Vec) -> Result { + let output = sign_round1(&key_package).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("sign_round1 error: {:?}", e), + ) + })?; + + serde_json::to_value(output).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("serialization error: {:?}", e), + ) + }) +} + +/// Round 2: Generate a signature share for a participant. +/// +/// # Arguments +/// * `message` - Message to sign +/// * `key_package` - The participant's serialized KeyPackage +/// * `nonces` - Nonces from round 1 +/// * `all_commitments` - JSON array of CommitmentEntry objects +#[napi] +pub fn napi_sign_round2_ed25519( + message: Vec, + key_package: Vec, + nonces: Vec, + all_commitments: serde_json::Value, +) -> Result { + let commitments: Vec = serde_json::from_value(all_commitments).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("deserialization error (all_commitments): {:?}", e), + ) + })?; + + let commitments_vec: Vec<(Vec, Vec)> = commitments + .into_iter() + .map(|c| (c.identifier, c.commitments)) + .collect(); + + let output = sign_round2(&message, &key_package, &nonces, &commitments_vec).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("sign_round2 error: {:?}", e), + ) + })?; + + serde_json::to_value(output).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("serialization error: {:?}", e), + ) + }) +} + +/// Aggregate signature shares into a final threshold signature. +/// +/// # Arguments +/// * `message` - Message that was signed +/// * `all_commitments` - JSON array of CommitmentEntry objects +/// * `all_signature_shares` - JSON array of SignatureShareEntry objects +/// * `public_key_package` - Serialized PublicKeyPackage +/// +/// Returns a JSON object containing: +/// - `signature`: The 64-byte Ed25519 signature +#[napi] +pub fn napi_aggregate_ed25519( + message: Vec, + all_commitments: serde_json::Value, + all_signature_shares: serde_json::Value, + public_key_package: Vec, +) -> Result { + let commitments: Vec = serde_json::from_value(all_commitments).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("deserialization error (all_commitments): {:?}", e), + ) + })?; + + let sig_shares: Vec = + serde_json::from_value(all_signature_shares).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("deserialization error (all_signature_shares): {:?}", e), + ) + })?; + + let commitments_vec: Vec<(Vec, Vec)> = commitments + .into_iter() + .map(|c| (c.identifier, c.commitments)) + .collect(); + + let sig_shares_vec: Vec<(Vec, Vec)> = sig_shares + .into_iter() + .map(|s| (s.identifier, s.signature_share)) + .collect(); + + let output = aggregate( + &message, + &commitments_vec, + &sig_shares_vec, + &public_key_package, + ) + .map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("aggregate error: {:?}", e), + ) + })?; + + serde_json::to_value(output).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("serialization error: {:?}", e), + ) + }) +} + +/// Verify a signature against a public key. +/// +/// # Arguments +/// * `message` - Original message +/// * `signature` - 64-byte Ed25519 signature +/// * `public_key_package` - Serialized PublicKeyPackage +/// +/// Returns `true` if the signature is valid, `false` otherwise. +#[napi] +pub fn napi_verify_ed25519( + message: Vec, + signature: Vec, + public_key_package: Vec, +) -> Result { + verify(&message, &signature, &public_key_package).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("verify error: {:?}", e), + ) + }) +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/addon/teddsa-addon.darwin-arm64.node b/crypto/teddsa/teddsa_keplr_addon_mock/addon/teddsa-addon.darwin-arm64.node new file mode 100755 index 000000000..cccc45e58 Binary files /dev/null and b/crypto/teddsa/teddsa_keplr_addon_mock/addon/teddsa-addon.darwin-arm64.node differ diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/package.json b/crypto/teddsa/teddsa_keplr_addon_mock/package.json new file mode 100644 index 000000000..93f69d288 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/package.json @@ -0,0 +1,22 @@ +{ + "name": "@oko-wallet/teddsa-keplr-addon-mock", + "private": true, + "version": "0.0.1", + "type": "module", + "workspaces": { + "packages": [ + "./addon" + ] + }, + "scripts": { + "build:addon": "cd addon && npm run build" + }, + "license": "MIT", + "dependencies": { + "@oko-wallet/teddsa-interface-mock": "workspace:*" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.8.3" + } +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/src/server/index.ts b/crypto/teddsa/teddsa_keplr_addon_mock/src/server/index.ts new file mode 100644 index 000000000..b934e3ba1 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/src/server/index.ts @@ -0,0 +1,124 @@ +import type { + TeddsaKeygenOutput, + TeddsaCentralizedKeygenOutput, + TeddsaSignRound1Output, + TeddsaSignRound2Output, + TeddsaAggregateOutput, + TeddsaCommitmentEntry, + TeddsaSignatureShareEntry, +} from "@oko-wallet/teddsa-interface-mock"; + +// Import from the native addon +// Note: The addon must be built first with `npm run build:addon` +import { + napiKeygenCentralizedEd25519, + napiKeygenImportEd25519, + napiSignRound1Ed25519, + napiSignRound2Ed25519, + napiAggregateEd25519, + napiVerifyEd25519, +} from "../../addon/index.js"; + +/** + * Generate a 2-of-2 threshold Ed25519 key using centralized key generation. + */ +export function runKeygenCentralizedEd25519(): TeddsaCentralizedKeygenOutput { + try { + return napiKeygenCentralizedEd25519(); + } catch (err: any) { + console.error("Error calling runKeygenCentralizedEd25519:", err.message); + throw err; + } +} + +/** + * Import an existing Ed25519 secret key and split it into threshold shares. + */ +export function runKeygenImportEd25519( + secretKey: Uint8Array, +): TeddsaCentralizedKeygenOutput { + try { + return napiKeygenImportEd25519(Array.from(secretKey)); + } catch (err: any) { + console.error("Error calling runKeygenImportEd25519:", err.message); + throw err; + } +} + +/** + * Generate signing commitments for a participant (Round 1). + */ +export function runSignRound1Ed25519( + keyPackage: Uint8Array, +): TeddsaSignRound1Output { + try { + return napiSignRound1Ed25519(Array.from(keyPackage)); + } catch (err: any) { + console.error("Error calling runSignRound1Ed25519:", err.message); + throw err; + } +} + +/** + * Generate a signature share for a participant (Round 2). + */ +export function runSignRound2Ed25519( + message: Uint8Array, + keyPackage: Uint8Array, + nonces: Uint8Array, + allCommitments: TeddsaCommitmentEntry[], +): TeddsaSignRound2Output { + try { + return napiSignRound2Ed25519( + Array.from(message), + Array.from(keyPackage), + Array.from(nonces), + allCommitments, + ); + } catch (err: any) { + console.error("Error calling runSignRound2Ed25519:", err.message); + throw err; + } +} + +/** + * Aggregate signature shares into a final Ed25519 signature. + */ +export function runAggregateEd25519( + message: Uint8Array, + allCommitments: TeddsaCommitmentEntry[], + allSignatureShares: TeddsaSignatureShareEntry[], + publicKeyPackage: Uint8Array, +): TeddsaAggregateOutput { + try { + return napiAggregateEd25519( + Array.from(message), + allCommitments, + allSignatureShares, + Array.from(publicKeyPackage), + ); + } catch (err: any) { + console.error("Error calling runAggregateEd25519:", err.message); + throw err; + } +} + +/** + * Verify an Ed25519 signature. + */ +export function runVerifyEd25519( + message: Uint8Array, + signature: Uint8Array, + publicKeyPackage: Uint8Array, +): boolean { + try { + return napiVerifyEd25519( + Array.from(message), + Array.from(signature), + Array.from(publicKeyPackage), + ); + } catch (err: any) { + console.error("Error calling runVerifyEd25519:", err.message); + throw err; + } +} diff --git a/crypto/teddsa/teddsa_keplr_addon_mock/tsconfig.json b/crypto/teddsa/teddsa_keplr_addon_mock/tsconfig.json new file mode 100644 index 000000000..e944089c6 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_addon_mock/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "addon"] +} diff --git a/crypto/teddsa/teddsa_keplr_mock/Cargo.toml b/crypto/teddsa/teddsa_keplr_mock/Cargo.toml new file mode 100644 index 000000000..37da1d955 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_mock/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "teddsa_keplr_mock" +description = "Threshold EdDSA (FROST) for Ed25519" +repository = "https://github.com/chainapsis/oko" +version = "0.0.1-rc.0" +edition = "2021" +license = "MIT" + +[dependencies] +frost_ed25519_keplr = { workspace = true, features = ["serialization"] } +rand_core = { workspace = true, features = ["getrandom"] } +getrandom = { version = "0.2", features = ["js"] } +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" + +[dev-dependencies] +rand = { workspace = true } diff --git a/crypto/teddsa/teddsa_keplr_mock/src/error.rs b/crypto/teddsa/teddsa_keplr_mock/src/error.rs new file mode 100644 index 000000000..ee287679c --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_mock/src/error.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum FrostError { + #[error("Key generation failed: {0}")] + KeygenError(String), + + #[error("Signing round 1 failed: {0}")] + Round1Error(String), + + #[error("Signing round 2 failed: {0}")] + Round2Error(String), + + #[error("Signature aggregation failed: {0}")] + AggregationError(String), + + #[error("Serialization failed: {0}")] + SerializationError(String), + + #[error("Deserialization failed: {0}")] + DeserializationError(String), + + #[error("Invalid parameter: {0}")] + InvalidParameter(String), +} diff --git a/crypto/teddsa/teddsa_keplr_mock/src/keygen_centralized.rs b/crypto/teddsa/teddsa_keplr_mock/src/keygen_centralized.rs new file mode 100644 index 000000000..b657cc1be --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_mock/src/keygen_centralized.rs @@ -0,0 +1,182 @@ +use frost::keys::KeyPackage; +use frost_ed25519_keplr as frost; +use rand_core::OsRng; + +use crate::error::FrostError; +use crate::types::{CentralizedKeygenOutput, KeygenOutput}; + +/// Generate a 2-of-2 threshold key using a trusted dealer. +/// +/// This is the centralized key generation approach where a single trusted +/// party generates the key and splits it into shares. This mirrors the +/// pattern used in cait_sith_keplr for secp256k1. +/// +/// Returns: +/// - `CentralizedKeygenOutput` containing the private key, public key, and +/// two key shares for the 2-of-2 threshold scheme. +pub fn keygen_centralized() -> Result { + let mut rng = OsRng; + + // 2-of-2 threshold: min_signers = 2, max_signers = 2 + let max_signers = 2; + let min_signers = 2; + + // Generate shares using trusted dealer + let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .map_err(|e| FrostError::KeygenError(e.to_string()))?; + + // Serialize public key package + let pubkey_package_bytes = pubkey_package + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + // Get the verifying key (public key) + let verifying_key = pubkey_package.verifying_key(); + let public_key_bytes = verifying_key + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + // Convert shares to KeygenOutput + let mut keygen_outputs = Vec::with_capacity(shares.len()); + + for (identifier, secret_share) in shares { + // Convert SecretShare to KeyPackage + let key_package = KeyPackage::try_from(secret_share) + .map_err(|e| FrostError::KeygenError(e.to_string()))?; + + let key_package_bytes = key_package + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + let identifier_bytes = identifier.serialize().to_vec(); + + keygen_outputs.push(KeygenOutput { + key_package: key_package_bytes, + public_key_package: pubkey_package_bytes.clone(), + identifier: identifier_bytes, + }); + } + + // Extract the private key from the first share for backup purposes + // Note: In a real threshold scheme, no single party should have the full private key. + // This is only for compatibility with the existing oko pattern where the + // centralized keygen exposes the private key. + // + // For FROST, we don't actually need this, but we include it for API consistency. + // The private_key field here is a placeholder. + let private_key_placeholder = vec![0u8; 32]; // Placeholder + + Ok(CentralizedKeygenOutput { + private_key: private_key_placeholder, + keygen_outputs, + public_key: public_key_bytes, + }) +} + +/// Import an existing 32-byte secret and split it into threshold shares. +/// +/// This allows importing an externally generated Ed25519 private key +/// and converting it into a 2-of-2 threshold scheme. +pub fn keygen_import(secret: [u8; 32]) -> Result { + let mut rng = OsRng; + + let max_signers = 2; + let min_signers = 2; + + // Create a signing key from the secret + let signing_key = frost::SigningKey::deserialize(&secret) + .map_err(|e| FrostError::KeygenError(format!("Invalid secret key: {}", e)))?; + + // Split the existing key into shares + let (shares, pubkey_package) = frost::keys::split( + &signing_key, + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .map_err(|e| FrostError::KeygenError(e.to_string()))?; + + // Serialize public key package + let pubkey_package_bytes = pubkey_package + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + // Get the verifying key (public key) + let verifying_key = pubkey_package.verifying_key(); + let public_key_bytes = verifying_key + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + // Convert shares to KeygenOutput + let mut keygen_outputs = Vec::with_capacity(shares.len()); + + for (identifier, secret_share) in shares { + let key_package = KeyPackage::try_from(secret_share) + .map_err(|e| FrostError::KeygenError(e.to_string()))?; + + let key_package_bytes = key_package + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + let identifier_bytes = identifier.serialize().to_vec(); + + keygen_outputs.push(KeygenOutput { + key_package: key_package_bytes, + public_key_package: pubkey_package_bytes.clone(), + identifier: identifier_bytes, + }); + } + + Ok(CentralizedKeygenOutput { + private_key: secret.to_vec(), + keygen_outputs, + public_key: public_key_bytes, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keygen_centralized() { + let result = keygen_centralized(); + assert!(result.is_ok()); + + let output = result.unwrap(); + assert_eq!(output.keygen_outputs.len(), 2); + assert_eq!(output.public_key.len(), 32); // Ed25519 public key is 32 bytes + } + + #[test] + fn test_keygen_import() { + // First generate a valid key, then import it + let keygen_result = keygen_centralized().expect("keygen should succeed"); + + // Use the generated public key's corresponding secret + // For testing, we'll create a new keygen and use that + // In practice, keygen_import would be used with an externally provided key + + // Generate a valid signing key for testing + use frost_ed25519_keplr as frost; + use rand_core::OsRng; + + let mut rng = OsRng; + let signing_key = frost::SigningKey::new(&mut rng); + let secret_vec = signing_key.serialize(); + let secret: [u8; 32] = secret_vec.try_into().expect("should be 32 bytes"); + + let result = keygen_import(secret); + assert!(result.is_ok(), "keygen_import failed: {:?}", result.err()); + + let output = result.unwrap(); + assert_eq!(output.keygen_outputs.len(), 2); + assert_eq!(output.private_key, secret.to_vec()); + } +} diff --git a/crypto/teddsa/teddsa_keplr_mock/src/lib.rs b/crypto/teddsa/teddsa_keplr_mock/src/lib.rs new file mode 100644 index 000000000..cc1a5a173 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_mock/src/lib.rs @@ -0,0 +1,79 @@ +//! FROST Ed25519 Threshold Signatures for Oko +//! +//! This library provides threshold EdDSA signatures using the FROST protocol +//! on the Ed25519 curve. It is designed to work alongside the existing +//! cait_sith_keplr library which provides threshold ECDSA on secp256k1. +//! +//! # Overview +//! +//! FROST (Flexible Round-Optimized Schnorr Threshold) is a threshold signature +//! scheme that allows a group of participants to collaboratively sign messages +//! without any single party having access to the full private key. +//! +//! This implementation uses a 2-of-2 threshold scheme, matching the security +//! model of oko's existing secp256k1 implementation: +//! - One share is held by the user (keygen_1) +//! - One share is held by the server (keygen_2) +//! - Both shares are required to produce a valid signature +//! +//! # Usage +//! +//! ## Key Generation +//! +//! ```rust,ignore +//! use teddsa_keplr_mock::keygen_centralized::keygen_centralized; +//! +//! let keygen_output = keygen_centralized().expect("keygen failed"); +//! +//! // keygen_output.keygen_outputs[0] -> user's share (keygen_1) +//! // keygen_output.keygen_outputs[1] -> server's share (keygen_2) +//! // keygen_output.public_key -> the Ed25519 public key (32 bytes) +//! ``` +//! +//! ## Signing +//! +//! The signing process has two rounds: +//! +//! 1. **Round 1**: Each participant generates nonces and commitments +//! 2. **Round 2**: Each participant generates their signature share +//! 3. **Aggregation**: Signature shares are combined into the final signature +//! +//! ```rust,ignore +//! use teddsa_keplr_mock::sign::{sign_round1, sign_round2, aggregate}; +//! +//! // Round 1: Generate commitments +//! let round1_1 = sign_round1(&key_package_1)?; +//! let round1_2 = sign_round1(&key_package_2)?; +//! +//! // Collect commitments +//! let all_commitments = vec![ +//! (round1_1.identifier, round1_1.commitments), +//! (round1_2.identifier, round1_2.commitments), +//! ]; +//! +//! // Round 2: Generate signature shares +//! let message = b"hello solana"; +//! let round2_1 = sign_round2(message, &key_package_1, &round1_1.nonces, &all_commitments)?; +//! let round2_2 = sign_round2(message, &key_package_2, &round1_2.nonces, &all_commitments)?; +//! +//! // Aggregate into final signature +//! let all_shares = vec![ +//! (round2_1.identifier, round2_1.signature_share), +//! (round2_2.identifier, round2_2.signature_share), +//! ]; +//! let signature = aggregate(message, &all_commitments, &all_shares, &public_key_package)?; +//! ``` + +pub mod error; +pub mod keygen_centralized; +pub mod sign; +pub mod types; + +// Re-exports for convenience +pub use error::FrostError; +pub use keygen_centralized::{keygen_centralized, keygen_import}; +pub use sign::{aggregate, sign_round1, sign_round2, verify}; +pub use types::{ + CentralizedKeygenOutput, KeygenOutput, SignatureOutput, SignatureShareOutput, + SigningCommitmentOutput, +}; diff --git a/crypto/teddsa/teddsa_keplr_mock/src/sign.rs b/crypto/teddsa/teddsa_keplr_mock/src/sign.rs new file mode 100644 index 000000000..2dd31b0e4 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_mock/src/sign.rs @@ -0,0 +1,252 @@ +use frost::keys::{KeyPackage, PublicKeyPackage}; +use frost::round1::{SigningCommitments, SigningNonces}; +use frost::round2::SignatureShare; +use frost::{Identifier, SigningPackage}; +use frost_ed25519_keplr as frost; +use rand_core::OsRng; +use std::collections::BTreeMap; + +use crate::error::FrostError; +use crate::types::{SignatureOutput, SignatureShareOutput, SigningCommitmentOutput}; + +/// Round 1: Generate signing commitments for a participant. +/// +/// Each participant calls this function to generate their nonces and commitments. +/// The nonces must be kept secret and used in round 2. +/// The commitments are sent to the coordinator. +pub fn sign_round1(key_package_bytes: &[u8]) -> Result { + let mut rng = OsRng; + + // Deserialize key package + let key_package = KeyPackage::deserialize(key_package_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + + // Generate nonces and commitments + let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), &mut rng); + + // Serialize outputs + let nonces_bytes = nonces + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + let commitments_bytes = commitments + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + let identifier_bytes = key_package.identifier().serialize().to_vec(); + + Ok(SigningCommitmentOutput { + nonces: nonces_bytes, + commitments: commitments_bytes, + identifier: identifier_bytes, + }) +} + +/// Round 2: Generate a signature share for a participant. +/// +/// After receiving commitments from all participants, the coordinator creates +/// a SigningPackage and distributes it. Each participant then generates their +/// signature share. +pub fn sign_round2( + message: &[u8], + key_package_bytes: &[u8], + nonces_bytes: &[u8], + all_commitments: &[(Vec, Vec)], // Vec of (identifier, commitments) +) -> Result { + // Deserialize key package + let key_package = KeyPackage::deserialize(key_package_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + + // Deserialize nonces + let nonces = SigningNonces::deserialize(nonces_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + + // Deserialize all commitments + let mut commitments_map: BTreeMap = BTreeMap::new(); + for (id_bytes, comm_bytes) in all_commitments { + let identifier = Identifier::deserialize(id_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + let commitments = SigningCommitments::deserialize(comm_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + commitments_map.insert(identifier, commitments); + } + + // Create signing package + let signing_package = SigningPackage::new(commitments_map, message); + + // Generate signature share + let signature_share = frost::round2::sign(&signing_package, &nonces, &key_package) + .map_err(|e| FrostError::Round2Error(e.to_string()))?; + + // Serialize outputs + let signature_share_bytes = signature_share.serialize(); + + let identifier_bytes = key_package.identifier().serialize().to_vec(); + + Ok(SignatureShareOutput { + signature_share: signature_share_bytes, + identifier: identifier_bytes, + }) +} + +/// Aggregate signature shares into a final threshold signature. +/// +/// The coordinator calls this function after receiving signature shares +/// from all participants. +pub fn aggregate( + message: &[u8], + all_commitments: &[(Vec, Vec)], // Vec of (identifier, commitments) + all_signature_shares: &[(Vec, Vec)], // Vec of (identifier, signature_share) + public_key_package_bytes: &[u8], +) -> Result { + // Deserialize public key package + let pubkey_package = PublicKeyPackage::deserialize(public_key_package_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + + // Deserialize all commitments + let mut commitments_map: BTreeMap = BTreeMap::new(); + for (id_bytes, comm_bytes) in all_commitments { + let identifier = Identifier::deserialize(id_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + let commitments = SigningCommitments::deserialize(comm_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + commitments_map.insert(identifier, commitments); + } + + // Create signing package + let signing_package = SigningPackage::new(commitments_map, message); + + // Deserialize all signature shares + let mut signature_shares: BTreeMap = BTreeMap::new(); + for (id_bytes, share_bytes) in all_signature_shares { + let identifier = Identifier::deserialize(id_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + let share = SignatureShare::deserialize(share_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + signature_shares.insert(identifier, share); + } + + // Aggregate signature + let signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package) + .map_err(|e| FrostError::AggregationError(e.to_string()))?; + + // Serialize signature (64 bytes for Ed25519) + let signature_bytes = signature + .serialize() + .map_err(|e| FrostError::SerializationError(e.to_string()))?; + + Ok(SignatureOutput { + signature: signature_bytes, + }) +} + +/// Verify a signature against a public key. +pub fn verify( + message: &[u8], + signature_bytes: &[u8], + public_key_package_bytes: &[u8], +) -> Result { + // Deserialize public key package + let pubkey_package = PublicKeyPackage::deserialize(public_key_package_bytes) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + + // Deserialize signature + let signature_array: [u8; 64] = signature_bytes + .try_into() + .map_err(|_| FrostError::DeserializationError("Invalid signature length".to_string()))?; + + let signature = frost::Signature::deserialize(&signature_array) + .map_err(|e| FrostError::DeserializationError(e.to_string()))?; + + // Verify + let verifying_key = pubkey_package.verifying_key(); + match verifying_key.verify(message, &signature) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::keygen_centralized::keygen_centralized; + + #[test] + fn test_full_signing_flow() { + // Step 1: Generate keys + let keygen_output = keygen_centralized().expect("keygen should succeed"); + assert_eq!(keygen_output.keygen_outputs.len(), 2); + + let key_package_1 = &keygen_output.keygen_outputs[0]; + let key_package_2 = &keygen_output.keygen_outputs[1]; + + // Step 2: Round 1 - Generate commitments + let round1_output_1 = + sign_round1(&key_package_1.key_package).expect("round1 should succeed"); + let round1_output_2 = + sign_round1(&key_package_2.key_package).expect("round1 should succeed"); + + // Collect all commitments + let all_commitments = vec![ + ( + round1_output_1.identifier.clone(), + round1_output_1.commitments.clone(), + ), + ( + round1_output_2.identifier.clone(), + round1_output_2.commitments.clone(), + ), + ]; + + // Step 3: Round 2 - Generate signature shares + let message = b"test message"; + + let round2_output_1 = sign_round2( + message, + &key_package_1.key_package, + &round1_output_1.nonces, + &all_commitments, + ) + .expect("round2 should succeed"); + + let round2_output_2 = sign_round2( + message, + &key_package_2.key_package, + &round1_output_2.nonces, + &all_commitments, + ) + .expect("round2 should succeed"); + + // Collect all signature shares + let all_signature_shares = vec![ + ( + round2_output_1.identifier.clone(), + round2_output_1.signature_share.clone(), + ), + ( + round2_output_2.identifier.clone(), + round2_output_2.signature_share.clone(), + ), + ]; + + // Step 4: Aggregate signature + let signature_output = aggregate( + message, + &all_commitments, + &all_signature_shares, + &key_package_1.public_key_package, + ) + .expect("aggregation should succeed"); + + // Step 5: Verify signature + let is_valid = verify( + message, + &signature_output.signature, + &key_package_1.public_key_package, + ) + .expect("verification should not error"); + + assert!(is_valid, "signature should be valid"); + assert_eq!(signature_output.signature.len(), 64); + } +} diff --git a/crypto/teddsa/teddsa_keplr_mock/src/types.rs b/crypto/teddsa/teddsa_keplr_mock/src/types.rs new file mode 100644 index 000000000..139e28c95 --- /dev/null +++ b/crypto/teddsa/teddsa_keplr_mock/src/types.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; + +/// Output from centralized key generation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CentralizedKeygenOutput { + /// The full private key (only available in centralized keygen) + pub private_key: Vec, + /// Key shares for each participant (2-of-2) + pub keygen_outputs: Vec, + /// The public verifying key + pub public_key: Vec, +} + +/// A single participant's key share +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeygenOutput { + /// Serialized KeyPackage for this participant + pub key_package: Vec, + /// Serialized PublicKeyPackage (shared among all participants) + pub public_key_package: Vec, + /// Participant identifier + pub identifier: Vec, +} + +/// Output from a signing round 1 (commitment) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SigningCommitmentOutput { + /// Serialized SigningNonces (must be kept secret, used in round 2) + pub nonces: Vec, + /// Serialized SigningCommitments (sent to coordinator) + pub commitments: Vec, + /// Participant identifier + pub identifier: Vec, +} + +/// Output from a signing round 2 (signature share) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignatureShareOutput { + /// Serialized SignatureShare + pub signature_share: Vec, + /// Participant identifier + pub identifier: Vec, +} + +/// Final aggregated signature +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignatureOutput { + /// The 64-byte Ed25519 signature + pub signature: Vec, +} diff --git a/crypto/teddsa/teddsa_wasm_mock/package.json b/crypto/teddsa/teddsa_wasm_mock/package.json new file mode 100644 index 000000000..6114fcd3e --- /dev/null +++ b/crypto/teddsa/teddsa_wasm_mock/package.json @@ -0,0 +1,16 @@ +{ + "name": "@oko-wallet/teddsa-wasm-mock", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "scripts": { + "build:wasm": "wasm-pack build wasm --target web --out-dir ../pkg" + }, + "dependencies": { + "wasm-pack": "^0.13.1" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.8.3" + } +} diff --git a/crypto/teddsa/teddsa_wasm_mock/src/index.ts b/crypto/teddsa/teddsa_wasm_mock/src/index.ts new file mode 100644 index 000000000..fe100df43 --- /dev/null +++ b/crypto/teddsa/teddsa_wasm_mock/src/index.ts @@ -0,0 +1,71 @@ +import * as wasmModule from "../pkg/teddsa_wasm_mock"; + +export { wasmModule }; + +// Flag to track module initialization status +let isInitialized = false; +// Store the initialized WASM module +let wasmInstance: typeof wasmModule | null = null; +// Store exported functions +const exportedFunctions: Record = {}; + +type WasmModule = typeof wasmModule; +export type WasmFunctionNames = keyof WasmModule; + +/** + * Function to initialize WASM module and export its functions globally + * + * @param wasmMod - WASM module (import * as wasmModule from "my-wasm-module") + * @param wasmPath - WASM binary file path (default: "/teddsa_wasm_bg.wasm") + * @returns Object containing all exported functions + */ +export async function initWasm( + wasmMod: typeof wasmModule, + wasmPath: string, +): Promise> { + // Return cached functions if already initialized + if (isInitialized && wasmInstance) { + return exportedFunctions; + } + + try { + // Load WASM binary file + const response = await fetch(wasmPath); + if (!response.ok) { + throw new Error( + `Cannot load WASM file: ${response.status} ${response.statusText}`, + ); + } + + const wasmBytes = await response.arrayBuffer(); + + // Initialize WASM module - initSync is a function generated by wasm-bindgen + (wasmMod as any).initSync(wasmBytes); + + // Store the initialized module + wasmInstance = wasmMod; + + // Extract all functions from the module + Object.keys(wasmMod) + .filter((key) => typeof (wasmMod as any)[key] === "function") + .forEach((funcName) => { + // Create a wrapper function that calls the WASM function + exportedFunctions[funcName] = (...args: unknown[]) => + (wasmMod as any)[funcName](...args); + }); + + // Mark initialization as successful + isInitialized = true; + + return exportedFunctions; + } catch (error) { + throw error; + } +} + +/** + * Helper function to check if WASM is initialized + */ +export function isWasmInitialized(): boolean { + return isInitialized; +} diff --git a/crypto/teddsa/teddsa_wasm_mock/wasm/Cargo.toml b/crypto/teddsa/teddsa_wasm_mock/wasm/Cargo.toml new file mode 100644 index 000000000..a2fd85993 --- /dev/null +++ b/crypto/teddsa/teddsa_wasm_mock/wasm/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "teddsa_wasm_mock" +description = "WASM bindings for FROST Ed25519 threshold signatures" +version = "0.0.1" +categories = ["wasm"] +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = true +strip = true + +[dependencies] +teddsa_keplr_mock = { path = "../../teddsa_keplr_mock" } +wasm-bindgen = "0.2.100" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +gloo-utils = "0.2.0" + +[dependencies.web-sys] +version = "0.3.22" +features = ["console"] + +[target."cfg(debug_assertions)".dependencies] +console_error_panic_hook = "0.1.5" + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crypto/teddsa/teddsa_wasm_mock/wasm/src/keygen.rs b/crypto/teddsa/teddsa_wasm_mock/wasm/src/keygen.rs new file mode 100644 index 000000000..45488562a --- /dev/null +++ b/crypto/teddsa/teddsa_wasm_mock/wasm/src/keygen.rs @@ -0,0 +1,33 @@ +use teddsa_keplr_mock::{keygen_centralized, keygen_import}; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::*; + +/// Generate a 2-of-2 threshold Ed25519 key using centralized key generation. +/// +/// Returns a JSON object containing: +/// - `keygen_outputs`: Array of two key shares +/// - `public_key`: The Ed25519 public key (32 bytes) +/// - `private_key`: Placeholder (not used in threshold scheme) +#[wasm_bindgen] +pub fn cli_keygen_centralized_ed25519() -> Result { + let out = keygen_centralized().map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} + +/// Import an existing Ed25519 secret key and split it into threshold shares. +/// +/// # Arguments +/// * `secret_key` - A 32-byte array representing the Ed25519 secret key +/// +/// Returns a JSON object containing the threshold shares. +#[wasm_bindgen] +pub fn cli_keygen_import_ed25519(secret_key: JsValue) -> Result { + let secret: [u8; 32] = secret_key + .into_serde() + .map_err(|err| JsValue::from_str(&format!("Invalid secret key format: {}", err)))?; + + let out = keygen_import(secret).map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} diff --git a/crypto/teddsa/teddsa_wasm_mock/wasm/src/lib.rs b/crypto/teddsa/teddsa_wasm_mock/wasm/src/lib.rs new file mode 100644 index 000000000..d191eedd7 --- /dev/null +++ b/crypto/teddsa/teddsa_wasm_mock/wasm/src/lib.rs @@ -0,0 +1,20 @@ +mod keygen; +mod sign; + +use std::sync::Once; +use wasm_bindgen::prelude::*; + +pub use keygen::*; +pub use sign::*; + +// Ensure initialization happens only once +static INIT: Once = Once::new(); + +// Module initialization function that runs automatically when the WASM module is loaded +#[wasm_bindgen(start)] +pub fn init() { + INIT.call_once(|| { + #[cfg(debug_assertions)] + console_error_panic_hook::set_once(); + }); +} diff --git a/crypto/teddsa/teddsa_wasm_mock/wasm/src/sign.rs b/crypto/teddsa/teddsa_wasm_mock/wasm/src/sign.rs new file mode 100644 index 000000000..873474792 --- /dev/null +++ b/crypto/teddsa/teddsa_wasm_mock/wasm/src/sign.rs @@ -0,0 +1,142 @@ +use teddsa_keplr_mock::{aggregate, sign_round1, sign_round2, verify}; +use gloo_utils::format::JsValueSerdeExt; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +/// Input for sign round 2 +#[derive(Serialize, Deserialize)] +pub struct SignRound2Input { + pub message: Vec, + pub key_package: Vec, + pub nonces: Vec, + pub all_commitments: Vec, +} + +/// A commitment entry (identifier + commitments) +#[derive(Serialize, Deserialize)] +pub struct CommitmentEntry { + pub identifier: Vec, + pub commitments: Vec, +} + +/// Input for signature aggregation +#[derive(Serialize, Deserialize)] +pub struct AggregateInput { + pub message: Vec, + pub all_commitments: Vec, + pub all_signature_shares: Vec, + pub public_key_package: Vec, +} + +/// A signature share entry (identifier + signature_share) +#[derive(Serialize, Deserialize)] +pub struct SignatureShareEntry { + pub identifier: Vec, + pub signature_share: Vec, +} + +/// Input for signature verification +#[derive(Serialize, Deserialize)] +pub struct VerifyInput { + pub message: Vec, + pub signature: Vec, + pub public_key_package: Vec, +} + +/// Round 1: Generate signing commitments for a participant. +/// +/// # Arguments +/// * `key_package` - The participant's serialized KeyPackage +/// +/// Returns a JSON object containing: +/// - `nonces`: Serialized nonces (keep secret, use in round 2) +/// - `commitments`: Serialized commitments (send to coordinator) +/// - `identifier`: Participant identifier +#[wasm_bindgen] +pub fn cli_sign_round1_ed25519(key_package: JsValue) -> Result { + let key_package_bytes: Vec = key_package + .into_serde() + .map_err(|err| JsValue::from_str(&format!("Invalid key_package format: {}", err)))?; + + let out = sign_round1(&key_package_bytes).map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} + +/// Round 2: Generate a signature share for a participant. +/// +/// # Arguments +/// * `input` - JSON object containing message, key_package, nonces, and all_commitments +/// +/// Returns a JSON object containing: +/// - `signature_share`: The participant's signature share +/// - `identifier`: Participant identifier +#[wasm_bindgen] +pub fn cli_sign_round2_ed25519(input: JsValue) -> Result { + let input: SignRound2Input = input + .into_serde() + .map_err(|err| JsValue::from_str(&format!("Invalid input format: {}", err)))?; + + let all_commitments: Vec<(Vec, Vec)> = input + .all_commitments + .into_iter() + .map(|c| (c.identifier, c.commitments)) + .collect(); + + let out = sign_round2(&input.message, &input.key_package, &input.nonces, &all_commitments) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} + +/// Aggregate signature shares into a final threshold signature. +/// +/// # Arguments +/// * `input` - JSON object containing message, all_commitments, all_signature_shares, and public_key_package +/// +/// Returns a JSON object containing: +/// - `signature`: The 64-byte Ed25519 signature +#[wasm_bindgen] +pub fn cli_aggregate_ed25519(input: JsValue) -> Result { + let input: AggregateInput = input + .into_serde() + .map_err(|err| JsValue::from_str(&format!("Invalid input format: {}", err)))?; + + let all_commitments: Vec<(Vec, Vec)> = input + .all_commitments + .into_iter() + .map(|c| (c.identifier, c.commitments)) + .collect(); + + let all_signature_shares: Vec<(Vec, Vec)> = input + .all_signature_shares + .into_iter() + .map(|s| (s.identifier, s.signature_share)) + .collect(); + + let out = aggregate( + &input.message, + &all_commitments, + &all_signature_shares, + &input.public_key_package, + ) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} + +/// Verify a signature against a public key. +/// +/// # Arguments +/// * `input` - JSON object containing message, signature, and public_key_package +/// +/// Returns `true` if the signature is valid, `false` otherwise. +#[wasm_bindgen] +pub fn cli_verify_ed25519(input: JsValue) -> Result { + let input: VerifyInput = input + .into_serde() + .map_err(|err| JsValue::from_str(&format!("Invalid input format: {}", err)))?; + + verify(&input.message, &input.signature, &input.public_key_package) + .map_err(|err| JsValue::from_str(&err.to_string())) +} diff --git a/package.json b/package.json index d688c4156..fda1cdf24 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "sdk/oko_sdk_core", "sdk/oko_sdk_cosmos", "sdk/oko_sdk_eth", + "sdk/oko_sdk_sol", "examples/cosmos_nextjs", "examples/cosmoskit_nextjs", "examples/evm_nextjs", @@ -55,8 +56,12 @@ "crypto/tecdsa/cait_sith_keplr_wasm", "crypto/tecdsa/client_example", "crypto/tecdsa/api_lib", + "crypto/teddsa/teddsa_interface_mock", + "crypto/teddsa/teddsa_wasm_mock", + "crypto/teddsa/teddsa_hooks_mock", + "crypto/teddsa/teddsa_keplr_addon_mock", "crypto/teddsa/frost_ed25519_keplr_wasm", - "internals2/manual_deployment" + "sandbox/sandbox_sol" ] }, "packageManager": "yarn@4.10.3", diff --git a/sandbox/sandbox_sol/.env.example b/sandbox/sandbox_sol/.env.example new file mode 100644 index 000000000..c58a426e5 --- /dev/null +++ b/sandbox/sandbox_sol/.env.example @@ -0,0 +1,6 @@ +NEXT_PUBLIC_OKO_API_KEY=YOUR_API_KEY_HERE +NEXT_PUBLIC_OKO_SDK_ENDPOINT=https://attached.oko.app + +# Solana RPC URL (optional, uses public mainnet by default) +# Get a free RPC from Helius: https://helius.dev or QuickNode: https://quicknode.com +NEXT_PUBLIC_SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY diff --git a/sandbox/sandbox_sol/next-env.d.ts b/sandbox/sandbox_sol/next-env.d.ts new file mode 100644 index 000000000..830fb594c --- /dev/null +++ b/sandbox/sandbox_sol/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/sandbox/sandbox_sol/next.config.ts b/sandbox/sandbox_sol/next.config.ts new file mode 100644 index 000000000..0d9807765 --- /dev/null +++ b/sandbox/sandbox_sol/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + webpack: (config) => { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + net: false, + tls: false, + }; + return config; + }, +}; + +export default nextConfig; diff --git a/sandbox/sandbox_sol/package.json b/sandbox/sandbox_sol/package.json new file mode 100644 index 000000000..d4872241d --- /dev/null +++ b/sandbox/sandbox_sol/package.json @@ -0,0 +1,27 @@ +{ + "name": "@oko-wallet/sandbox-sol", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev -p 3205", + "build": "next build", + "start": "next start -p 3205", + "lint": "next lint" + }, + "dependencies": { + "@oko-wallet/oko-sdk-sol": "workspace:*", + "@solana/web3.js": "^1.98.0", + "bs58": "^6.0.0", + "next": "^15.1.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0" + } +} diff --git a/sandbox/sandbox_sol/src/app/globals.css b/sandbox/sandbox_sol/src/app/globals.css new file mode 100644 index 000000000..90aa83685 --- /dev/null +++ b/sandbox/sandbox_sol/src/app/globals.css @@ -0,0 +1,37 @@ +:root { + --bg-primary: #0a0a0a; + --bg-secondary: #141414; + --bg-tertiary: #1a1a1a; + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --border: #2a2a2a; + --accent: #9945ff; + --accent-hover: #7c35d9; + --success: #14f195; + --error: #ff4444; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +} + +body { + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/sandbox/sandbox_sol/src/app/layout.tsx b/sandbox/sandbox_sol/src/app/layout.tsx new file mode 100644 index 000000000..de24b58b1 --- /dev/null +++ b/sandbox/sandbox_sol/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Sandbox SOL - Oko Wallet", + description: "Solana SDK testing sandbox for Oko Wallet", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/sandbox/sandbox_sol/src/app/page.module.css b/sandbox/sandbox_sol/src/app/page.module.css new file mode 100644 index 000000000..4af6fbe32 --- /dev/null +++ b/sandbox/sandbox_sol/src/app/page.module.css @@ -0,0 +1,41 @@ +.main { + min-height: 100vh; + padding: 2rem; +} + +.container { + max-width: 800px; + margin: 0 auto; +} + +.header { + text-align: center; + margin-bottom: 2rem; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient(90deg, #9945ff, #14f195); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.5rem; +} + +.subtitle { + color: var(--text-secondary); + font-size: 1.125rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.widgets { + display: flex; + flex-direction: column; + gap: 1.5rem; +} diff --git a/sandbox/sandbox_sol/src/app/page.tsx b/sandbox/sandbox_sol/src/app/page.tsx new file mode 100644 index 000000000..b2326c025 --- /dev/null +++ b/sandbox/sandbox_sol/src/app/page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useOkoSol } from "@/hooks/use_oko_sol"; +import { WalletStatus } from "@/components/wallet_status"; +import { SignMessageWidget } from "@/components/sign_message_widget"; +import { SignTransactionWidget } from "@/components/sign_transaction_widget"; +import styles from "./page.module.css"; + +export default function Home() { + const { + isInitialized, + isInitializing, + isConnected, + publicKey, + error, + connect, + disconnect, + } = useOkoSol(); + + return ( +
+
+
+

Sandbox SOL

+

Oko Wallet Solana SDK Testing

+
+ +
+ + + {isConnected && ( +
+ + +
+ )} +
+
+
+ ); +} diff --git a/sandbox/sandbox_sol/src/components/sign_message_widget.tsx b/sandbox/sandbox_sol/src/components/sign_message_widget.tsx new file mode 100644 index 000000000..a3df63108 --- /dev/null +++ b/sandbox/sandbox_sol/src/components/sign_message_widget.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; +import { useSdkStore } from "@/store/sdk"; +import bs58 from "bs58"; +import styles from "./widget.module.css"; + +export function SignMessageWidget() { + const { okoSolWallet } = useSdkStore(); + const [message, setMessage] = useState("Hello, Solana!"); + const [signature, setSignature] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSignMessage = async () => { + if (!okoSolWallet) return; + + setIsLoading(true); + setError(null); + setSignature(null); + + try { + const messageBytes = new TextEncoder().encode(message); + const signatureBytes = await okoSolWallet.signMessage(messageBytes); + + const signatureBase58 = bs58.encode(signatureBytes); + setSignature(signatureBase58); + console.log("[sandbox_sol] Message signed:", signatureBase58); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + setError(errorMessage); + console.error("[sandbox_sol] Failed to sign message:", errorMessage); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Sign Message

+

+ Sign an arbitrary message with your Solana wallet +

+ +
+ +