From a5d73dda6e646cbcf2bac5f9cd1d107498a263f0 Mon Sep 17 00:00:00 2001 From: Alejandro Busse Date: Wed, 14 Jun 2023 19:00:08 -0300 Subject: [PATCH] feat(express): implement EdDSA commitments for external signer implemented EdDSA commitment step for external signer added unit test WP-94 --- modules/express/src/clientRoutes.ts | 16 ++-- .../test/unit/clientRoutes/externalSign.ts | 66 ++++++++++++--- .../src/bitgo/utils/tss/baseTSSUtils.ts | 21 ++++- .../sdk-core/src/bitgo/utils/tss/baseTypes.ts | 8 ++ .../src/bitgo/utils/tss/eddsa/eddsa.ts | 80 ++++++++++++++++++- 5 files changed, 171 insertions(+), 20 deletions(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 6c3c6f03f4..4573b3af7a 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -409,13 +409,17 @@ export async function handleV2GenerateShareTSS(req: express.Request): Promise { 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(() => { @@ -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); @@ -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: [ { @@ -231,6 +270,7 @@ describe('External signer', () => { }, ], }, + encryptedUserToBitgoRShare, }, params: { coin: 'tsol', @@ -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], ]); @@ -271,7 +313,7 @@ describe('External signer', () => { }, ], }, - userToBitgoRShare: result.rShare, + userToBitgoRShare: rResult.rShare, bitgoToUserRShare: signatureShareRec, bitgoToUserCommitment: bitgoToUserCommitmentShare, }, @@ -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'), diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index 187d26ced1..5877309d86 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -25,6 +25,7 @@ import { PopulatedIntentForTypedDataSigning, CreateBitGoKeychainParamsBase, CommitmentShareRecord, + EncryptedSignerShareRecord, } from './baseTypes'; import { GShare, SignShare, YShare } from '../../../account-lib/mpc/tss'; @@ -123,6 +124,22 @@ export default class BaseTssUtils 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 * @@ -133,6 +150,8 @@ export default class BaseTssUtils 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.'); } @@ -144,7 +163,7 @@ export default class BaseTssUtils 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 from User to BitGo */ createGShareFromTxRequest(params: { diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index 280623c077..dc3cac9a94 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -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; @@ -341,9 +342,16 @@ export interface ITssUtils { externalSignerRShareGenerator: CustomRShareGeneratingFunction, externalSignerGShareGenerator: CustomGShareGeneratingFunction ): Promise; + 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; diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index 5651d89255..9fd3533e2a 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -364,10 +364,16 @@ export class EddsaUtils extends baseTSSUtils { 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; @@ -394,8 +400,67 @@ export class EddsaUtils extends baseTSSUtils { 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: { @@ -603,6 +668,15 @@ export class EddsaUtils extends baseTSSUtils { 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