Skip to content
Draft
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
31 changes: 26 additions & 5 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { EddsaMPCv2KeyGenCallbacks } from '../../../wallet/iWallets';
import { ed25519 } from '@noble/curves/ed25519';
import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
import { KeychainsTriplet } from '../../../baseCoin';
import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
import { AddKeychainOptions, DecryptedRetrofitPayload, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys';
import { getBitgoSignatureShare, getTxRequest, sendSignatureShareV2, sendTxRequest } from '../../../tss/common';
import { decodeWithCodec } from '../../codecs';
Expand Down Expand Up @@ -63,6 +63,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
passphrase: string;
enterprise: string;
originalPasscodeEncryptionCode?: string;
retrofit?: DecryptedRetrofitPayload;
webauthnInfo?: WebauthnKeyEncryptionInfo;
encryptionVersion?: EncryptionVersion;
// Wallet Safes v1 (@experimental): tags the resulting user/backup/bitgo root keys with this safe.
Expand Down Expand Up @@ -93,8 +94,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
const bitgoPk = await MPSComms.extractEd25519PublicKey(bitgoKeyObj);

// Create DKG sessions for user (party 0) and backup (party 1)
const userDkg = new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER);
const backupDkg = new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP);
const { userDkg, backupDkg } = await this.getUserAndBackupSession(params.retrofit);

// #region round 1
await userDkg.initDkg(userSk, [backupPk, bitgoPk]);
Expand All @@ -116,6 +116,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
backupGpgPublicKey,
userMsg1: userSignedMsg1,
backupMsg1: backupSignedMsg1,
...(params.retrofit?.walletId ? { walletId: params.retrofit.walletId } : {}),
},
params.safeId
);
Expand Down Expand Up @@ -459,15 +460,15 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {

async sendKeyGenerationRound1(
enterprise: string,
payload: EddsaMPCv2KeyGenRound1Request,
payload: EddsaMPCv2KeyGenRound1Request & { walletId?: string },
safeId?: string
): Promise<EddsaMPCv2KeyGenRound1Response> {
return this.sendKeyGenerationRound1BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise, safeId), payload);
}

async sendKeyGenerationRound1BySender(
senderFn: EddsaMPCv2KeyGenSendFn<EddsaMPCv2KeyGenRound1Response>,
payload: EddsaMPCv2KeyGenRound1Request
payload: EddsaMPCv2KeyGenRound1Request & { walletId?: string }
): Promise<EddsaMPCv2KeyGenRound1Response> {
return senderFn(MPCv2KeyGenStateEnum['MPCv2-R1'], payload);
}
Expand Down Expand Up @@ -1070,6 +1071,26 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {

// #region retrofit

private async getUserAndBackupSession(retrofit?: DecryptedRetrofitPayload): Promise<{
userDkg: EddsaMPSDkg.DKG;
backupDkg: EddsaMPSDkg.DKG;
}> {
if (retrofit) {
const { userRetrofitData, backupRetrofitData } = await this.getMpcV2RetrofitDataFromMpcV1Keys({
mpcv1UserKeyShare: retrofit.decryptedUserKey,
mpcv1BackupKeyShare: retrofit.decryptedBackupKey,
});
return {
userDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER, userRetrofitData),
backupDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP, backupRetrofitData),
};
}
return {
userDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER),
backupDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP),
};
}

async getMpcV2RetrofitDataFromMpcV1Keys(params: { mpcv1UserKeyShare: string; mpcv1BackupKeyShare: string }): Promise<{
userRetrofitData: MPSTypes.EddsaRetrofitData;
backupRetrofitData: MPSTypes.EddsaRetrofitData;
Expand Down
146 changes: 146 additions & 0 deletions modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2467,3 +2467,149 @@ describe('EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Keys', () => {
);
});
});

describe('EddsaMPCv2Utils.getUserAndBackupSession', () => {
let utils: EddsaMPCv2Utils;
let userSigningMaterial: Record<string, unknown>;
let backupSigningMaterial: Record<string, unknown>;

before(async () => {
const MPC = await getInitializedMpcInstance();
const user = MPC.keyShare(1, 2, 3);
const backup = MPC.keyShare(2, 2, 3);
const bitgo = MPC.keyShare(3, 2, 3);
userSigningMaterial = {
uShare: user.uShare,
bitgoYShare: bitgo.yShares[1],
backupYShare: backup.yShares[1],
};
backupSigningMaterial = {
uShare: backup.uShare,
bitgoYShare: bitgo.yShares[2],
userYShare: user.yShares[2],
};
});

beforeEach(() => {
const mockBitGo = {} as unknown as BitGoBase;
const mockCoin = {} as unknown as IBaseCoin;
utils = new EddsaMPCv2Utils(mockBitGo, mockCoin);
});

afterEach(() => {
sinon.restore();
});

it('returns plain DKG sessions when retrofit is undefined', async () => {
const { userDkg, backupDkg } = await (utils as any).getUserAndBackupSession(undefined);
assert.ok(userDkg, 'user DKG should be created');
assert.ok(backupDkg, 'backup DKG should be created');
});

it('returns retrofit-seeded DKG sessions when retrofit payload is supplied', async () => {
const retrofit = {
decryptedUserKey: JSON.stringify(userSigningMaterial),
decryptedBackupKey: JSON.stringify(backupSigningMaterial),
walletId: 'wallet-123',
};
const { userDkg, backupDkg } = await (utils as any).getUserAndBackupSession(retrofit);
assert.ok(userDkg, 'user DKG should be created with retrofit data');
assert.ok(backupDkg, 'backup DKG should be created with retrofit data');
});
});

