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
52 changes: 51 additions & 1 deletion modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
MPCv2KeyGenStateEnum,
MPCv2PartyFromStringOrNumber,
} from '@bitgo/public-types';
import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
import * as nacl from 'tweetnacl';
import { deriveUnhardenedMps, 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 Down Expand Up @@ -985,3 +986,52 @@ export async function getEddsaMpcV2RecoveryKeySharesFromReducedKey(
commonKeyChain: userPub + userChainCode,
};
}

/**
* Sign a message for recovery using EdDSA MPCv2 (MPS) with user and backup key shares.
*
* Runs the MPS DSG protocol locally to round 3, then verifies the resulting
* 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 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
* @returns 64-byte Ed25519 signature Buffer
*/
export function signRecoveryEddsaMPCv2(
message: Buffer,
derivationPath: string,
userKeyShare: Buffer,
backupKeyShare: Buffer,
commonKeyChain: string
): Buffer {
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
const backupDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BACKUP);

const signature = MPSUtil.executeTillRound(
3,
userDsg,
backupDsg,
userKeyShare,
backupKeyShare,
message,
derivationPath
) as Buffer;

// deriveUnhardenedMps returns 128 hex chars: first 64 are the 32-byte public key
const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath);
const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex');

const verified = nacl.sign.detached.verify(
new Uint8Array(message),
new Uint8Array(signature),
new Uint8Array(publicKeyBytes)
);
if (!verified) {
throw new Error('EdDSA MPCv2 recovery signature verification failed');
}

return signature;
}
74 changes: 73 additions & 1 deletion 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,8 @@ import * as assert from 'assert';
import * as sinon from 'sinon';
import * as pgp from 'openpgp';
import { randomBytes } from 'crypto';
import { EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
import { deriveUnhardenedMps, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
import * as nacl from 'tweetnacl';
import * as sjcl from '@bitgo/sjcl';
import {
EddsaMPCv2SignatureShareRound1Input,
Expand Down Expand Up @@ -1767,3 +1768,74 @@ describe('EddsaMPCv2Utils.isEddsaMpcV1SigningMaterial', () => {
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false);
});
});

describe('signRecoveryEddsaMPCv2', () => {
const derivationPath = 'm/0/0';

it('should return a 64-byte signature that verifies against the derived public key', async () => {
const [userDkg, backupDkg] = await MPSUtil.generateEdDsaDKGKeyShares();
const message = Buffer.from('deadbeef', 'hex');
const commonKeyChain = userDkg.getCommonKeychain();

const signature = EDDSAUtils.signRecoveryEddsaMPCv2(
message,
derivationPath,
userDkg.getKeyShare(),
backupDkg.getKeyShare(),
commonKeyChain
);

assert.strictEqual(signature.length, 64);

const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath);
const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex');
const ok = nacl.sign.detached.verify(
new Uint8Array(message),
new Uint8Array(signature),
new Uint8Array(publicKeyBytes)
);
assert.strictEqual(ok, true);
});

it('should throw when the signed message is different from the verified message', async () => {
const [userDkg, backupDkg] = await MPSUtil.generateEdDsaDKGKeyShares();
const message = Buffer.from('deadbeef', 'hex');
const commonKeyChain = userDkg.getCommonKeychain();

const signature = EDDSAUtils.signRecoveryEddsaMPCv2(
message,
derivationPath,
userDkg.getKeyShare(),
backupDkg.getKeyShare(),
commonKeyChain
);

const differentMessage = Buffer.from('cafebabe', 'hex');
const derivedKeychain = deriveUnhardenedMps(commonKeyChain, derivationPath);
const publicKeyBytes = Buffer.from(derivedKeychain.slice(0, 64), 'hex');
const ok = nacl.sign.detached.verify(
new Uint8Array(differentMessage),
new Uint8Array(signature),
new Uint8Array(publicKeyBytes)
);
assert.strictEqual(ok, false);
});

it('should throw when a wrong commonKeyChain is provided (verification mismatch)', async () => {
const [userDkg, backupDkg] = await MPSUtil.generateEdDsaDKGKeyShares();
const [wrongDkg] = await MPSUtil.generateEdDsaDKGKeyShares();
const message = Buffer.from('deadbeef', 'hex');

assert.throws(
() =>
EDDSAUtils.signRecoveryEddsaMPCv2(
message,
derivationPath,
userDkg.getKeyShare(),
backupDkg.getKeyShare(),
wrongDkg.getCommonKeychain() // key chain from a different wallet
),
/EdDSA MPCv2 recovery signature verification failed/
);
});
});