From 0a53cbd0c28e6ab4ea913f91699cf04da0e9dca3 Mon Sep 17 00:00:00 2001 From: Marzooqa Naeema Kather Date: Tue, 5 May 2026 12:07:06 +0530 Subject: [PATCH] feat: add eddsa mpcv2 gpg key support to tss signing - extend getBitgoMpcGpgPubKey to accept 'eddsaMpcv2' mpc version - populate all eddsaMpcv2 gpg pub keys (onprem + nitro, test + prod) - add isEddsaMpcv2 param to pickBitgoPubGpgKeyForSigning - add getBitgoEddsaMpcv2PublicGpgKey mirroring getBitgoMpcv2PublicGpgKey - add isBitgoEddsaMpcv2PubKey helper - add tests for eddsa mpcv2 key selection paths Co-Authored-By: Claude Sonnet 4.6 TICKET: WCI-311 --- .../internal/tssUtils/bitgoMpcGpgPubKeys.ts | 64 ++++++++++++++++++- .../sdk-core/src/bitgo/tss/bitgoPubKeys.ts | 10 +-- .../src/bitgo/utils/tss/baseTSSUtils.ts | 34 ++++++++-- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/bitgoMpcGpgPubKeys.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/bitgoMpcGpgPubKeys.ts index 69ca509780..cf1562b131 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/bitgoMpcGpgPubKeys.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/bitgoMpcGpgPubKeys.ts @@ -2,6 +2,7 @@ import { BitgoMpcGpgPubKeys, common, ECDSAUtils, + EDDSAUtils, EddsaUtils, EnvironmentName, IRequestTracer, @@ -28,9 +29,21 @@ class TestEddsaMpcv1Utils extends EddsaUtils { public async testPickBitgoPubGpgKeyForSigning( isMpcv2: boolean, reqId?: IRequestTracer, - enterpriseId?: string + enterpriseId?: string, + isEddsaMpcv2?: boolean ): Promise { - return this.pickBitgoPubGpgKeyForSigning(isMpcv2, reqId, enterpriseId); + return this.pickBitgoPubGpgKeyForSigning(isMpcv2, reqId, enterpriseId, isEddsaMpcv2); + } +} + +class TestEddsaMpcv2Utils extends EDDSAUtils.EddsaMPCv2Utils { + public async testPickBitgoPubGpgKeyForSigning( + isMpcv2: boolean, + reqId?: IRequestTracer, + enterpriseId?: string, + isEddsaMpcv2?: boolean + ): Promise { + return this.pickBitgoPubGpgKeyForSigning(isMpcv2, reqId, enterpriseId, isEddsaMpcv2); } } @@ -58,6 +71,7 @@ describe('TSS MPC Pick BitGo GPG Pub Key Utils:', function () { const envs: EnvironmentName[] = ['test', 'staging', 'prod']; const ecdsaMpcv2Utils: TestEcdsaMpcv2Utils[] = []; const eddsaMpcv1Utils: TestEddsaMpcv1Utils[] = []; + const eddsaMpcv2Utils: TestEddsaMpcv2Utils[] = []; before(async function () { nock.cleanAll(); @@ -72,6 +86,9 @@ describe('TSS MPC Pick BitGo GPG Pub Key Utils:', function () { eddsaMpcv1Utils.push( new TestEddsaMpcv1Utils(bitgoInstance, coinInstance, new Wallet(bitgoInstance, coinInstance, eddsaWalletData)) ); + eddsaMpcv2Utils.push( + new TestEddsaMpcv2Utils(bitgoInstance, coinInstance, new Wallet(bitgoInstance, coinInstance, eddsaWalletData)) + ); } }); @@ -225,6 +242,49 @@ describe('TSS MPC Pick BitGo GPG Pub Key Utils:', function () { gpgKey.armor().should.equal(capturedKey); }); + describe('EdDSA MPCv2 pickBitgoPubGpgKeyForSigning', function () { + it('should pick correct EdDSA MPCv2 BitGo GPG Pub Key when keychain returns hsmType onprem', async function () { + nock.cleanAll(); + // Mock hsmType as 'onprem' for the EdDSA coin so the onprem eddsaMpcv2 key is selected + const bgUrl = common.Environments['test'].uri; + nock(bgUrl).get(`/api/v2/${eddsaCoinName}/key/key3`).reply(200, { hsmType: 'onprem' }); + const bitgoGpgPubKey = await eddsaMpcv2Utils[0].testPickBitgoPubGpgKeyForSigning( + true, + undefined, + undefined, + true + ); + bitgoGpgPubKey.armor().should.equal(BitgoMpcGpgPubKeys.bitgoMpcGpgPubKeys['eddsaMpcv2']['onprem']['test']); + }); + + it('should pick EdDSA MPCv2 BitGo GPG Pub Key based on feature flags for mock env', async function () { + const bgUrl = common.Environments['mock'].uri; + const testBitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); + const testCoin = testBitgo.coin(eddsaCoinName); + const bitgoGPGKey = await openpgp.generateKey({ + userIDs: [ + { + name: 'bitgo', + email: 'bitgo@test.com', + }, + ], + type: 'ecc', + curve: 'ed25519', + }); + nock(bgUrl) + .get(`/api/v2/${eddsaCoinName}/tss/pubkey`) + .query({ enterpriseId }) + .reply(200, { mpcv2PublicKey: bitgoGPGKey.publicKey, eddsaMpcv2PublicKey: bitgoGPGKey.publicKey }); + const eddsaMpcv2Util = new TestEddsaMpcv2Utils( + testBitgo, + testCoin, + new Wallet(testBitgo, testCoin, eddsaWalletData) + ); + const bitgoGpgPubKey = await eddsaMpcv2Util.testPickBitgoPubGpgKeyForSigning(true, undefined, enterpriseId, true); + bitgoGpgPubKey.armor().should.equal(bitgoGPGKey.publicKey); + }); + }); + describe('BitgoMpcGpgPubKeys.isBitgoEddsaMpcv2PubKey', function () { it('should return true for the hardcoded on-prem test EdDSA MPCv2 key', function () { const key = BitgoMpcGpgPubKeys.bitgoMpcGpgPubKeys['eddsaMpcv2']['onprem']['test']; diff --git a/modules/sdk-core/src/bitgo/tss/bitgoPubKeys.ts b/modules/sdk-core/src/bitgo/tss/bitgoPubKeys.ts index 83325de52b..a0bd2a2fea 100644 --- a/modules/sdk-core/src/bitgo/tss/bitgoPubKeys.ts +++ b/modules/sdk-core/src/bitgo/tss/bitgoPubKeys.ts @@ -24,12 +24,12 @@ export const bitgoMpcGpgPubKeys = { }, eddsaMpcv2: { nitro: { - test: '', // TODO WCI-205: add eddsaMpcv2 key for nitro - prod: '', + test: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEae9GZBYJKwYBBAHaRw8BAQdASKNi5MpJRAvINROrOEYFGmJcYKzW3WM7\nkIPow2z0P/bNGGhzbSA8aHNtQHRlc3QuYml0Z28uY29tPsKEBBMWCgA2BYJp\n70Z8AgsJCZDqsFQZg1ASRAIVCgIWAAKbAwIeARYhBLHdfJtdM3kc4o7/wuqw\nVBmDUBJEAAB6WgD/XYI8GR6BZl32N4fu6VZiJ4Ean7ahPCZ+eKYuc8qnJeMB\nAIYqc0Tw9QcqvgrrZtqizUfbYzJDZjNY2R51LrszLFAOwoQEEBMIADYFgmnv\nRnwCCwkJkJsMk/69BgLvAhUKAhYAApsDAh4BFiEE1QB10qpW/55I4JfamwyT\n/r0GAu8AAGVbAP9l4Ffwk1MRYwMSgsXQdJIY8srwSrWaJqFavRZi2tl0iwD+\nJaJdr2DcPP6Pb/zq4kh7YnhjrEgHv2Ta4qTTJtEEq+bOOARp70ZkEgorBgEE\nAZdVAQUBAQdA7gOSvYvpCo8TmgPBHeTqlPDJpohcuPXYMRQ5g228WRgDAQgH\nwoUEGBYKADcFgmnvRnwCCwkJkOqwVBmDUBJEAhUKAhYAA5sECAIeARYhBLHd\nfJtdM3kc4o7/wuqwVBmDUBJEAADrrQD+N+V+lJLtaC3W6E1pHQhr9I+0FsdJ\n5+dQFcacKPls3k0A/0Jp6WA+BUcErw4ahkm4hkbbjbuwRLG9H4PSJfoFgccH\nzjMEae9GZBYJKwYBBAHaRw8BAQdAa3z6M5sueZ+yaWp6jQ5GCqo53k4dkGxg\npj1kB9XTYvTCwEoEGBYKALwFgmnvRnwCCwkJkOqwVBmDUBJEAhUKAhYAApsC\nAh4BhaAEGBYKADYFgmnvRnwCCwkJkA35LiZC8HBmAhUKAhYAApsCAh4BFiEE\n0Xa0VhKDLiNQVIiaDfkuJkLwcGYAAIODAQD+YEcJeL0Wi8jY5bdJXzE8lRkE\noikaQMn3pR6dPtL9SwD/eXJamcMAS6L59dawdD0gwCZn9GlKXmj6FFXEJdcd\nOQoWIQSx3XybXTN5HOKO/8LqsFQZg1ASRAAAiU4A/ji0YOT8ceTgDQn3a5P4\nRz9Fv+OUZxj9hMc8K1lB0N/FAQDgv6gXtzEnpGOjtiwPNuuRynnh9OeXzhgL\nPcQIOmW6DA==\n=FAug\n-----END PGP PUBLIC KEY BLOCK-----\n', + prod: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEae97HxYJKwYBBAHaRw8BAQdAkqY/gBSnHQ0bJv6Yik473lXLulCWdhW2\nyEjNq+YEf1bNC0JpdEdvIE5pdHJvwoQEExYKADYFgmnveyICCwkJkKw4yRSD\nFu2mAhUKAhYAApsDAh4BFiEEropjTBjLvFsKdb8ErDjJFIMW7aYAACjjAQC7\n5Sc9kjLdScHTbzsM4l90i9lyHFdTVbruSYvmZrHQAAEAusZMX2qpIJdOdtB7\nyiXeCEMxBm1PcdfdvDSGnUruRQ7ChAQQEwgANgWCae97IgILCQmQtCPvhcuG\nfK8CFQoCFgACmwMCHgEWIQSH0CHgaKWZ1TGhqs+0I++Fy4Z8rwAAM9QA/Ap9\n6YjnvCrv1nO0XT/mqUgSMirGzF6AIk//AfqC8kKHAPsH5I9fKTz8i8vWo6ES\nDSVilnIaEyV6sczxZ2drsLV0Ms44BGnvex8SCisGAQQBl1UBBQEBB0DzGVFB\n74c3y9lShX5DG8or1nJoyjpmb7+aZ710EO7YQwMBCAfChQQYFgoANwWCae97\nIgILCQmQrDjJFIMW7aYCFQoCFgADmwQIAh4BFiEEropjTBjLvFsKdb8ErDjJ\nFIMW7aYAAHthAQCPwozH6edvb4G6JRLFrX8i0rEqCje1yty9sXcwI6XQdwD/\nTm38FbU3qGPop4tePWBTEQYEhE5ams9MIWyvX5P0TA7OMwRp73sfFgkrBgEE\nAdpHDwEBB0AMfvO8IOn4Y9TEtx7IQeutQkgFKapa+jZlVcSmGm8yBsLASgQY\nFgoAvAWCae97IgILCQmQrDjJFIMW7aYCFQoCFgACmwICHgGFoAQYFgoANgWC\nae97IgILCQmQVobXdL8ULVQCFQoCFgACmwICHgEWIQQ+ApqjNkpq4BvXiLdW\nhtd0vxQtVAAAzL8A/2ZASI7HhAAYTmNXvTBRSkfGlgPrurY9cXnwvaUN3Mss\nAP9dCqsaPx6XY5z4GGpGMLVJx4gOu9kpLIonHnZVI6idDhYhBK6KY0wYy7xb\nCnW/BKw4yRSDFu2mAACiPAEA1lDVNbiG0nZFbgatcFF5xKd0urF0f9CpOfpB\ntrIGGbUBAOD7FkKLVXWGFYICYipOUzzGzpFiMNaJl1qbCpa9m1kN\n=R6r4\n-----END PGP PUBLIC KEY BLOCK-----\n', }, onprem: { test: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEad+QCxYJKwYBBAHaRw8BAQdAG0FBM/1JRvo7KlLvhp1Mwi6IWmV3V9xy\nZZcByg0fDQ3NGGhzbSA8aHNtQHRlc3QuYml0Z28uY29tPsKEBBMWCgA2BYJp\n35AMAgsJCZCuV6d9sal31QIVCgIWAAKbAwIeARYhBKSiXcKo4xMT5wwfh65X\np32xqXfVAAA2sAEAgmk543UetoUoOoOvEAhOrRBbF4h6VwcH9cyR9UGSwygA\n/2KJJadiAvaepqFZxyE77rFM7ZfqhRMsoAc2MfslvuQMwoQEEBMIADYFgmnf\nkA0CCwkJkN2vJwOOuE03AhUKAhYAApsDAh4BFiEEjMSQwTRbUtG1fSvR3a8n\nA464TTcAANNVAQD1RTu/bJmPBRvWbvuIiuT1WUxYsSuoXWwki1YImN1gMAD+\nOPU+v056hkdoD8Rcd8D+HhoNlJAbRbZWg/qjxr+S6lLOOARp35AMEgorBgEE\nAZdVAQUBAQdAgqwA9UhQGuseztLr2ZM189pBjrW6sAJ5m6icDYOWMHEDAQgH\nwoUEGBYKADcFgmnfkAwCCwkJkK5Xp32xqXfVAhUKAhYAA5sECAIeARYhBKSi\nXcKo4xMT5wwfh65Xp32xqXfVAAC4uwEAlkVzGDPJYETIV4pXYpCdaeGLBjm9\ny1sRb2nx9ET7m+4BANpb0vKKBrKZTAx/+rINgWoxKPnKPsycOE8bYHY3zKAN\nzjMEad+QDBYJKwYBBAHaRw8BAQdAanwKEY5QEAPafbhM5/BIJZRyLmyNpBTo\ntntTIq0nOt/CwEoEGBYKALwFgmnfkA0CCwkJkK5Xp32xqXfVAhUKAhYAApsC\nAh4BhaAEGBYKADYFgmnfkA0CCwkJkAuXAU6A6KYvAhUKAhYAApsCAh4BFiEE\neaGtxZYsjWFFYrD6C5cBToDopi8AAEp3AQCP4bCSYbhjNJfGnCCOq24DaozR\nUFg0hNlpfSA9NYZ3bwD/fdtV5m5a1QyvcyGEnv37l1H7UGbQlG1Zp8roqZh3\nqwMWIQSkol3CqOMTE+cMH4euV6d9sal31QAAMegA/i3sBuAwvrBsp8ozp7O2\nzQlIbjuvDBMomIXj1rRmgoOkAQDRqsQpAvITd0LMFN2dCqCBIGAeIqznFX0C\nyqvU0m8sCQ==\n=/R3k\n-----END PGP PUBLIC KEY BLOCK-----\n', - prod: '', // TODO WCI-142: add prod eddsaMpcv2 key + prod: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEafDHRxYJKwYBBAHaRw8BAQdA4veo9hvphbH0gq+YFo8AwdtTR6SQN4tJ\nOWiA++eBqMvNBUJpdEdvwoQEExYKADYFgmnwx1UCCwkJkM4UKK9zJ+KPAhUK\nAhYAApsDAh4BFiEEB3Cyce1GqKAWbPdJzhQor3Mn4o8AAKS2AQDSo5ubHHyx\nvlAyRUgiyzVvupK8x1Jl/fPf54CHsWlIfgD+OIi8v+73cUoHTW3YxC3B9aPO\nqzAEV58w76dgZ2qeTgjChAQQEwgANgWCafDHVgILCQmQjcXpcUn3c9gCFQoC\nFgACmwMCHgEWIQS4vN2O383+kvuFGdaNxelxSfdz2AAAoVUA/A6NI89QMuvO\ngtX65BnmHP+D8vBJbwTwazDHpS0H62/SAPwJVaRXrRNkOTzc6bcAxg97sFBw\nhxJFzcFpAY0BaT5ods44BGnwx0gSCisGAQQBl1UBBQEBB0DZ18qpCkv6tiua\nCCf4Ct9Yfas7EilWG8LPNevujNh7EAMBCAfChQQYFgoANwWCafDHVgILCQmQ\nzhQor3Mn4o8CFQoCFgADmwQIAh4BFiEEB3Cyce1GqKAWbPdJzhQor3Mn4o8A\nAPBxAQD9MYbwZ6NxRabvF1lFMOAofAM4N8htqy6T0ZItfIt6KgEA9K3sycqT\nnWEFPCkJ1hl6QeCnp0qjGcbLxLRVGTWwxwrOMwRp8MdIFgkrBgEEAdpHDwEB\nB0DYMGRlbkTErdLqeeroxGghkYpoTNPHmgb5barxfnu2oMLASgQYFgoAvAWC\nafDHVgILCQmQzhQor3Mn4o8CFQoCFgACmwICHgGFoAQYFgoANgWCafDHVgIL\nCQmQU3o2I0o7U2YCFQoCFgACmwICHgEWIQRREfH7DgNjyYJM1c9TejYjSjtT\nZgAAa4ABAIPcFd0PzHQIQiMW/mvk57vQeOTSJmnY+/aXZ58ba3ykAP4oC5Mv\nRlTjUAmQdd67FX/Kmm49ayKKExrbUxMCHopGDBYhBAdwsnHtRqigFmz3Sc4U\nKK9zJ+KPAACetAEA0KpGMATxFZto3zinmPaHJAmc33RShm9mQJ7XkN8eIPMB\nAMK9J0qdW9jR7iOmJvLmULqUEAYWHAKJ1Ey9PWDSmUwM\n=REv+\n-----END PGP PUBLIC KEY BLOCK-----\n', }, }, }; @@ -37,11 +37,11 @@ export const bitgoMpcGpgPubKeys = { export function getBitgoMpcGpgPubKey( env: EnvironmentName, pubKeyType: 'nitro' | 'onprem', - mpcVersion: 'mpcv1' | 'mpcv2' + mpcVersion: 'mpcv1' | 'mpcv2' | 'eddsaMpcv2' ): string { assert( mpcVersion in bitgoMpcGpgPubKeys, - `Invalid mpcVersion in getBitgoMpcGpgPubKey, got: ${mpcVersion}, expected: mpcv1 or mpcv2` + `Invalid mpcVersion in getBitgoMpcGpgPubKey, got: ${mpcVersion}, expected: mpcv1, mpcv2, or eddsaMpcv2` ); assert( pubKeyType in bitgoMpcGpgPubKeys[mpcVersion], diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index e4dfebe65c..ad19a07359 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -86,7 +86,8 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil public async pickBitgoPubGpgKeyForSigning( isMpcv2: boolean, reqId?: IRequestTracer, - enterpriseId?: string + enterpriseId?: string, + isEddsaMpcv2?: boolean ): Promise { let bitgoGpgPubKey; try { @@ -98,7 +99,7 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil armoredKey: getBitgoMpcGpgPubKey( this.bitgo.getEnv(), bitgoKeyChain.hsmType === 'nitro' ? 'nitro' : 'onprem', - isMpcv2 ? 'mpcv2' : 'mpcv1' + isEddsaMpcv2 ? 'eddsaMpcv2' : isMpcv2 ? 'mpcv2' : 'mpcv1' ), }); } catch (e) { @@ -108,11 +109,20 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil ); // First try to get the key based on feature flags, if that fails, fallback to the default key from constants api. bitgoGpgPubKey = await this.getBitgoGpgPubkeyBasedOnFeatureFlags(enterpriseId, isMpcv2, reqId) - .then( - async ({ mpcv2PublicKey }) => + .then(async ({ mpcv2PublicKey, eddsaMpcv2PublicKey }) => { + if (isEddsaMpcv2) { + return eddsaMpcv2PublicKey ?? (await this.getBitgoEddsaMpcv2PublicGpgKey()); + } + return ( mpcv2PublicKey ?? (isMpcv2 ? await this.getBitgoMpcv2PublicGpgKey() : await this.getBitgoPublicGpgKey()) - ) - .catch(async (e) => (isMpcv2 ? await this.getBitgoMpcv2PublicGpgKey() : await this.getBitgoPublicGpgKey())); + ); + }) + .catch(async () => { + if (isEddsaMpcv2) { + return this.getBitgoEddsaMpcv2PublicGpgKey(); + } + return isMpcv2 ? await this.getBitgoMpcv2PublicGpgKey() : await this.getBitgoPublicGpgKey(); + }); } else { throw new Error( `Environment "${this.bitgo.getEnv()}" requires a BitGo GPG Pub Key Config in BitGoJS for TSS. Error thrown while getting the key from config: ${e}` @@ -146,6 +156,18 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil return this.bitgoMPCv2PublicGpgKey; } + async getBitgoEddsaMpcv2PublicGpgKey(): Promise { + if (!this.bitgoEddsaMpcv2PublicGpgKey) { + // retry getting bitgo's gpg key + await this.setBitgoGpgPubKey(this.bitgo); + if (!this.bitgoEddsaMpcv2PublicGpgKey) { + throw new Error("Failed to get Bitgo's EdDSA MPCv2 gpg key"); + } + } + + return this.bitgoEddsaMpcv2PublicGpgKey; + } + async createBitgoHeldBackupKeyShare( userGpgKey: SerializedKeyPair, enterprise: string | undefined