describe('EddsaMPCv2Utils.createKeychains with retrofit wiring', () => {
let utils: EddsaMPCv2Utils;
let userSigningMaterial: Record<string, unknown>;
let backupSigningMaterial: Record<string, unknown>;
let bitgoGpgPublicKeyArmored: string;
const enterprise = 'enterprise-id';
const sessionId = 'session-001';
const walletId = 'wallet-retrofit-123';

before(async () => {
const MPC = await getInitializedMpcInstance();
const user = MPC.keyShare(1, 2, 3);
const backup = MPC.keyShare(2, 2, 3);
const bitgo = MPC.keyShare(3, 2, 3);
userSigningMaterial = {
uShare: user.uShare,
bitgoYShare: bitgo.yShares[1],
backupYShare: backup.yShares[1],
};
backupSigningMaterial = {
uShare: backup.uShare,
bitgoYShare: bitgo.yShares[2],
userYShare: user.yShares[2],
};
// Generate a real Ed25519 GPG key to stand in for the BitGo GPG key
const bitgoGpgKeyPair = await generateGPGKeyPair('ed25519');
bitgoGpgPublicKeyArmored = bitgoGpgKeyPair.publicKey;
});

beforeEach(() => {
const mockBitGo = {
getEnv: sinon.stub().returns('dev'),
encrypt: sinon.stub().resolves('encrypted'),
} as any;
const mockKeychains = {
add: sinon
.stub()
.callsFake((params: any) =>
Promise.resolve({ id: `${params.source}-key-id`, commonKeychain: 'a'.repeat(128), isMPCv2: true })
),
};
const mockCoin = {
keychains: sinon.stub().returns(mockKeychains),
} as any;

utils = new EddsaMPCv2Utils(mockBitGo, mockCoin);
sinon.stub(utils, 'getBitgoGpgPubkeyBasedOnFeatureFlags' as any).resolves({ eddsaMpcv2PublicKey: null });
// Use a real armored GPG public key so pgp.readKey() succeeds inside createKeychains
(utils as any).bitgoEddsaMpcv2PublicGpgKey = { armor: () => bitgoGpgPublicKeyArmored };
sinon.stub(utils as any, 'addBitgoKeychain').resolves({ id: 'bitgo-key-id', commonKeychain: 'a'.repeat(128) });
});

afterEach(() => {
sinon.restore();
});

it('spreads walletId into round-1 payload when retrofit is provided', async () => {
const capturedPayloads: any[] = [];
sinon.stub(utils, 'sendKeyGenerationRound1').callsFake(async (_enterprise: string, payload: any) => {
capturedPayloads.push(payload);
// Return a bad bitgoMsg1 to short-circuit the ceremony after R1 capture
return { sessionId: sessionId as any, bitgoMsg1: { message: '', signature: '' } as any };
});

const retrofit = {
decryptedUserKey: JSON.stringify(userSigningMaterial),
decryptedBackupKey: JSON.stringify(backupSigningMaterial),
walletId,
};

await assert.rejects(
() => utils.createKeychains({ passphrase: 'test', enterprise, retrofit }),
() => true
);

assert.strictEqual(capturedPayloads.length, 1, 'sendKeyGenerationRound1 should be called once');
assert.strictEqual(capturedPayloads[0].walletId, walletId, 'walletId must be present in round-1 payload');
});

it('omits walletId from round-1 payload when retrofit is absent', async () => {
const capturedPayloads: any[] = [];
sinon.stub(utils, 'sendKeyGenerationRound1').callsFake(async (_enterprise: string, payload: any) => {
capturedPayloads.push(payload);
return { sessionId: sessionId as any, bitgoMsg1: { message: '', signature: '' } as any };
});

await assert.rejects(
() => utils.createKeychains({ passphrase: 'test', enterprise }),
() => true
);

assert.strictEqual(capturedPayloads.length, 1, 'sendKeyGenerationRound1 should be called once');
assert.strictEqual(capturedPayloads[0].walletId, undefined, 'walletId must be absent when no retrofit');
});
});
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.11.0",
"@bitgo/wasm-mps": "1.12.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
24 changes: 21 additions & 3 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MsgState, Share } from '@bitgo/wasm-mps';
import { encode } from 'cbor-x';
import crypto from 'crypto';
import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeyShare } from './types';
import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeyShare, EddsaRetrofitData } from './types';

