Skip to content
Draft
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
232 changes: 232 additions & 0 deletions src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CreateKeychainCallback,
EcdsaMPCv2KeyGenCallbacks,
EddsaKeyGenCallbacks,
EddsaMPCv2KeyGenCallbacks,
} from '@bitgo-beta/sdk-core';
import { KeySource } from '../../shared/types';
import {
Expand Down Expand Up @@ -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.
*
Expand Down
Loading