diff --git a/modules/express/test/unit/clientRoutes/externalSign.ts b/modules/express/test/unit/clientRoutes/externalSign.ts index 0bffdab458..977a9b1fea 100644 --- a/modules/express/test/unit/clientRoutes/externalSign.ts +++ b/modules/express/test/unit/clientRoutes/externalSign.ts @@ -33,7 +33,6 @@ import { MPSUtil, MPSComms, MPSTypes, - deriveUnhardenedMps, } from '@bitgo/sdk-lib-mpc'; import { MPCv2PartyFromStringOrNumber, @@ -1224,7 +1223,7 @@ describe('External signer', () => { // legacy EdDSA v1 test (MPC.verify) and ECDSA MPCv2 test (DklsUtils.verifyAndConvert...). // Uses Node.js built-in crypto — no extra npm dependency needed. const signature = bitgoDsg.getSignature(); - const derivedKeychainHex = deriveUnhardenedMps(userDkg.getCommonKeychain(), derivationPath); + const derivedKeychainHex = MPC.deriveUnhardened(userDkg.getCommonKeychain(), derivationPath); const derivedPubKeyBytes = Buffer.from(derivedKeychainHex.slice(0, 64), 'hex'); // Ed25519 SubjectPublicKeyInfo DER header: SEQUENCE { SEQUENCE { OID 1.3.101.112 } BIT STRING } const spkiDer = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), derivedPubKeyBytes]); diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index d0bb886e2b..473e3b1549 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -13,7 +13,7 @@ import { MPCv2PartyFromStringOrNumber, } from '@bitgo/public-types'; import { ed25519 } from '@noble/curves/ed25519'; -import { deriveUnhardenedMps, EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; +import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; import { KeychainsTriplet } from '../../../baseCoin'; import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain'; import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys'; @@ -26,6 +26,7 @@ import { verifyPeerMessageRoundOne, verifyPeerMessageRoundTwo, } from '../../../tss/eddsa/eddsaMPCv2'; +import { getInitializedMpcInstance } from '../../../tss/eddsa/eddsa'; import { generateGPGKeyPair } from '../../opengpgUtils'; import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2'; import { @@ -1015,7 +1016,7 @@ export async function getEddsaMpcV2RecoveryKeySharesFromReducedKey( * Ed25519 signature against the public key derived from the common keychain. * * @param message raw bytes to sign - * @param derivationPath BIP-32-style derivation path, e.g. `"m/0/0"` + * @param derivationPath BIP-32-style derivation path, e.g. `"m/0"` * @param userKeyShare opaque MPS signing key-share bytes for the user party * @param backupKeyShare opaque MPS signing key-share bytes for the backup party * @param commonKeyChain 128-hex-char string: 32-byte pub + 32-byte rootChainCode @@ -1041,8 +1042,10 @@ export async function signRecoveryEddsaMPCv2( derivationPath )) as Buffer; - // deriveUnhardenedMps returns 128 hex chars: first 64 are the 32-byte public key - const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath); + // Use Eddsa.deriveUnhardened (BIP32-Ed25519), which matches what DSG uses + // internally for path derivation since wasm-mps 1.9.0. + const mpc = await getInitializedMpcInstance(); + const derivedKeychain = mpc.deriveUnhardened(commonKeyChain, derivationPath); const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex'); const verified = ed25519.verify(new Uint8Array(signature), new Uint8Array(message), new Uint8Array(publicKeyBytes)); diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index b115f3b8c8..4fe1e2a48b 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as pgp from 'openpgp'; import { randomBytes } from 'crypto'; -import { deriveUnhardenedMps, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; +import { EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; import { ed25519 } from '@noble/curves/ed25519'; import * as sjcl from '@bitgo/sjcl'; import { @@ -34,6 +34,7 @@ import { verifyPeerMessageRoundOne, verifyPeerMessageRoundTwo, } from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2'; +import { getInitializedMpcInstance } from '../../../../../../src/bitgo/tss/eddsa/eddsa'; import { getBitgoSignatureShare } from '../../../../../../src/bitgo/tss/common'; import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs'; import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils'; @@ -1792,13 +1793,14 @@ describe('signRecoveryEddsaMPCv2', () => { assert.strictEqual(signature.length, 64); - const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath); + const mpc = await getInitializedMpcInstance(); + const derivedKeychain = mpc.deriveUnhardened(commonKeyChain, derivationPath); const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex'); const ok = ed25519.verify(new Uint8Array(signature), new Uint8Array(message), new Uint8Array(publicKeyBytes)); assert.strictEqual(ok, true); }); - it('should throw when the signed message is different from the verified message', async () => { + it('should return false when verifying the signature against a different message', async () => { const [userDkg, backupDkg] = await MPSUtil.generateEdDsaDKGKeyShares(); const message = Buffer.from('deadbeef', 'hex'); const commonKeyChain = userDkg.getCommonKeychain(); @@ -1812,7 +1814,8 @@ describe('signRecoveryEddsaMPCv2', () => { ); const differentMessage = Buffer.from('cafebabe', 'hex'); - const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath); + const mpc = await getInitializedMpcInstance(); + const derivedKeychain = mpc.deriveUnhardened(commonKeyChain, derivationPath); const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex'); const ok = ed25519.verify( new Uint8Array(signature), @@ -1828,13 +1831,14 @@ describe('signRecoveryEddsaMPCv2', () => { const message = Buffer.from('deadbeef', 'hex'); await assert.rejects( - EDDSAUtils.signRecoveryEddsaMPCv2( - message, - derivationPath, - userDkg.getKeyShare(), - backupDkg.getKeyShare(), - wrongDkg.getCommonKeychain() // key chain from a different wallet - ), + () => + EDDSAUtils.signRecoveryEddsaMPCv2( + message, + derivationPath, + userDkg.getKeyShare(), + backupDkg.getKeyShare(), + wrongDkg.getCommonKeychain() // key chain from a different wallet + ), /EdDSA MPCv2 recovery signature verification failed/ ); }); diff --git a/modules/sdk-lib-mpc/package.json b/modules/sdk-lib-mpc/package.json index 4029b9a751..bd2b744f6d 100644 --- a/modules/sdk-lib-mpc/package.json +++ b/modules/sdk-lib-mpc/package.json @@ -36,7 +36,7 @@ ] }, "dependencies": { - "@bitgo/wasm-mps": "1.8.1", + "@bitgo/wasm-mps": "1.10.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", diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/derive.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/derive.ts index e88df3d8c4..f15dc83b3f 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/derive.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/derive.ts @@ -3,20 +3,8 @@ import { ed25519 } from '@noble/curves/ed25519'; import { pathToIndices } from '../../curves/util'; /** - * Derives a child public key from a common keychain using the Silence Labs - * BIP32-Ed25519 non-hardened derivation formula: - * - * HMAC = HMAC-SHA512(key=chaincode, data=pk_bytes || index_BE_4) - * child_pk = parent_pk + 8 * LE(trunc28(HMAC_left)) * G - * child_chaincode = HMAC_right (right 32 bytes) - * - * This differs from the Cardano BIP32-Ed25519 formula used by - * `Eddsa.deriveUnhardened` in three ways: no 0x02 prefix byte, big-endian - * index, and a single HMAC instead of two. The formulas produce completely - * different child keys at every derived level. - * - * Returns the same on-the-wire format as `Eddsa.deriveUnhardened`: - * 128-char hex = 64-char derived pk + 64-char derived chaincode + * @deprecated Use `Eddsa.deriveUnhardened` instead. wasm-mps >=1.9.0 uses + * standard BIP32-Ed25519 (the Cardano formula) for DSG path derivation. */ export function deriveUnhardenedMps(commonKeychainHex: string, path: string): string { if (commonKeychainHex.length !== 128) { diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/derive.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/derive.ts index 0fa8a43c13..dc8c17a59b 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/derive.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/derive.ts @@ -2,6 +2,8 @@ import assert from 'assert'; import { ed25519 } from '@noble/curves/ed25519'; import { EddsaMPSDsg, MPSUtil } from '../../../../src/tss/eddsa-mps'; import { deriveUnhardenedMps } from '../../../../src/tss/eddsa-mps/derive'; +import { Ed25519Bip32HdTree } from '../../../../src/curves/ed25519Bip32HdTree'; +import { bigIntFromBufferBE, bigIntFromBufferLE, bigIntToBufferLE } from '../../../../src/util'; import { generateEdDsaDKGKeyShares } from './util'; const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'); @@ -14,6 +16,7 @@ describe('deriveUnhardenedMps', function () { let rootPubKey: Buffer; let userKeyShare: Buffer; let bitgoKeyShare: Buffer; + let hdTree: Ed25519Bip32HdTree; before(async function () { const [userDkg, , bitgoDkg] = await generateEdDsaDKGKeyShares(); @@ -21,8 +24,19 @@ describe('deriveUnhardenedMps', function () { rootPubKey = userDkg.getSharePublicKey(); userKeyShare = userDkg.getKeyShare(); bitgoKeyShare = bitgoDkg.getKeyShare(); + hdTree = await Ed25519Bip32HdTree.initialize(); }); + // Local mock of Eddsa.deriveUnhardened (BIP32-Ed25519 dual-HMAC formula). + function deriveUnhardened(keychain: string, path: string): Buffer { + const buf = Buffer.from(keychain, 'hex'); + const derived = hdTree.publicDerive( + { pk: bigIntFromBufferLE(buf.slice(0, 32)), chaincode: bigIntFromBufferBE(buf.slice(32, 64)) }, + path + ); + return bigIntToBufferLE(derived.pk, 32); + } + describe('input validation', function () { it('throws when commonKeychainHex is shorter than 128 chars', function () { assert.throws(() => deriveUnhardenedMps('deadbeef', 'm'), /expected 128 hex chars/); @@ -63,7 +77,7 @@ describe('deriveUnhardenedMps', function () { }); }); - describe('DSG signature cross-check against the public key derived by deriveUnhardenedMps', function () { + describe('DSG signature cross-check against Ed25519Bip32HdTree (deriveUnhardened)', function () { let sigAtRoot: Buffer; let sigAtM0: Buffer; let sigAtM01: Buffer; @@ -86,19 +100,19 @@ describe('deriveUnhardenedMps', function () { assert(ed25519.verify(sigAtRoot, MESSAGE, rootPubKey), 'DSG at "m" should verify against the raw DKG public key'); }); - it('signature from DSG at "m/0" verifies against deriveUnhardenedMps(commonKeychain, "m/0")', function () { - const derivedPk = Buffer.from(deriveUnhardenedMps(commonKeychain, 'm/0').slice(0, 64), 'hex'); + it('signature from DSG at "m/0" verifies against Eddsa.deriveUnhardened key at "m/0"', function () { + const derivedPk = deriveUnhardened(commonKeychain, 'm/0'); assert( ed25519.verify(sigAtM0, MESSAGE, derivedPk), - 'DSG at "m/0" should verify against deriveUnhardenedMps result at "m/0"' + 'DSG at "m/0" should verify against Eddsa.deriveUnhardened result at "m/0"' ); }); - it('signature from DSG at "m/0/1" verifies against deriveUnhardenedMps(commonKeychain, "m/0/1")', function () { - const derivedPk = Buffer.from(deriveUnhardenedMps(commonKeychain, 'm/0/1').slice(0, 64), 'hex'); + it('signature from DSG at "m/0/1" verifies against Eddsa.deriveUnhardened key at "m/0/1"', function () { + const derivedPk = deriveUnhardened(commonKeychain, 'm/0/1'); assert( ed25519.verify(sigAtM01, MESSAGE, derivedPk), - 'DSG at "m/0/1" should verify against deriveUnhardenedMps result at "m/0/1"' + 'DSG at "m/0/1" should verify against Eddsa.deriveUnhardened result at "m/0/1"' ); }); }); diff --git a/yarn.lock b/yarn.lock index 4c021ec4d1..cbfc4a7046 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,10 +1013,10 @@ resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz" integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ== -"@bitgo/wasm-mps@1.8.1": - version "1.8.1" - resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.8.1.tgz#946673f5845696cdcf744f8122fd1fc2be3edce1" - integrity sha512-CV8EXYc1BGYtXdCRDxJ5h04nj/LpMgu3VlkfowlodI6UKcj1zotAvk4OMIdgiPPbKVr1l+xibHDXZYx/uf3rnw== +"@bitgo/wasm-mps@1.10.0": + version "1.10.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.10.0.tgz#df6a056247ce04c7d92369d257b659876e03261d" + integrity sha512-f42sMCyqqlaId3AtcvdpOfR+mOjAyVopCxCCAqW7wcTAQ8ZBS9rMGQIzTMiqFmZDBMWsAe+QHWA6XseIuzTVdQ== "@bitgo/wasm-solana@^2.6.0": version "2.6.0"