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
3 changes: 1 addition & 2 deletions modules/express/test/unit/clientRoutes/externalSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
MPSUtil,
MPSComms,
MPSTypes,
deriveUnhardenedMps,
} from '@bitgo/sdk-lib-mpc';
import {
MPCv2PartyFromStringOrNumber,
Expand Down Expand Up @@ -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]);
Expand Down
11 changes: 7 additions & 4 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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));
Expand Down
26 changes: 15 additions & 11 deletions modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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),
Expand All @@ -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/
);
});
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.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",
Expand Down
16 changes: 2 additions & 14 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
28 changes: 21 additions & 7 deletions modules/sdk-lib-mpc/test/unit/tss/eddsa/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -14,15 +16,27 @@ describe('deriveUnhardenedMps', function () {
let rootPubKey: Buffer;
let userKeyShare: Buffer;
let bitgoKeyShare: Buffer;
let hdTree: Ed25519Bip32HdTree;

before(async function () {
const [userDkg, , bitgoDkg] = await generateEdDsaDKGKeyShares();
commonKeychain = userDkg.getCommonKeychain();
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/);
Expand Down Expand Up @@ -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;
Expand All @@ -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"'
);
});
});
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading