Skip to content

Commit 241a90d

Browse files
committed
feat(sdk-core): add dedicated EdDSA MPCv2 BitGo GPG public key support
EdDSA MPCv2 (MPS) DKG requires an ed25519 GPG key with an X25519 encryption subkey. Using the existing secp256k1 mpcv2PublicKey caused a WASM "Invalid Input" error. This adds a dedicated eddsaMpcv2PublicKey field throughout the key-fetching pipeline so EdDSA and ECDSA MPCv2 wallets each use the correct BitGo GPG key. WCI-223 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> TICKET: WCI-223
1 parent e323cfb commit 241a90d

9 files changed

Lines changed: 74 additions & 21 deletions

File tree

modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@ describe('TSS Ecdsa Utils:', async function () {
239239
],
240240
});
241241
const nockGPGKey = await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, 'enterprise_id', nitroGPGKeypair);
242-
const bitgoGpgPublicKey = await tssUtils.getBitgoGpgPubkeyBasedOnFeatureFlags('enterprise_id');
242+
const { mpcv2PublicKey: bitgoGpgPublicKey } = await tssUtils.getBitgoGpgPubkeyBasedOnFeatureFlags(
243+
'enterprise_id'
244+
);
243245
should.equal(nockGPGKey.publicKey, bitgoGpgPublicKey.armor());
244246
});
245247

@@ -1691,6 +1693,8 @@ describe('TSS Ecdsa Utils:', async function () {
16911693
const bitgoGPGPublicKeyResponse: BitgoGPGPublicKey = {
16921694
name: 'irrelevant',
16931695
publicKey: bitgoGpgKeyPair.publicKey,
1696+
mpcv2PublicKey: bitgoGpgKeyPair.publicKey,
1697+
eddsaMpcv2PublicKey: bitgoGpgKeyPair.publicKey,
16941698
enterpriseId,
16951699
};
16961700
nock(bgUrl).get(`/api/v2/${coin}/tss/pubkey`).query({ enterpriseId }).reply(200, bitgoGPGPublicKeyResponse);

modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
310310
name: 'irrelevant',
311311
publicKey: bitgoKeyPair.publicKey,
312312
mpcv2PublicKey: bitgoKeyPair.publicKey,
313+
eddsaMpcv2PublicKey: bitgoKeyPair.publicKey,
313314
enterpriseId: enterprise,
314315
};
315316
nock(bgUrl).get(`/api/v2/${coin}/tss/pubkey`).query({ enterpriseId: enterprise }).reply(200, response);

modules/sdk-core/src/bitgo/tss/bitgoPubKeys.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ export const bitgoMpcGpgPubKeys = {
2222
prod: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EZmHyKBMFK4EEAAoCAwS+tBY/2P47G0mgYRhq90jK475f02f3f3W4VbKA\nSwd9s6aI5spk7GeYsjRvP6rBf4vFIjLj7Ty7K2V03rZPQc8bzQVCaXRHb8KE\nBBMTCAA2BQJmYfIpAgsJCRAKMB4ATA5V7QIVCAIWAAIbAwIeARYhBAIdflLB\nK4deHok+gQowHgBMDlXtAACRpAD/UUbTsFEkjt+CCJmVq2v5l6oocR9hXXkT\nzhRQKQIwSigA/RVvS2RsoZLkaL68GUHLy63XVHtG149pN3BYPwb63EcQwoQE\nEBMIADYFAmZh8ioCCwkJEFlh2DLM6IVNAhUIAhYAAhsDAh4BFiEEsb9f1VA0\n9rOLgFM+WWHYMszohU0AAFC8AP4wH0ndmzCSg2O/a+ZfqW2yA465BFvDM1ij\nvMtCJYSxzAD/RjcfDfkN4Ipjaa2LRuHxfHZbvgCgoOChsJLv4KQLTafOUwRm\nYfIoEgUrgQQACgIDBM+W01KEUaAm8a3hMBWG9EShyNrZxbtv9ryd8JIIxeEb\nEckLTVQvIer3YvDUyjeY/v83VCRdm6H5cahV92sydrIDAQgHwoQEGBMIADYF\nAmZh8ikCCwkJEAowHgBMDlXtAhUIAhYAAhsMAh4BFiEEAh1+UsErh14eiT6B\nCjAeAEwOVe0AAEcSAP9H96t/z9uKe9lAoq2d9Dt3Hrq9eM6sLQ2+cVblngP+\nDQD/dCqHYQzDdsuc9Y3HmWbhCK1Um6ewppkct1v5lmbaJ1bOTwRmYfIoEwUr\ngQQACgIDBJDIofWOLj/JkBFkZDh3a++LNEH8TBNlDZvU7tNfURXWApxV2VAb\nFBKYddN03Q1SBpMR0GkPl42rH7whYdeaEBHCwEoEGBMIALwFAmZh8ioCCwkJ\nEAowHgBMDlXtAhUIAhYAAhsCAh4BhSAEGBMIADYFAmZh8ioCCwkJEGAfBsMT\nFzVjAhUIAhYAAhsCAh4BFiEE2zAGHSaLnswqIvBrYB8GwxMXNWMAANroAP0f\ntFPumKFwQrCf7OMHQWsesrQYpKT6Z65VbewBoGaGigD/UkeeygTtlyzTV2YF\nNAjWAzaQtXWmmzRgnOj0IKub39MWIQQCHX5SwSuHXh6JPoEKMB4ATA5V7QAA\nTjMA/jDSVXJNblr/kSLNFTordgDjKP0nN1aElvFUFh/QEVT0AP9lmf2Fc/o7\nyYOGPPg4OvvU6odrTsuNgljvPqBlaCc2EA==\n=ZLkt\n-----END PGP PUBLIC KEY BLOCK-----\n',
2323
},
2424
},
25+
eddsaMpcv2: {
26+
nitro: {
27+
test: '', // TODO WCI-205: add eddsaMpcv2 key for nitro
28+
prod: '',
29+
},
30+
onprem: {
31+
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',
32+
prod: '', // TODO WCI-142: add prod eddsaMpcv2 key
33+
},
34+
},
2535
};
2636

