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 14, 2023
1 parent c690ed7 commit a5d73dd
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 20 deletions.
16 changes: 10 additions & 6 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,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
66 changes: 56 additions & 10 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 @@ -194,7 +204,7 @@ describe('External signer', () => {
envStub.restore();
});

it('should read an encrypted prv from signerFileSystemPath and pass it to R and G share generators, with commitment', 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 @@ -215,12 +225,41 @@ describe('External signer', () => {
.value({ WALLET_62fe536a6b4cf70007acb48c0e7bb0b0_PASSPHRASE: walletPassphrase });
const tMessage = 'testMessage';
const bgTest = new BitGo({ env: 'test' });

const reqCommitment = {
bitgo: bgTest,
body: {
txRequest: {
apiVersion: 'full',
walletId: walletID,
transactions: [
{
unsignedTx: {
derivationPath: 'm/0',
signableHex: tMessage,
},
},
],
},
},
params: {
coin: 'tsol',
sharetype: 'commitment',
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as express.Request;
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: [
{
Expand All @@ -231,6 +270,7 @@ describe('External signer', () => {
},
],
},
encryptedUserToBitgoRShare,
},
params: {
coin: 'tsol',
Expand All @@ -240,8 +280,10 @@ 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');
rResult.should.have.property('signingKeyYShare');
const bitgoCombine = MPC.keyCombine(bitgo.uShare, [rResult.signingKeyYShare, backup.yShares[3]]);
const bitgoSignShare = await MPC.signShare(Buffer.from(tMessage, 'hex'), bitgoCombine.pShare, [
bitgoCombine.jShares[1],
]);
Expand Down Expand Up @@ -271,7 +313,7 @@ describe('External signer', () => {
},
],
},
userToBitgoRShare: result.rShare,
userToBitgoRShare: rResult.rShare,
bitgoToUserRShare: signatureShareRec,
bitgoToUserCommitment: bitgoToUserCommitmentShare,
},
Expand All @@ -284,14 +326,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: rResult.signingKeyYShare.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
21 changes: 20 additions & 1 deletion modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
PopulatedIntentForTypedDataSigning,
CreateBitGoKeychainParamsBase,
CommitmentShareRecord,
EncryptedSignerShareRecord,
} from './baseTypes';
import { GShare, SignShare, YShare } from '../../../account-lib/mpc/tss';

Expand Down Expand Up @@ -123,6 +124,22 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
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 {TxRequest} txRequest - transaction request with unsigned transaction
* @param {string} prv - user signing material
* @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
*
Expand All @@ -133,6 +150,8 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
createRShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
walletPassphrase?: string;
encryptedUserToBitgoRShare?: EncryptedSignerShareRecord;
}): Promise<{ rShare: SignShare; signingKeyYShare: YShare }> {
throw new Error('Method not implemented.');
}
Expand All @@ -144,7 +163,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
* @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 {CommitmentShareRecord} [bitgoToUserCommitment] - BitGo to User Commitment
* @returns {Promise<GShare>} - GShare from User to BitGo
*/
createGShareFromTxRequest(params: {
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export interface ExchangeCommitmentResponse {

export enum EncryptedSignerShareType {
ENCRYPTED_SIGNER_SHARE = 'encryptedSignerShare',
ENCRYPTED_R_SHARE = 'encryptedRShare',
}
export interface EncryptedSignerShareRecord extends ShareBaseRecord {
type: EncryptedSignerShareType;
Expand Down Expand Up @@ -341,9 +342,16 @@ export interface ITssUtils<KeyShare = EDDSA.KeyShare> {
externalSignerRShareGenerator: CustomRShareGeneratingFunction,
externalSignerGShareGenerator: CustomGShareGeneratingFunction
): Promise<TxRequest>;
createCommitmentShareFromTxRequest(params: { txRequest: TxRequest; prv: string; walletPassphrase: string }): Promise<{
userToBitgoCommitment: CommitmentShareRecord;
encryptedSignerShare: EncryptedSignerShareRecord;
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
}>;
createRShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
walletPassphrase?: string;
encryptedUserToBitgoRShare?: EncryptedSignerShareRecord;
}): Promise<{ rShare: SignShare; signingKeyYShare: YShare }>;
createGShareFromTxRequest(params: {
txRequest: TxRequest;
Expand Down
80 changes: 77 additions & 3 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,16 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
return keychains;
}

async createRShareFromTxRequest(params: {
async createCommitmentShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
}): Promise<{ rShare: SignShare; signingKeyYShare: YShare }> {
walletPassphrase: string;
}): Promise<{
userToBitgoCommitment: CommitmentShareRecord;
encryptedSignerShare: EncryptedSignerShareRecord;
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
}> {
const bitgoIndex = 3;
const { txRequest, prv } = params;
const txRequestResolved: TxRequest = txRequest;

Expand All @@ -394,8 +400,67 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
const signablePayload = Buffer.from(unsignedTx.signableHex, 'hex');

const userSignShare = await createUserSignShare(signablePayload, signingKey.pShare);
const commitment = userSignShare.rShares[bitgoIndex]?.commitment;
assert(commitment, 'Unable to find commitment in userSignShare');
const userToBitgoCommitment = this.createUserToBitgoCommitmentShare(commitment);

const signerShare = signingKey.yShares[bitgoIndex].u + signingKey.yShares[bitgoIndex].chaincode;
const bitgoGpgKey = await getBitgoGpgPubKey(this.bitgo);
const bitgoToUserEncryptedSignerShare = await encryptText(signerShare, bitgoGpgKey);

const encryptedSignerShare = this.createUserToBitgoEncryptedSignerShare(bitgoToUserEncryptedSignerShare);
const stringifiedRShare = JSON.stringify(userSignShare);
const encryptedRShare = this.bitgo.encrypt({ input: stringifiedRShare, password: params.walletPassphrase });
const encryptedUserToBitgoRShare = this.createUserToBitgoEncryptedRShare(encryptedRShare);

return { userToBitgoCommitment, encryptedSignerShare, encryptedUserToBitgoRShare };
}

async createRShareFromTxRequest(params: {
txRequest: TxRequest;
prv: string;
walletPassphrase?: string;
encryptedUserToBitgoRShare?: EncryptedSignerShareRecord;
}): Promise<{ rShare: SignShare; signingKeyYShare: YShare }> {
const { txRequest, prv, walletPassphrase, encryptedUserToBitgoRShare } = params;
const txRequestResolved: TxRequest = txRequest;

const hdTree = await Ed25519BIP32.initialize();
const MPC = await Eddsa.initialize(hdTree);

const userSigningMaterial: SigningMaterial = JSON.parse(prv);
if (!userSigningMaterial.backupYShare) {
throw new Error('Invalid user key - missing backupYShare');
}

assert(txRequestResolved.transactions || txRequestResolved.unsignedTxs, 'Unable to find transactions in txRequest');
const unsignedTx =
txRequestResolved.apiVersion === 'full'
? txRequestResolved.transactions![0].unsignedTx
: txRequestResolved.unsignedTxs[0];

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

let rShare: SignShare;
if (walletPassphrase && encryptedUserToBitgoRShare) {
const decryptedRShare = this.bitgo.decrypt({
input: encryptedUserToBitgoRShare.share,
password: walletPassphrase,
});
rShare = JSON.parse(decryptedRShare);
assert(rShare.xShare, 'Unable to find xShare in decryptedRShare');
assert(rShare.rShares, 'Unable to find rShares in decryptedRShare');
} else {
const signablePayload = Buffer.from(unsignedTx.signableHex, 'hex');

rShare = await createUserSignShare(signablePayload, signingKey.pShare);
}

return { rShare: userSignShare, signingKeyYShare: signingKey.yShares[3] };
return { rShare, signingKeyYShare: signingKey.yShares[3] };
}

async createGShareFromTxRequest(params: {
Expand Down Expand Up @@ -603,6 +668,15 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
type: EncryptedSignerShareType.ENCRYPTED_SIGNER_SHARE,
};
}

createUserToBitgoEncryptedRShare(encryptedRShare: string): EncryptedSignerShareRecord {
return {
from: SignatureShareType.USER,
to: SignatureShareType.BITGO,
share: encryptedRShare,
type: EncryptedSignerShareType.ENCRYPTED_R_SHARE,
};
}
}
/**
* @deprecated - use EddsaUtils
Expand Down

0 comments on commit a5d73dd

Please sign in to comment.