Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(express): implement EdDSA commitments for external signer #3661

Merged
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
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