Skip to content
Merged
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
8 changes: 8 additions & 0 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export class DSG {
return this.dsgState;
}

getPartyIdx(): number {
return this.partyIdx;
}

getOtherPartyIdx(): number | null {
return this.otherPartyIdx;
}

/**
* Initialises the DSG session. The keyshare must come from a prior DKG run, and
* `otherPartyIdx` must be the single counterpart who will co-sign with this party.
Expand Down
46 changes: 46 additions & 0 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import crypto from 'crypto';
import assert from 'assert';
import { x25519 } from '@noble/curves/ed25519';
import { DKG } from './dkg';
import { DSG } from './dsg';
import { DeserializedMessages } from './types';

/**
* Concatenates multiple Uint8Array instances into a single Uint8Array
Expand Down Expand Up @@ -74,3 +76,47 @@ export async function generateEdDsaDKGKeyShares(

return [user, backup, bitgo];
}

/**
* Initializes two DSG parties and drives them through the protocol until the specified round.
*
* @param round - Round to execute until (1–3). Returns intermediate message arrays for 1–2,
* or the 64-byte Ed25519 signature Buffer for 3.
* @param party1Dsg - First DSG party (`new DSG(partyIdx)`), not yet initialized.
* @param party2Dsg - Second DSG party (`new DSG(partyIdx)`), not yet initialized.
* @param keyShare1 - Key share for the first party.
* @param keyShare2 - Key share for the second party.
* @param message - Raw message bytes to sign.
* @param derivationPath - BIP-32-style derivation path, e.g. `"m"` or `"m/0/0"`.
*/
export function executeTillRound(
round: number,
party1Dsg: DSG,
party2Dsg: DSG,
keyShare1: Buffer,
keyShare2: Buffer,
message: Buffer,
derivationPath: string
): DeserializedMessages[] | Buffer {
if (round < 1 || round > 3) {
throw Error('Invalid round number');
}
party1Dsg.initDsg(keyShare1, message, derivationPath, party2Dsg.getPartyIdx());
party2Dsg.initDsg(keyShare2, message, derivationPath, party1Dsg.getPartyIdx());
const party1Round0Message = party1Dsg.getFirstMessage();
const party2Round0Message = party2Dsg.getFirstMessage();

const [party2Round1Messages] = party2Dsg.handleIncomingMessages([party1Round0Message, party2Round0Message]);
const [party1Round1Messages] = party1Dsg.handleIncomingMessages([party1Round0Message, party2Round0Message]);
if (round === 1) return [[party1Round1Messages], [party2Round1Messages]];

const [party1Round2Messages] = party1Dsg.handleIncomingMessages([party1Round1Messages, party2Round1Messages]);
const [party2Round2Messages] = party2Dsg.handleIncomingMessages([party1Round1Messages, party2Round1Messages]);
if (round === 2) return [[party1Round2Messages], [party2Round2Messages]];

party1Dsg.handleIncomingMessages([party1Round2Messages, party2Round2Messages]);
party2Dsg.handleIncomingMessages([party1Round2Messages, party2Round2Messages]);

assert(party1Dsg.getSignature().toString('hex') === party2Dsg.getSignature().toString('hex'));
return party1Dsg.getSignature();
}
124 changes: 105 additions & 19 deletions modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'assert';
import { ed25519 } from '@noble/curves/ed25519';
import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes } from '../../../../src/tss/eddsa-mps';
import { generateEdDsaDKGKeyShares, runEdDsaDSG } from './util';
import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes, MPSUtil } from '../../../../src/tss/eddsa-mps';
import { generateEdDsaDKGKeyShares } from './util';

const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');

