Skip to content

Commit 190b1d0

Browse files
Merge pull request #8496 from BitGo/wal-392-fix-dkg-retrofit-session-id
fix(sdk-lib-mpc): derive final_session_id deterministically in DKG retrofit
2 parents dd8559a + d614055 commit 190b1d0

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { KeygenSession, Keyshare, Message } from '@silencelaboratories/dkls-wasm-ll-node';
22
import { decode, encode } from 'cbor-x';
3+
import { createHash } from 'crypto';
34
import { Secp256k1Curve } from '../../curves';
45
import { bigIntToBufferBE } from '../../util';
56
import { DeserializedBroadcastMessage, DeserializedMessages, DkgState, ReducedKeyShare, RetrofitData } from './types';
@@ -86,7 +87,12 @@ export class Dkg {
8687
party_id: this.partyIdx,
8788
public_key: Array.from(Buffer.from(this.retrofitData.xShare.y, 'hex')),
8889
root_chain_code: Array.from(Buffer.from(this.retrofitData.xShare.chaincode, 'hex')),
89-
final_session_id: Array(32).fill(0),
90+
final_session_id: Array.from(
91+
createHash('sha256')
92+
.update(Buffer.from(this.retrofitData.xShare.y, 'hex'))
93+
.update(Buffer.from(this.retrofitData.xShare.chaincode, 'hex'))
94+
.digest()
95+
),
9096
seed_ot_receivers: new Array(this.n - 1).fill(Array(32832).fill(0)),
9197
seed_ot_senders: new Array(this.n - 1).fill(Array(32768).fill(0)),
9298
sent_seed_list: [Array(32).fill(0)],

modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsDkg.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as fixtures from './fixtures/mpcv1shares';
1616
import * as openpgp from 'openpgp';
1717
import { decode } from 'cbor-x';
1818
import { generate2of2KeyShares, generateDKGKeyShares } from '../../../../src/tss/ecdsa-dkls/util';
19+
import { createHash } from 'crypto';
1920

2021
describe('DKLS Dkg 2x3', function () {
2122
it(`should create key shares`, async function () {
@@ -112,6 +113,153 @@ describe('DKLS Dkg 2x3', function () {
112113
assert.deepEqual(bKeyCombine.xShare.y, Buffer.from(decode(backupKeyShare).public_key).toString('hex'));
113114
});
114115

116+
it(`should create retrofit key shares with non-zero final_session_id`, async function () {
117+
const aKeyCombine = {
118+
xShare: fixtures.mockDKeyShare.xShare,
119+
};
120+
const bKeyCombine = {
121+
xShare: fixtures.mockEKeyShare.xShare,
122+
};
123+
const cKeyCombine = {
124+
xShare: fixtures.mockFKeyShare.xShare,
125+
};
126+
const retrofitDataA: RetrofitData = {
127+
xShare: aKeyCombine.xShare,
128+
};
129+
const retrofitDataB: RetrofitData = {
130+
xShare: bKeyCombine.xShare,
131+
};
132+
const retrofitDataC: RetrofitData = {
133+
xShare: cKeyCombine.xShare,
134+
};
135+
const [user] = await generateDKGKeyShares(retrofitDataA, retrofitDataB, retrofitDataC);
136+
137+
const userKeyShare = user.getKeyShare();
138+
const decodedKeyShare = decode(userKeyShare);
139+
const finalSessionId = decodedKeyShare.final_session_id;
140+
141+
// Assert final_session_id is NOT all zeros
142+
assert(!finalSessionId.every((byte: number) => byte === 0), 'final_session_id should not be all zeros');
143+
});
144+
145+
it(`should create retrofit key shares with 32-byte final_session_id`, async function () {
146+
const aKeyCombine = {
147+
xShare: fixtures.mockDKeyShare.xShare,
148+
};
149+
const bKeyCombine = {
150+
xShare: fixtures.mockEKeyShare.xShare,
151+
};
152+
const cKeyCombine = {
153+
xShare: fixtures.mockFKeyShare.xShare,
154+
};
155+
const retrofitDataA: RetrofitData = {
156+
xShare: aKeyCombine.xShare,
157+
};
158+
const retrofitDataB: RetrofitData = {
159+
xShare: bKeyCombine.xShare,
160+
};
161+
const retrofitDataC: RetrofitData = {
162+
xShare: cKeyCombine.xShare,
163+
};
164+
const [user] = await generateDKGKeyShares(retrofitDataA, retrofitDataB, retrofitDataC);
165+
166+
const userKeyShare = user.getKeyShare();
167+
const decodedKeyShare = decode(userKeyShare);
168+
const finalSessionId = decodedKeyShare.final_session_id;
169+
170+
// Assert final_session_id is exactly 32 bytes
171+
assert.strictEqual(finalSessionId.length, 32, 'final_session_id must be 32 bytes');
172+
});
173+
174+
it(`should produce deterministic final_session_id for same retrofit inputs`, async function () {
175+
const aKeyCombine = {
176+
xShare: fixtures.mockDKeyShare.xShare,
177+
};
178+
const retrofitDataA: RetrofitData = {
179+
xShare: aKeyCombine.xShare,
180+
};
181+
182+
// Test the INPUT keyshare (before WASM protocol), not the output
183+
// Create first Dkg instance and call _createDKLsRetrofitKeyShare
184+
const dkg1 = new DklsDkg.Dkg(3, 2, 0, undefined, retrofitDataA);
185+
await (dkg1 as any).loadDklsWasm();
186+
(dkg1 as any)._createDKLsRetrofitKeyShare();
187+
const keyshareObj1 = (dkg1 as any).dklsKeyShareRetrofitObject;
188+
const decoded1 = decode(keyshareObj1.toBytes());
189+
const finalSessionId1 = decoded1.final_session_id;
190+
191+
// Create second Dkg instance with same retrofit data
192+
const dkg2 = new DklsDkg.Dkg(3, 2, 0, undefined, retrofitDataA);
193+
await (dkg2 as any).loadDklsWasm();
194+
(dkg2 as any)._createDKLsRetrofitKeyShare();
195+
const keyshareObj2 = (dkg2 as any).dklsKeyShareRetrofitObject;
196+
const decoded2 = decode(keyshareObj2.toBytes());
197+
const finalSessionId2 = decoded2.final_session_id;
198+
199+
// Assert both runs produce identical final_session_id
200+
assert.deepEqual(finalSessionId1, finalSessionId2, 'final_session_id should be deterministic for same inputs');
201+
});
202+
203+
it(`should derive final_session_id as sha256(public_key || chaincode)`, async function () {
204+
const aKeyCombine = {
205+
xShare: fixtures.mockDKeyShare.xShare,
206+
};
207+
const retrofitDataA: RetrofitData = {
208+
xShare: aKeyCombine.xShare,
209+
};
210+
211+
// Test the INPUT keyshare (before WASM protocol), not the output
212+
const dkg = new DklsDkg.Dkg(3, 2, 0, undefined, retrofitDataA);
213+
await (dkg as any).loadDklsWasm();
214+
(dkg as any)._createDKLsRetrofitKeyShare();
215+
const keyshareObj = (dkg as any).dklsKeyShareRetrofitObject;
216+
const decoded = decode(keyshareObj.toBytes());
217+
const finalSessionId = decoded.final_session_id;
218+
219+
// Compute expected final_session_id: sha256(public_key_bytes || chaincode_bytes)
220+
const publicKeyBuffer = Buffer.from(aKeyCombine.xShare.y, 'hex');
221+
const chaincodeBuffer = Buffer.from(aKeyCombine.xShare.chaincode, 'hex');
222+
const expectedHash = Array.from(createHash('sha256').update(publicKeyBuffer).update(chaincodeBuffer).digest());
223+
224+
// Assert actual final_session_id matches the computed hash
225+
assert.deepEqual(finalSessionId, expectedHash, 'final_session_id should be sha256(public_key || chaincode)');
226+
});
227+
228+
it(`should produce the same final_session_id for all parties in a retrofit`, async function () {
229+
const aKeyCombine = {
230+
xShare: fixtures.mockDKeyShare.xShare,
231+
};
232+
const bKeyCombine = {
233+
xShare: fixtures.mockEKeyShare.xShare,
234+
};
235+
const cKeyCombine = {
236+
xShare: fixtures.mockFKeyShare.xShare,
237+
};
238+
const retrofitDataA: RetrofitData = {
239+
xShare: aKeyCombine.xShare,
240+
};
241+
const retrofitDataB: RetrofitData = {
242+
xShare: bKeyCombine.xShare,
243+
};
244+
const retrofitDataC: RetrofitData = {
245+
xShare: cKeyCombine.xShare,
246+
};
247+
const [user, backup, bitgo] = await generateDKGKeyShares(retrofitDataA, retrofitDataB, retrofitDataC);
248+
249+
const userKeyShare = user.getKeyShare();
250+
const backupKeyShare = backup.getKeyShare();
251+
const bitgoKeyShare = bitgo.getKeyShare();
252+
253+
const userFinalSessionId = decode(userKeyShare).final_session_id;
254+
const backupFinalSessionId = decode(backupKeyShare).final_session_id;
255+
const bitgoFinalSessionId = decode(bitgoKeyShare).final_session_id;
256+
257+
// Assert all parties have the same final_session_id
258+
assert.deepEqual(userFinalSessionId, backupFinalSessionId, 'user and backup final_session_id should match');
259+
assert.deepEqual(backupFinalSessionId, bitgoFinalSessionId, 'backup and bitgo final_session_id should match');
260+
assert.deepEqual(userFinalSessionId, bitgoFinalSessionId, 'user and bitgo final_session_id should match');
261+
});
262+
115263
it(`should create key shares with authenticated encryption`, async function () {
116264
const user = new DklsDkg.Dkg(3, 2, 0);
117265
const backup = new DklsDkg.Dkg(3, 2, 1);

0 commit comments

Comments
 (0)