PHP WebShell
Текущая директория: /opt/BitGoJS/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2
Просмотр файла: signTxRequest.ts
import { Hash } from 'crypto';
import {
BaseCoin,
BitgoGPGPublicKey,
common,
ECDSAUtils,
RequestTracer,
SignatureShareRecord,
SignatureShareType,
TxRequest,
Wallet,
} from '@bitgo/sdk-core';
import { DklsDsg, DklsTypes, DklsComms } from '@bitgo/sdk-lib-mpc';
import * as fs from 'fs';
import { getRoute } from '../common';
import {
MPCv2SignatureShareRound1Output,
MPCv2SignatureShareRound1Input,
MPCv2SignatureShareRound2Input,
MPCv2SignatureShareRound2Output,
MPCv2SignatureShareRound3Input,
MPCv2PartyFromStringOrNumber,
} from '@bitgo/public-types';
import * as openpgp from 'openpgp';
import * as nock from 'nock';
import { TestableBG, TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../../../../src';
const createKeccakHash = require('keccak');
interface SignatureShareApiBody {
signatureShares: SignatureShareRecord[];
signerGpgPublicKey: string;
}
describe('signTxRequest:', function () {
let tssUtils: ECDSAUtils.EcdsaMPCv2Utils;
let wallet: Wallet;
let bitgo: TestableBG & BitGo;
let baseCoin: BaseCoin;
let bitgoGpgKey: openpgp.SerializedKeyPair<string>;
const coinName = 'hteth';
const reqId = new RequestTracer();
const txRequestId = 'randomTxReqId';
const signableHex = 'e27aecaea559fbedc9ae8a22b0ab6654c2d686403c2aeb434b302545c94eed3b';
const txRequest: TxRequest = {
txRequestId,
enterpriseId: '4517abfb-f567-4b7a-9f91-407509d29403',
transactions: [
{
unsignedTx: {
serializedTxHex: 'TOO MANY SECRETS',
signableHex,
derivationPath: 'm/0', // Needs this when key derivation is supported
},
state: 'pendingSignature',
signatureShares: [],
},
],
unsignedTxs: [],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
apiVersion: 'full',
};
const txRequestForMessageSigning: TxRequest = {
txRequestId,
enterpriseId: '4517abfb-f567-4b7a-9f91-407509d29403',
messages: [
{
messageRaw: 'TOO MANY SECRETS',
derivationPath: 'm/0',
state: 'pendingSignature',
signatureShares: [],
},
],
unsignedTxs: [],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
apiVersion: 'full',
};
const vector = {
party1: 0,
party2: 2,
party3: 1,
};
// To generate the fixtures, run DKG as in the dklsDkg.ts tests and save the resulting party.getKeyShare in a file by doing fs.writeSync(party.getKeyShare()).
const shareFiles = [
`${__dirname}/fixtures/userShare`,
`${__dirname}/fixtures/backupShare`,
`${__dirname}/fixtures/bitgoShare`,
];
let bitgoParty: DklsDsg.Dsg;
before(async () => {
bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
bitgo.initializeTestVars();
const bgUrl = common.Environments[bitgo.getEnv()].uri;
bitgoGpgKey = await openpgp.generateKey({
userIDs: [
{
name: 'bitgo',
},
],
curve: 'secp256k1',
config: {
rejectCurves: new Set(),
},
});
const constants = {
mpc: {
bitgoPublicKey: bitgoGpgKey.publicKey,
bitgoMPCv2PublicKey: bitgoGpgKey.publicKey,
},
};
nock(bgUrl).get('/api/v1/client/constants').times(20).reply(200, { ttl: 3600, constants });
baseCoin = bitgo.coin(coinName);
let hashFn: Hash;
try {
hashFn = baseCoin.getHashFunction();
} catch (err) {
hashFn = createKeccakHash('keccak256') as Hash;
}
const hashBuffer = hashFn.update(Buffer.from(signableHex, 'hex')).digest();
// Nock out both the user and bitgo side responses to create valid signatures
bitgoParty = new DklsDsg.Dsg(
fs.readFileSync(shareFiles[vector.party2]),
vector.party2,
txRequest.transactions![0].unsignedTx.derivationPath,
hashBuffer
);
// // Round 1 ////
const walletData = {
id: txRequest.walletId,
enterprise: txRequest.enterpriseId,
coin: coinName,
coinSpecific: {},
multisigType: 'tss',
multisigTypeVersion: 'MPCv2',
};
wallet = new Wallet(bitgo, baseCoin, walletData);
tssUtils = new ECDSAUtils.EcdsaMPCv2Utils(bitgo, baseCoin, wallet);
});
beforeEach(async function () {
await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, txRequest.enterpriseId!, bitgoGpgKey);
});
after(function () {
nock.cleanAll();
});
afterEach(function () {
bitgoParty.endSession();
nock.cleanAll();
});
it('successfully signs a txRequest with user key for a dkls hot wallet with WP', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey),
await nockTxRequestResponseSignatureShareRoundThree(txRequest),
await nockSendTxRequest(txRequest),
];
await Promise.all(nockPromises);
const userShare = fs.readFileSync(shareFiles[vector.party1]);
const userPrvBase64 = Buffer.from(userShare).toString('base64');
await tssUtils.signTxRequest({
txRequest,
prv: userPrvBase64,
reqId,
});
nockPromises[0].isDone().should.be.true();
nockPromises[1].isDone().should.be.true();
nockPromises[2].isDone().should.be.true();
});
it('successfully signs a txRequest with backup key for a dkls hot wallet with WP', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey, 1),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey, 1),
await nockTxRequestResponseSignatureShareRoundThree(txRequest),
await nockSendTxRequest(txRequest),
];
await Promise.all(nockPromises);
const backupShare = fs.readFileSync(shareFiles[vector.party3]);
const backupPrvBase64 = Buffer.from(backupShare).toString('base64');
await tssUtils.signTxRequest({
txRequest,
prv: backupPrvBase64,
mpcv2PartyId: 1,
reqId,
});
nockPromises[0].isDone().should.be.true();
nockPromises[1].isDone().should.be.true();
nockPromises[2].isDone().should.be.true();
});
it('successfully signs a txRequest with a message for a dkls hot wallet with WP', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequestForMessageSigning, bitgoGpgKey),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequestForMessageSigning, bitgoGpgKey),
await nockTxRequestResponseSignatureShareRoundThree(txRequestForMessageSigning),
await nockSendTxRequest(txRequestForMessageSigning),
];
await Promise.all(nockPromises);
const userShare = fs.readFileSync(shareFiles[vector.party1]);
const userPrvBase64 = Buffer.from(userShare).toString('base64');
await tssUtils.signTxRequest({
txRequest,
prv: userPrvBase64,
reqId,
});
nockPromises[0].isDone().should.be.true();
nockPromises[1].isDone().should.be.true();
nockPromises[2].isDone().should.be.true();
});
it('successfully signs a txRequest for a dkls hot wallet after receiving multiple 429 errors', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey, 0, 3),
await nockTxRequestResponseSignatureShareRoundThree(txRequest),
await nockSendTxRequest(txRequest),
];
await Promise.all(nockPromises);
const userShare = fs.readFileSync(shareFiles[vector.party1]);
const userPrvBase64 = Buffer.from(userShare).toString('base64');
await tssUtils.signTxRequest({
txRequest,
prv: userPrvBase64,
reqId,
});
nockPromises[0].isDone().should.be.true();
nockPromises[1].isDone().should.be.true();
nockPromises[2].isDone().should.be.true();
});
it('fails to signs a txRequest for a dkls hot wallet after receiving over 3 429 errors', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey, 0, 4),
];
await Promise.all(nockPromises);
const userShare = fs.readFileSync(shareFiles[vector.party1]);
const userPrvBase64 = Buffer.from(userShare).toString('base64');
await tssUtils
.signTxRequest({
txRequest,
prv: userPrvBase64,
reqId,
})
.should.be.rejectedWith('Too many requests, slow down!');
nockPromises[0].isDone().should.be.true();
nockPromises[1].isDone().should.be.false();
});
});
export function getBitGoPartyGpgKeyPrv(key: openpgp.SerializedKeyPair<string>): DklsTypes.PartyGpgKey {
return {
partyId: 2,
gpgKey: key.privateKey,
};
}
export function getUserPartyGpgKeyPublic(userPubKey: string, partyId: 0 | 1 = 0): DklsTypes.PartyGpgKey {
return {
partyId: partyId,
gpgKey: userPubKey,
};
}
async function nockTxRequestResponseSignatureShareRoundOne(
bitgoSession: DklsDsg.Dsg,
txRequest: TxRequest,
bitgoGpgKey: openpgp.SerializedKeyPair<string>,
partyId: 0 | 1 = 0
): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
return nock('https://bitgo.fakeurl')
.persist(true)
.post(
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`,
(body) => (JSON.parse(body.signatureShares[0].share) as MPCv2SignatureShareRound1Input).type === 'round1Input'
)
.times(1)
.reply(200, async (uri, body: SignatureShareApiBody) => {
// 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({
p2pMessages: [],
broadcastMessages: [
{
from: signatureShare.data.msg1.from,
payload: signatureShare.data.msg1.message,
},
],
});
if (signatureShare.type === 'round1Input') {
const bitgoToUserRound1BroadcastMsg = await bitgoSession.init();
const bitgoToUserRound2Msg = bitgoSession.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: deserializedMessages.broadcastMessages,
});
const serializedBitGoToUserRound1And2Msgs = DklsTypes.serializeMessages({
p2pMessages: bitgoToUserRound2Msg.p2pMessages,
broadcastMessages: [bitgoToUserRound1BroadcastMsg],
});
const authEncMessages = await DklsComms.encryptAndAuthOutgoingMessages(
serializedBitGoToUserRound1And2Msgs,
[getUserPartyGpgKeyPublic(body.signerGpgPublicKey, partyId)],
[getBitGoPartyGpgKeyPrv(bitgoGpgKey)]
);
const bitgoToUserSignatureShare: MPCv2SignatureShareRound1Output = {
type: 'round1Output',
data: {
msg1: {
from: authEncMessages.broadcastMessages[0].from as MPCv2PartyFromStringOrNumber,
signature: authEncMessages.broadcastMessages[0].payload.signature,
message: authEncMessages.broadcastMessages[0].payload.message,
},
msg2: {
from: authEncMessages.p2pMessages[0].from as MPCv2PartyFromStringOrNumber,
to: authEncMessages.p2pMessages[0].to as MPCv2PartyFromStringOrNumber,
encryptedMessage: authEncMessages.p2pMessages[0].payload.encryptedMessage,
signature: authEncMessages.p2pMessages[0].payload.signature,
},
},
};
return {
txRequestId: txRequest.txRequestId,
transactions: [
{
signatureShares: [
{
from: SignatureShareType.BITGO,
to: partyId === 0 ? SignatureShareType.USER : SignatureShareType.BACKUP,
share: JSON.stringify(bitgoToUserSignatureShare),
},
],
},
],
};
}
});
}
async function nockTxRequestResponseSignatureShareRoundTwo(
bitgoSession: DklsDsg.Dsg,
txRequest: TxRequest,
bitgoGpgKey: openpgp.SerializedKeyPair<string>,
partyId: 0 | 1 = 0,
rateLimitErrorCount = 0
): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
const scope = nock('https://bitgo.fakeurl');
if (rateLimitErrorCount > 0) {
scope
.post(
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`,
(body) => (JSON.parse(body.signatureShares[0].share) as MPCv2SignatureShareRound2Input).type === 'round2Input'
)
.times(rateLimitErrorCount)
.reply(429, {
error: 'Too many requests, slow down!',
name: 'TooManyRequests',
requestId: 'cm5qx01lh0013b2ek2sxl4w00',
context: {},
});
}
return scope
.post(
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`,
(body) => (JSON.parse(body.signatureShares[0].share) as MPCv2SignatureShareRound2Input).type === 'round2Input'
)
.times(1)
.reply(200, async (uri, body: SignatureShareApiBody) => {
// 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(
{
p2pMessages: [
{
from: parsedSignatureShare.data.msg2.from,
to: parsedSignatureShare.data.msg2.to,
payload: {
encryptedMessage: parsedSignatureShare.data.msg2.encryptedMessage,
signature: parsedSignatureShare.data.msg2.signature,
},
},
{
from: parsedSignatureShare.data.msg3.from,
to: parsedSignatureShare.data.msg3.to,
payload: {
encryptedMessage: parsedSignatureShare.data.msg3.encryptedMessage,
signature: parsedSignatureShare.data.msg3.signature,
},
},
],
broadcastMessages: [],
},
[getUserPartyGpgKeyPublic(body.signerGpgPublicKey, partyId)],
[getBitGoPartyGpgKeyPrv(bitgoGpgKey)]
);
const deserializedMessages = DklsTypes.deserializeMessages({
p2pMessages: [serializedMessages.p2pMessages[0]],
broadcastMessages: [],
});
if (parsedSignatureShare.type === 'round2Input') {
const bitgoToUserRound3Msg = bitgoSession.handleIncomingMessages(deserializedMessages);
const serializedBitGoToUserRound3Msgs = DklsTypes.serializeMessages(bitgoToUserRound3Msg);
const authEncMessages = await DklsComms.encryptAndAuthOutgoingMessages(
serializedBitGoToUserRound3Msgs,
[getUserPartyGpgKeyPublic(body.signerGpgPublicKey, partyId)],
[getBitGoPartyGpgKeyPrv(bitgoGpgKey)]
);
const bitgoToUserSignatureShare: MPCv2SignatureShareRound2Output = {
type: 'round2Output',
data: {
msg3: {
from: authEncMessages.p2pMessages[0].from as MPCv2PartyFromStringOrNumber,
to: authEncMessages.p2pMessages[0].to as MPCv2PartyFromStringOrNumber,
encryptedMessage: authEncMessages.p2pMessages[0].payload.encryptedMessage,
signature: authEncMessages.p2pMessages[0].payload.signature,
},
},
};
return {
txRequestId: txRequest.txRequestId,
transactions: [
{
signatureShares: [
{
from: partyId === 0 ? SignatureShareType.USER : SignatureShareType.BACKUP,
to: SignatureShareType.BITGO,
share: 'some old share we dont care about',
},
{
from: SignatureShareType.BITGO,
to: partyId === 0 ? SignatureShareType.USER : SignatureShareType.BACKUP,
share: JSON.stringify(bitgoToUserSignatureShare),
},
],
},
],
};
}
});
}
async function nockTxRequestResponseSignatureShareRoundThree(txRequest: TxRequest): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
return nock('https://bitgo.fakeurl')
.post(
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`,
(body: SignatureShareApiBody) =>
(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
return {
txRequestId: txRequest.txRequestId,
};
});
}
async function nockSendTxRequest(txRequest: TxRequest): Promise<nock.Scope> {
const transactions = getRoute('ecdsa');
return nock('https://bitgo.fakeurl')
.post(`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/send`)
.times(1)
.reply(200, {
txRequestId: txRequest.txRequestId,
});
}
async function nockGetBitgoPublicKeyBasedOnFeatureFlags(
coin: string,
enterpriseId: string,
bitgoGpgKeyPair: openpgp.SerializedKeyPair<string>
): Promise<BitgoGPGPublicKey> {
const bitgoGPGPublicKeyResponse: BitgoGPGPublicKey = {
name: 'irrelevant',
publicKey: bitgoGpgKeyPair.publicKey,
mpcv2PublicKey: bitgoGpgKeyPair.publicKey,
enterpriseId,
};
nock('https://bitgo.fakeurl')
.get(`/api/v2/${coin}/tss/pubkey`)
.times(4)
.query({ enterpriseId })
.reply(200, bitgoGPGPublicKeyResponse);
return bitgoGPGPublicKeyResponse;
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!