2737
export function getBitgoMpcGpgPubKey(
@@ -61,6 +71,10 @@ export function isBitgoMpcPubKey(key: string, mpcvVersion: 'mpcv1' | 'mpcv2'): b
6171
return Object.values(bitgoMpcGpgPubKeys[mpcvVersion]).some((envKeys) => Object.values(envKeys).includes(key));
6272
}
6373

74+
export function isBitgoEddsaMpcv2PubKey(key: string): boolean {
75+
return Object.values(bitgoMpcGpgPubKeys.eddsaMpcv2).some((envKeys) => Object.values(envKeys).includes(key));
76+
}
77+
6478
export function envRequiresBitgoPubGpgKeyConfig(env: EnvironmentName): boolean {
6579
return env === 'prod' || env === 'test' || env === 'staging' || env === 'adminProd' || env === 'adminTest';
6680
}

modules/sdk-core/src/bitgo/utils/opengpgUtils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export type AuthEncMessage = {
3535
* @param {BitGoBase} bitgo BitGo object
3636
* @return {Key} public gpg key
3737
*/
38-
export async function getBitgoGpgPubKey(bitgo: BitGoBase): Promise<{ mpcV1: Key; mpcV2: Key | undefined }> {
38+
export async function getBitgoGpgPubKey(
39+
bitgo: BitGoBase
40+
): Promise<{ mpcV1: Key; mpcV2: Key | undefined; eddsaMpcV2: Key | undefined }> {
3941
const constants = await bitgo.fetchConstants();
4042
if (!constants.mpc || !constants.mpc.bitgoPublicKey) {
4143
throw new Error('Unable to create MPC keys - bitgoPublicKey is missing from constants');
@@ -45,7 +47,14 @@ export async function getBitgoGpgPubKey(bitgo: BitGoBase): Promise<{ mpcV1: Key;
4547
const bitgoMPCv2PublicKeyStr = constants.mpc.bitgoMPCv2PublicKey
4648
? await readKey({ armoredKey: constants.mpc.bitgoMPCv2PublicKey as string })
4749
: undefined;
48-
return { mpcV1: await readKey({ armoredKey: bitgoPublicKeyStr }), mpcV2: bitgoMPCv2PublicKeyStr };
50+
const bitgoEddsaMpcv2PublicKeyStr = constants.mpc.bitgoEddsaMpcv2PublicKey
51+
? await readKey({ armoredKey: constants.mpc.bitgoEddsaMpcv2PublicKey as string })
52+
: undefined;
53+
return {
54+
mpcV1: await readKey({ armoredKey: bitgoPublicKeyStr }),
55+
mpcV2: bitgoMPCv2PublicKeyStr,
56+
eddsaMpcV2: bitgoEddsaMpcv2PublicKeyStr,
57+
};
4958
}
5059

5160
/**

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
5353
private _wallet?: IWallet;
5454
protected bitgoPublicGpgKey: openpgp.Key;
5555
protected bitgoMPCv2PublicGpgKey: openpgp.Key | undefined;
56+
protected bitgoEddsaMpcv2PublicGpgKey: openpgp.Key | undefined;
5657

5758
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, wallet?: IWallet) {
5859
super(bitgo, baseCoin);
@@ -67,7 +68,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
6768
}
6869

6970
protected async setBitgoGpgPubKey(bitgo) {
70-
const { mpcV1, mpcV2 } = await getBitgoGpgPubKey(bitgo);
71+
const { mpcV1, mpcV2, eddsaMpcV2 } = await getBitgoGpgPubKey(bitgo);
7172
// Do not unset the MPCv1 key if it is already set. This is to avoid unsetting if extra constants api calls fail.
7273
if (mpcV1 !== undefined) {
7374
this.bitgoPublicGpgKey = mpcV1;
@@ -76,6 +77,10 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
7677
if (mpcV2 !== undefined) {
7778
this.bitgoMPCv2PublicGpgKey = mpcV2;
7879
}
80+
// Do not unset the EdDSA MPCv2 key if it is already set
81+
if (eddsaMpcV2 !== undefined) {
82+
this.bitgoEddsaMpcv2PublicGpgKey = eddsaMpcV2;
83+
}
7984
}
8085

8186
public async pickBitgoPubGpgKeyForSigning(
@@ -104,8 +109,8 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
104109
// First try to get the key based on feature flags, if that fails, fallback to the default key from constants api.
105110
bitgoGpgPubKey = await this.getBitgoGpgPubkeyBasedOnFeatureFlags(enterpriseId, isMpcv2, reqId)
106111
.then(
107-
async (pubKey) =>
108-
pubKey ?? (isMpcv2 ? await this.getBitgoMpcv2PublicGpgKey() : await this.getBitgoPublicGpgKey())
112+
async ({ mpcv2PublicKey }) =>
113+
mpcv2PublicKey ?? (isMpcv2 ? await this.getBitgoMpcv2PublicGpgKey() : await this.getBitgoPublicGpgKey())
109114
)
110115
.catch(async (e) => (isMpcv2 ? await this.getBitgoMpcv2PublicGpgKey() : await this.getBitgoPublicGpgKey()));
111116
} else {
@@ -141,6 +146,17 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
141146
return this.bitgoMPCv2PublicGpgKey;
142147
}
143148

149+
async getBitgoEddsaMpcv2PublicGpgKey(): Promise<openpgp.Key> {
150+
if (!this.bitgoEddsaMpcv2PublicGpgKey) {
151+
// retry getting bitgo's EdDSA MPCv2 gpg key
152+
await this.setBitgoGpgPubKey(this.bitgo);
153+
if (!this.bitgoEddsaMpcv2PublicGpgKey) {
154+
throw new Error("Failed to get Bitgo's EdDSA MPCv2 gpg key");
155+
}
156+
}
157+
return this.bitgoEddsaMpcv2PublicGpgKey;
158+
}
159+
144160
async createBitgoHeldBackupKeyShare(
145161
userGpgKey: SerializedKeyPair<string>,
146162
enterprise: string | undefined
@@ -552,8 +568,9 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
552568
}
553569

554570
/**
555-
* It gets the appropriate BitGo GPG public key for key creation based on a
571+
* It gets the appropriate BitGo GPG public keys for key creation based on a
556572
* combination of coin and the feature flags on the user and their enterprise if set.
573+
* Returns both the default MPCv2 key and the EdDSA-specific MPCv2 key (if present).
557574
* @param enterpriseId - enterprise under which user wants to create the wallet
558575
* @param isMPCv2 - true to get the MPCv2 GPG public key, defaults to false
559576
* @param reqId - request tracer request id
@@ -562,16 +579,21 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
562579
enterpriseId: string | undefined,
563580
isMPCv2 = false,
564581
reqId?: IRequestTracer
565-
): Promise<Key> {
582+
): Promise<{ mpcv2PublicKey: Key; eddsaMpcv2PublicKey: Key | undefined }> {
566583
const reqTracer = reqId || new RequestTracer();
567584
this.bitgo.setRequestTracer(reqTracer);
568585
const response: BitgoGPGPublicKey = await this.bitgo
569586
.get(this.baseCoin.url('/tss/pubkey'))
570587
.query({ enterpriseId })
571588
.retry(3)
572589
.result();
573-
const bitgoPublicKeyStr = isMPCv2 ? response.mpcv2PublicKey : response.publicKey;
574-
return readKey({ armoredKey: bitgoPublicKeyStr as string });
590+
const mpcv2PublicKey = await readKey({
591+
armoredKey: (isMPCv2 ? response.mpcv2PublicKey : response.publicKey) as string,
592+
});
593+
const eddsaMpcv2PublicKey = response.eddsaMpcv2PublicKey
594+
? await readKey({ armoredKey: response.eddsaMpcv2PublicKey })
595+
: undefined;
596+
return { mpcv2PublicKey, eddsaMpcv2PublicKey };
575597
}
576598

577599
/**

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ export interface BitgoGPGPublicKey {
572572
name: string;
573573
publicKey: string;
574574
mpcv2PublicKey?: string;
575+
eddsaMpcv2PublicKey?: string;
575576
enterpriseId: string;
576577
}
577578

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
118118

119119
// Get the BitGo public key based on user/enterprise feature flags
120120
// If it doesn't work, use the default public key from the constants
121-
const bitgoPublicGpgKey =
122-
(await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise)) ?? this.bitgoPublicGpgKey;
121+
const { mpcv2PublicKey: primaryGpgKey } = await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise);
122+
const bitgoPublicGpgKey = primaryGpgKey ?? this.bitgoPublicGpgKey;
123123

124124
const bitgoKeychain = await this.createBitgoKeychain({
125125
userGpgKey,

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
6464

6565
// Get the BitGo public key based on user/enterprise feature flags
6666
// If it doesn't work, use the default public key from the constants
67-
const bitgoPublicGpgKey = (
68-
(await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise, true)) ?? this.bitgoMPCv2PublicGpgKey
69-
).armor();
67+
const { mpcv2PublicKey } = await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise, true);
68+
const mpcv2Key = mpcv2PublicKey ?? this.bitgoMPCv2PublicGpgKey;
69+
assert(mpcv2Key, 'Failed to get BitGo MPCv2 GPG public key');
70+
const bitgoPublicGpgKey = mpcv2Key.armor();
7071

7172
if (envRequiresBitgoPubGpgKeyConfig(this.bitgo.getEnv())) {
7273
// Ensure the public key is one of the expected BitGo public keys when in test or prod.

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { EddsaMPSDkg, MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
1313
import { KeychainsTriplet } from '../../../baseCoin';
1414
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
15-
import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys';
15+
import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys';
1616
import { generateGPGKeyPair } from '../../opengpgUtils';
1717
import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2';
1818
import { BaseEddsaUtils } from './base';
@@ -35,14 +35,15 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
3535
const backupGpgPublicKey = backupKeyPair.publicKey;
3636
const [backupPk, backupSk] = await MPSComms.extractEd25519KeyPair(backupGpgKey);
3737

38-
// Get the BitGo public key based on user/enterprise feature flags;
39-
// fall back to the hardcoded MPCv2 public key from constants.
40-
const bitgoPublicGpgKey =
41-
(await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise, true)) ?? this.bitgoMPCv2PublicGpgKey;
38+
// Get the BitGo EdDSA MPCv2 public key (ed25519). Using the default mpcv2PublicKey (secp256k1)
39+
// here would cause a WASM "Invalid Input" error, so we require the dedicated eddsaMpcv2PublicKey.
40+
const { eddsaMpcv2PublicKey } = await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise, true);
41+
const bitgoPublicGpgKey = eddsaMpcv2PublicKey ?? this.bitgoEddsaMpcv2PublicGpgKey;
42+
assert(bitgoPublicGpgKey, 'Failed to get BitGo EdDSA MPCv2 GPG public key');
4243
const bitgoPublicGpgKeyArmored = bitgoPublicGpgKey.armor();
4344

4445
if (envRequiresBitgoPubGpgKeyConfig(this.bitgo.getEnv())) {
45-
assert(isBitgoMpcPubKey(bitgoPublicGpgKeyArmored, 'mpcv2'), 'Invalid BitGo GPG public key');
46+
assert(isBitgoEddsaMpcv2PubKey(bitgoPublicGpgKeyArmored), 'Invalid BitGo GPG public key');
4647
}
4748

4849
const bitgoKeyObj = await pgp.readKey({ armoredKey: bitgoPublicGpgKeyArmored });

0 commit comments

Comments
 (0)