diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 94594fd2b5..28985d7d07 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -54,7 +54,6 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { private static readonly MPS_DSG_SIGNING_USER_GPG_KEY = 'MPS_DSG_SIGNING_USER_GPG_KEY'; private static readonly MPS_DSG_SIGNING_ROUND1_STATE = 'MPS_DSG_SIGNING_ROUND1_STATE'; private static readonly MPS_DSG_SIGNING_ROUND2_STATE = 'MPS_DSG_SIGNING_ROUND2_STATE'; - private static readonly MPS_DKG_KEYGEN_ROUND1_STATE = 'MPS_DKG_KEYGEN_ROUND1_STATE'; /** @inheritdoc */ async createKeychains(params: { @@ -649,60 +648,6 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { // #region external signer - // #region KeyGenRound1Share - async createOfflineKeyGenRound1Share(params: { - walletPassphrase: string; - encryptedUserGpgPrvKey: string; - bitgoGpgPubKey: string; - counterPartyGpgPubKey: string; - partyId?: MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP; - }): Promise<{ - signedMsg1: MPSTypes.MPSSignedMessage; - encryptedRound1Session: string; - }> { - const { walletPassphrase, encryptedUserGpgPrvKey, bitgoGpgPubKey, counterPartyGpgPubKey } = params; - const partyId = params.partyId ?? MPCv2PartiesEnum.USER; - - const decryptedGpgPrvKey = isV2Envelope(encryptedUserGpgPrvKey) - ? await this.bitgo.decryptAsync({ input: encryptedUserGpgPrvKey, password: walletPassphrase }) - : this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase }); - const gpgPrvKey = await pgp.readPrivateKey({ armoredKey: decryptedGpgPrvKey }); - - const [, ownSk] = await MPSComms.extractEd25519KeyPair(gpgPrvKey); - - if (envRequiresBitgoPubGpgKeyConfig(this.bitgo.getEnv())) { - assert(isBitgoEddsaMpcv2PubKey(bitgoGpgPubKey), 'Invalid BitGo GPG public key'); - } - - const bitgoKeyObj = await pgp.readKey({ armoredKey: bitgoGpgPubKey }); - const bitgoPk = await MPSComms.extractEd25519PublicKey(bitgoKeyObj); - - const counterPartyKeyObj = await pgp.readKey({ armoredKey: counterPartyGpgPubKey }); - const counterPartyPk = await MPSComms.extractEd25519PublicKey(counterPartyKeyObj); - - const dkg = new EddsaMPSDkg.DKG(3, 2, partyId); - await dkg.initDkg(ownSk, [counterPartyPk, bitgoPk]); - - const msg1 = dkg.getFirstMessage(); - const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(msg1.payload), gpgPrvKey); - - const sessionPayload = JSON.stringify({ - dkgSession: dkg.getSession(), - ownMsgPayload: Buffer.from(msg1.payload).toString('base64'), - ownMsgFrom: msg1.from, - }); - - const encryptedRound1Session = await this.bitgo.encryptAsync({ - input: sessionPayload, - password: walletPassphrase, - adata: `${EddsaMPCv2Utils.MPS_DKG_KEYGEN_ROUND1_STATE}:${partyId}`, - encryptionVersion: 1, - }); - - return { signedMsg1, encryptedRound1Session }; - } - // #endregion - // #region Round1Share async createOfflineRound1Share(params: { txRequest: TxRequest; diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 664eab8e09..82fa36479c 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as pgp from 'openpgp'; import { randomBytes } from 'crypto'; -import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; +import { EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; import { ed25519 } from '@noble/curves/ed25519'; import * as sjcl from '@bitgo/sjcl'; import { @@ -608,175 +608,6 @@ describe('EddsaMPCv2Utils.createOfflineRound1Share', () => { }); }); -describe('EddsaMPCv2Utils.createOfflineKeyGenRound1Share', () => { - let eddsaMPCv2Utils: EddsaMPCv2Utils; - let mockBitgo: BitGoBase; - let userGpgKeyPair: pgp.SerializedKeyPair; - let backupGpgKeyPair: pgp.SerializedKeyPair; - let bitgoGpgKeyPair: pgp.SerializedKeyPair; - - const walletPassphrase = 'testPass'; - - before('generate GPG key pairs', async () => { - userGpgKeyPair = await generateGPGKeyPair('ed25519'); - backupGpgKeyPair = await generateGPGKeyPair('ed25519'); - bitgoGpgKeyPair = await generateGPGKeyPair('ed25519'); - }); - - beforeEach(() => { - const sjclEncrypt = (params: { password: string; input: string; adata?: string }) => { - const salt = randomBytes(8); - const iv = randomBytes(16); - return sjcl.encrypt(params.password, params.input, { - salt: [bytesToWord(salt.subarray(0, 4)), bytesToWord(salt.subarray(4))], - iv: [ - bytesToWord(iv.subarray(0, 4)), - bytesToWord(iv.subarray(4, 8)), - bytesToWord(iv.subarray(8, 12)), - bytesToWord(iv.subarray(12, 16)), - ], - adata: params.adata, - }); - }; - mockBitgo = { - encrypt: sinon.stub().callsFake(sjclEncrypt), - encryptAsync: sinon.stub().callsFake(async (params) => sjclEncrypt(params)), - decrypt: sinon.stub().callsFake((params) => sjcl.decrypt(params.password, params.input)), - decryptAsync: sinon.stub().callsFake(async (params) => sjcl.decrypt(params.password, params.input)), - getEnv: sinon.stub().returns('mock'), - } as unknown as BitGoBase; - - const mockCoin = { - getMPCAlgorithm: sinon.stub().returns('eddsa'), - } as unknown as IBaseCoin; - - eddsaMPCv2Utils = new EddsaMPCv2Utils(mockBitgo, mockCoin); - }); - - it('should produce valid signedMsg1 and encrypted round-1 session', async () => { - const encryptedUserGpgPrvKey = sjcl.encrypt(walletPassphrase, userGpgKeyPair.privateKey); - - const result = await eddsaMPCv2Utils.createOfflineKeyGenRound1Share({ - walletPassphrase, - encryptedUserGpgPrvKey, - bitgoGpgPubKey: bitgoGpgKeyPair.publicKey, - counterPartyGpgPubKey: backupGpgKeyPair.publicKey, - }); - - assert.ok(result.signedMsg1.message, 'signedMsg1.message should be set'); - assert.ok( - result.signedMsg1.signature.includes('BEGIN PGP SIGNATURE'), - 'signedMsg1.signature should be PGP armored' - ); - assert.ok(JSON.parse(result.encryptedRound1Session).ct, 'encryptedRound1Session should be an SJCL JSON blob'); - }); - - it('encryptedRound1Session should only be decryptable with correct walletPassphrase', async () => { - const encryptedUserGpgPrvKey = sjcl.encrypt(walletPassphrase, userGpgKeyPair.privateKey); - - const result = await eddsaMPCv2Utils.createOfflineKeyGenRound1Share({ - walletPassphrase, - encryptedUserGpgPrvKey, - bitgoGpgPubKey: bitgoGpgKeyPair.publicKey, - counterPartyGpgPubKey: backupGpgKeyPair.publicKey, - }); - - const decrypted = sjcl.decrypt(walletPassphrase, result.encryptedRound1Session); - const session = JSON.parse(decrypted); - assert.ok(session.dkgSession, 'dkgSession should be persisted'); - assert.ok(session.ownMsgPayload, 'ownMsgPayload should be persisted'); - assert.strictEqual(typeof session.ownMsgFrom, 'number', 'ownMsgFrom should be a number'); - - assert.throws( - () => sjcl.decrypt('wrongpassphrase', result.encryptedRound1Session), - 'should throw on wrong passphrase' - ); - }); - - it('should restore DKG session and handle round-2 messages after round 1', async () => { - const encryptedUserGpgPrvKey = sjcl.encrypt(walletPassphrase, userGpgKeyPair.privateKey); - - const result = await eddsaMPCv2Utils.createOfflineKeyGenRound1Share({ - walletPassphrase, - encryptedUserGpgPrvKey, - bitgoGpgPubKey: bitgoGpgKeyPair.publicKey, - counterPartyGpgPubKey: backupGpgKeyPair.publicKey, - }); - - const decrypted = sjcl.decrypt(walletPassphrase, result.encryptedRound1Session); - const { dkgSession, ownMsgPayload, ownMsgFrom } = JSON.parse(decrypted) as { - dkgSession: string; - ownMsgPayload: string; - ownMsgFrom: number; - }; - - const restoredDkg = new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER); - await restoredDkg.restoreSession(dkgSession); - - const ownMsg1: MPSTypes.DeserializedMessage = { - from: ownMsgFrom, - payload: new Uint8Array(Buffer.from(ownMsgPayload, 'base64')), - }; - assert.ok(ownMsg1.payload.length > 0, 'restored payload should be non-empty'); - - const userGpgKey = await pgp.readKey({ armoredKey: userGpgKeyPair.publicKey }); - const rawBytes = await MPSComms.verifyMpsMessage(result.signedMsg1, userGpgKey); - assert.ok(rawBytes.length > 0, 'signedMsg1 should verify against user GPG key'); - }); - - it('should reject invalid encryptedUserGpgPrvKey', async () => { - await assert.rejects( - () => - eddsaMPCv2Utils.createOfflineKeyGenRound1Share({ - walletPassphrase, - encryptedUserGpgPrvKey: sjcl.encrypt(walletPassphrase, 'not-a-gpg-key'), - bitgoGpgPubKey: bitgoGpgKeyPair.publicKey, - counterPartyGpgPubKey: backupGpgKeyPair.publicKey, - }), - 'should throw when GPG key cannot be parsed' - ); - }); - - it('USER and BACKUP partyId produce different encrypted session state', async () => { - const userEncryptedGpgPrvKey = sjcl.encrypt(walletPassphrase, userGpgKeyPair.privateKey); - - const userResult = await eddsaMPCv2Utils.createOfflineKeyGenRound1Share({ - walletPassphrase, - encryptedUserGpgPrvKey: userEncryptedGpgPrvKey, - bitgoGpgPubKey: bitgoGpgKeyPair.publicKey, - counterPartyGpgPubKey: backupGpgKeyPair.publicKey, - partyId: MPCv2PartiesEnum.USER, - }); - - const backupEncryptedGpgPrvKey2 = sjcl.encrypt(walletPassphrase, backupGpgKeyPair.privateKey); - const backupResult = await eddsaMPCv2Utils.createOfflineKeyGenRound1Share({ - walletPassphrase, - encryptedUserGpgPrvKey: backupEncryptedGpgPrvKey2, - bitgoGpgPubKey: bitgoGpgKeyPair.publicKey, - counterPartyGpgPubKey: userGpgKeyPair.publicKey, - partyId: MPCv2PartiesEnum.BACKUP, - }); - - const userSession = JSON.parse(sjcl.decrypt(walletPassphrase, userResult.encryptedRound1Session)); - const backupSession = JSON.parse(sjcl.decrypt(walletPassphrase, backupResult.encryptedRound1Session)); - - assert.strictEqual(userSession.ownMsgFrom, MPCv2PartiesEnum.USER); - assert.strictEqual(backupSession.ownMsgFrom, MPCv2PartiesEnum.BACKUP); - assert.notStrictEqual(userSession.dkgSession, backupSession.dkgSession, 'Sessions should differ between parties'); - - // Sessions encrypted with party-bound adata — tampered adata fails CCM tag check - const tamperedSession = (() => { - const parsed = JSON.parse(userResult.encryptedRound1Session); - parsed.adata = encodeURIComponent(`MPS_DKG_KEYGEN_ROUND1_STATE:${MPCv2PartiesEnum.BACKUP}`); - return JSON.stringify(parsed); - })(); - assert.throws( - () => sjcl.decrypt(walletPassphrase, tamperedSession), - 'should fail to decrypt with wrong party adata' - ); - }); -}); - describe('EddsaMPCv2Utils.createOfflineRound2Share', () => { let eddsaMPCv2Utils: EddsaMPCv2Utils; let mockBitgo: BitGoBase;