From b14b64fbcb4cf65880154586b777992be0e49d37 Mon Sep 17 00:00:00 2001 From: Alejandro Busse Date: Fri, 19 May 2023 22:50:14 -0300 Subject: [PATCH] feat(root): use eddsa commitment for tss utils and signing use eddsa with commitment for tss utils and signing flow BG-70694 --- .../test/v2/unit/internal/tssUtils/common.ts | 14 +- .../test/v2/unit/internal/tssUtils/eddsa.ts | 231 +++++++++++++++--- modules/bitgo/test/v2/unit/tss/eddsa.ts | 75 +++++- modules/bitgo/test/v2/unit/wallet.ts | 1 + .../test/unit/clientRoutes/externalSign.ts | 113 +++++++++ .../src/account-lib/mpc/tss/eddsa/types.ts | 4 - modules/sdk-core/src/bitgo/tss/common.ts | 36 ++- modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts | 26 +- .../src/bitgo/utils/tss/baseTSSUtils.ts | 3 + .../sdk-core/src/bitgo/utils/tss/baseTypes.ts | 27 +- .../src/bitgo/utils/tss/eddsa/eddsa.ts | 63 ++++- 11 files changed, 538 insertions(+), 55 deletions(-) diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/common.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/common.ts index 69e3a1ad38..efd23ab3e5 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/common.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/common.ts @@ -1,4 +1,4 @@ -import { SignatureShareRecord } from '@bitgo/sdk-core'; +import { ExchangeCommitmentResponse, SignatureShareRecord } from '@bitgo/sdk-core'; import * as nock from 'nock'; export async function nockSendTxRequest(params: {coin:string, walletId: string, txRequestId: string}): Promise { @@ -42,6 +42,18 @@ export async function nockGetTxRequest(params: {walletId: string, txRequestId: s .reply(200, params.response); } +export async function nockExchangeCommitments(params: {walletId: string, txRequestId: string, response: ExchangeCommitmentResponse, apiMode?: 'lite' | 'full', notPersist?: boolean}): Promise { + const { apiMode = 'lite' } = params; + let addendum = ''; + if (apiMode === 'full') { + addendum = '/transactions/0'; + } + return nock('https://bitgo.fakeurl') + .persist(true) + .post(`/api/v2/wallet/${params.walletId}/txrequests/${params.txRequestId}${addendum}/commit`) + .reply(200, params.response); +} + export function getRoute(tssType: 'eddsa' | 'ecdsa' = 'eddsa'): string { if (tssType === 'ecdsa') { return '/transactions/0'; diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index 8ac9b6ea4c..5d8d128de5 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -9,10 +9,8 @@ import { TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../../../src/bitgo'; import { common, - createUserSignShare, Keychain, RequestTracer, - ShareKeyPosition, SignatureShareRecord, SignatureShareType, TssUtils, @@ -22,9 +20,20 @@ import { KeyShare, Ed25519BIP32, createSharedDataProof, + CommitmentShareRecord, + CommitmentType, + ExchangeCommitmentResponse, + EncryptedSignerShareType, } from '@bitgo/sdk-core'; import { createWalletSignatures } from '../../tss/helpers'; -import { nockSendSignatureShare, nockGetTxRequest, nockCreateTxRequest, nockDeleteSignatureShare, nockSendTxRequest } from './common'; +import { + nockSendSignatureShare, + nockGetTxRequest, + nockCreateTxRequest, + nockDeleteSignatureShare, + nockSendTxRequest, + nockExchangeCommitments, +} from './common'; openpgp.config.rejectCurves = new Set(); @@ -45,38 +54,80 @@ describe('TSS Utils:', async function () { i: 1, t: 2, n: 3, - y: '2f97f50e81507c2d3aaaf35673a9ec2eb5be706ab2aa6302a8a1dbc6dc8b13a4', - seed: 'e49bcb10aed940823180ec841b562593a5111d221319920720dee209633a7c1e', - chaincode: '1175780188727cdc222b55ab36a21e4f9a81f34fd66b216d55d497e806095404', + y: '093c8603ad86c41d5ee25a814b88185b435dd3a9ceccf9c9fd691a465ac4a8b0', + seed: 'ca40c789813250c334ddd2ba19050f6ed20b5a08853ceca492358f2711ad4b15', + chaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca', }, - commonChaincode: '3b8ceb8c78b151ba2e6cf123b385ab83fd16b50554335fb9d30967a2977b1c18', + commonChaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca', bitgoYShare: { i: 1, j: 3, - y: '367523d7d49fbaf5b3ddac8d95fbae6b5ae5df93bcd28facdf5b2845db96a456', - v: '2757f7b44ba338c4d917c46ce59e64d5d5b340c8f56bed4d6548674b7262326a', - u: '657127c5ad7f019ca2d13564a61dcea69b36869a5e0b1d0bcb4e9aff12f4f702', - chaincode: '5a16fcb16a53904409f41cb9ea253df50b3396ac5ecce9c17124f5a79b890f24', + y: '59d8000ba5e85fa402f39382960e7d5ede82b1b6e22b146a18b7df238c3a3225', + v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517', + u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b', + chaincode: 'd21dbd8eae5d4789292ecea2efa53e0165b2439d57f5158eb4dd57dc26b59236', }, backupYShare: { i: 1, j: 2, - y: 'a287d3396a25a30a11d893212ede64823687e86b3e8ab7d9d30511bda225bdee', - v: 'dd1fd4864d9739ec5e5d99fb2d4fa2841179c03f1661d9c0dd7383a870bed975', - u: '012e025099bffc0b97522cec86d043ced552bc076ba29f1045e480a717a2150b', - chaincode: 'd00076d985eb449a024d7ebe92be4f3f57612b091efb548b0c0fda12f5e8b8f0', + y: 'e0ae75077715686a121acb41b29a55bde426971154f40a41fc317f7f774a9424', + v: 'f76ef629dfc15ab5e4531e532b5d67f2176637ca752b195876b7e3172459c969', + u: 'fe6b89fb6acfcd7392c35c084f58bde0846b888c4df57e466caf0a3271b06a05', + chaincode: '1c34e5dfbbd4a870f4479caaa5e6a46e3438f976ad5aefd4905b8fe8bca1101e', + }, + }; + + const validUserSignShare = { + xShare: { + i: 1, + y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296', + u: '1e159d6a0ae3a8dccc74615113e7c3e25d3080e5e0ffeb0ae04dd6a967268102', + r: 'c8f64cc48926216c3f60e1d8ff1e24eba060d7c1ff020d0fc1d735d4564efd03', + R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c', + }, + rShares: { 3: { + i: 3, + j: 1, + u: 'd675f9099fbef03aa9fcdca4009286f435e56369c374d0042f03cc60b49e690a', + v: '3c090e88ed42da0dd0bade35c8d6b88bc050284536b98e5b27d33ff45da9755b', + r: '7f16224dbf5b02adb6c21380fcb2a8ee00323daae62cac3575a4d328fd23a905', + R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c', + commitment: '445c8cb1dee0166b6bdd5ad1d0a53fbfe86c4d3a470f184745530a863eedff28', + }, }, }; + + const validBitgoToUserSignShare = { + xShare: { + i: 3, + y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296', + u: '1315dbe18069825b4a27188b813eae7ff2917a614499ed553e70d65d4fa4820b', + r: 'd0539375e6566f2fe540cba48c5e56bd1cdf68cfe1f0d527d2b730fe4e879809', + R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32', + }, + rShares: { + 1: { + i: 1, + j: 3, + u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b', + v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517', + r: '0375e8c5a5691a73c21df00d49d423e3f83fe08d7b5d5af33c5c6aa9cae59d0a', + R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32', + commitment: '62b21f98bf885841ad469145192d4df0697b3f42c581e3e926394eae0b101ecb', + }, + }, + }; + const txRequest = { txRequestId: 'randomId', - unsignedTxs: [{ signableHex: 'randomhex', serializedTxHex: 'randomhex2', derivationPath: 'm/0/1/2', - }], + unsignedTxs: [{ signableHex: 'MPC on a Friday night', serializedTxHex: 'MPC on a Friday night' }], signatureShares: [ - { from: 'bitgo', + { + from: 'bitgo', to: 'user', - share: '9d7159a76700635b10edf1fb983ce1ad1690a9686927e137cb4a70b32ad77b0e7f0394fa3ac3db433aec1097ca88ee43034d3a9fb0c4f87b63ff38fc1d1c8f4f' }], + share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, + }], }; - const signablePayload = Buffer.from(txRequest.unsignedTxs[0].signableHex, 'hex'); beforeEach(function () { sandbox = sinon.createSandbox(); @@ -471,9 +522,9 @@ describe('TSS Utils:', async function () { transactions: [], unsignedTxs: [ { - serializedTxHex: 'ababfefe', - signableHex: 'deadbeef', - derivationPath: 'm/0/1/2', + serializedTxHex: 'MPC on a Friday night', + signableHex: 'MPC on a Friday night', + derivationPath: 'm/0', }, ], date: new Date().toISOString(), @@ -490,14 +541,8 @@ describe('TSS Utils:', async function () { }; beforeEach(async function () { - const signingKey = MPC.keyDerive( - validUserSigningMaterial.uShare, - [validUserSigningMaterial.bitgoYShare, validUserSigningMaterial.backupYShare], - txRequest.unsignedTxs[0].derivationPath - ); - - const userSignShare = await createUserSignShare(signablePayload, signingKey.pShare); - const rShare = userSignShare.rShares[ShareKeyPosition.BITGO]; + const userSignShare = validUserSignShare; + const rShare = userSignShare.rShares[3]; const signatureShare: SignatureShareRecord = { from: SignatureShareType.USER, to: SignatureShareType.BITGO, @@ -513,11 +558,104 @@ describe('TSS Utils:', async function () { const signatureShare2: SignatureShareRecord = { from: SignatureShareType.BITGO, to: SignatureShareType.USER, - share: rShare.r + rShare.R, + share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, }; const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] }; await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response }); + const bitgoToUserCommitmentShare: CommitmentShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + type: CommitmentType.COMMITMENT, + share: validBitgoToUserSignShare.rShares[1].commitment, + }; + const exchangeCommitResponse: ExchangeCommitmentResponse = { commitmentShare: bitgoToUserCommitmentShare }; + await nockExchangeCommitments( { walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: exchangeCommitResponse }); + + }); + + it('signTxRequest should succeed with txRequest object as input', async function () { + const signedTxRequest = await tssUtils.signTxRequest({ + txRequest, + prv: JSON.stringify(validUserSigningMaterial), + reqId, + }); + signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); + + sandbox.verifyAndRestore(); + }); + + it('signTxRequest should succeed with txRequest id as input', async function () { + const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest'); + getTxRequest.resolves(txRequest); + getTxRequest.calledWith(txRequestId); + + const signedTxRequest = await tssUtils.signTxRequest({ + txRequest: txRequestId, + prv: JSON.stringify(validUserSigningMaterial), + reqId, + }); + signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); + + sandbox.verifyAndRestore(); + }); + }); + + describe('signTxRequest With Commitment:', function() { + const txRequestId = 'randomid'; + const txRequest: TxRequest = { + txRequestId, + transactions: [], + unsignedTxs: [ + { + serializedTxHex: 'MPC on a Friday night', + signableHex: 'MPC on a Friday night', + derivationPath: 'm/0', + }, + ], + date: new Date().toISOString(), + intent: { + intentType: 'payment', + }, + latest: true, + state: 'pendingUserSignature', + walletType: 'hot', + walletId: 'walletId', + policiesChecked: true, + version: 1, + userId: 'userId', + }; + + beforeEach(async function () { + const userSignShare = validUserSignShare; + const rShare = userSignShare.rShares[3]; + const signatureShare: SignatureShareRecord = { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: rShare.r + rShare.R, + }; + + await nockSendSignatureShare({ + walletId: wallet.id(), + txRequestId: txRequest.txRequestId, + signatureShare, + }); + + const signatureShare2: SignatureShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, + }; + const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] }; + await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response }); + const bitgoToUserCommitmentShare: CommitmentShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + type: CommitmentType.COMMITMENT, + share: validBitgoToUserSignShare.rShares[1].commitment, + }; + const exchangeCommitResponse: ExchangeCommitmentResponse = { commitmentShare: bitgoToUserCommitmentShare }; + await nockExchangeCommitments( { walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: exchangeCommitResponse }); }); it('signTxRequest should succeed with txRequest object as input', async function () { @@ -653,6 +791,35 @@ describe('TSS Utils:', async function () { }); }); + describe('createUserToBitgoCommitmentShare', function() { + it('should create a valid commitmentShare', async function() { + const value = 'randomstring'; + const validUserToBitgoCommitmentShare = { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + type: CommitmentType.COMMITMENT, + share: value, + }; + const commitmentShare = tssUtils.createUserToBitgoCommitmentShare(value); + commitmentShare.should.deepEqual(validUserToBitgoCommitmentShare); + + }); + }); + + describe('createUserToBitgoEncryptedSignerShare', function() { + it('should create a valid encryptedSignerShare', async function() { + const value = 'randomstring'; + const validUserToBitgoEncryptedSignerShare = { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + type: EncryptedSignerShareType.ENCRYPTED_SIGNER_SHARE, + share: value, + }; + const encryptedSignerShare = tssUtils.createUserToBitgoEncryptedSignerShare(value); + encryptedSignerShare.should.deepEqual(validUserToBitgoEncryptedSignerShare); + }); + }); + // #region Nock helpers async function generateBitgoKeychain(params: { diff --git a/modules/bitgo/test/v2/unit/tss/eddsa.ts b/modules/bitgo/test/v2/unit/tss/eddsa.ts index 70c8a2a6fe..c81583b705 100644 --- a/modules/bitgo/test/v2/unit/tss/eddsa.ts +++ b/modules/bitgo/test/v2/unit/tss/eddsa.ts @@ -16,6 +16,9 @@ import { Ed25519BIP32, Eddsa, SignShare, + CommitmentShareRecord, + CommitmentType, + SignatureShareType, } from '@bitgo/sdk-core'; import * as openpgp from 'openpgp'; import * as should from 'should'; @@ -350,7 +353,12 @@ describe('test tss helper functions', function () { }], }; const signablePayload = Buffer.from(txRequest.unsignedTxs[0].signableHex); - + const bitgoToUserCommitment: CommitmentShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: validBitgoToUserSignShare.rShares[1].commitment!, + type: CommitmentType.COMMITMENT, + }; let MPC: Eddsa; @@ -562,5 +570,70 @@ describe('test tss helper functions', function () { }); }); + describe('createUserToBitGoGShare with commitment:', async function() { + it('should succeed to create a UserToBitGo GShare', async function() { + + const userToBitgoGShare = await createUserToBitGoGShare( + validUserSignShare, + txRequest.signatureShares[0] as SignatureShareRecord, + validUserSigningMaterial.backupYShare, + validUserSigningMaterial.bitgoYShare, + signablePayload, + bitgoToUserCommitment, + ); + userToBitgoGShare.should.deepEqual(validUserToBitgoGShare); + }); + + it('should fail when XShare doesnt belong to the user', async function() { + const invalidUserSignShare = _.cloneDeep(validUserSignShare) ; + invalidUserSignShare.xShare.i = 3; + await createUserToBitGoGShare( + invalidUserSignShare, + txRequest.signatureShares[0] as SignatureShareRecord, + validUserSigningMaterial.backupYShare, + validUserSigningMaterial.bitgoYShare, + signablePayload, + bitgoToUserCommitment, + ).should.be.rejectedWith('Invalid XShare, doesnt belong to the User'); + }); + + it('should fail when commitment is invalid', async function() { + const invalidBitgoToUserCommitment = _.cloneDeep(bitgoToUserCommitment) ; + invalidBitgoToUserCommitment.share = 'deadbeef'; + await createUserToBitGoGShare( + validUserSignShare, + txRequest.signatureShares[0] as SignatureShareRecord, + validUserSigningMaterial.backupYShare, + validUserSigningMaterial.bitgoYShare, + signablePayload, + invalidBitgoToUserCommitment, + ).should.be.rejectedWith('Could not verify other player share'); + }); + + it('should fail when RShare doesnt belong to Bitgo', async function() { + const invalidBitgoRShare = _.cloneDeep(txRequest.signatureShares[0]); + invalidBitgoRShare.from = 'user'; + await createUserToBitGoGShare( + validUserSignShare, + invalidBitgoRShare as SignatureShareRecord, + validUserSigningMaterial.backupYShare, + validUserSigningMaterial.bitgoYShare, + signablePayload, + bitgoToUserCommitment, + ).should.be.rejectedWith('Invalid RShare, is not from Bitgo to User'); + + const invalidBitgoRShare2 = _.cloneDeep(txRequest.signatureShares[0]); + invalidBitgoRShare2.to = 'bitgo'; + await createUserToBitGoGShare( + validUserSignShare, + invalidBitgoRShare2 as SignatureShareRecord, + validUserSigningMaterial.backupYShare, + validUserSigningMaterial.bitgoYShare, + signablePayload, + bitgoToUserCommitment, + ).should.be.rejectedWith('Invalid RShare, is not from Bitgo to User'); + }); + }); + }); }); diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index a3f79e0b9e..ca3c6a9113 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -1852,6 +1852,7 @@ describe('V2 Wallet:', function () { derivationPath: 'm/0', }, signatureShares: [], + commitmentShares: [], }], unsignedTxs: [], apiVersion: 'full', diff --git a/modules/express/test/unit/clientRoutes/externalSign.ts b/modules/express/test/unit/clientRoutes/externalSign.ts index 576be45828..df39dc4357 100644 --- a/modules/express/test/unit/clientRoutes/externalSign.ts +++ b/modules/express/test/unit/clientRoutes/externalSign.ts @@ -177,6 +177,7 @@ describe('External signer', () => { 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, }; @@ -193,6 +194,118 @@ 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 () => { + 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 reqR = { + bitgo: bgTest, + body: { + txRequest: { + apiVersion: 'full', + state: 'pendingCommitment', + 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 bitgoToUserCommitmentShare = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: bitgoSignShare.rShares[1].commitment, + type: 'commitment', + }; + const reqG = { + bitgo: bgTest, + body: { + txRequest: { + apiVersion: 'full', + walletId: walletID, + transactions: [ + { + unsignedTx: { + derivationPath: 'm/0', + signableHex: tMessage, + }, + }, + ], + }, + userToBitgoRShare: result.rShare, + bitgoToUserRShare: signatureShareRec, + bitgoToUserCommitment: bitgoToUserCommitmentShare, + }, + params: { + coin: 'tsol', + sharetype: 'G', + }, + 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, + commitment: result.rShare.rShares[3].commitment, + }; + 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 accept a local secret and password for a wallet', async () => { const accessToken = ''; const walletIds = { diff --git a/modules/sdk-core/src/account-lib/mpc/tss/eddsa/types.ts b/modules/sdk-core/src/account-lib/mpc/tss/eddsa/types.ts index 1a0329ed9c..7a2249a055 100644 --- a/modules/sdk-core/src/account-lib/mpc/tss/eddsa/types.ts +++ b/modules/sdk-core/src/account-lib/mpc/tss/eddsa/types.ts @@ -69,10 +69,6 @@ export interface SignShare { rShares: Record; } -export interface ExchangeCommitmentResponse { - commitment: string; -} - export interface GShare { i: number; y: string; diff --git a/modules/sdk-core/src/bitgo/tss/common.ts b/modules/sdk-core/src/bitgo/tss/common.ts index 14a449d7e3..46147f11fb 100644 --- a/modules/sdk-core/src/bitgo/tss/common.ts +++ b/modules/sdk-core/src/bitgo/tss/common.ts @@ -2,8 +2,16 @@ import assert from 'assert'; import openpgp from 'openpgp'; import { BitGoBase } from '../bitgoBase'; -import { RequestType, TxRequest, verifyPrimaryUserWrapper, SignatureShareRecord } from '../utils'; import { TxRequestChallengeResponse } from './types'; +import { + RequestType, + TxRequest, + verifyPrimaryUserWrapper, + SignatureShareRecord, + CommitmentShareRecord, + EncryptedSignerShareRecord, + ExchangeCommitmentResponse, +} from '../utils'; /** * Gets the latest Tx Request by id @@ -74,6 +82,32 @@ export async function sendSignatureShare( .result(); } +/** + * Sends the client commitment and encrypted signer share to the server, getting back the server commitment + * @param {BitGoBase} bitgo - the bitgo instance + * @param {string} walletId - the wallet id + * @param {string} txRequestId - the txRequest Id + * @param {CommitmentShareRecord} commitmentShare - the client commitment share + * @param {EncryptedSignerShareRecord} encryptedSignerShare - the client encrypted signer share + * @param {string} [apiMode] - the txRequest api mode (full or lite) - defaults to lite + * @returns {Promise} - the server commitment share + */ +export async function exchangeEddsaCommitments( + bitgo: BitGoBase, + walletId: string, + txRequestId: string, + commitmentShare: CommitmentShareRecord, + encryptedSignerShare: EncryptedSignerShareRecord, + apiMode: 'full' | 'lite' = 'lite' +): Promise { + let addendum = ''; + if (apiMode === 'full') { + addendum = '/transactions/0'; + } + const urlPath = '/wallet/' + walletId + '/txrequests/' + txRequestId + addendum + '/commit'; + return await bitgo.post(bitgo.url(urlPath, 2)).send({ commitmentShare, encryptedSignerShare }).result(); +} + /** * Verifies that a TSS wallet signature was produced with the expected key and that the signed data contains the * expected common keychain as well as the expected user and backup key ids diff --git a/modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts index e13d51728d..58ccd06d8d 100644 --- a/modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts @@ -18,6 +18,8 @@ import { SignatureShareRecord, SignatureShareType, RequestType, + CommitmentShareRecord, + CommitmentType, } from './../../utils'; import { BaseTransaction } from './../../../account-lib/baseCoin/baseTransaction'; import { Ed25519BIP32 } from './../../../account-lib/mpc/hdTree'; @@ -105,7 +107,7 @@ export async function createCombinedKey(params: { } /** - * Creates the User Sign Share containing the User XShare and the User to Bitgo RShare + * Creates the User Sign Share containing the User XShare , the User to Bitgo RShare and User to Bitgo commitment * * @param {Buffer} signablePayload - the signablePayload as a buffer * @param {PShare} pShare - User's signing material @@ -128,6 +130,7 @@ export async function createUserSignShare(signablePayload: Buffer, pShare: PShar * @param {SignatureShareRecord} bitgoToUserRShare - the Bitgo to User RShare * @param {YShare} backupToUserYShare - the backup key Y share received during wallet creation * @param {Buffer} signablePayload - the signable payload from a tx + * @param {CommitmentShareRecord} [bitgoToUserCommitment] - the Bitgo to User Commitment * @returns {Promise} - the User to Bitgo GShare */ export async function createUserToBitGoGShare( @@ -135,7 +138,8 @@ export async function createUserToBitGoGShare( bitgoToUserRShare: SignatureShareRecord, backupToUserYShare: YShare, bitgoToUserYShare: YShare, - signablePayload: Buffer + signablePayload: Buffer, + bitgoToUserCommitment?: CommitmentShareRecord ): Promise { if (userSignShare.xShare.i !== ShareKeyPosition.USER) { throw new Error('Invalid XShare, doesnt belong to the User'); @@ -160,7 +164,7 @@ export async function createUserToBitGoGShare( R = bitgoToUserRShare.share.substring(64, 128); } - const RShare: RShare = { + const updatedBitgoToUserRShare: RShare = { i: ShareKeyPosition.USER, j: ShareKeyPosition.BITGO, u: bitgoToUserYShare.u, @@ -170,7 +174,21 @@ export async function createUserToBitGoGShare( }; const MPC = await Eddsa.initialize(); - return MPC.sign(signablePayload, userSignShare.xShare, [RShare], [backupToUserYShare]); + + if (bitgoToUserCommitment) { + if ( + bitgoToUserCommitment.from !== SignatureShareType.BITGO || + bitgoToUserCommitment.to !== SignatureShareType.USER + ) { + throw new Error('Invalid Commitment, is not from Bitgo to User'); + } + if (bitgoToUserCommitment.type !== CommitmentType.COMMITMENT) { + throw new Error('Invalid Commitment type, got: ' + bitgoToUserCommitment.type + ' expected: commitment'); + } + updatedBitgoToUserRShare.commitment = bitgoToUserCommitment.share; + } + + return MPC.sign(signablePayload, userSignShare.xShare, [updatedBitgoToUserRShare], [backupToUserYShare]); } /** diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index 33c6be92b1..187d26ced1 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -24,6 +24,7 @@ import { IntentOptionsForTypedData, PopulatedIntentForTypedDataSigning, CreateBitGoKeychainParamsBase, + CommitmentShareRecord, } from './baseTypes'; import { GShare, SignShare, YShare } from '../../../account-lib/mpc/tss'; @@ -143,6 +144,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 * @returns {Promise} - GShare from User to BitGo */ createGShareFromTxRequest(params: { @@ -150,6 +152,7 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil prv: string; bitgoToUserRShare: SignatureShareRecord; userToBitgoRShare: SignShare; + bitgoToUserCommitment?: CommitmentShareRecord; }): Promise { throw new Error('Method not implemented.'); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index 8fe70670c2..280623c077 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -143,6 +143,7 @@ export interface PopulatedIntent extends PopulatedIntentBase { } export type TxRequestState = + | 'pendingCommitment' | 'pendingApproval' | 'canceled' | 'rejected' @@ -154,6 +155,7 @@ export type TxRequestState = export type TransactionState = | 'initialized' + | 'pendingCommitment' | 'pendingSignature' | 'signed' | 'held' @@ -204,6 +206,7 @@ export type TxRequest = { pendingApprovalId?: string; policiesChecked: boolean; signatureShares?: SignatureShareRecord[]; + commitmentShares?: CommitmentShareRecord[]; pendingTxHashes?: string[]; txHashes?: string[]; unsignedMessages?: UnsignedMessageTss[]; @@ -214,6 +217,7 @@ export type TxRequest = { state: TransactionState; unsignedTx: UnsignedTransactionTss; // Should override with blockchain / sig specific unsigned tx signatureShares: SignatureShareRecord[]; + commitmentShares?: CommitmentShareRecord[]; }[]; messages?: { state: TransactionState; @@ -241,15 +245,36 @@ export enum SignatureShareType { BITGO = 'bitgo', } -export interface SignatureShareRecord { +interface ShareBaseRecord { from: SignatureShareType; to: SignatureShareType; share: string; +} + +export interface SignatureShareRecord extends ShareBaseRecord { vssProof?: string; privateShareProof?: string; publicShare?: string; } +export enum CommitmentType { + COMMITMENT = 'commitment', +} +export interface CommitmentShareRecord extends ShareBaseRecord { + type: CommitmentType; +} + +export interface ExchangeCommitmentResponse { + commitmentShare: CommitmentShareRecord; +} + +export enum EncryptedSignerShareType { + ENCRYPTED_SIGNER_SHARE = 'encryptedSignerShare', +} +export interface EncryptedSignerShareRecord extends ShareBaseRecord { + type: EncryptedSignerShareType; +} + export type TSSParams = { txRequest: string | TxRequest; // can be either a string or TxRequest prv: string; 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 6ec22fff03..5651d89255 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -6,7 +6,7 @@ import * as bs58 from 'bs58'; import * as openpgp from 'openpgp'; import { Ed25519BIP32 } from '../../../../account-lib'; import Eddsa, { SignShare, GShare } from '../../../../account-lib/mpc/tss'; -import { AddKeychainOptions, Keychain, KeyType, CreateBackupOptions } from '../../../keychain'; +import { AddKeychainOptions, Keychain, CreateBackupOptions } from '../../../keychain'; import { verifyWalletSignature } from '../../../tss/eddsa/eddsa'; import { encryptText, getBitgoGpgPubKey, createShareProof, generateGPGKeyPair } from '../../opengpgUtils'; import { @@ -19,15 +19,21 @@ import { SigningMaterial, } from '../../../tss'; import { + CommitmentShareRecord, + CommitmentType, CustomGShareGeneratingFunction, CustomRShareGeneratingFunction, + EncryptedSignerShareRecord, + EncryptedSignerShareType, SignatureShareRecord, + SignatureShareType, TSSParams, TxRequest, } from '../baseTypes'; import { CreateEddsaBitGoKeychainParams, CreateEddsaKeychainParams, KeyShare, YShare } from './types'; import baseTSSUtils from '../baseTSSUtils'; import { KeychainsTriplet } from '../../../baseCoin'; +import { exchangeEddsaCommitments } from '../../../tss/common'; /** * Utility functions for TSS work flows. @@ -151,7 +157,7 @@ export class EddsaUtils extends baseTSSUtils { const userKeychainParams: AddKeychainOptions = { source: 'user', - keyType: 'tss' as KeyType, + keyType: 'tss', commonKeychain: bitgoKeychain.commonKeychain, originalPasscodeEncryptionCode, }; @@ -397,10 +403,11 @@ export class EddsaUtils extends baseTSSUtils { prv: string; bitgoToUserRShare: SignatureShareRecord; userToBitgoRShare: SignShare; + bitgoToUserCommitment?: CommitmentShareRecord; }): Promise { let txRequestResolved: TxRequest; - const { txRequest, prv, bitgoToUserRShare, userToBitgoRShare } = params; + const { txRequest, prv, bitgoToUserCommitment, bitgoToUserRShare, userToBitgoRShare } = params; if (typeof txRequest === 'string') { txRequestResolved = await getTxRequest(this.bitgo, this.wallet.id(), txRequest); @@ -426,7 +433,8 @@ export class EddsaUtils extends baseTSSUtils { bitgoToUserRShare, userSigningMaterial.backupYShare, userSigningMaterial.bitgoYShare, - signablePayload + signablePayload, + bitgoToUserCommitment ); return userToBitGoGShare; } @@ -476,7 +484,7 @@ export class EddsaUtils extends baseTSSUtils { let txRequestResolved: TxRequest; let txRequestId: string; - const { txRequest, prv, apiVersion } = params; + const { txRequest, prv } = params; if (typeof txRequest === 'string') { txRequestResolved = await getTxRequest(this.bitgo, this.wallet.id(), txRequest); @@ -494,11 +502,10 @@ export class EddsaUtils extends baseTSSUtils { throw new Error('Invalid user key - missing backupYShare'); } + const { apiVersion } = txRequestResolved; assert(txRequestResolved.transactions || txRequestResolved.unsignedTxs, 'Unable to find transactions in txRequest'); const unsignedTx = - txRequestResolved.apiVersion === 'full' - ? txRequestResolved.transactions![0].unsignedTx - : txRequestResolved.unsignedTxs[0]; + apiVersion === 'full' ? txRequestResolved.transactions![0].unsignedTx : txRequestResolved.unsignedTxs[0]; const signingKey = MPC.keyDerive( userSigningMaterial.uShare, @@ -513,7 +520,7 @@ export class EddsaUtils extends baseTSSUtils { const bitgoIndex = 3; const signerShare = signingKey.yShares[bitgoIndex].u + signingKey.yShares[bitgoIndex].chaincode; const bitgoGpgKey = await getBitgoGpgPubKey(this.bitgo); - const encryptedSignerShare = await encryptText(signerShare, bitgoGpgKey); + const bitgoToUserEncryptedSignerShare = await encryptText(signerShare, bitgoGpgKey); const userGpgKey = await generateGPGKeyPair('secp256k1'); const privateShareProof = await createShareProof(userGpgKey.privateKey, signingKey.yShares[bitgoIndex].u, 'eddsa'); @@ -521,12 +528,27 @@ export class EddsaUtils extends baseTSSUtils { const userPublicGpgKey = userGpgKey.publicKey; const publicShare = signingKey.yShares[bitgoIndex].y + signingKey.yShares[bitgoIndex].chaincode; + const userToBitgoCommitment = userSignShare.rShares[bitgoIndex].commitment; + assert(userToBitgoCommitment, 'Missing userToBitgoCommitment commitment'); + + const commitmentShare = this.createUserToBitgoCommitmentShare(userToBitgoCommitment); + const encryptedSignerShare = this.createUserToBitgoEncryptedSignerShare(bitgoToUserEncryptedSignerShare); + + const { commitmentShare: bitgoToUserCommitment } = await exchangeEddsaCommitments( + this.bitgo, + this.wallet.id(), + txRequestId, + commitmentShare, + encryptedSignerShare, + apiVersion + ); + await offerUserToBitgoRShare( this.bitgo, this.wallet.id(), txRequestId, userSignShare, - encryptedSignerShare, + bitgoToUserEncryptedSignerShare, apiVersion, vssProof, privateShareProof, @@ -541,7 +563,8 @@ export class EddsaUtils extends baseTSSUtils { bitgoToUserRShare, userSigningMaterial.backupYShare, userSigningMaterial.bitgoYShare, - signablePayload + signablePayload, + bitgoToUserCommitment ); await sendUserToBitgoGShare(this.bitgo, this.wallet.id(), txRequestId, userToBitGoGShare, apiVersion); @@ -562,6 +585,24 @@ export class EddsaUtils extends baseTSSUtils { const commonPubHexStr = commonKeychain.slice(0, 64); return bs58.encode(Buffer.from(commonPubHexStr, 'hex')); } + + createUserToBitgoCommitmentShare(commitment: string): CommitmentShareRecord { + return { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: commitment, + type: CommitmentType.COMMITMENT, + }; + } + + createUserToBitgoEncryptedSignerShare(encryptedSignerShare: string): EncryptedSignerShareRecord { + return { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: encryptedSignerShare, + type: EncryptedSignerShareType.ENCRYPTED_SIGNER_SHARE, + }; + } } /** * @deprecated - use EddsaUtils