diff --git a/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts b/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts index fdd46145..f95f3b7f 100644 --- a/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts +++ b/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts @@ -4,6 +4,7 @@ import { CreateKeychainCallback, EcdsaMPCv2KeyGenCallbacks, EddsaKeyGenCallbacks, + EddsaMPCv2KeyGenCallbacks, } from '@bitgo-beta/sdk-core'; import { KeySource } from '../../shared/types'; import { @@ -319,6 +320,237 @@ export function createEcdsaMPCv2KeyGenCallbacks( }; } +/** Opaque EdDSA MPCv2 keygen fields the ME threads between callbacks through the SDK state. */ +interface EddsaMPCv2KeyGenMpcState { + encryptedDataKey: string; + encryptedUserGpgPrvKey: string; + bitgoGpgPubKey?: string; + counterPartyGpgPubKey?: string; + encryptedRound1Session?: string; + encryptedRound2Session?: string; +} + +function packEddsaMpcState(state: EddsaMPCv2KeyGenMpcState) { + return { encryptedData: JSON.stringify(state), encryptedDataKey: state.encryptedDataKey }; +} + +function unpackEddsaMpcState(mpcState: { + encryptedData: string; + encryptedDataKey: string; +}): EddsaMPCv2KeyGenMpcState { + const state = JSON.parse(mpcState.encryptedData) as EddsaMPCv2KeyGenMpcState; + assert( + state.encryptedUserGpgPrvKey, + 'encryptedUserGpgPrvKey is missing from EdDSA MPCv2 keygen state', + ); + return { ...state, encryptedDataKey: mpcState.encryptedDataKey }; +} + +/** + * Creates EdDSA MPCv2 key generation callbacks for the SDK external signer flow. + * + * Each callback drives both the user and the backup AWM through the single shareType-parameterized + * keygen route (WCI-892). The SDK relays messages to and from BitGo, while the counterparty + * round-2 messages — which finalize needs but the SDK does not pass into finalizeCallback — are + * carried in this closure. + */ +export function createEddsaMPCv2KeyGenCallbacks( + awmUserClient: AdvancedWalletManagerClient, + awmBackupClient: AdvancedWalletManagerClient, +): EddsaMPCv2KeyGenCallbacks { + // The SDK's finalizeCallback params do not include the party round-2 messages, but the user's + // finalize needs the backup's signedMsg2 (and vice versa); they are stashed here. + let userSignedMsg2: { message: string; signature: string } | undefined; + let backupSignedMsg2: { message: string; signature: string } | undefined; + + return { + initializeCallback: async () => { + assert(!userSignedMsg2, 'initializeCallback called more than once'); + const [userInit, backupInit] = await Promise.all([ + awmUserClient.eddsaMPCv2KeyGen({ shareType: 'initialize', source: KeySource.USER }), + awmBackupClient.eddsaMPCv2KeyGen({ shareType: 'initialize', source: KeySource.BACKUP }), + ]); + assert( + userInit.gpgPub && userInit.encryptedUserGpgPrvKey && userInit.encryptedDataKey, + 'Missing EdDSA MPCv2 initialize fields for user', + ); + assert( + backupInit.gpgPub && backupInit.encryptedUserGpgPrvKey && backupInit.encryptedDataKey, + 'Missing EdDSA MPCv2 initialize fields for backup', + ); + + return { + userGpgPublicKey: userInit.gpgPub, + backupGpgPublicKey: backupInit.gpgPub, + userState: packEddsaMpcState({ + encryptedDataKey: userInit.encryptedDataKey, + encryptedUserGpgPrvKey: userInit.encryptedUserGpgPrvKey, + }), + backupState: packEddsaMpcState({ + encryptedDataKey: backupInit.encryptedDataKey, + encryptedUserGpgPrvKey: backupInit.encryptedUserGpgPrvKey, + }), + }; + }, + + round1Callback: async ({ + bitgoPublicGpgKey, + userGpgPublicKey, + backupGpgPublicKey, + userState, + backupState, + }) => { + const userMpcState = unpackEddsaMpcState(userState); + const backupMpcState = unpackEddsaMpcState(backupState); + const [userRound1, backupRound1] = await Promise.all([ + awmUserClient.eddsaMPCv2KeyGen({ + shareType: 'round1', + source: KeySource.USER, + encryptedDataKey: userMpcState.encryptedDataKey, + encryptedUserGpgPrvKey: userMpcState.encryptedUserGpgPrvKey, + bitgoGpgPubKey: bitgoPublicGpgKey, + counterPartyGpgPubKey: backupGpgPublicKey, + }), + awmBackupClient.eddsaMPCv2KeyGen({ + shareType: 'round1', + source: KeySource.BACKUP, + encryptedDataKey: backupMpcState.encryptedDataKey, + encryptedUserGpgPrvKey: backupMpcState.encryptedUserGpgPrvKey, + bitgoGpgPubKey: bitgoPublicGpgKey, + counterPartyGpgPubKey: userGpgPublicKey, + }), + ]); + assert( + userRound1.signedMsg1 && userRound1.encryptedRound1Session, + 'Missing EdDSA MPCv2 round1 fields for user', + ); + assert( + backupRound1.signedMsg1 && backupRound1.encryptedRound1Session, + 'Missing EdDSA MPCv2 round1 fields for backup', + ); + + return { + userSignedMsg1: userRound1.signedMsg1, + backupSignedMsg1: backupRound1.signedMsg1, + userState: packEddsaMpcState({ + ...userMpcState, + bitgoGpgPubKey: bitgoPublicGpgKey, + counterPartyGpgPubKey: backupGpgPublicKey, + encryptedRound1Session: userRound1.encryptedRound1Session, + }), + backupState: packEddsaMpcState({ + ...backupMpcState, + bitgoGpgPubKey: bitgoPublicGpgKey, + counterPartyGpgPubKey: userGpgPublicKey, + encryptedRound1Session: backupRound1.encryptedRound1Session, + }), + }; + }, + + round2Callback: async ({ + bitgoMsg1, + userSignedMsg1, + backupSignedMsg1, + userState, + backupState, + }) => { + const userMpcState = unpackEddsaMpcState(userState); + const backupMpcState = unpackEddsaMpcState(backupState); + const [userRound2, backupRound2] = await Promise.all([ + awmUserClient.eddsaMPCv2KeyGen({ + shareType: 'round2', + source: KeySource.USER, + encryptedDataKey: userMpcState.encryptedDataKey, + encryptedUserGpgPrvKey: userMpcState.encryptedUserGpgPrvKey, + encryptedRound1Session: userMpcState.encryptedRound1Session, + bitgoGpgPubKey: userMpcState.bitgoGpgPubKey, + counterPartyGpgPubKey: userMpcState.counterPartyGpgPubKey, + bitgoMsg1, + counterPartyMsg1: backupSignedMsg1, + }), + awmBackupClient.eddsaMPCv2KeyGen({ + shareType: 'round2', + source: KeySource.BACKUP, + encryptedDataKey: backupMpcState.encryptedDataKey, + encryptedUserGpgPrvKey: backupMpcState.encryptedUserGpgPrvKey, + encryptedRound1Session: backupMpcState.encryptedRound1Session, + bitgoGpgPubKey: backupMpcState.bitgoGpgPubKey, + counterPartyGpgPubKey: backupMpcState.counterPartyGpgPubKey, + bitgoMsg1, + counterPartyMsg1: userSignedMsg1, + }), + ]); + assert( + userRound2.signedMsg2 && userRound2.encryptedRound2Session, + 'Missing EdDSA MPCv2 round2 fields for user', + ); + assert( + backupRound2.signedMsg2 && backupRound2.encryptedRound2Session, + 'Missing EdDSA MPCv2 round2 fields for backup', + ); + userSignedMsg2 = userRound2.signedMsg2; + backupSignedMsg2 = backupRound2.signedMsg2; + + return { + userSignedMsg2: userRound2.signedMsg2, + backupSignedMsg2: backupRound2.signedMsg2, + userState: packEddsaMpcState({ + ...userMpcState, + encryptedRound2Session: userRound2.encryptedRound2Session, + }), + backupState: packEddsaMpcState({ + ...backupMpcState, + encryptedRound2Session: backupRound2.encryptedRound2Session, + }), + }; + }, + + finalizeCallback: async ({ bitgoMsg2, bitgoCommonKeychain, userState, backupState }) => { + assert( + userSignedMsg2 && backupSignedMsg2, + 'finalizeCallback called before round2Callback completed', + ); + const userMpcState = unpackEddsaMpcState(userState); + const backupMpcState = unpackEddsaMpcState(backupState); + const [userFinalize, backupFinalize] = await Promise.all([ + awmUserClient.eddsaMPCv2KeyGen({ + shareType: 'finalize', + source: KeySource.USER, + encryptedDataKey: userMpcState.encryptedDataKey, + encryptedRound2Session: userMpcState.encryptedRound2Session, + bitgoGpgPubKey: userMpcState.bitgoGpgPubKey, + counterPartyGpgPubKey: userMpcState.counterPartyGpgPubKey, + bitgoMsg2, + counterPartyMsg2: backupSignedMsg2, + bitgoCommonKeychain, + }), + awmBackupClient.eddsaMPCv2KeyGen({ + shareType: 'finalize', + source: KeySource.BACKUP, + encryptedDataKey: backupMpcState.encryptedDataKey, + encryptedRound2Session: backupMpcState.encryptedRound2Session, + bitgoGpgPubKey: backupMpcState.bitgoGpgPubKey, + counterPartyGpgPubKey: backupMpcState.counterPartyGpgPubKey, + bitgoMsg2, + counterPartyMsg2: userSignedMsg2, + bitgoCommonKeychain, + }), + ]); + assert(userFinalize.commonKeychain, 'Missing EdDSA MPCv2 finalize commonKeychain for user'); + assert( + backupFinalize.commonKeychain, + 'Missing EdDSA MPCv2 finalize commonKeychain for backup', + ); + assert( + userFinalize.commonKeychain === backupFinalize.commonKeychain, + 'User and backup common keychains do not match', + ); + + return { commonKeychain: userFinalize.commonKeychain }; + }, + }; +} + /** * Creates EdDSA (MPCv1) key generation callbacks for the SDK external signer flow. *