Expand Down Expand Up @@ -84,7 +84,9 @@ describe('EdDSA MPS DSG', function () {

describe('DSG Protocol Execution (2-of-3)', function () {
it('should complete full DSG between user (0) and bitgo (2) and produce identical signatures', function () {
const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
const dsgA = new EddsaMPSDsg.DSG(0);
const dsgB = new EddsaMPSDsg.DSG(2);
MPSUtil.executeTillRound(3, dsgA, dsgB, userKeyShare, bitgoKeyShare, MESSAGE, 'm');

assert.strictEqual(dsgA.getState(), 'Complete');
assert.strictEqual(dsgB.getState(), 'Complete');
Expand All @@ -97,17 +99,48 @@ describe('EdDSA MPS DSG', function () {
});

it('should produce a signature that verifies under the DKG public key', function () {
const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
const sig = dsgA.getSignature();
const sig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
userKeyShare,
bitgoKeyShare,
MESSAGE,
'm'
) as Buffer;

const isValid = ed25519.verify(sig, MESSAGE, dkgPubKey);
assert(isValid, 'Signature should verify under DKG public key');
});

it('should sign the same message identically across all 2-of-3 party combinations', function () {
const userBackupSig = runEdDsaDSG(userKeyShare, backupKeyShare, 0, 1, MESSAGE).dsgA.getSignature();
const backupBitgoSig = runEdDsaDSG(backupKeyShare, bitgoKeyShare, 1, 2, MESSAGE).dsgA.getSignature();
const userBitgoSig = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE).dsgA.getSignature();
const userBackupSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(1),
userKeyShare,
backupKeyShare,
MESSAGE,
'm'
) as Buffer;
const backupBitgoSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(1),
new EddsaMPSDsg.DSG(2),
backupKeyShare,
bitgoKeyShare,
MESSAGE,
'm'
) as Buffer;
const userBitgoSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
userKeyShare,
bitgoKeyShare,
MESSAGE,
'm'
) as Buffer;

// Per-session nonce randomisation means signatures across DIFFERENT signing
// sessions WILL differ. The invariant we test is that every 2-of-3 subset
Expand All @@ -121,27 +154,78 @@ describe('EdDSA MPS DSG', function () {
const shortMsg = Buffer.from([0x01]);
const longMsg = Buffer.alloc(4096, 0xab);

const { dsgA: short } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, shortMsg);
const { dsgA: long } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, longMsg);

assert(ed25519.verify(short.getSignature(), shortMsg, dkgPubKey), '1-byte message signature should verify');
assert(ed25519.verify(long.getSignature(), longMsg, dkgPubKey), '4096-byte message signature should verify');
const shortSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
userKeyShare,
bitgoKeyShare,
shortMsg,
'm'
) as Buffer;
const longSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
userKeyShare,
bitgoKeyShare,
longMsg,
'm'
) as Buffer;

assert(ed25519.verify(shortSig, shortMsg, dkgPubKey), '1-byte message signature should verify');
assert(ed25519.verify(longSig, longMsg, dkgPubKey), '4096-byte message signature should verify');
});

it('should throw when handleIncomingMessages is called after completion', function () {
const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
const dsgA = new EddsaMPSDsg.DSG(0);
MPSUtil.executeTillRound(3, dsgA, new EddsaMPSDsg.DSG(2), userKeyShare, bitgoKeyShare, MESSAGE, 'm');
assert.throws(() => dsgA.handleIncomingMessages([]), /already completed/);
});

it('should fail when parties sign different messages', function () {
const dsg1 = new EddsaMPSDsg.DSG(0);
const dsg2 = new EddsaMPSDsg.DSG(2);
dsg1.initDsg(userKeyShare, Buffer.from('MESSAGE'), 'm', 2);
dsg2.initDsg(bitgoKeyShare, Buffer.from('DIFFERENT_MESSAGE'), 'm', 0);

const r0_1 = dsg1.getFirstMessage();
const r0_2 = dsg2.getFirstMessage();

const [r1_1] = dsg1.handleIncomingMessages([r0_1, r0_2]);
const [r1_2] = dsg2.handleIncomingMessages([r0_1, r0_2]);

assert.throws(
() => dsg1.handleIncomingMessages([r1_1, r1_2]),
/Error while creating messages from party 0, round WaitMsg2: Protocol Error/
Comment thread
vibhavgo marked this conversation as resolved.
);
});
});