type NodeWasmer = typeof import('@bitgo/wasm-mps');
type WebWasmer = typeof import('@bitgo/wasm-mps/web');
Expand Down Expand Up @@ -44,13 +44,16 @@ export class DKG {
private shareChaincode: Buffer | null = null;
/** Lazily loaded WASM module */
private wasmMps: WasmMps | null = null;
/** Optional MPCv1 retrofit data; when set, round0 uses ed25519_dkg_round0_import */
private retrofitData: EddsaRetrofitData | undefined;

protected dkgState: DkgState = DkgState.Uninitialized;

constructor(n: number, t: number, partyIdx: number) {
constructor(n: number, t: number, partyIdx: number, retrofitData?: EddsaRetrofitData) {
this.n = n;
this.t = t;
this.partyIdx = partyIdx;
this.retrofitData = retrofitData;
}

private async loadWasmMps(): Promise<void> {
Expand Down Expand Up @@ -124,13 +127,26 @@ export class DKG {
const wasm = this.getWasmMps();
let result: MsgState;
try {
result = wasm.ed25519_dkg_round0_process(this.partyIdx, this.decryptionKey!, this.otherPubKeys!, seed);
if (this.retrofitData) {
result = wasm.ed25519_dkg_round0_import(
this.partyIdx,
this.decryptionKey!,
this.otherPubKeys!,
Buffer.from(this.retrofitData.s_i_0, 'hex'),
Buffer.from(this.retrofitData.expectedPk, 'hex'),
Buffer.from(this.retrofitData.chainCode, 'hex')
);
} else {
result = wasm.ed25519_dkg_round0_process(this.partyIdx, this.decryptionKey!, this.otherPubKeys!, seed);
}
} catch (err) {
throw new Error(`Error while creating the first message from party ${this.partyIdx}: ${err}`);
}

this.dkgStateBytes = Buffer.from(result.state);
this.dkgState = DkgState.WaitMsg1;
// Clear retrofit key material once consumed — it is not needed after round 0
this.retrofitData = undefined;
return { payload: new Uint8Array(result.msg), from: this.partyIdx };
}

Expand Down Expand Up @@ -267,6 +283,7 @@ export class DKG {
dkgRound: this.dkgState,
decryptionKey: this.decryptionKey?.toString('base64') ?? null,
otherPubKeys: this.otherPubKeys?.map((k) => k.toString('base64')) ?? null,
retrofitData: this.retrofitData,
});
}

Expand All @@ -280,5 +297,6 @@ export class DKG {
this.dkgState = data.dkgRound;
this.decryptionKey = data.decryptionKey ? Buffer.from(data.decryptionKey, 'base64') : null;
this.otherPubKeys = data.otherPubKeys ? (data.otherPubKeys as string[]).map((k) => Buffer.from(k, 'base64')) : null;
this.retrofitData = data.retrofitData ?? undefined;
}
}
1 change: 1 addition & 0 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * as EddsaMPSDsg from './dsg';
export * as MPSUtil from './util';
export * as MPSTypes from './types';
export * as MPSComms from './commsLayer';
export type { EddsaRetrofitData } from './types';
13 changes: 8 additions & 5 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'assert';
import { x25519 } from '@noble/curves/ed25519';
import { DKG } from './dkg';
import { DSG } from './dsg';
import { DeserializedMessages } from './types';
import { DeserializedMessages, EddsaRetrofitData } from './types';

/**
* Concatenates multiple Uint8Array instances into a single Uint8Array
Expand Down Expand Up @@ -40,15 +40,18 @@ function validateSeed(seed?: EdDsaDKGPartySeed): EdDsaDKGPartySeed {
export async function generateEdDsaDKGKeyShares(
seedUser?: EdDsaDKGPartySeed,
seedBackup?: EdDsaDKGPartySeed,
seedBitgo?: EdDsaDKGPartySeed
seedBitgo?: EdDsaDKGPartySeed,
retrofitUser?: EddsaRetrofitData,
retrofitBackup?: EddsaRetrofitData,
retrofitBitgo?: EddsaRetrofitData
): Promise<[DKG, DKG, DKG]> {
const { encKey: userEncKey, dkgSeed: userDkgSeed } = validateSeed(seedUser);
const { encKey: backupEncKey, dkgSeed: backupDkgSeed } = validateSeed(seedBackup);
const { encKey: bitgoEncKey, dkgSeed: bitgoDkgSeed } = validateSeed(seedBitgo);

const user = new DKG(3, 2, 0);
const backup = new DKG(3, 2, 1);
const bitgo = new DKG(3, 2, 2);
const user = new DKG(3, 2, 0, retrofitUser);
const backup = new DKG(3, 2, 1, retrofitBackup);
const bitgo = new DKG(3, 2, 2, retrofitBitgo);

const userKP = generateX25519Keypair(userEncKey);
const backupKP = generateX25519Keypair(backupEncKey);
Expand Down
Loading