Skip to content

Commit 63ad0bf

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 63ad0bf

7 files changed

Lines changed: 52 additions & 43 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: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,8 @@ import { ed25519 } from '@noble/curves/ed25519';
33
import { pathToIndices } from '../../curves/util';
44

55
/**
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
6+
* @deprecated Use `Eddsa.deriveUnhardened` instead. wasm-mps >=1.9.0 uses
7+
* standard BIP32-Ed25519 (the Cardano formula) for DSG path derivation.
208
*/
219
export function deriveUnhardenedMps(commonKeychainHex: string, path: string): string {
2210
if (commonKeychainHex.length !== 128) {

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import assert from 'assert';
22
import { ed25519 } from '@noble/curves/ed25519';
33
import { EddsaMPSDsg, MPSUtil } from '../../../../src/tss/eddsa-mps';
44
import { deriveUnhardenedMps } from '../../../../src/tss/eddsa-mps/derive';
5+
import { Ed25519Bip32HdTree } from '../../../../src/curves/ed25519Bip32HdTree';
6+
import { bigIntFromBufferBE, bigIntFromBufferLE, bigIntToBufferLE } from '../../../../src/util';
57
import { generateEdDsaDKGKeyShares } from './util';
68

79
const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');
@@ -14,15 +16,28 @@ describe('deriveUnhardenedMps', function () {
1416
let rootPubKey: Buffer;
1517
let userKeyShare: Buffer;
1618
let bitgoKeyShare: Buffer;
19+
let hdTree: Ed25519Bip32HdTree;
1720

1821
before(async function () {
1922
const [userDkg, , bitgoDkg] = await generateEdDsaDKGKeyShares();
2023
commonKeychain = userDkg.getCommonKeychain();
2124
rootPubKey = userDkg.getSharePublicKey();
2225
userKeyShare = userDkg.getKeyShare();
2326
bitgoKeyShare = bitgoDkg.getKeyShare();
27+
hdTree = await Ed25519Bip32HdTree.initialize();
2428
});
2529

30+
// Derives a public key from commonKeychain using the standard Ed25519 BIP32
31+
// formula (same as Eddsa.deriveUnhardened). Returns the 32-byte public key.
32+
function derivePublicKeyBip32(keychain: string, path: string): Buffer {
33+
const buf = Buffer.from(keychain, 'hex');
34+
const derived = hdTree.publicDerive(
35+
{ pk: bigIntFromBufferLE(buf.slice(0, 32)), chaincode: bigIntFromBufferBE(buf.slice(32, 64)) },
36+
path
37+
);
38+
return bigIntToBufferLE(derived.pk, 32);
39+
}
40+
2641
describe('input validation', function () {
2742
it('throws when commonKeychainHex is shorter than 128 chars', function () {
2843
assert.throws(() => deriveUnhardenedMps('deadbeef', 'm'), /expected 128 hex chars/);
@@ -63,7 +78,7 @@ describe('deriveUnhardenedMps', function () {
6378
});
6479
});
6580

66-
describe('DSG signature cross-check against the public key derived by deriveUnhardenedMps', function () {
81+
describe('DSG signature cross-check against Ed25519Bip32HdTree (deriveUnhardened)', function () {
6782
let sigAtRoot: Buffer;
6883
let sigAtM0: Buffer;
6984
let sigAtM01: Buffer;
@@ -86,19 +101,19 @@ describe('deriveUnhardenedMps', function () {
86101
assert(ed25519.verify(sigAtRoot, MESSAGE, rootPubKey), 'DSG at "m" should verify against the raw DKG public key');
87102
});
88103

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');
104+
it('signature from DSG at "m/0" verifies against Eddsa.deriveUnhardened key at "m/0"', function () {
105+
const derivedPk = derivePublicKeyBip32(commonKeychain, 'm/0');
91106
assert(
92107
ed25519.verify(sigAtM0, MESSAGE, derivedPk),
93-
'DSG at "m/0" should verify against deriveUnhardenedMps result at "m/0"'
108+
'DSG at "m/0" should verify against Eddsa.deriveUnhardened result at "m/0"'
94109
);
95110
});
96111

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');
112+
it('signature from DSG at "m/0/1" verifies against Eddsa.deriveUnhardened key at "m/0/1"', function () {
113+
const derivedPk = derivePublicKeyBip32(commonKeychain, 'm/0/1');
99114
assert(
100115
ed25519.verify(sigAtM01, MESSAGE, derivedPk),
101-
'DSG at "m/0/1" should verify against deriveUnhardenedMps result at "m/0/1"'
116+
'DSG at "m/0/1" should verify against Eddsa.deriveUnhardened result at "m/0/1"'
102117
);
103118
});
104119
});

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)