Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/
import { InvalidTransactionError } from '../../../errors';
import { BitGoBase } from '../../../bitgoBase';
import { resolveEffectiveTxParams } from '../recipientUtils';
import type { EcdsaMPCv2KeyGenCallbacks } from '../../../wallet/iWallets';

export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
private static readonly DKLS23_SIGNING_USER_GPG_KEY = 'DKLS23_SIGNING_USER_GPG_KEY';
Expand Down Expand Up @@ -647,6 +648,92 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
}
// #endregion

/**
* Creates ECDSA MPCv2 keychains using external signer callbacks instead of a passphrase.
* The external signer holds all private key material; the SDK only coordinates the DKG protocol.
*/
async createKeychainsWithExternalSigner(params: {
enterprise: string;
callbacks: EcdsaMPCv2KeyGenCallbacks;
}): Promise<KeychainsTriplet> {
const { enterprise, callbacks } = params;
const { mpcv2PublicKey } = await this.getBitgoGpgPubkeyBasedOnFeatureFlags(enterprise, true);
const mpcv2Key = mpcv2PublicKey ?? this.bitgoMPCv2PublicGpgKey;
assert(mpcv2Key, 'Failed to get BitGo MPCv2 GPG public key');
const bitgoPublicGpgKey = mpcv2Key.armor();

if (envRequiresBitgoPubGpgKeyConfig(this.bitgo.getEnv())) {
assert(isBitgoMpcPubKey(bitgoPublicGpgKey, 'mpcv2'), 'Invalid BitGo GPG public key');
}

// Round 1: external signer generates GPG keys and initial DKG messages
const { userGpgPublicKey, backupGpgPublicKey, round1Messages, userState, backupState } =
await callbacks.initializeCallback({
enterprise,
bitgoPublicGpgKey,
});

const { sessionId, bitgoMsg1, bitgoToUserMsg2, bitgoToBackupMsg2 } = await this.sendKeyGenerationRound1(
enterprise,
userGpgPublicKey,
backupGpgPublicKey,
round1Messages
);

// Round 2: external signer processes BitGo's round 1 response
const {
round2Messages,
userState: userStateAfter2,
backupState: backupStateAfter2,
} = await callbacks.round2Callback({
sessionId,
bitgoMsg1,
bitgoToUserMsg2,
bitgoToBackupMsg2,
userState,
backupState,
});

const round2Response = await this.sendKeyGenerationRound2(enterprise, sessionId, round2Messages);
assert.strictEqual(round2Response.sessionId, sessionId, 'Session ID mismatch after round 2');

// Round 3: external signer processes BitGo's round 2 response
const {
round3Messages,
userState: userStateAfter3,
backupState: backupStateAfter3,
} = await callbacks.round3Callback({
sessionId,
bitgoCommitment2: round2Response.bitgoCommitment2,
bitgoToUserMsg3: round2Response.bitgoToUserMsg3,
bitgoToBackupMsg3: round2Response.bitgoToBackupMsg3,
userState: userStateAfter2,
backupState: backupStateAfter2,
});

const round3Response = await this.sendKeyGenerationRound3(enterprise, sessionId, round3Messages);
assert.strictEqual(round3Response.sessionId, sessionId, 'Session ID mismatch after round 3');

// Finalize: external signer verifies BitGo's final message against the derived common keychain
const { commonKeychain } = await callbacks.finalizeCallback({
sessionId,
bitgoMsg4: round3Response.bitgoMsg4,
bitgoCommonKeychain: round3Response.commonKeychain,
userState: userStateAfter3,
backupState: backupStateAfter3,
});
assert.strictEqual(commonKeychain, round3Response.commonKeychain, 'Common keychains do not match');

const keychains = this.baseCoin.keychains();
const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([
keychains.add({ source: 'user', keyType: 'tss', commonKeychain, isMPCv2: true }),
keychains.add({ source: 'backup', keyType: 'tss', commonKeychain, isMPCv2: true }),
this.addBitgoKeychain(commonKeychain),
]);

return { userKeychain, backupKeychain, bitgoKeychain };
}

