Skip to content

Commit

Permalink
feat(express): implement EdDSA commitments for external signer
Browse files Browse the repository at this point in the history
implemented EdDSA commitment step for external signer added unit test

WP-94
  • Loading branch information
alebusse committed Jun 27, 2023
1 parent 4287d38 commit 2a38dba
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 136 deletions.
45 changes: 37 additions & 8 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
UnsupportedCoinError,
GShare,
SignShare,
YShare,
CustomCommitmentGeneratingFunction,
CommitmentShareRecord,
EncryptedSignerShareRecord,
} from '@bitgo/sdk-core';
import { BitGo, BitGoOptions, Coin, CustomSigningFunction, SignedTransaction, SignedTransactionRequest } from 'bitgo';
import * as bodyParser from 'body-parser';
Expand Down Expand Up @@ -409,13 +411,17 @@ export async function handleV2GenerateShareTSS(req: express.Request): Promise<an
const coin = bitgo.coin(req.params.coin);
const eddUtils = new EddsaUtils(bitgo, coin);
req.body.prv = privKey;
req.body.walletPassphrase = walletPw;
try {
if (req.params.sharetype == 'R') {
return await eddUtils.createRShareFromTxRequest(req.body);
} else if (req.params.sharetype == 'G') {
return await eddUtils.createGShareFromTxRequest(req.body);
} else {
throw new Error('Share type not supported, only G and R share generation is supported.');
switch (req.params.sharetype) {
case 'commitment':
return await eddUtils.createCommitmentShareFromTxRequest(req.body);
case 'R':
return await eddUtils.createRShareFromTxRequest(req.body);
case 'G':
return await eddUtils.createGShareFromTxRequest(req.body);
default:
throw new Error('Share type not supported, only commitment, G and R share generation is supported.');
}
} catch (error) {
console.error('error while signing wallet transaction ', error);
Expand Down Expand Up @@ -788,6 +794,10 @@ function createTSSSendParams(req: express.Request) {
if (req.config.externalSignerUrl !== undefined) {
return {
...req.body,
customCommitmentGeneratingFunction: createCustomCommitmentGenerator(
req.config.externalSignerUrl,
req.params.coin
),
customRShareGeneratingFunction: createCustomRShareGenerator(req.config.externalSignerUrl, req.params.coin),
customGShareGeneratingFunction: createCustomGShareGenerator(req.config.externalSignerUrl, req.params.coin),
};
Expand Down Expand Up @@ -1092,8 +1102,27 @@ export function createCustomSigningFunction(externalSignerUrl: string): CustomSi
};
}

export function createCustomCommitmentGenerator(
externalSignerUrl: string,
coin: string
): CustomCommitmentGeneratingFunction {
return async function (params): Promise<{
userToBitgoCommitment: CommitmentShareRecord;
encryptedSignerShare: EncryptedSignerShareRecord;
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
}> {
const { body: result } = await retryPromise(
() => superagent.post(`${externalSignerUrl}/api/v2/${coin}/tssshare/commitment`).type('json').send(params),
(err, tryCount) => {
debug(`failed to connect to external signer (attempt ${tryCount}, error: ${err.message})`);
}
);
return result;
};
}

export function createCustomRShareGenerator(externalSignerUrl: string, coin: string): CustomRShareGeneratingFunction {
return async function (params): Promise<{ rShare: SignShare; signingKeyYShare: YShare }> {
return async function (params): Promise<{ rShare: SignShare }> {
const { body: rShare } = await retryPromise(
() => superagent.post(`${externalSignerUrl}/api/v2/${coin}/tssshare/R`).type('json').send(params),
(err, tryCount) => {
Expand Down
139 changes: 45 additions & 94 deletions modules/express/test/unit/clientRoutes/externalSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ describe('External signer', () => {
bgUrl = common.Environments[bitgo.getEnv()].uri;
hdTree = await Ed25519BIP32.initialize();
MPC = await Eddsa.initialize(hdTree);

const bitgoPublicKey =
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EZIol0hMFK4EEAAoCAwTBJZKgCNfBZuD5AgIDM2hQky3Iw3T6EITaMnW2\nG9uKxFadVpslF0Dyp+kieW7JYPffUzSI+mCR7L/4rSsnLLHszRZiaXRnbyA8\nYml0Z29AdGVzdC5jb20+wowEEBMIAB0FAmSKJdIECwkHCAMVCAoEFgACAQIZ\nAQIbAwIeAQAhCRAL9sROnSoDRhYhBEFZxeQAYNOvaj3GZAv2xE6dKgNGj7MB\nAOJBnZqaWPway3B4fNB/Mi0v1wb9d2uDD28SgzzpsV/YAP90cryseKMF+dKw\n+to1vXTl8xb49cIU9gvcJYLqYUd+Fs5TBGSKJdISBSuBBAAKAgMErB+qJoUf\nvTyMP/9GGNsHY7ykqbwi/QYjim4bR560TyRQ8LKaxGwHN/1cbq4iQt45lYK2\nWpNQovBJ6U3DwKUFnQMBCAfCeAQYEwgACQUCZIol0gIbDAAhCRAL9sROnSoD\nRhYhBEFZxeQAYNOvaj3GZAv2xE6dKgNGSA8A/25BLEgyRERJFlDvGnavxRKu\nhHHV6kyzK9speNeTs1vzAP0cFkbE5Kvg6Xz9lag+cr6rFwrHC8m7znTbrbHq\n6eOi3w==\n=XFoJ\n-----END PGP PUBLIC KEY BLOCK-----\n';
const constants = {
mpc: {
bitgoPublicKey,
},
};

nock(bgUrl).persist().get('/api/v1/client/constants').reply(200, { ttl: 3600, constants });
});

after(() => {
Expand Down Expand Up @@ -91,7 +101,8 @@ describe('External signer', () => {
signTransactionStub.restore();
envStub.restore();
});
it('should read an encrypted prv from signerFileSystemPath and pass it to R and G share generators', async () => {

it('should read an encrypted prv from signerFileSystemPath and pass it to commitment, R and G share generators', async () => {
const walletID = '62fe536a6b4cf70007acb48c0e7bb0b0';
const user = MPC.keyShare(1, 2, 3);
const backup = MPC.keyShare(2, 2, 3);
Expand All @@ -112,41 +123,9 @@ describe('External signer', () => {
.value({ WALLET_62fe536a6b4cf70007acb48c0e7bb0b0_PASSPHRASE: walletPassphrase });
const tMessage = 'testMessage';
const bgTest = new BitGo({ env: 'test' });
const reqR = {
bitgo: bgTest,
body: {
txRequest: {
apiVersion: 'full',
walletId: walletID,
transactions: [
{
unsignedTx: {
derivationPath: 'm/0',
signableHex: tMessage,
},
},
],
},
},
params: {
coin: 'tsol',
sharetype: 'R',
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as express.Request;
const result = await handleV2GenerateShareTSS(reqR);
const bitgoCombine = MPC.keyCombine(bitgo.uShare, [result.signingKeyYShare, backup.yShares[3]]);
const bitgoSignShare = await MPC.signShare(Buffer.from(tMessage, 'hex'), bitgoCombine.pShare, [
bitgoCombine.jShares[1],
]);
const signatureShareRec = {
from: SignatureShareType.BITGO,
to: SignatureShareType.USER,
share: bitgoSignShare.rShares[1].r + bitgoSignShare.rShares[1].R,
};
const reqG = {
const derivationPath = 'm/0';

const reqCommitment = {
bitgo: bgTest,
body: {
txRequest: {
Expand All @@ -155,82 +134,42 @@ describe('External signer', () => {
transactions: [
{
unsignedTx: {
derivationPath: 'm/0',
derivationPath,
signableHex: tMessage,
},
},
],
},
userToBitgoRShare: result.rShare,
bitgoToUserRShare: signatureShareRec,
},
params: {
coin: 'tsol',
sharetype: 'G',
sharetype: 'commitment',
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as express.Request;
const userGShare = await handleV2GenerateShareTSS(reqG);
const userToBitgoRShare = {
i: ShareKeyPosition.BITGO,
j: ShareKeyPosition.USER,
u: result.signingKeyYShare.u,
v: result.rShare.rShares[3].v,
r: result.rShare.rShares[3].r,
R: result.rShare.rShares[3].R,
};
const bitgoGShare = MPC.sign(
Buffer.from(tMessage, 'hex'),
bitgoSignShare.xShare,
[userToBitgoRShare],
[backup.yShares[3]]
);
const signature = MPC.signCombine([userGShare, bitgoGShare]);
const veriResult = MPC.verify(Buffer.from(tMessage, 'hex'), signature);
veriResult.should.be.true();
readFileStub.restore();
envStub.restore();
});

it('should read an encrypted prv from signerFileSystemPath and pass it to R and G share generators, with commitment', async () => {
const walletID = '62fe536a6b4cf70007acb48c0e7bb0b0';
const user = MPC.keyShare(1, 2, 3);
const backup = MPC.keyShare(2, 2, 3);
const bitgo = MPC.keyShare(3, 2, 3);
const userSigningMaterial = {
uShare: user.uShare,
bitgoYShare: bitgo.yShares[1],
backupYShare: backup.yShares[1],
};
const bg = new BitGo({ env: 'test' });
const walletPassphrase = 'testPass';
const validPrv = bg.encrypt({ input: JSON.stringify(userSigningMaterial), password: walletPassphrase });
const output: Output = {};
output[walletID] = validPrv;
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(JSON.stringify(output));
const envStub = sinon
.stub(process, 'env')
.value({ WALLET_62fe536a6b4cf70007acb48c0e7bb0b0_PASSPHRASE: walletPassphrase });
const tMessage = 'testMessage';
const bgTest = new BitGo({ env: 'test' });
const cResult = await handleV2GenerateShareTSS(reqCommitment);
cResult.should.have.property('userToBitgoCommitment');
cResult.should.have.property('encryptedSignerShare');
cResult.should.have.property('encryptedUserToBitgoRShare');
const encryptedUserToBitgoRShare = cResult.encryptedUserToBitgoRShare;
const reqR = {
bitgo: bgTest,
body: {
txRequest: {
apiVersion: 'full',
state: 'pendingCommitment',
walletId: walletID,
transactions: [
{
unsignedTx: {
derivationPath: 'm/0',
derivationPath,
signableHex: tMessage,
},
},
],
},
encryptedUserToBitgoRShare,
},
params: {
coin: 'tsol',
Expand All @@ -240,8 +179,16 @@ describe('External signer', () => {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as express.Request;
const result = await handleV2GenerateShareTSS(reqR);
const bitgoCombine = MPC.keyCombine(bitgo.uShare, [result.signingKeyYShare, backup.yShares[3]]);
const rResult = await handleV2GenerateShareTSS(reqR);
rResult.should.have.property('rShare');

const signingKey = MPC.keyDerive(
userSigningMaterial.uShare,
[userSigningMaterial.bitgoYShare, userSigningMaterial.backupYShare],
derivationPath
);

const bitgoCombine = MPC.keyCombine(bitgo.uShare, [signingKey.yShares[3], backup.yShares[3]]);
const bitgoSignShare = await MPC.signShare(Buffer.from(tMessage, 'hex'), bitgoCombine.pShare, [
bitgoCombine.jShares[1],
]);
Expand All @@ -265,13 +212,13 @@ describe('External signer', () => {
transactions: [
{
unsignedTx: {
derivationPath: 'm/0',
derivationPath,
signableHex: tMessage,
},
},
],
},
userToBitgoRShare: result.rShare,
userToBitgoRShare: rResult.rShare,
bitgoToUserRShare: signatureShareRec,
bitgoToUserCommitment: bitgoToUserCommitmentShare,
},
Expand All @@ -284,14 +231,18 @@ describe('External signer', () => {
},
} as unknown as express.Request;
const userGShare = await handleV2GenerateShareTSS(reqG);
userGShare.should.have.property('i');
userGShare.should.have.property('y');
userGShare.should.have.property('gamma');
userGShare.should.have.property('R');
const userToBitgoRShare = {
i: ShareKeyPosition.BITGO,
j: ShareKeyPosition.USER,
u: result.signingKeyYShare.u,
v: result.rShare.rShares[3].v,
r: result.rShare.rShares[3].r,
R: result.rShare.rShares[3].R,
commitment: result.rShare.rShares[3].commitment,
u: signingKey.yShares[3].u,
v: rResult.rShare.rShares[3].v,
r: rResult.rShare.rShares[3].r,
R: rResult.rShare.rShares[3].R,
commitment: rResult.rShare.rShares[3].commitment,
};
const bitgoGShare = MPC.sign(
Buffer.from(tMessage, 'hex'),
Expand Down
51 changes: 39 additions & 12 deletions modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import {
PopulatedIntentForTypedDataSigning,
CreateBitGoKeychainParamsBase,
CommitmentShareRecord,
EncryptedSignerShareRecord,
CustomCommitmentGeneratingFunction,
} from './baseTypes';
import { GShare, SignShare, YShare } from '../../../account-lib/mpc/tss';
import { GShare, SignShare } from '../../../account-lib/mpc/tss';

/**
* BaseTssUtil class which different signature schemes have to extend
Expand Down Expand Up @@ -117,42 +119,67 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
*/
signUsingExternalSigner(
txRequest: string | TxRequest,
externalSignerCommitmentGenerator: CustomCommitmentGeneratingFunction,
externalSignerRShareGenerator: CustomRShareGeneratingFunction,
externalSignerGShareGenerator: CustomGShareGeneratingFunction
): Promise<TxRequest> {
throw new Error('Method not implemented.');
}

/**
* Create an Commitment (User to BitGo) share from an unsigned transaction and private user signing material
* EDDSA only
*
* @param {Object} params - params object
* @param {TxRequest} params.txRequest - transaction request with unsigned transaction
* @param {string} params.prv - user signing material
* @param {string} params.walletPassphrase - wallet passphrase
*
* @returns {Promise<{ userToBitgoCommitment: CommitmentShareRecor, encryptedSignerShare: EncryptedSignerShareRecord }>} - Commitment Share and the Encrypted Signer Share to BitGo
*/
createCommitmentShareFromTxRequest(params: { txRequest: TxRequest; prv: string; walletPassphrase: string }): Promise<{
userToBitgoCommitment: CommitmentShareRecord;
encryptedSignerShare: EncryptedSignerShareRecord;
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
}> {
throw new Error('Method not implemented.');
}

/**
* Create an R (User to BitGo) share from an unsigned transaction and private user signing material
*
* @param {TxRequest} txRequest - transaction request with unsigned transaction
* @param {string} prv - user signing material
* @returns {Promise<{ rShare: SignShare; signingKeyYShare: YShare }>} - R Share and the Signing Key's Y share to BitGo
* @param {Object} params - params object
* @param {TxRequest} params.txRequest - transaction request with unsigned transaction
* @param {string} params.prv - user signing material
* @param {string} [params.walletPassphrase] - wallet passphrase
* @param {EncryptedSignerShareRecord} [params.encryptedUserToBitgoRShare] - encrypted user to bitgo R share generated in the commitment phase
* @returns {Promise<{ rShare: SignShare }>} - R Share to BitGo
*/
createRShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
}): Promise<{ rShare: SignShare; signingKeyYShare: YShare }> {
walletPassphrase: string;
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
}): Promise<{ rShare: SignShare }> {
throw new Error('Method not implemented.');
}

/**
* Create a G (User to BitGo) share from an unsigned transaction and private user signing material
*
* @param {TxRequest} txRequest - transaction request with unsigned transaction
* @param {string} prv - user signing material
* @param {SignatureShareRecord} bitgoToUserRShare - BitGo to User R Share
* @param {SignShare} userToBitgoRShare - User to BitGo R Share
* @param {string} [bitgoToUserCommitment] - BitGo to User Commitment
* @param {Object} params - params object
* @param {TxRequest} params.txRequest - transaction request with unsigned transaction
* @param {string} params.prv - user signing material
* @param {SignatureShareRecord} params.bitgoToUserRShare - BitGo to User R Share
* @param {SignShare} params.userToBitgoRShare - User to BitGo R Share
* @param {CommitmentShareRecord} params.bitgoToUserCommitment - BitGo to User Commitment
* @returns {Promise<GShare>} - GShare from User to BitGo
*/
createGShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
bitgoToUserRShare: SignatureShareRecord;
userToBitgoRShare: SignShare;
bitgoToUserCommitment?: CommitmentShareRecord;
bitgoToUserCommitment: CommitmentShareRecord;
}): Promise<GShare> {
throw new Error('Method not implemented.');
}
Expand Down
Loading

0 comments on commit 2a38dba

Please sign in to comment.