Skip to content

Commit 7133688

Browse files
Marzooqaclaude
andcommitted
feat(sdk-lib-mpc): bump @bitgo/wasm-mps to 1.10.0 and use Eddsa.deriveUnhardened
- Bump @bitgo/wasm-mps from 1.8.1 to 1.10.0 in @bitgo/sdk-lib-mpc - wasm-mps 1.10.0 uses BIP32-Ed25519 dual-HMAC (Eddsa.deriveUnhardened) for DSG path derivation, replacing the Silence Labs single-HMAC formula - Fix express externalSign test: replace deriveUnhardenedMps with MPC.deriveUnhardened for DSG signature verification - Mark deriveUnhardenedMps @deprecated in sdk-lib-mpc Ticket: WCI-793 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eddba81 commit 7133688

8 files changed

Lines changed: 62 additions & 46 deletions

File tree

modules/express/test/unit/clientRoutes/externalSign.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333
MPSUtil,
3434
MPSComms,
3535
MPSTypes,
36-
deriveUnhardenedMps,
3736
} from '@bitgo/sdk-lib-mpc';
3837
import {
3938
MPCv2PartyFromStringOrNumber,
@@ -1224,7 +1223,7 @@ describe('External signer', () => {
12241223
// legacy EdDSA v1 test (MPC.verify) and ECDSA MPCv2 test (DklsUtils.verifyAndConvert...).
12251224
// Uses Node.js built-in crypto — no extra npm dependency needed.
12261225
const signature = bitgoDsg.getSignature();
1227-
const derivedKeychainHex = deriveUnhardenedMps(userDkg.getCommonKeychain(), derivationPath);
1226+
const derivedKeychainHex = MPC.deriveUnhardened(userDkg.getCommonKeychain(), derivationPath);
12281227
const derivedPubKeyBytes = Buffer.from(derivedKeychainHex.slice(0, 64), 'hex');
12291228
// Ed25519 SubjectPublicKeyInfo DER header: SEQUENCE { SEQUENCE { OID 1.3.101.112 } BIT STRING }
12301229
const spkiDer = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), derivedPubKeyBytes]);

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
MPCv2PartyFromStringOrNumber,
1414
} from '@bitgo/public-types';
1515
import { ed25519 } from '@noble/curves/ed25519';
16-
import { deriveUnhardenedMps, EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
16+
import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
1717
import { KeychainsTriplet } from '../../../baseCoin';
1818
import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
1919
import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys';
@@ -26,6 +26,7 @@ import {
2626
verifyPeerMessageRoundOne,
2727
verifyPeerMessageRoundTwo,
2828
} from '../../../tss/eddsa/eddsaMPCv2';
29+
import { getInitializedMpcInstance } from '../../../tss/eddsa/eddsa';
2930
import { generateGPGKeyPair } from '../../opengpgUtils';
3031
import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2';
3132
import {
@@ -1015,7 +1016,7 @@ export async function getEddsaMpcV2RecoveryKeySharesFromReducedKey(
10151016
* Ed25519 signature against the public key derived from the common keychain.
10161017
*
10171018
* @param message raw bytes to sign
1018-
* @param derivationPath BIP-32-style derivation path, e.g. `"m/0/0"`
1019+
* @param derivationPath BIP-32-style derivation path, e.g. `"m/0"`
10191020
* @param userKeyShare opaque MPS signing key-share bytes for the user party
10201021
* @param backupKeyShare opaque MPS signing key-share bytes for the backup party
10211022
* @param commonKeyChain 128-hex-char string: 32-byte pub + 32-byte rootChainCode
@@ -1041,8 +1042,10 @@ export async function signRecoveryEddsaMPCv2(
10411042
derivationPath
10421043
)) as Buffer;
10431044

1044-
// deriveUnhardenedMps returns 128 hex chars: first 64 are the 32-byte public key
1045-
const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath);
1045+
// Use Eddsa.deriveUnhardened (BIP32-Ed25519), which matches what DSG uses
1046+
// internally for path derivation since wasm-mps 1.9.0.
1047+
const mpc = await getInitializedMpcInstance();
1048+
const derivedKeychain = mpc.deriveUnhardened(commonKeyChain, derivationPath);
10461049
const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex');
10471050

10481051
const verified = ed25519.verify(new Uint8Array(signature), new Uint8Array(message), new Uint8Array(publicKeyBytes));

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as assert from 'assert';
22
import * as sinon from 'sinon';
33
import * as pgp from 'openpgp';
44
import { randomBytes } from 'crypto';
5-
import { deriveUnhardenedMps, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
5+
import { EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
66
import { ed25519 } from '@noble/curves/ed25519';
77
import * as sjcl from '@bitgo/sjcl';
88
import {
@@ -34,6 +34,7 @@ import {
3434
verifyPeerMessageRoundOne,
3535
verifyPeerMessageRoundTwo,
3636
} from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2';
37+
import { getInitializedMpcInstance } from '../../../../../../src/bitgo/tss/eddsa/eddsa';
3738
import { getBitgoSignatureShare } from '../../../../../../src/bitgo/tss/common';
3839
import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs';
3940
import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils';
@@ -1792,13 +1793,14 @@ describe('signRecoveryEddsaMPCv2', () => {
17921793

17931794
assert.strictEqual(signature.length, 64);
17941795

1795-
const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath);
1796+
const mpc = await getInitializedMpcInstance();
1797+
const derivedKeychain = mpc.deriveUnhardened(commonKeyChain, derivationPath);
17961798
const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex');
17971799
const ok = ed25519.verify(new Uint8Array(signature), new Uint8Array(message), new Uint8Array(publicKeyBytes));
17981800
assert.strictEqual(ok, true);
17991801
});
18001802

1801-
it('should throw when the signed message is different from the verified message', async () => {
1803+
it('should return false when verifying the signature against a different message', async () => {
18021804
const [userDkg, backupDkg] = await MPSUtil.generateEdDsaDKGKeyShares();
18031805
const message = Buffer.from('deadbeef', 'hex');
18041806
const commonKeyChain = userDkg.getCommonKeychain();
@@ -1812,7 +1814,8 @@ describe('signRecoveryEddsaMPCv2', () => {
18121814
);
18131815

18141816
const differentMessage = Buffer.from('cafebabe', 'hex');
1815-
const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath);
1817+
const mpc = await getInitializedMpcInstance();
1818+
const derivedKeychain = mpc.deriveUnhardened(commonKeyChain, derivationPath);
18161819
const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex');
18171820
const ok = ed25519.verify(
18181821
new Uint8Array(signature),
@@ -1828,13 +1831,14 @@ describe('signRecoveryEddsaMPCv2', () => {
18281831
const message = Buffer.from('deadbeef', 'hex');
18291832

18301833
await assert.rejects(
1831-
EDDSAUtils.signRecoveryEddsaMPCv2(
1832-
message,
1833-
derivationPath,
1834-
userDkg.getKeyShare(),
1835-
backupDkg.getKeyShare(),
1836-
wrongDkg.getCommonKeychain() // key chain from a different wallet
1837-
),
1834+
() =>
1835+
EDDSAUtils.signRecoveryEddsaMPCv2(
1836+
message,
1837+
derivationPath,
1838+
userDkg.getKeyShare(),
1839+
backupDkg.getKeyShare(),
1840+
wrongDkg.getCommonKeychain() // key chain from a different wallet
1841+
),
18381842
/EdDSA MPCv2 recovery signature verification failed/
18391843
);
18401844
});

modules/sdk-lib-mpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
]
3737
},
3838
"dependencies": {
39-
"@bitgo/wasm-mps": "1.8.1",
39+
"@bitgo/wasm-mps": "1.10.0",
4040
"@noble/curves": "1.8.1",
4141
"@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4",
4242
"@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4",

modules/sdk-lib-mpc/src/tss/eddsa-mps/derive.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import { createHmac } from 'crypto';
22
import { ed25519 } from '@noble/curves/ed25519';
33
import { pathToIndices } from '../../curves/util';
4+
import { Ed25519Bip32HdTree } from '../../curves/ed25519Bip32HdTree';
5+
import { bigIntFromBufferBE, bigIntFromBufferLE, bigIntToBufferBE, bigIntToBufferLE } from '../../util';
46

57
/**
6-
* Derives a child public key from a common keychain using the Silence Labs
7-
* BIP32-Ed25519 non-hardened derivation formula:
8-
*
9-
* HMAC = HMAC-SHA512(key=chaincode, data=pk_bytes || index_BE_4)
10-
* child_pk = parent_pk + 8 * LE(trunc28(HMAC_left)) * G
11-
* child_chaincode = HMAC_right (right 32 bytes)
12-
*
13-
* This differs from the Cardano BIP32-Ed25519 formula used by
14-
* `Eddsa.deriveUnhardened` in three ways: no 0x02 prefix byte, big-endian
15-
* index, and a single HMAC instead of two. The formulas produce completely
16-
* different child keys at every derived level.
17-
*
18-
* Returns the same on-the-wire format as `Eddsa.deriveUnhardened`:
19-
* 128-char hex = 64-char derived pk + 64-char derived chaincode
8+
* @deprecated Use `Eddsa.deriveUnhardened` instead. wasm-mps >=1.9.0 uses
9+
* standard BIP32-Ed25519 (the Cardano formula) for DSG path derivation.
2010
*/
2111
export function deriveUnhardenedMps(commonKeychainHex: string, path: string): string {
2212
if (commonKeychainHex.length !== 128) {
@@ -55,3 +45,24 @@ export function deriveUnhardenedMps(commonKeychainHex: string, path: string): st
5545

5646
return pkBytes.toString('hex') + ccBytes.toString('hex');
5747
}
48+
49+
/**
50+
* Derives a child public key from a common keychain using the standard
51+
* BIP32-Ed25519 dual-HMAC formula (same as `Eddsa.deriveUnhardened`).
52+
*
53+
* Returns 128-char hex: 64-char derived pk + 64-char derived chaincode.
54+
*/
55+
export async function deriveUnhardened(commonKeychainHex: string, path: string): Promise<string> {
56+
if (commonKeychainHex.length !== 128) {
57+
throw new Error(
58+
`Invalid commonKeychain: expected 128 hex chars (32-byte pk + 32-byte chaincode), got ${commonKeychainHex.length}`
59+
);
60+
}
61+
const hdTree = await Ed25519Bip32HdTree.initialize();
62+
const buf = Buffer.from(commonKeychainHex, 'hex');
63+
const derived = hdTree.publicDerive(
64+
{ pk: bigIntFromBufferLE(buf.slice(0, 32)), chaincode: bigIntFromBufferBE(buf.slice(32, 64)) },
65+
path
66+
);
67+
return bigIntToBufferLE(derived.pk, 32).toString('hex') + bigIntToBufferBE(derived.chaincode, 32).toString('hex');
68+
}

modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ export * as EddsaMPSDsg from './dsg';
33
export * as MPSUtil from './util';
44
export * as MPSTypes from './types';
55
export * as MPSComms from './commsLayer';
6-
export { deriveUnhardenedMps } from './derive';
6+
export { deriveUnhardenedMps, deriveUnhardened } from './derive';

modules/sdk-lib-mpc/test/unit/tss/eddsa/derive.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'assert';
22
import { ed25519 } from '@noble/curves/ed25519';
33
import { EddsaMPSDsg, MPSUtil } from '../../../../src/tss/eddsa-mps';
4-
import { deriveUnhardenedMps } from '../../../../src/tss/eddsa-mps/derive';
4+
import { deriveUnhardenedMps, deriveUnhardened } from '../../../../src/tss/eddsa-mps/derive';
55
import { generateEdDsaDKGKeyShares } from './util';
66

77
const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');
@@ -14,7 +14,6 @@ describe('deriveUnhardenedMps', function () {
1414
let rootPubKey: Buffer;
1515
let userKeyShare: Buffer;
1616
let bitgoKeyShare: Buffer;
17-
1817
before(async function () {
1918
const [userDkg, , bitgoDkg] = await generateEdDsaDKGKeyShares();
2019
commonKeychain = userDkg.getCommonKeychain();
@@ -63,7 +62,7 @@ describe('deriveUnhardenedMps', function () {
6362
});
6463
});
6564

66-
describe('DSG signature cross-check against the public key derived by deriveUnhardenedMps', function () {
65+
describe('DSG signature cross-check against Ed25519Bip32HdTree (deriveUnhardened)', function () {
6766
let sigAtRoot: Buffer;
6867
let sigAtM0: Buffer;
6968
let sigAtM01: Buffer;
@@ -86,19 +85,19 @@ describe('deriveUnhardenedMps', function () {
8685
assert(ed25519.verify(sigAtRoot, MESSAGE, rootPubKey), 'DSG at "m" should verify against the raw DKG public key');
8786
});
8887

89-
it('signature from DSG at "m/0" verifies against deriveUnhardenedMps(commonKeychain, "m/0")', function () {
90-
const derivedPk = Buffer.from(deriveUnhardenedMps(commonKeychain, 'm/0').slice(0, 64), 'hex');
88+
it('signature from DSG at "m/0" verifies against Eddsa.deriveUnhardened key at "m/0"', async function () {
89+
const derivedPk = Buffer.from((await deriveUnhardened(commonKeychain, 'm/0')).slice(0, 64), 'hex');
9190
assert(
9291
ed25519.verify(sigAtM0, MESSAGE, derivedPk),
93-
'DSG at "m/0" should verify against deriveUnhardenedMps result at "m/0"'
92+
'DSG at "m/0" should verify against Eddsa.deriveUnhardened result at "m/0"'
9493
);
9594
});
9695

97-
it('signature from DSG at "m/0/1" verifies against deriveUnhardenedMps(commonKeychain, "m/0/1")', function () {
98-
const derivedPk = Buffer.from(deriveUnhardenedMps(commonKeychain, 'm/0/1').slice(0, 64), 'hex');
96+
it('signature from DSG at "m/0/1" verifies against Eddsa.deriveUnhardened key at "m/0/1"', async function () {
97+
const derivedPk = Buffer.from((await deriveUnhardened(commonKeychain, 'm/0/1')).slice(0, 64), 'hex');
9998
assert(
10099
ed25519.verify(sigAtM01, MESSAGE, derivedPk),
101-
'DSG at "m/0/1" should verify against deriveUnhardenedMps result at "m/0/1"'
100+
'DSG at "m/0/1" should verify against Eddsa.deriveUnhardened result at "m/0/1"'
102101
);
103102
});
104103
});

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,10 +1013,10 @@
10131013
resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz"
10141014
integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ==
10151015

1016-
"@bitgo/wasm-mps@1.8.1":
1017-
version "1.8.1"
1018-
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.8.1.tgz#946673f5845696cdcf744f8122fd1fc2be3edce1"
1019-
integrity sha512-CV8EXYc1BGYtXdCRDxJ5h04nj/LpMgu3VlkfowlodI6UKcj1zotAvk4OMIdgiPPbKVr1l+xibHDXZYx/uf3rnw==
1016+
"@bitgo/wasm-mps@1.10.0":
1017+
version "1.10.0"
1018+
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.10.0.tgz#df6a056247ce04c7d92369d257b659876e03261d"
1019+
integrity sha512-f42sMCyqqlaId3AtcvdpOfR+mOjAyVopCxCCAqW7wcTAQ8ZBS9rMGQIzTMiqFmZDBMWsAe+QHWA6XseIuzTVdQ==
10201020

10211021
"@bitgo/wasm-solana@^2.6.0":
10221022
version "2.6.0"

0 commit comments

Comments
 (0)