Skip to content
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
25 changes: 20 additions & 5 deletions modules/bitgo/test/v2/unit/internal/tssUtils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,27 @@ export async function nockSendSignatureShare(
tssType?: 'eddsa' | 'ecdsa';
requestType?: RequestType;
apiMode?: 'lite' | 'full';
requestBodies?: Record<string, unknown>[];
},
status = 200
): Promise<nock.Scope> {
params.requestType = params.requestType || RequestType.tx;
params.apiMode = params.apiMode || 'lite';
const transactions = getRoute(params.tssType, params.requestType, params.apiMode);
return nock('https://bitgo.fakeurl')
.persist(true)
.post(`/api/v2/wallet/${params.walletId}/txrequests/${params.txRequestId + transactions}/signatureshares`)
.reply(status, status === 200 ? (params.signatureShare as nock.Body) : ({ error: 'some error' } as nock.Body));
const scope = nock('https://bitgo.fakeurl');
if (!params.requestBodies) {
scope.persist(true);
}
const signatureShareRequest = scope.post(
`/api/v2/wallet/${params.walletId}/txrequests/${params.txRequestId + transactions}/signatureshares`
);
if (params.requestBodies) {
signatureShareRequest.times(params.tssType === 'ecdsa' ? 3 : 2);
}
return signatureShareRequest.reply(status, (_uri, body) => {
params.requestBodies?.push(body as Record<string, unknown>);
return status === 200 ? (params.signatureShare as nock.Body) : ({ error: 'some error' } as nock.Body);
});
}

export async function nockSendSignatureShareWithResponse(
Expand All @@ -40,13 +51,17 @@ export async function nockSendSignatureShareWithResponse(
signatureShare: unknown;
response: unknown;
tssType?: 'eddsa' | 'ecdsa';
requestBodies?: Record<string, unknown>[];
},
status = 200
): Promise<nock.Scope> {
const transactions = getRoute(params.tssType);
return nock('https://bitgo.fakeurl')
.post(`/api/v2/wallet/${params.walletId}/txrequests/${params.txRequestId + transactions}/signatureshares`)
.reply(status, status === 200 ? (params.response as Body) : ({ error: 'some error' } as nock.Body));
.reply(status, (_uri, body) => {
params.requestBodies?.push(body as Record<string, unknown>);
return status === 200 ? (params.response as Body) : ({ error: 'some error' } as nock.Body);
});
}