describe('Derivation Paths', function () {
it('should produce different signatures for different derivation paths', function () {
const { dsgA: rootSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm');
const { dsgA: derivedSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm/0/1');
const rootSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
userKeyShare,
bitgoKeyShare,
MESSAGE,
'm'
) as Buffer;
const derivedSig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
userKeyShare,
bitgoKeyShare,
MESSAGE,
'm/0/1'
) as Buffer;

assert.notStrictEqual(
rootSig.getSignature().toString('hex'),
derivedSig.getSignature().toString('hex'),
rootSig.toString('hex'),
derivedSig.toString('hex'),
'Different derivation paths should produce different signatures'
);
});
Expand Down Expand Up @@ -238,7 +322,9 @@ describe('EdDSA MPS DSG', function () {
});

it('should throw when exporting session after completion', function () {
const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
const dsgA = new EddsaMPSDsg.DSG(0);
const dsgB = new EddsaMPSDsg.DSG(2);
MPSUtil.executeTillRound(3, dsgA, dsgB, userKeyShare, bitgoKeyShare, MESSAGE, 'm');
assert.throws(() => dsgA.getSession(), /DSG session is complete\. Exporting the session is not allowed\./);
assert.throws(() => dsgB.getSession(), /DSG session is complete\. Exporting the session is not allowed\./);
});
Expand Down
113 changes: 113 additions & 0 deletions modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import assert from 'assert';
import { ed25519 } from '@noble/curves/ed25519';
import { EddsaMPSDkg, EddsaMPSDsg, MPSUtil } from '../../../../src/tss/eddsa-mps';
import { concatBytes, generateEdDsaDKGKeyShares } from '../../../../src/tss/eddsa-mps/util';

describe('EdDSA Utility Functions', function () {
Expand Down Expand Up @@ -51,4 +53,115 @@ describe('EdDSA Utility Functions', function () {
);
});
});

describe('executeTillRound', function () {
const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');

let userDkg: EddsaMPSDkg.DKG;
let keySharesByIdx: [Buffer, Buffer, Buffer];
let dkgPubKey: Buffer;

before(async function () {
const [user, backup, bitgo] = await generateEdDsaDKGKeyShares();
userDkg = user;
keySharesByIdx = [user.getKeyShare(), backup.getKeyShare(), bitgo.getKeyShare()];
dkgPubKey = user.getSharePublicKey();
});

// All three 2-of-3 signing combinations: user+backup, user+BitGo, backup+BitGo.
const PARTY_PAIRS: Array<[number, number]> = [
[0, 1],
[0, 2],
[1, 2],
];

PARTY_PAIRS.forEach(([p1, p2]) => {
it(`should produce a valid signature verifying under the DKG public key for parties ${p1}+${p2}`, function () {
const sig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(p1),
new EddsaMPSDsg.DSG(p2),
keySharesByIdx[p1],
keySharesByIdx[p2],
MESSAGE,
'm'
) as Buffer;
assert.strictEqual(sig.length, 64);
assert(ed25519.verify(sig, MESSAGE, dkgPubKey));
});
});

it('should verify round-3 signature against root public key from getCommonKeychain()', function () {
const sig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
keySharesByIdx[0],
keySharesByIdx[2],
MESSAGE,
'm'
) as Buffer;
const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex');
assert(ed25519.verify(sig, MESSAGE, rootPubKey), 'should verify under root public key from getCommonKeychain()');
});

it('should not verify under the root public key when signing at a derived path (m/0/0)', function () {
const sig = MPSUtil.executeTillRound(
3,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
keySharesByIdx[0],
keySharesByIdx[2],
MESSAGE,
'm/0/0'
) as Buffer;
assert.strictEqual(sig.length, 64, 'Derived path signature must be 64 bytes');
const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex');
assert(
!ed25519.verify(sig, MESSAGE, rootPubKey),
'derived-path signature should not verify under root public key'
);
});

it('should return message arrays (not a Buffer) for intermediate round 1', function () {
const result = MPSUtil.executeTillRound(
1,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
keySharesByIdx[0],
keySharesByIdx[2],
MESSAGE,
'm'
);
assert(!Buffer.isBuffer(result), 'round 1 should return message arrays, not a Buffer');
assert.strictEqual(result.length, 2, 'should contain message arrays for both parties');
});

it('should return message arrays (not a Buffer) for intermediate round 2', function () {
const result = MPSUtil.executeTillRound(
2,
new EddsaMPSDsg.DSG(0),
new EddsaMPSDsg.DSG(2),
keySharesByIdx[0],
keySharesByIdx[2],
MESSAGE,
'm'
);
assert(!Buffer.isBuffer(result), 'round 2 should return message arrays, not a Buffer');
assert.strictEqual(result.length, 2, 'should contain message arrays for both parties');
});
Comment thread
vibhavgo marked this conversation as resolved.

it('should throw for round out of range', function () {
const dsg1 = new EddsaMPSDsg.DSG(0);
const dsg2 = new EddsaMPSDsg.DSG(2);
assert.throws(
() => MPSUtil.executeTillRound(0, dsg1, dsg2, keySharesByIdx[0], keySharesByIdx[2], MESSAGE, 'm'),
/Invalid round number/
);
assert.throws(
() => MPSUtil.executeTillRound(4, dsg1, dsg2, keySharesByIdx[0], keySharesByIdx[2], MESSAGE, 'm'),
/Invalid round number/
);
});
});
});
Loading
Loading