async sendKeyGenerationRound1(
enterprise: string,
userGpgPublicKey: string,
Expand Down
83 changes: 82 additions & 1 deletion modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ import { KeychainsTriplet } from '../../../baseCoin';
import { exchangeEddsaCommitments } from '../../../tss/common';
import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc';
import { EncryptionVersion, IRequestTracer } from '../../../../api';
import { getBitgoMpcGpgPubKey } from '../../../tss/bitgoPubKeys';
import { envRequiresBitgoPubGpgKeyConfig, getBitgoMpcGpgPubKey, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys';
import { EnvironmentName } from '../../../environments';
import { readKey } from 'openpgp';
import type { EddsaKeyGenCallbacks } from '../../../wallet/iWallets';

/**
* Utility functions for TSS work flows.
Expand Down Expand Up @@ -438,6 +439,86 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
}
}

/**
* Creates EdDSA TSS keychains using external signer callbacks instead of a passphrase.
* The external signer holds all private key material and pre-encrypts shares to BitGo's GPG key.
*/
async createKeychainsWithExternalSigner(params: {
enterprise: string;
callbacks: EddsaKeyGenCallbacks;
}): Promise<KeychainsTriplet> {
const { enterprise, callbacks } = params;
const bitgoGpgPubKey = (await getBitgoGpgPubKey(this.bitgo)).mpcV1;

if (envRequiresBitgoPubGpgKeyConfig(this.bitgo.getEnv())) {
assert(isBitgoMpcPubKey(bitgoGpgPubKey.armor(), 'mpcv1'), 'Invalid BitGo GPG public key');
}

const {
userGpgPublicKey,
backupGpgPublicKey,
userToBitgoKeyShare,
backupToBitgoKeyShare,
userState,
backupState,
backupCounterPartyKeyShare,
} = await callbacks.initializeCallback({ enterprise, bitgoPublicGpgKey: bitgoGpgPubKey.armor() });

// Create BitGo keychain with pre-encrypted shares from the external signer
const bitgoKeychain = await this.baseCoin.keychains().add({
keyType: 'tss',
source: 'bitgo',
keyShares: [
{ from: 'user', to: 'bitgo', ...userToBitgoKeyShare },
{ from: 'backup', to: 'bitgo', ...backupToBitgoKeyShare },
],
userGPGPublicKey: userGpgPublicKey,
backupGPGPublicKey: backupGpgPublicKey,
enterprise,
});

// Finalize runs sequentially: user finalize produces a counterparty key share that backup finalize consumes.
const coin = this.baseCoin.getChain();
const userResult = await callbacks.finalizeCallback({
source: 'user',
coin,
bitgoKeychain,
counterPartyGPGKey: backupGpgPublicKey,
counterPartyKeyShare: backupCounterPartyKeyShare,
state: userState,
});
assert(userResult.counterpartyKeyShare, 'User finalize did not produce a counterparty key share');

const backupResult = await callbacks.finalizeCallback({
source: 'backup',
coin,
bitgoKeychain,
counterPartyGPGKey: userGpgPublicKey,
counterPartyKeyShare: userResult.counterpartyKeyShare,
state: backupState,
});

assert(bitgoKeychain.commonKeychain, 'BitGo keychain missing commonKeychain');
assert.strictEqual(
userResult.commonKeychain,
bitgoKeychain.commonKeychain,
'User common keychain does not match BitGo'
);
assert.strictEqual(
backupResult.commonKeychain,
bitgoKeychain.commonKeychain,
'Backup common keychain does not match BitGo'
);

const { commonKeychain } = bitgoKeychain;
const [userKeychain, backupKeychain] = await Promise.all([
this.baseCoin.keychains().add({ source: 'user', keyType: 'tss', commonKeychain }),
this.baseCoin.keychains().add({ source: 'backup', keyType: 'tss', commonKeychain }),
]);

return { userKeychain, backupKeychain, bitgoKeychain };
}

async createCommitmentShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
Expand Down
107 changes: 105 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/iWallets.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as t from 'io-ts';
import { DklsTypes } from '@bitgo/sdk-lib-mpc';
import { MPCv2BroadcastMessage, MPCv2P2PMessage } from '@bitgo/public-types';

import { EncryptionVersion, IRequestTracer } from '../../api';
import { KeychainsTriplet, LightningKeychainsTriplet } from '../baseCoin';
import { Keychain, WebauthnInfo } from '../keychain';
import { ApiKeyShare, Keychain, WebauthnInfo } from '../keychain';
import { IWallet, PaginationOptions, WalletShare } from './iWallet';
import { Wallet } from './wallet';

Expand Down Expand Up @@ -76,6 +78,105 @@ export interface CreateKeychainCallbackResult {

export type CreateKeychainCallback = (params: CreateKeychainCallbackParams) => Promise<CreateKeychainCallbackResult>;

/** Per-source encrypted session state threaded through MPC rounds by the external signer. */
export interface ExternalSignerMpcState {
encryptedData: string;
encryptedDataKey: string;
}

/** A key share supplied by the external signer — routing fields (from/to) are added by the SDK. */
export type ExternalSignerKeyShare = Omit<ApiKeyShare, 'from' | 'to'> & {
privateShareProof: string;
vssProof: string;
gpgKey: string;
};

export type EcdsaMPCv2KeyGenInitializeCallback = (params: {
enterprise: string;
bitgoPublicGpgKey: string;
}) => Promise<{
userGpgPublicKey: string;
backupGpgPublicKey: string;
round1Messages: DklsTypes.AuthEncMessages;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
}>;

export type EcdsaMPCv2KeyGenRound2Callback = (params: {
sessionId: string;
bitgoMsg1: MPCv2BroadcastMessage;
bitgoToUserMsg2: MPCv2P2PMessage;
bitgoToBackupMsg2: MPCv2P2PMessage;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
}) => Promise<{
round2Messages: DklsTypes.AuthEncMessages;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
}>;

export type EcdsaMPCv2KeyGenRound3Callback = (params: {
sessionId: string;
bitgoCommitment2: string;
bitgoToUserMsg3: MPCv2P2PMessage;
bitgoToBackupMsg3: MPCv2P2PMessage;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
}) => Promise<{
round3Messages: DklsTypes.AuthEncMessages;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
}>;

export type EcdsaMPCv2KeyGenFinalizeCallback = (params: {
sessionId: string;
bitgoMsg4: MPCv2BroadcastMessage;
bitgoCommonKeychain: string;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
}) => Promise<{ commonKeychain: string }>;

export interface EcdsaMPCv2KeyGenCallbacks {
initializeCallback: EcdsaMPCv2KeyGenInitializeCallback;
round2Callback: EcdsaMPCv2KeyGenRound2Callback;
round3Callback: EcdsaMPCv2KeyGenRound3Callback;
finalizeCallback: EcdsaMPCv2KeyGenFinalizeCallback;
}

export interface EddsaKeyGenInitializeResult {
userGpgPublicKey: string;
backupGpgPublicKey: string;
userToBitgoKeyShare: ExternalSignerKeyShare;
backupToBitgoKeyShare: ExternalSignerKeyShare;
userState: ExternalSignerMpcState;
backupState: ExternalSignerMpcState;
backupCounterPartyKeyShare: ExternalSignerKeyShare;
}

export type EddsaKeyGenInitializeCallback = (params: {
enterprise: string;
bitgoPublicGpgKey: string;
}) => Promise<EddsaKeyGenInitializeResult>;

export type EddsaKeyGenFinalizeResult = {
commonKeychain: string;
counterpartyKeyShare?: ExternalSignerKeyShare;
};

export type EddsaKeyGenFinalizeCallback = (params: {
source: 'user' | 'backup';
coin: string;
bitgoKeychain: Keychain;
counterPartyGPGKey: string;
counterPartyKeyShare: ExternalSignerKeyShare;
state: ExternalSignerMpcState;
}) => Promise<EddsaKeyGenFinalizeResult>;

export interface EddsaKeyGenCallbacks {
initializeCallback: EddsaKeyGenInitializeCallback;
finalizeCallback: EddsaKeyGenFinalizeCallback;
}

export interface GenerateWalletOptions {
label?: string;
passphrase?: string;
Expand Down Expand Up @@ -115,9 +216,11 @@ export interface GenerateWalletOptions {
export interface GenerateWalletWithExternalSignerOptions
extends Omit<GenerateWalletOptions, 'passphrase' | 'userKey' | 'backupXpub' | 'backupXpubProvider'> {
label: string;
createKeychainCallback: CreateKeychainCallback;
createKeychainCallback?: CreateKeychainCallback;
/** Optional user-key signatures over backup/bitgo pubs. Omit when the external signer cannot produce them (equivalent to a cold wallet). */
keySignatures?: { backup: string; bitgo: string };
ecdsaMPCv2Callbacks?: EcdsaMPCv2KeyGenCallbacks;
eddsaCallbacks?: EddsaKeyGenCallbacks;
}

export const GenerateLightningWalletOptionsCodec = t.intersection(
Expand Down
Loading
Loading