export async function nockDeleteSignatureShare(
Expand Down
128 changes: 128 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import {
Keychain,
RequestTracer,
SignatureShareRecord,
ShareKeyPosition,
SignatureShareType,
TssTxRecipientSource,
TxRequest,
Wallet,
} from '@bitgo/sdk-core';
Expand Down Expand Up @@ -597,8 +599,16 @@ describe('TSS Ecdsa Utils:', async function () {
userId: 'userId',
};
let aShare, dShare, wShare, oShare, userSignShare, bitgoChallenges, enterpriseChallenges;
const attestation = {
signature: 'test-signature',
credentialId: 'test-credential',
clientDataJSON: 'test-client-data',
authenticatorData: 'test-authenticator-data',
};
let requestBodies: Record<string, unknown>[] = [];

beforeEach(async () => {
requestBodies = [];
// Initializing user and bitgo for creating shares for nocks
const userSigningKey = MPC.keyCombine(userKeyShare.pShare, [bitgoKeyShare.nShares[1], backupKeyShare.nShares[1]]);
const bitgoSigningKey = MPC.keyCombine(bitgoKeyShare.pShare, [
Expand Down Expand Up @@ -681,6 +691,7 @@ describe('TSS Ecdsa Utils:', async function () {
signatureShare: signatureShareOneFromUser,
response: signatureShareOneFromBitgo,
tssType: 'ecdsa',
requestBodies,
});
/** END STEP ONE */

Expand Down Expand Up @@ -725,6 +736,7 @@ describe('TSS Ecdsa Utils:', async function () {
signatureShare: signatureShareTwoFromUser,
response: signatureShareTwoFromBitgo,
tssType: 'ecdsa',
requestBodies,
});
/** END STEP TWO */

Expand Down Expand Up @@ -779,6 +791,7 @@ describe('TSS Ecdsa Utils:', async function () {
signatureShare: signatureShareThreeFromUser,
response: signatureShareThreeFromBitgo,
tssType: 'ecdsa',
requestBodies,
});
/* END STEP THREE */
const signature = MPC.constructSignature([userSShare, getBitGoSShare]);
Expand Down Expand Up @@ -826,6 +839,121 @@ describe('TSS Ecdsa Utils:', async function () {
userGpgActual.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
});

it('sends attestation only with the first signature share', async function () {
await setupSignTxRequestNocks(false, userSignShare, aShare, dShare, enterpriseData);
sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);

await tssUtils.signTxRequest({
txRequest,
prv: JSON.stringify({
pShare: userKeyShare.pShare,
bitgoNShare: bitgoKeyShare.nShares[1],
backupNShare: backupKeyShare.nShares[1],
}),
reqId,
recipientSource: TssTxRecipientSource.Explicit,
txParams: {
type: 'transfer',
recipients: [{ address: '0xa1cfb9d51c0af191ff21c5f0f01723e056f7dc12', amount: '1788849860263936' }],
},
attestation,
});

requestBodies.should.have.length(3);
assert.deepStrictEqual(requestBodies[0].attestation, attestation);
requestBodies.slice(1).forEach((body) => body.should.not.have.property('attestation'));
});

it('omits attestation from every signature share when absent', async function () {
await setupSignTxRequestNocks(false, userSignShare, aShare, dShare, enterpriseData);
sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);

await tssUtils.signTxRequest({
txRequest,
prv: JSON.stringify({
pShare: userKeyShare.pShare,
bitgoNShare: bitgoKeyShare.nShares[1],
backupNShare: backupKeyShare.nShares[1],
}),
reqId,
recipientSource: TssTxRecipientSource.Explicit,
txParams: {
type: 'transfer',
recipients: [{ address: '0xa1cfb9d51c0af191ff21c5f0f01723e056f7dc12', amount: '1788849860263936' }],
},
});

requestBodies.should.have.length(3);
requestBodies.forEach((body) => body.should.not.have.property('attestation'));
});

async function signWithExternalSigner(attestation?: {
signature: string;
credentialId: string;
clientDataJSON: string;
authenticatorData: string;
}) {
await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData);
let externalOShare: ECDSA.OShare | undefined;
sinon.stub(baseCoin, 'verifyTransaction').resolves(true);

return wallet.signTransaction({
txRequestId,
...(attestation ? { attestation } : {}),
customPaillierModulusGeneratingFunction: async () => ({ userPaillierModulus: 'test-paillier-modulus' }),
customKShareGeneratingFunction: async () => ({
kShare: userSignShare.kShare,
wShare: userSignShare.wShare,
privateShareProof: 'test-private-share-proof',
vssProof: 'test-vss-proof',
userPublicGpgKey: userGpgKey.publicKey,
publicShare: 'test-public-share',
encryptedSignerOffsetShare: 'test-encrypted-offset-share',
}),
customMuDeltaShareGeneratingFunction: async () => {
const userGammaAndMuShares = await ECDSAMethods.createUserGammaAndMuShare(userSignShare.wShare, aShare);
const userOShareAndDShares = MPC.signCombine({
gShare: userGammaAndMuShares.gShare as ECDSA.GShare,
signIndex: { i: 1, j: 3 },
});
const userOShare = userOShareAndDShares.oShare as ECDSA.OShare;
externalOShare = userOShare;
return {
muDShare: {
muShare: userGammaAndMuShares.muShare!,
dShare: userOShareAndDShares.dShare as ECDSA.DShare,
i: ShareKeyPosition.BITGO,
},
oShare: userOShare,
};
},
customSShareGeneratingFunction: async ({ dShareFromBitgo }) => {
assert.ok(externalOShare);
return MPC.sign(
Buffer.from(txRequest.unsignedTxs[0].signableHex, 'hex'),
externalOShare,
dShareFromBitgo,
createKeccakHash('keccak256') as Hash
);
},
});
}

it('external signer sends attestation only with the first signature share', async function () {
await signWithExternalSigner(attestation);

requestBodies.should.have.length(3);
assert.deepStrictEqual(requestBodies[0].attestation, attestation);
requestBodies.slice(1).forEach((body) => body.should.not.have.property('attestation'));
});

it('external signer omits attestation from every signature share when absent', async function () {
await signWithExternalSigner();

requestBodies.should.have.length(3);
requestBodies.forEach((body) => body.should.not.have.property('attestation'));
});

it('signTxRequest should succeed with txRequest id as input', async function () {
const sendShareSpy = sinon.spy(ECDSAMethods, 'sendShareToBitgo' as any);
await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { Hash } from 'crypto';
import {
BaseCoin,
Expand Down Expand Up @@ -31,6 +33,7 @@ const createKeccakHash = require('keccak');
interface SignatureShareApiBody {
signatureShares: SignatureShareRecord[];
signerGpgPublicKey: string;
attestation?: unknown;
}

describe('signTxRequest:', function () {
Expand All @@ -41,6 +44,12 @@ describe('signTxRequest:', function () {
let bitgoGpgKey: openpgp.SerializedKeyPair<string>;
const coinName = 'hteth';

const attestation = {
signature: 'test-signature',
credentialId: 'test-credential',
clientDataJSON: 'test-client-data',
authenticatorData: 'test-authenticator-data',
};
const reqId = new RequestTracer();
const txRequestId = 'randomTxReqId';
const signableHex = 'e27aecaea559fbedc9ae8a22b0ab6654c2d686403c2aeb434b302545c94eed3b';
Expand Down Expand Up @@ -177,6 +186,7 @@ describe('signTxRequest:', function () {
afterEach(function () {
bitgoParty.endSession();
nock.cleanAll();
sinon.restore();
});

it('successfully signs a txRequest with user key for a dkls hot wallet with WP', async function () {
Expand All @@ -200,6 +210,55 @@ describe('signTxRequest:', function () {
nockPromises[2].isDone().should.be.true();
});

it('sends attestation only with the first MPCv2 signature submission', async function () {
sinon.stub(baseCoin, 'verifyTransaction').resolves(true);
const requestBodies: Record<string, unknown>[] = [];
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey, 0, requestBodies),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey, 0, 0, requestBodies),
await nockTxRequestResponseSignatureShareRoundThree(txRequest, requestBodies),
await nockSendTxRequest(txRequest),
];
await Promise.all(nockPromises);

const userShare = fs.readFileSync(shareFiles[vector.party1]);
await tssUtils.signTxRequest({
txRequest,
prv: Buffer.from(userShare).toString('base64'),
reqId,
recipientSource: TssTxRecipientSource.Explicit,
txParams: { recipients: [{ address: '0x0000000000000000000000000000000000000001', amount: '1' }] },
attestation,
});
requestBodies.should.have.length(3);
assert.deepStrictEqual(requestBodies[0].attestation, attestation);
requestBodies.slice(1).forEach((body) => body.should.not.have.property('attestation'));
});

it('omits attestation from every MPCv2 signature submission when absent', async function () {
sinon.stub(baseCoin, 'verifyTransaction').resolves(true);
const requestBodies: Record<string, unknown>[] = [];
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey, 0, requestBodies),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey, 0, 0, requestBodies),
await nockTxRequestResponseSignatureShareRoundThree(txRequest, requestBodies),
await nockSendTxRequest(txRequest),
];
await Promise.all(nockPromises);

const userShare = fs.readFileSync(shareFiles[vector.party1]);
await tssUtils.signTxRequest({
txRequest,
prv: Buffer.from(userShare).toString('base64'),
reqId,
recipientSource: TssTxRecipientSource.Explicit,
txParams: { recipients: [{ address: '0x0000000000000000000000000000000000000001', amount: '1' }] },
});

requestBodies.should.have.length(3);
requestBodies.forEach((body) => body.should.not.have.property('attestation'));
});

it('successfully signs when recipientSource is explicit and txParams.recipients is non-empty', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),
Expand Down Expand Up @@ -465,7 +524,8 @@ async function nockTxRequestResponseSignatureShareRoundOne(
bitgoSession: DklsDsg.Dsg,
txRequest: TxRequest,
bitgoGpgKey: openpgp.SerializedKeyPair<string>,
partyId: 0 | 1 = 0
partyId: 0 | 1 = 0,
requestBodies?: Record<string, unknown>[]
): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
return nock('https://bitgo.fakeurl')
Expand All @@ -476,6 +536,7 @@ async function nockTxRequestResponseSignatureShareRoundOne(
)
.times(1)
.reply(200, async (uri, body: SignatureShareApiBody) => {
requestBodies?.push(body as unknown as Record<string, unknown>);
// Do the actual signing on BitGo's side based on User's messages
const signatureShare = JSON.parse(body.signatureShares[0].share) as MPCv2SignatureShareRound1Input;
const deserializedMessages = DklsTypes.deserializeMessages({
Expand Down Expand Up @@ -544,7 +605,8 @@ async function nockTxRequestResponseSignatureShareRoundTwo(
txRequest: TxRequest,
bitgoGpgKey: openpgp.SerializedKeyPair<string>,
partyId: 0 | 1 = 0,
rateLimitErrorCount = 0
rateLimitErrorCount = 0,
requestBodies?: Record<string, unknown>[]
): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
const scope = nock('https://bitgo.fakeurl');
Expand All @@ -570,6 +632,7 @@ async function nockTxRequestResponseSignatureShareRoundTwo(
)
.times(1)
.reply(200, async (uri, body: SignatureShareApiBody) => {
requestBodies?.push(body as unknown as Record<string, unknown>);
// Do the actual signing on BitGo's side based on User's messages
const parsedSignatureShare = JSON.parse(body.signatureShares[0].share) as MPCv2SignatureShareRound2Input;
const serializedMessages = await DklsComms.decryptAndVerifyIncomingMessages(
Expand Down Expand Up @@ -645,7 +708,10 @@ async function nockTxRequestResponseSignatureShareRoundTwo(
});
}

async function nockTxRequestResponseSignatureShareRoundThree(txRequest: TxRequest): Promise<nock.Scope> {
async function nockTxRequestResponseSignatureShareRoundThree(
txRequest: TxRequest,
requestBodies?: Record<string, unknown>[]
): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
return nock('https://bitgo.fakeurl')
.post(
Expand All @@ -654,9 +720,8 @@ async function nockTxRequestResponseSignatureShareRoundThree(txRequest: TxReques
(JSON.parse(body.signatureShares[0].share) as MPCv2SignatureShareRound3Input).type === 'round3Input'
)
.times(1)
.reply(200, async (uri, body) => {
// Do the actual signing on BitGo's side based on User's messages

.reply(200, async (_uri, body: SignatureShareApiBody) => {
requestBodies?.push(body as unknown as Record<string, unknown>);
return {
txRequestId: txRequest.txRequestId,
};
Expand Down
Loading
Loading