Skip to content
Open
Show file tree
Hide file tree
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
22 changes: 8 additions & 14 deletions modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1216,27 +1216,21 @@ describe('TSS Utils:', async function () {
}

describe('getPublicKeyFromCommonKeychain', function () {
// 32-byte ed25519 public key as hex (64 chars) — the format produced by DKG getSharePublicKey().toString('hex')
const mpcv2CommonKeychain = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270';
// MPCv1 appends a 32-byte chaincode after the public key
// 32-byte ed25519 public key as hex (64 chars) + 32-byte chaincode (64 chars) = 128 chars
const pubHex = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270';
const chaincode = '9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9';
const mpcv1CommonKeychain = mpcv2CommonKeychain + chaincode;
const commonKeychain = pubHex + chaincode;

it('should decode to the same 32-byte public key for both MPCv1 (128 chars) and MPCv2 (64 chars)', function () {
mpcv1CommonKeychain.length.should.equal(128);
mpcv2CommonKeychain.length.should.equal(64);

const v1Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv1CommonKeychain);
const v2Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv2CommonKeychain);

v1Result.should.equal(v2Result);
v1Result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb');
it('should decode the 32-byte public key from a 128-char commonKeychain', function () {
commonKeychain.length.should.equal(128);
const result = TssUtils.getPublicKeyFromCommonKeychain(commonKeychain);
result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb');
});

it('should throw for an invalid commonKeychain length', function () {
should.throws(
() => TssUtils.getPublicKeyFromCommonKeychain('abcd'),
/Invalid commonKeychain length, expected 64 \(MPCv2\) or 128 \(MPCv1\), got 4/
/Invalid commonKeychain length, expected 128, got 4/
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
.once()
.reply(200, {
sessionId: 'test-session-id',
commonPublicKey: 'a'.repeat(64),
commonPublicKeychain: 'a'.repeat(128),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR didn't change what gets sent to the server, so how come this mock was changed? Was it just ignored before?

bitgoMsg2: {
message: Buffer.from('garbage').toString('base64'),
signature: '-----BEGIN PGP SIGNATURE-----\nFAKE\n-----END PGP SIGNATURE-----',
Expand All @@ -221,7 +221,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
.once()
.reply(200, {
sessionId: 'different-session-id',
commonPublicKey: 'a'.repeat(64),
commonPublicKeychain: 'a'.repeat(128),
bitgoMsg2: { message: '', signature: '' },
});

Expand All @@ -231,7 +231,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
);
});

it('should reject when commonPublicKey from BitGo does not match the locally computed key', async function () {
it('should reject when commonPublicKeychain from BitGo does not match the locally computed keychain', async function () {
const bitgoSession = new EddsaMPSDkg.DKG(3, 2, 2);
const bitgoState: { msg2?: MPSTypes.DeserializedMessage } = {};
await nockMPSKeyGenRound1(bitgoSession, bitgoState, 1);
Expand All @@ -255,14 +255,14 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {

return {
sessionId: 'test-session-id',
commonPublicKey: 'fakefakeee'.repeat(8), // mutated — will not match user/backup computed key
commonPublicKeychain: 'fakefakeee'.repeat(16), // mutated — will not match user/backup computed keychain
bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj),
};
});

await assert.rejects(
tssUtils.createKeychains({ passphrase: 'test', enterprise: enterpriseId }),
/does not match BitGo common public key/
/does not match BitGo common keychain/
);
});

Expand All @@ -286,6 +286,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
name: 'irrelevant',
publicKey: bitgoGpgKeyPair.publicKey,
mpcv2PublicKey: bitgoGpgKeyPair.publicKey,
eddsaMpcv2PublicKey: bitgoGpgKeyPair.publicKey,
enterpriseId,
});
nock(stagingBgUrl).get('/api/v1/client/constants').reply(200, { ttl: 3600, constants });
Expand Down Expand Up @@ -390,7 +391,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {

return {
sessionId,
commonPublicKey: bitgoSession.getSharePublicKey().toString('hex'),
commonPublicKeychain: bitgoSession.getCommonKeychain(),
bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj),
};
});
Expand Down
11 changes: 3 additions & 8 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@ export class BaseEddsaUtils extends baseTSSUtils<KeyShare> {
/**
* Get the commonPub portion of an EdDSA commonKeychain.
*
* MPCv1 keychains are 128 hex chars (32-byte public key + 32-byte chaincode).
* MPCv2 keychains are 64 hex chars (32-byte public key only — no chaincode).
* Keychains are 128 hex chars: 32-byte public key + 32-byte chaincode.
*
* @param {string} commonKeychain
* @returns {string} base58-encoded public key
*/
static getPublicKeyFromCommonKeychain(commonKeychain: string): string {
if (commonKeychain.length !== 64 && commonKeychain.length !== 128) {
throw new Error(
`Invalid commonKeychain length, expected 64 (MPCv2) or 128 (MPCv1), got ${commonKeychain.length}`
);
if (commonKeychain.length !== 128) {
throw new Error(`Invalid commonKeychain length, expected 128, got ${commonKeychain.length}`);
}
// For MPCv1 (128 chars): the first 64 hex chars are the 32-byte public key.
// For MPCv2 (64 chars): the entire string is the 32-byte public key.
const pubHex = commonKeychain.slice(0, 64);
return bs58.encode(Buffer.from(pubHex, 'hex'));
}
Expand Down
24 changes: 16 additions & 8 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {

const {
sessionId: sessionIdRound2,
commonPublicKeychain: commonPublicKey,
commonPublicKeychain,
bitgoMsg2,
} = await this.sendKeyGenerationRound2(params.enterprise, {
sessionId,
Expand All @@ -123,32 +123,40 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
assert(userFinalMsgs.length === 0, 'WASM round 2 should produce no output messages for user');
assert(backupFinalMsgs.length === 0, 'WASM round 2 should produce no output messages for backup');

const userCommonKey = userDkg.getSharePublicKey().toString('hex');
const backupCommonKey = backupDkg.getSharePublicKey().toString('hex');
const userCommonKeychain = userDkg.getCommonKeychain();
const backupCommonKeychain = backupDkg.getCommonKeychain();

assert.equal(userCommonKey, commonPublicKey, 'User computed public key does not match BitGo common public key');
assert.equal(backupCommonKey, commonPublicKey, 'Backup computed public key does not match BitGo common public key');
assert.equal(
userCommonKeychain,
commonPublicKeychain,
'User computed keychain does not match BitGo common keychain'
);
assert.equal(
backupCommonKeychain,
commonPublicKeychain,
'Backup computed keychain does not match BitGo common keychain'
);

const userPrivateMaterial = userDkg.getKeyShare();
const backupPrivateMaterial = backupDkg.getKeyShare();
const userReducedPrivateMaterial = userDkg.getReducedKeyShare();
const backupReducedPrivateMaterial = backupDkg.getReducedKeyShare();

const userKeychainPromise = this.addUserKeychain(
commonPublicKey,
userCommonKeychain,
userPrivateMaterial,
userReducedPrivateMaterial,
params.passphrase,
params.originalPasscodeEncryptionCode
);
const backupKeychainPromise = this.addBackupKeychain(
commonPublicKey,
backupCommonKeychain,
backupPrivateMaterial,
backupReducedPrivateMaterial,
params.passphrase,
params.originalPasscodeEncryptionCode
);
const bitgoKeychainPromise = this.addBitgoKeychain(commonPublicKey);
const bitgoKeychainPromise = this.addBitgoKeychain(userCommonKeychain);

const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([
userKeychainPromise,
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-lib-mpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
]
},
"dependencies": {
"@bitgo/wasm-mps": "1.6.0",
"@bitgo/wasm-mps": "1.7.0",
"@noble/curves": "1.8.1",
"@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4",
"@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4",
Expand Down
16 changes: 14 additions & 2 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export class DKG {
private keyShare: Buffer | null = null;
/** 32-byte Ed25519 public key from round2 */
private sharePk: Buffer | null = null;
/** 32-byte chain code from round2 */
private shareChaincode: Buffer | null = null;

protected dkgState: DkgState = DkgState.Uninitialized;

Expand Down Expand Up @@ -153,6 +155,7 @@ export class DKG {
}
this.keyShare = Buffer.from(share.share);
this.sharePk = Buffer.from(share.pk);
this.shareChaincode = Buffer.from(share.chaincode);
this.dkgStateBytes = null;
this.dkgState = DkgState.Complete;
return [];
Expand Down Expand Up @@ -182,10 +185,19 @@ export class DKG {
return this.sharePk;
}

/**
* Returns the 128-char hex common keychain: 64-char public key + 64-char chain code.
* This matches the format expected by address derivation (Eddsa.deriveUnhardened).
*/
getCommonKeychain(): string {
if (!this.sharePk || !this.shareChaincode) {
throw Error('DKG session not initialized');
}
return this.sharePk.toString('hex') + this.shareChaincode.toString('hex');
}

/**
* Returns a CBOR-encoded reduced representation containing the public key.
* Note: private key material and chain code are not separately accessible
* from @bitgo/wasm-mps; the full keyshare is available via getKeyShare().
*/
getReducedKeyShare(): Buffer {
if (!this.sharePk) {
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1015,10 +1015,10 @@
resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz"
integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ==

"@bitgo/wasm-mps@1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.6.0.tgz#3e1f0618c1efac35ccd56301f8198f19d934e5ed"
integrity sha512-4Mzs124Wj3QbqaZqTYX4t2vSVNKblL/53SQFddoPgggfCnZpuV4tYovpD2sIwhbWe8hVWJXZR2/1CP+zUHKMaw==
"@bitgo/wasm-mps@1.7.0":
version "1.7.0"
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.7.0.tgz#e7ebca1afd2df757e69c5cdac702d6a06156867c"
integrity sha512-SNO7as4UvnE2ptDXp1oUXjABA8Y3/71lgVpAQyAGSfSaURjz4rG19+JZR54GBRIaA6hvUPr029b4gFyqoZPcgg==

"@bitgo/wasm-solana@^2.6.0":
version "2.6.0"
Expand Down
Loading