PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-core/src/bitgo/utils/tss/ecdsa
Просмотр файла: ecdsa.ts
import assert from 'assert';
import { Buffer } from 'buffer';
import * as openpgp from 'openpgp';
import { Key, SerializedKeyPair } from 'openpgp';
import { Hash } from 'crypto';
import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes, hexToBigInt, minModulusBitLength } from '@bitgo/sdk-lib-mpc';
import { bip32 } from '@bitgo/utxo-lib';
import { ECDSA, Ecdsa } from '../../../../account-lib/mpc/tss';
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
import ECDSAMethods, { ECDSAMethodTypes } from '../../../tss/ecdsa';
import { KeychainsTriplet } from '../../../baseCoin';
import {
BitGoProofSignatures,
CreateEcdsaBitGoKeychainParams,
CreateEcdsaKeychainParams,
DecryptableNShare,
GetBitGoChallengesApi,
KeyShare,
} from './types';
import {
BackupKeyShare,
BitgoHeldBackupKeyShare,
CustomKShareGeneratingFunction,
CustomMuDeltaShareGeneratingFunction,
CustomPaillierModulusGetterFunction,
CustomSShareGeneratingFunction,
RequestType,
TSSParams,
TSSParamsForMessage,
TSSParamsForMessageWithPrv,
TSSParamsWithPrv,
TxRequest,
} from '../baseTypes';
import { getTxRequest } from '../../../tss';
import { AShare, DShare, EncryptedNShare, SendShareType, SShare, WShare, OShare } from '../../../tss/ecdsa/types';
import { createShareProof, generateGPGKeyPair, getBitgoGpgPubKey } from '../../opengpgUtils';
import { BitGoBase } from '../../../bitgoBase';
import { verifyWalletSignature } from '../../../tss/ecdsa/ecdsa';
import { signMessageWithDerivedEcdhKey, verifyEcdhSignature } from '../../../ecdh';
import { getTxRequestChallenge } from '../../../tss/common';
import {
ShareKeyPosition,
TssEcdsaStep1ReturnMessage,
TssEcdsaStep2ReturnMessage,
TxRequestChallengeResponse,
} from '../../../tss/types';
import { BaseEcdsaUtils } from './base';
import { IRequestTracer } from '../../../../api';
const encryptNShare = ECDSAMethods.encryptNShare;
/** @inheritdoc */
export class EcdsaUtils extends BaseEcdsaUtils {
async finalizeBitgoHeldBackupKeyShare(
keyId: string,
commonKeychain: string,
userKeyShare: KeyShare,
bitgoKeychain: Keychain,
userGpgKey: SerializedKeyPair<string>,
thirdPartyBackupPublicGpgKey: Key
): Promise<BitgoHeldBackupKeyShare> {
const encryptedUserToBackupShare = await encryptNShare(
userKeyShare,
2,
thirdPartyBackupPublicGpgKey.armor(),
userGpgKey
);
const bitgoToBackupKeyShare = bitgoKeychain.keyShares?.find(
(keyShare) => keyShare.from === 'bitgo' && keyShare.to === 'backup'
);
const userPublicShare = Buffer.concat([
Buffer.from(userKeyShare.nShares[2].y, 'hex'),
Buffer.from(userKeyShare.nShares[2].chaincode, 'hex'),
]).toString('hex');
assert(bitgoToBackupKeyShare);
const keyResponse = await this.bitgo
.put(this.baseCoin.url(`/krs/backupkeys/${keyId}`))
.send({
commonKeychain,
keyShares: [
{
from: 'user',
to: 'backup',
publicShare: userPublicShare,
privateShare: encryptedUserToBackupShare.encryptedPrivateShare,
privateShareProof: encryptedUserToBackupShare.privateShareProof,
vssProof: encryptedUserToBackupShare.vssProof,
},
bitgoToBackupKeyShare,
],
})
.result();
if (!keyResponse || !keyResponse.commonKeychain) {
throw new Error('Failed backup key verification.');
}
return {
id: keyResponse.id,
keyShares: keyResponse.keyShares,
commonKeychain: keyResponse.commonKeychain,
};
}
/** @inheritdoc */
async createKeychains(params: {
passphrase: string;
enterprise?: string | undefined;
originalPasscodeEncryptionCode?: string | undefined;
}): Promise<KeychainsTriplet> {
const MPC = new Ecdsa();
const m = 2;
const n = 3;
const userKeyShare = await MPC.keyShare(1, m, n);
const userGpgKey = await generateGPGKeyPair('secp256k1');
const backupKeyShare = await this.createBackupKeyShares();
const backupGpgKey = await this.getBackupGpgPubKey();
// Get the BitGo public key based on user/enterprise feature flags
// If it doesn't work, use the default public key from the constants
const bitgoPublicGpgKey =
(await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise)) ?? this.bitgoPublicGpgKey;
const bitgoKeychain = await this.createBitgoKeychain({
userGpgKey,
backupGpgKey,
bitgoPublicGpgKey,
userKeyShare,
backupKeyShare,
enterprise: params.enterprise,
});
const userKeychainPromise = this.createUserKeychain({
userGpgKey,
backupGpgKey,
bitgoPublicGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase: params.passphrase,
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
});
const backupKeychainPromise = this.createBackupKeychain({
userGpgKey,
backupGpgKey,
bitgoPublicGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase: params.passphrase,
});
const [userKeychain, backupKeychain] = await Promise.all([userKeychainPromise, backupKeychainPromise]);
return {
userKeychain,
backupKeychain,
bitgoKeychain,
};
}
async createBackupKeyShares(): Promise<BackupKeyShare> {
const MPC = new Ecdsa();
const m = 2;
const n = 3;
const backupKeyShare = {
userHeldKeyShare: await MPC.keyShare(2, m, n),
};
return backupKeyShare;
}
createUserKeychain({
userGpgKey,
backupGpgKey,
bitgoPublicGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase,
originalPasscodeEncryptionCode,
}: CreateEcdsaKeychainParams): Promise<Keychain> {
if (!passphrase) {
throw new Error('Please provide a wallet passphrase');
}
assert(backupKeyShare.userHeldKeyShare);
return this.createParticipantKeychain(
userGpgKey,
backupGpgKey as SerializedKeyPair<string>,
bitgoPublicGpgKey,
1,
userKeyShare,
backupKeyShare.userHeldKeyShare,
bitgoKeychain,
passphrase,
originalPasscodeEncryptionCode
);
}
async createBackupKeychain({
userGpgKey,
userKeyShare,
backupGpgKey,
backupKeyShare,
bitgoKeychain,
bitgoPublicGpgKey,
passphrase,
}: CreateEcdsaKeychainParams): Promise<Keychain> {
assert(backupKeyShare.userHeldKeyShare);
assert(passphrase);
return this.createParticipantKeychain(
userGpgKey,
backupGpgKey as SerializedKeyPair<string>,
bitgoPublicGpgKey,
2,
userKeyShare,
backupKeyShare.userHeldKeyShare,
bitgoKeychain,
passphrase
);
}
/** @inheritdoc */
async createBitgoKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
enterprise,
bitgoPublicGpgKey,
}: CreateEcdsaBitGoKeychainParams): Promise<Keychain> {
const recipientIndex = 3;
const userToBitgoShare = await encryptNShare(userKeyShare, recipientIndex, bitgoPublicGpgKey.armor(), userGpgKey);
const backupToBitgoShare = await this.getBackupEncryptedNShare(
backupKeyShare,
recipientIndex,
bitgoPublicGpgKey.armor(),
backupGpgKey as SerializedKeyPair<string>
);
const createBitGoMPCParams: AddKeychainOptions = {
keyType: 'tss' as KeyType,
source: 'bitgo',
keyShares: [
{
from: 'user',
to: 'bitgo',
publicShare: userToBitgoShare.publicShare,
privateShare: userToBitgoShare.encryptedPrivateShare,
n: userToBitgoShare.n,
vssProof: userToBitgoShare.vssProof,
privateShareProof: userToBitgoShare.privateShareProof,
},
{
from: 'backup',
to: 'bitgo',
publicShare: backupToBitgoShare.publicShare,
privateShare: backupToBitgoShare.encryptedPrivateShare,
n: backupToBitgoShare.n,
vssProof: backupToBitgoShare.vssProof,
privateShareProof: backupToBitgoShare.privateShareProof,
},
],
userGPGPublicKey: userGpgKey.publicKey,
backupGPGPublicKey: (backupGpgKey as SerializedKeyPair<string>).publicKey,
enterprise: enterprise,
algoUsed: 'ecdsa',
};
return await this.baseCoin.keychains().add(createBitGoMPCParams);
}
/**
* This builds the relevant backup encryptedNShare based on whether the
* backup key is user or third party generated
* @param backupShare can either have key shares from the user or third party
* @param recipientIndex index of the party receiving the backup shares
* @param recipientGpgPublicArmor gpg armor of the party receiving the backup shares
* @param backupGpgKey backup gpg key
* @param isThirdPartyBackup whether the backup is generated by third party
*/
async getBackupEncryptedNShare(
backupShare: BackupKeyShare,
recipientIndex: number,
recipientGpgPublicArmor: string,
backupGpgKey: SerializedKeyPair<string>
): Promise<EncryptedNShare> {
assert(backupShare.userHeldKeyShare);
const backupToRecipientShare = await encryptNShare(
backupShare.userHeldKeyShare,
recipientIndex,
recipientGpgPublicArmor,
backupGpgKey
);
return backupToRecipientShare;
}
/** @inheritdoc */
async createParticipantKeychain(
userGpgKey: openpgp.SerializedKeyPair<string>,
userLocalBackupGpgKey: openpgp.SerializedKeyPair<string>,
bitgoPublicGpgKey: Key,
recipientIndex: number,
userKeyShare: KeyShare,
backupKeyShare: KeyShare,
bitgoKeychain: Keychain,
passphrase: string,
originalPasscodeEncryptionCode?: string
): Promise<Keychain> {
const bitgoKeyShares = bitgoKeychain.keyShares;
if (!bitgoKeyShares) {
throw new Error('Missing BitGo key shares');
}
if (!bitgoKeychain.commonKeychain) {
throw new Error(`Missing common key chain: ${bitgoKeychain.commonKeychain}`);
}
let recipient: string;
let keyShare: KeyShare;
let otherShare: KeyShare;
let recipientGpgKey: openpgp.SerializedKeyPair<string>;
let senderGpgKey: openpgp.SerializedKeyPair<string>;
if (recipientIndex === 1) {
keyShare = userKeyShare;
otherShare = backupKeyShare;
recipient = 'user';
recipientGpgKey = userGpgKey;
senderGpgKey = userLocalBackupGpgKey;
} else if (recipientIndex === 2) {
keyShare = backupKeyShare;
otherShare = userKeyShare;
recipient = 'backup';
recipientGpgKey = userLocalBackupGpgKey;
senderGpgKey = userGpgKey;
} else {
throw new Error('Invalid user index');
}
const bitGoToRecipientShare = bitgoKeyShares.find(
(keyShare) => keyShare.from === 'bitgo' && keyShare.to === recipient
);
if (!bitGoToRecipientShare) {
throw new Error(`Missing BitGo to ${recipient} key share`);
}
const decryptedShare = await this.decryptPrivateShare(bitGoToRecipientShare.privateShare, recipientGpgKey);
await this.verifyWalletSignatures(
userGpgKey.publicKey,
userLocalBackupGpgKey.publicKey,
bitgoKeychain,
decryptedShare,
recipientIndex
);
const senderToRecipientShare = await encryptNShare(
otherShare,
recipientIndex,
recipientGpgKey.publicKey,
senderGpgKey
);
const encryptedNShares: DecryptableNShare[] = [
{
// userToBackup or backupToUser
nShare: senderToRecipientShare,
recipientPrivateArmor: recipientGpgKey.privateKey,
senderPublicArmor: senderGpgKey.publicKey,
},
{
// bitgoToRecipient
nShare: {
i: recipientIndex,
j: 3,
publicShare: bitGoToRecipientShare.publicShare,
encryptedPrivateShare: bitGoToRecipientShare.privateShare,
n: bitGoToRecipientShare.n!,
vssProof: bitGoToRecipientShare.vssProof,
privateShareProof: bitGoToRecipientShare.privateShareProof,
},
recipientPrivateArmor: recipientGpgKey.privateKey,
senderPublicArmor: bitgoPublicGpgKey.armor(),
isbs58Encoded: false,
},
];
const recipientCombinedKey = await ECDSAMethods.createCombinedKey(
keyShare,
encryptedNShares,
bitgoKeychain.commonKeychain
);
const prv = JSON.stringify(recipientCombinedKey.signingMaterial);
const recipientKeychainParams = {
source: recipient,
keyType: 'tss' as KeyType,
commonKeychain: bitgoKeychain.commonKeychain,
prv: prv,
encryptedPrv: this.bitgo.encrypt({
input: prv,
password: passphrase,
}),
originalPasscodeEncryptionCode,
};
const keychains = this.baseCoin.keychains();
return recipientIndex === 1
? await keychains.add(recipientKeychainParams)
: await keychains.createBackup(recipientKeychainParams);
}
private async createTssEcdsaStep1SigningMaterial(params: {
challenges: {
enterpriseChallenge: EcdsaTypes.SerializedEcdsaChallenges;
bitgoChallenge: TxRequestChallengeResponse;
};
prv: string;
derivationPath: string;
walletPassphrase?: string;
}): Promise<TssEcdsaStep1ReturnMessage> {
const { challenges, derivationPath, prv } = params;
const userSigningMaterial: ECDSAMethodTypes.SigningMaterial = JSON.parse(prv);
if (userSigningMaterial.pShare.i !== 1) {
throw new Error('Invalid user key');
}
if (!userSigningMaterial.backupNShare) {
throw new Error('Invalid user key - missing backupNShare');
}
const MPC = new Ecdsa();
const signingKey = MPC.keyDerive(
userSigningMaterial.pShare,
[userSigningMaterial.bitgoNShare, userSigningMaterial.backupNShare],
derivationPath
);
const bitgoIndex = ShareKeyPosition.BITGO;
const userIndex = userSigningMaterial.pShare.i;
const { ntilde: ntildea, h1: h1a, h2: h2a, p: pa } = challenges.enterpriseChallenge;
const { ntilde: ntildeb, h1: h1b, h2: h2b, p: pb, n: nb } = challenges.bitgoChallenge;
const userXShare = MPC.appendChallenge(signingKey.xShare, { ntilde: ntildea, h1: h1a, h2: h2a }, { p: pa });
const bitgoYShare = MPC.appendChallenge(
{
i: userIndex,
j: bitgoIndex,
n: nb,
},
{ ntilde: ntildeb, h1: h1b, h2: h2b },
{ p: pb }
);
const userSignShare = await ECDSAMethods.createUserSignShare(userXShare, bitgoYShare);
const u = signingKey.nShares[bitgoIndex].u;
let chaincode = userSigningMaterial.bitgoNShare.chaincode;
while (chaincode.length < 64) {
chaincode = '0' + chaincode;
}
const signerShare = bip32.fromPrivateKey(Buffer.from(u, 'hex'), Buffer.from(chaincode, 'hex')).toBase58();
const bitgoGpgKey = (await getBitgoGpgPubKey(this.bitgo)).mpcV1;
const encryptedSignerShare = (await openpgp.encrypt({
message: await openpgp.createMessage({
text: signerShare,
}),
config: {
rejectCurves: new Set(),
},
encryptionKeys: [bitgoGpgKey],
})) as string;
const userGpgKey = await generateGPGKeyPair('secp256k1');
const privateShareProof = await createShareProof(userGpgKey.privateKey, signingKey.nShares[bitgoIndex].u, 'ecdsa');
const vssProof = signingKey.nShares[bitgoIndex].v;
const userPublicGpgKey = userGpgKey.publicKey;
const publicShare = signingKey.nShares[bitgoIndex].y + signingKey.nShares[bitgoIndex].chaincode;
return {
privateShareProof: privateShareProof,
vssProof: vssProof,
publicShare: publicShare,
encryptedSignerOffsetShare: encryptedSignerShare,
userPublicGpgKey: userPublicGpgKey,
kShare: userSignShare.kShare,
wShare: params.walletPassphrase
? this.bitgo.encrypt({ input: JSON.stringify(userSignShare.wShare), password: params.walletPassphrase })
: userSignShare.wShare,
};
}
private async createTssEcdsaStep2SigningMaterial(params: {
bitgoChallenge: TxRequestChallengeResponse;
wShare: WShare;
aShareFromBitgo: Omit<AShare, 'h1' | 'h2' | 'ntilde'>;
walletPassphrase?: string;
}): Promise<TssEcdsaStep2ReturnMessage> {
// Append the BitGo challenge to the Ashare to be used in subsequent proofs
const bitgoToUserAShareWithNtilde: AShare = {
...params.aShareFromBitgo,
...params.bitgoChallenge,
};
const userGammaAndMuShares = await ECDSAMethods.createUserGammaAndMuShare(
params.wShare,
bitgoToUserAShareWithNtilde
);
const userOmicronAndDeltaShare = await ECDSAMethods.createUserOmicronAndDeltaShare(
userGammaAndMuShares.gShare as ECDSA.GShare
);
return {
muDShare: {
muShare: userGammaAndMuShares.muShare,
dShare: userOmicronAndDeltaShare.dShare,
i: userGammaAndMuShares.muShare.i,
},
oShare: params.walletPassphrase
? this.bitgo.encrypt({
input: JSON.stringify(userOmicronAndDeltaShare.oShare),
password: params.walletPassphrase,
})
: userOmicronAndDeltaShare.oShare,
};
}
getOfflineSignerPaillierModulus(params: { prv: string }): { userPaillierModulus: string } {
assert(params.prv, 'Params to get paillier modulus are missing prv.');
const userSigningMaterial: ECDSAMethodTypes.SigningMaterial = JSON.parse(params.prv);
return { userPaillierModulus: userSigningMaterial.pShare.n };
}
async createOfflineKShare(params: {
tssParams: TSSParams | TSSParamsForMessage;
challenges: {
enterpriseChallenge: EcdsaTypes.SerializedEcdsaChallenges;
bitgoChallenge: TxRequestChallengeResponse;
};
requestType: RequestType;
prv: string;
walletPassphrase: string;
}): Promise<TssEcdsaStep1ReturnMessage> {
const { tssParams, prv, requestType, challenges } = params;
assert(typeof tssParams.txRequest !== 'string', 'Invalid txRequest type');
const txRequest: TxRequest = tssParams.txRequest;
let derivationPath;
if (requestType === RequestType.tx) {
assert(
txRequest.transactions || (txRequest as TxRequest).unsignedTxs,
'Unable to find transactions in txRequest'
);
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
derivationPath = unsignedTx.derivationPath;
} else if (requestType === RequestType.message) {
// TODO BG-67299 Message signing with derivation path
derivationPath = '';
}
return this.createTssEcdsaStep1SigningMaterial({
prv: prv,
challenges: challenges,
derivationPath: derivationPath,
walletPassphrase: params.walletPassphrase,
});
}
async createOfflineMuDeltaShare(params: {
aShareFromBitgo: Omit<AShare, 'ntilde' | 'h1' | 'h2'>;
bitgoChallenge: TxRequestChallengeResponse;
encryptedWShare: string;
walletPassphrase: string;
}): Promise<TssEcdsaStep2ReturnMessage> {
const decryptedWShare = this.bitgo.decrypt({ input: params.encryptedWShare, password: params.walletPassphrase });
return await this.createTssEcdsaStep2SigningMaterial({
aShareFromBitgo: params.aShareFromBitgo,
bitgoChallenge: params.bitgoChallenge,
wShare: JSON.parse(decryptedWShare),
walletPassphrase: params.walletPassphrase,
});
}
async createOfflineSShare(params: {
tssParams: TSSParams | TSSParamsForMessage;
dShareFromBitgo: DShare;
requestType: RequestType;
encryptedOShare: string;
walletPassphrase: string;
}): Promise<SShare> {
const { tssParams, requestType, dShareFromBitgo, encryptedOShare, walletPassphrase } = params;
assert(typeof tssParams.txRequest !== 'string', 'Invalid txRequest type');
const txRequest: TxRequest = tssParams.txRequest;
let signablePayload;
if (requestType === RequestType.tx) {
assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest');
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
signablePayload = Buffer.from(unsignedTx.signableHex, 'hex');
} else if (requestType === RequestType.message) {
signablePayload = (params.tssParams as TSSParamsForMessage).bufferToSign;
}
let hash: Hash | undefined;
try {
hash = this.baseCoin.getHashFunction();
} catch (err) {
hash = undefined;
}
const decryptedOShare = this.bitgo.decrypt({ input: encryptedOShare, password: walletPassphrase });
const { i, R, s, y } = await ECDSAMethods.createUserSignatureShare(
JSON.parse(decryptedOShare),
dShareFromBitgo,
signablePayload,
hash
);
// return only required SShare without bigints from VAShare
return {
i,
R,
s,
y,
};
}
async signEcdsaTssUsingExternalSigner(
params: TSSParams | TSSParamsForMessage,
requestType: RequestType,
externalSignerPaillierModulusGetter: CustomPaillierModulusGetterFunction,
externalSignerKShareGenerator: CustomKShareGeneratingFunction,
externalSignerMuDeltaShareGenerator: CustomMuDeltaShareGeneratingFunction,
externalSignerSShareGenerator: CustomSShareGeneratingFunction
): Promise<TxRequest> {
const { txRequest } = params;
const pendingEcdsaTssInitialization = this.wallet.coinSpecific()?.pendingEcdsaTssInitialization;
if (pendingEcdsaTssInitialization) {
throw new Error(
'Wallet is not ready for TSS ECDSA signing. Please contact your enterprise admin to finish the enterprise TSS initialization.'
);
}
const txRequestObj: TxRequest = await getTxRequest(this.bitgo, this.wallet.id(), txRequest as string, params.reqId);
const { userPaillierModulus } = await externalSignerPaillierModulusGetter({ txRequest: txRequestObj });
const { enterpriseChallenge, bitgoChallenge } = await this.getEcdsaSigningChallenges(
txRequest as string,
requestType,
userPaillierModulus,
0,
params.reqId
);
const step1SigningMaterial = await externalSignerKShareGenerator({
tssParams: {
...params,
txRequest: txRequestObj,
},
challenges: { enterpriseChallenge, bitgoChallenge },
requestType: requestType,
});
// signing stage one with K share send to bitgo and receives A share
const bitgoToUserAShare = (await ECDSAMethods.sendShareToBitgo(
this.bitgo,
this.wallet.id(),
txRequestObj.txRequestId,
requestType,
SendShareType.KShare,
step1SigningMaterial.kShare,
step1SigningMaterial.encryptedSignerOffsetShare,
step1SigningMaterial.vssProof,
step1SigningMaterial.privateShareProof,
step1SigningMaterial.publicShare,
step1SigningMaterial.userPublicGpgKey,
params.reqId
)) as Omit<AShare, 'ntilde' | 'h1' | 'h2'>; // WP/HSM does not return the initial challenge
const step2Return = await externalSignerMuDeltaShareGenerator({
txRequest: txRequestObj,
aShareFromBitgo: bitgoToUserAShare,
bitgoChallenge: bitgoChallenge,
encryptedWShare: step1SigningMaterial.wShare as string,
});
// signing stage two with muShare and dShare send to bitgo and receives D share
const bitgoToUserDShare = (await ECDSAMethods.sendShareToBitgo(
this.bitgo,
this.wallet.id(),
txRequestObj.txRequestId,
requestType,
SendShareType.MUShare,
step2Return.muDShare,
undefined,
undefined,
undefined,
undefined,
undefined,
params.reqId
)) as DShare;
const userSShare = await externalSignerSShareGenerator({
tssParams: {
...params,
txRequest: txRequestObj,
},
dShareFromBitgo: bitgoToUserDShare,
requestType: requestType,
encryptedOShare: step2Return.oShare as string,
});
// signing stage three with SShare send to bitgo and receives SShare
await ECDSAMethods.sendShareToBitgo(
this.bitgo,
this.wallet.id(),
txRequestObj.txRequestId,
requestType,
SendShareType.SShare,
userSShare,
undefined,
undefined,
undefined,
undefined,
undefined,
params.reqId
);
return await getTxRequest(this.bitgo, this.wallet.id(), txRequestObj.txRequestId, params.reqId);
}
/**
* Gets signing key, txRequestResolved and txRequestId
* @param {string | TxRequest} params.txRequest - transaction request object or id
* @param {string} params.prv - decrypted private key
* @param { string} params.reqId - request id
* @returns {Promise<TxRequest>}
*/
private async signRequestBase(
params: TSSParamsWithPrv | TSSParamsForMessageWithPrv,
requestType: RequestType
): Promise<TxRequest> {
const pendingEcdsaTssInitialization = this.wallet.coinSpecific()?.pendingEcdsaTssInitialization;
if (pendingEcdsaTssInitialization) {
throw new Error(
'Wallet is not ready for TSS ECDSA signing. Please contact your enterprise admin to finish the enterprise TSS initialization.'
);
}
const userSigningMaterial: ECDSAMethodTypes.SigningMaterial = JSON.parse(params.prv);
if (userSigningMaterial.pShare.i !== 1) {
throw new Error('Invalid user key');
}
if (!userSigningMaterial.backupNShare) {
throw new Error('Invalid user key - missing backupNShare');
}
const txRequest: TxRequest =
typeof params.txRequest === 'string'
? await getTxRequest(this.bitgo, this.wallet.id(), params.txRequest, params.reqId)
: params.txRequest;
let signablePayload = new Buffer('');
let derivationPath = '';
if (requestType === RequestType.tx) {
assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest');
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
// For ICP transactions, the HSM signs the serializedTxHex, while the user signs the signableHex separately.
// Verification cannot be performed directly on the signableHex alone. However, we can parse the serializedTxHex
// to regenerate the signableHex and compare it against the provided value for verification.
// In contrast, for other coin families, verification is typically done using just the signableHex.
if (this.baseCoin.getConfig().family === 'icp') {
await this.baseCoin.verifyTransaction({
txPrebuild: { txHex: unsignedTx.serializedTxHex, txInfo: unsignedTx.signableHex },
txParams: params.txParams || { recipients: [] },
wallet: this.wallet,
walletType: this.wallet.multisigType(),
});
} else {
await this.baseCoin.verifyTransaction({
txPrebuild: { txHex: unsignedTx.signableHex },
txParams: params.txParams || { recipients: [] },
wallet: this.wallet,
walletType: this.wallet.multisigType(),
});
}
signablePayload = Buffer.from(unsignedTx.signableHex, 'hex');
derivationPath = unsignedTx.derivationPath;
} else if (requestType === RequestType.message) {
signablePayload = (params as TSSParamsForMessage).bufferToSign;
// TODO BG-67299 Message signing with derivation path
}
const paillierModulus = this.getOfflineSignerPaillierModulus({ prv: params.prv });
const challenges = await this.getEcdsaSigningChallenges(
txRequest.txRequestId,
requestType,
paillierModulus.userPaillierModulus,
0,
params.reqId
);
const step1Return = await this.createTssEcdsaStep1SigningMaterial({
prv: params.prv,
challenges: challenges,
derivationPath: derivationPath,
});
// signing stage one with K share send to bitgo and receives A share
const bitgoToUserAShare = (await ECDSAMethods.sendShareToBitgo(
this.bitgo,
this.wallet.id(),
txRequest.txRequestId,
requestType,
SendShareType.KShare,
step1Return.kShare,
step1Return.encryptedSignerOffsetShare,
step1Return.vssProof,
step1Return.privateShareProof,
step1Return.publicShare,
step1Return.userPublicGpgKey,
params.reqId
)) as Omit<AShare, 'ntilde' | 'h1' | 'h2'>; // WP/HSM does not return the initial challenge
const step2Return = await this.createTssEcdsaStep2SigningMaterial({
aShareFromBitgo: bitgoToUserAShare,
bitgoChallenge: challenges.bitgoChallenge,
wShare: step1Return.wShare as WShare,
});
// signing stage two with muShare and dShare send to bitgo and receives D share
const bitgoToUserDShare = (await ECDSAMethods.sendShareToBitgo(
this.bitgo,
this.wallet.id(),
txRequest.txRequestId,
requestType,
SendShareType.MUShare,
step2Return.muDShare,
undefined,
undefined,
undefined,
undefined,
undefined,
params.reqId
)) as DShare;
// If only the getHashFunction() is defined for the coin use it otherwise
// pass undefined hash, default hash will be used in that case.
let hash: Hash | undefined;
try {
hash = this.baseCoin.getHashFunction();
} catch (err) {
hash = undefined;
}
const userSShare = await ECDSAMethods.createUserSignatureShare(
step2Return.oShare as OShare,
bitgoToUserDShare,
signablePayload,
hash
);
// signing stage three with SShare send to bitgo and receives SShare
await ECDSAMethods.sendShareToBitgo(
this.bitgo,
this.wallet.id(),
txRequest.txRequestId,
requestType,
SendShareType.SShare,
userSShare,
undefined,
undefined,
undefined,
undefined,
undefined,
params.reqId
);
return await getTxRequest(this.bitgo, this.wallet.id(), txRequest.txRequestId, params.reqId);
}
/**
* Signs the transaction associated to the transaction request.
* @param {string | TxRequest} params.txRequest - transaction request object or id
* @param {string} params.prv - decrypted private key
* @param {string} params.reqId - request id
* @returns {Promise<TxRequest>} fully signed TxRequest object
*/
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
this.bitgo.setRequestTracer(params.reqId);
return this.signRequestBase(params, RequestType.tx);
}
/**
* Signs the message associated to the transaction request.
* @param {string | TxRequest} params.txRequest - transaction request object or id
* @param {string} params.prv - decrypted private key
* @param {string} params.reqId - request id
* @returns {Promise<TxRequest>} fully signed TxRequest object
*/
async signTxRequestForMessage(params: TSSParamsForMessageWithPrv): Promise<TxRequest> {
if (!params.messageRaw) {
throw new Error('Raw message required to sign message');
}
return this.signRequestBase(params, RequestType.message);
}
/**
* Get the challenge values for enterprise and BitGo in ECDSA signing
* Only returns the challenges if they are verified by the user's enterprise admin's ecdh key
* @param {string} txRequestId - transaction request id
* @param {RequestType} requestType - (0 for tx, 1 for message)
* @param {string} walletPaillierModulus - paillier pubkey $n$
* @param {number} index - index of the requestType
* @param {IRequestTracer} reqId - request tracer request id
*/
async getEcdsaSigningChallenges(
txRequestId: string,
requestType: RequestType,
walletPaillierModulus: string,
index = 0,
reqId?: IRequestTracer
): Promise<{
enterpriseChallenge: EcdsaTypes.SerializedEcdsaChallenges;
bitgoChallenge: TxRequestChallengeResponse;
}> {
const enterpriseId = this.wallet.toJSON().enterprise;
if (!enterpriseId) {
throw new Error('Wallet must be an enterprise wallet.');
}
// create BitGo range proof and paillier proof challenge
const createBitgoChallengeResponse = await getTxRequestChallenge(
this.bitgo,
this.wallet.id(),
txRequestId,
index.toString(),
requestType,
walletPaillierModulus,
reqId
);
const bitgoToEnterprisePaillierChallenge = { p: createBitgoChallengeResponse.p };
const enterpriseToBitgoPaillierChallenge = EcdsaTypes.serializePaillierChallenge({
p: await EcdsaPaillierProof.generateP(hexToBigInt(createBitgoChallengeResponse.n)),
});
// TODO(BG-78764): once the paillier proofs are complete, reduce challenge creation to one API call
const walletChallenges = await this.wallet.getChallengesForEcdsaSigning();
const challengeVerifierUserId = walletChallenges.createdBy;
const adminSigningKeyResponse = await this.bitgo.getSigningKeyForUser(enterpriseId, challengeVerifierUserId);
const pubkeyOfAdminEcdhKeyHex = adminSigningKeyResponse.derivedPubkey;
// Verify enterprise's challenge is signed by the respective admins ecdh keychain
const enterpriseRawChallenge = {
ntilde: walletChallenges.enterpriseChallenge.ntilde,
h1: walletChallenges.enterpriseChallenge.h1,
h2: walletChallenges.enterpriseChallenge.h2,
};
const adminSignatureOnEntChallenge: string = walletChallenges.enterpriseChallenge.verifiers.adminSignature;
if (
!verifyEcdhSignature(
EcdsaUtils.getMessageToSignFromChallenge(enterpriseRawChallenge),
adminSignatureOnEntChallenge,
Buffer.from(pubkeyOfAdminEcdhKeyHex, 'hex')
)
) {
throw new Error(`Admin signature for enterprise challenge is not valid. Please contact your enterprise admin.`);
}
// Verify that the BitGo challenge's ZK proofs have been verified by the admin
const bitgoChallenge: TxRequestChallengeResponse = {
ntilde: walletChallenges.bitgoChallenge.ntilde,
h1: walletChallenges.bitgoChallenge.h1,
h2: walletChallenges.bitgoChallenge.h2,
p: bitgoToEnterprisePaillierChallenge.p,
n: createBitgoChallengeResponse.n,
};
const adminVerificationSignatureForBitGoChallenge = walletChallenges.bitgoChallenge.verifiers.adminSignature;
if (
!verifyEcdhSignature(
EcdsaUtils.getMessageToSignFromChallenge(bitgoChallenge),
adminVerificationSignatureForBitGoChallenge,
Buffer.from(pubkeyOfAdminEcdhKeyHex, 'hex')
)
) {
throw new Error(`Admin signature for BitGo's challenge is not valid. Please contact your enterprise admin.`);
}
return {
enterpriseChallenge: {
...enterpriseRawChallenge,
p: enterpriseToBitgoPaillierChallenge.p,
},
bitgoChallenge,
};
}
/**
* Verifies the u-value proofs and GPG keys used in generating a TSS ECDSA wallet.
* @param userGpgPub The user's public GPG key for encryption between user/server
* @param backupGpgPub The backup's public GPG key for encryption between backup/server
* @param bitgoKeychain previously created BitGo keychain; must be compatible with user and backup key shares
* @param decryptedShare The decrypted bitgo-to-user/backup private share retrieved from the keychain
* @param verifierIndex The index of the party to verify: 1 = user, 2 = backup
*/
async verifyWalletSignatures(
userGpgPub: string,
backupGpgPub: string,
bitgoKeychain: Keychain,
decryptedShare: string,
verifierIndex: 1 | 2
): Promise<void> {
assert(bitgoKeychain.commonKeychain);
assert(bitgoKeychain.walletHSMGPGPublicKeySigs);
const bitgoGpgKey = (await getBitgoGpgPubKey(this.bitgo)).mpcV1;
const userKeyPub = await openpgp.readKey({ armoredKey: userGpgPub });
const userKeyId = userKeyPub.keyPacket.getFingerprint();
const backupKeyPub = await openpgp.readKey({ armoredKey: backupGpgPub });
const backupKeyId = backupKeyPub.keyPacket.getFingerprint();
const walletSignatures = await openpgp.readKeys({ armoredKeys: bitgoKeychain.walletHSMGPGPublicKeySigs });
if (walletSignatures.length !== 2) {
throw new Error('Invalid wallet signatures');
}
if (userKeyId !== walletSignatures[0].keyPacket.getFingerprint()) {
throw new Error(`first wallet signature's fingerprint does not match passed user gpg key's fingerprint`);
}
if (backupKeyId !== walletSignatures[1].keyPacket.getFingerprint()) {
throw new Error(`second wallet signature's fingerprint does not match passed backup gpg key's fingerprint`);
}
await verifyWalletSignature({
walletSignature: walletSignatures[0],
commonKeychain: bitgoKeychain.commonKeychain,
userKeyId,
backupKeyId,
bitgoPub: bitgoGpgKey,
decryptedShare,
verifierIndex,
});
await verifyWalletSignature({
walletSignature: walletSignatures[1],
commonKeychain: bitgoKeychain.commonKeychain,
userKeyId,
backupKeyId,
bitgoPub: bitgoGpgKey,
decryptedShare,
verifierIndex,
});
}
/**
* Signs a challenge with the provided v1 ecdh key at a derived path
* @param challenge challenge to sign
* @param ecdhXprv xprv of the ecdh key
* @param derivationPath the derived path at which the ecdh key will sign
*/
static signChallenge(challenge: EcdsaTypes.SerializedNtilde, ecdhXprv: string, derivationPath: string): Buffer {
const messageToSign = this.getMessageToSignFromChallenge(challenge);
return signMessageWithDerivedEcdhKey(messageToSign, ecdhXprv, derivationPath);
}
/**
* Converts challenge to a common message format which can be signed.
* @param challenge
*/
static getMessageToSignFromChallenge(challenge: EcdsaTypes.SerializedNtilde): string {
return challenge.ntilde.concat(challenge.h1).concat(challenge.h2);
}
/**
Verifies ZK proofs of BitGo's challenges for both nitro and institutional HSMs
which are fetched from the WP API.
*/
static async verifyBitGoChallenges(bitgoChallenges: GetBitGoChallengesApi): Promise<boolean> {
// Verify institutional hsm challenge proof
const instChallengeVerified = await this.verifyBitGoChallenge({
ntilde: bitgoChallenges.bitgoInstitutionalHsm.ntilde,
h1: bitgoChallenges.bitgoInstitutionalHsm.h1,
h2: bitgoChallenges.bitgoInstitutionalHsm.h2,
ntildeProof: bitgoChallenges.bitgoInstitutionalHsm.ntildeProof,
});
// Verify nitro hsm challenge proof
const nitroChallengeVerified = await this.verifyBitGoChallenge({
ntilde: bitgoChallenges.bitgoNitroHsm.ntilde,
h1: bitgoChallenges.bitgoNitroHsm.h1,
h2: bitgoChallenges.bitgoNitroHsm.h2,
ntildeProof: bitgoChallenges.bitgoNitroHsm.ntildeProof,
});
return instChallengeVerified && nitroChallengeVerified;
}
/**
* Verifies ZK proof for a single BitGo challenge
* @param bitgoChallenge
*/
static async verifyBitGoChallenge(bitgoChallenge: EcdsaTypes.SerializedNtildeWithProofs): Promise<boolean> {
const deserializedInstChallenge = EcdsaTypes.deserializeNtildeWithProofs(bitgoChallenge);
const ntildeProofH1WrtH2Verified = await EcdsaRangeProof.verifyNtildeProof(
{
ntilde: deserializedInstChallenge.ntilde,
h1: deserializedInstChallenge.h1,
h2: deserializedInstChallenge.h2,
},
deserializedInstChallenge.ntildeProof.h1WrtH2
);
const ntildeProofH2WrtH1Verified = await EcdsaRangeProof.verifyNtildeProof(
{
ntilde: deserializedInstChallenge.ntilde,
h1: deserializedInstChallenge.h2,
h2: deserializedInstChallenge.h1,
},
deserializedInstChallenge.ntildeProof.h2WrtH1
);
return ntildeProofH1WrtH2Verified && ntildeProofH2WrtH1Verified;
}
/**
* Gets the bitgo challenges for both nitro and institutional HSMs from WP API.
* @param bitgo
*/
static async getBitGoChallenges(bitgo: BitGoBase): Promise<GetBitGoChallengesApi> {
const res = await bitgo.get(bitgo.url('/tss/ecdsa/challenges', 2)).send().result();
if (
!res.bitgoNitroHsm ||
!res.bitgoNitroHsm.ntilde ||
!res.bitgoNitroHsm.h1 ||
!res.bitgoNitroHsm.h2 ||
!res.bitgoNitroHsm.ntildeProof ||
!res.bitgoInstitutionalHsm ||
!res.bitgoInstitutionalHsm.ntilde ||
!res.bitgoInstitutionalHsm.h1 ||
!res.bitgoInstitutionalHsm.h2 ||
!res.bitgoInstitutionalHsm.ntildeProof
) {
throw new Error('Expected BitGo challenge proof to be present. Contact support@bitgo.com.');
}
return res;
}
/**
* Gets BitGo's proofs from API and signs them if the proofs are valid.
* @param bitgo
* @param enterpriseId
* @param userPassword
*/
static async getVerifyAndSignBitGoChallenges(
bitgo: BitGoBase,
enterpriseId: string,
userPassword: string
): Promise<BitGoProofSignatures> {
// Fetch BitGo's challenge and verify
const bitgoChallengesWithProofs = await EcdsaUtils.getBitGoChallenges(bitgo);
if (!(await EcdsaUtils.verifyBitGoChallenges(bitgoChallengesWithProofs))) {
throw new Error(
`Failed to verify BitGo's challenge needed to enable ECDSA signing. Please contact support@bitgo.com`
);
}
return await EcdsaUtils.signBitgoChallenges(bitgo, enterpriseId, userPassword, bitgoChallengesWithProofs);
}
/**
* Sign Bitgo's proofs, verification of proofs is left to the caller
* @param bitgo
* @param enterpriseId
* @param userPassword
* @param bitgoChallengesWithProofs Optionally provide Bitgo Challaenge & Proofs instead of fetching from API
*/
static async signBitgoChallenges(
bitgo: BitGoBase,
enterpriseId: string,
userPassword: string,
bitgoChallengesWithProofs?: GetBitGoChallengesApi
): Promise<BitGoProofSignatures> {
// fetch challenge & proof if none are provided
const challengesWithProofs = bitgoChallengesWithProofs
? bitgoChallengesWithProofs
: await EcdsaUtils.getBitGoChallenges(bitgo);
// Fetch user's ecdh public keychain needed for signing the challenges
const ecdhKeypair = await bitgo.getEcdhKeypairPrivate(userPassword, enterpriseId);
const signedBitGoInstChallenge = EcdsaUtils.signChallenge(
challengesWithProofs.bitgoInstitutionalHsm,
ecdhKeypair.xprv,
ecdhKeypair.derivationPath
);
const signedBitGoNitroChallenge = EcdsaUtils.signChallenge(
challengesWithProofs.bitgoNitroHsm,
ecdhKeypair.xprv,
ecdhKeypair.derivationPath
);
return {
bitgoInstHsmAdminSignature: signedBitGoInstChallenge,
bitgoNitroHsmAdminSignature: signedBitGoNitroChallenge,
};
}
/**
* This is needed to enable ecdsa signing on the enterprise.
* It receives the enterprise challenge and signatures of verified bitgo proofs
* and uploads them on the enterprise.
* @param bitgo
* @param entId - enterprise id to enable ecdsa signing on
* @param userPassword - enterprise admin's login pw
* @param bitgoInstChallengeProofSignature - signature on bitgo's institutional HSM challenge after verification
* @param bitgoNitroChallengeProofSignature - signature on bitgo's nitro HSM challenge after verification
* @param challenge - optionally use the challenge for enterprise challenge
*/
static async initiateChallengesForEnterprise(
bitgo: BitGoBase,
entId: string,
userPassword: string,
bitgoInstChallengeProofSignature: Buffer,
bitgoNitroChallengeProofSignature: Buffer,
openSSLBytes: Uint8Array,
challenge?: EcdsaTypes.DeserializedNtildeWithProofs
): Promise<void> {
// Fetch user's ecdh public keychain needed for signing the challenges
const ecdhKeypair = await bitgo.getEcdhKeypairPrivate(userPassword, entId);
// Generate and sign enterprise challenge
const entChallengeWithProof =
challenge ?? (await EcdsaRangeProof.generateNtilde(openSSLBytes, minModulusBitLength));
const serializedEntChallengeWithProof = EcdsaTypes.serializeNtildeWithProofs(entChallengeWithProof);
const signedEnterpriseChallenge = EcdsaUtils.signChallenge(
serializedEntChallengeWithProof,
ecdhKeypair.xprv,
ecdhKeypair.derivationPath
);
await this.uploadChallengesToEnterprise(
bitgo,
entId,
serializedEntChallengeWithProof,
signedEnterpriseChallenge.toString('hex'),
bitgoInstChallengeProofSignature.toString('hex'),
bitgoNitroChallengeProofSignature.toString('hex')
);
}
/**
* Uploads the signed challenges and their proofs on the enterprise.
* This initiates ecdsa signing for the enterprise users.
* @param bitgo
* @param entId - enterprise to enable ecdsa signing on
* @param entChallenge - client side generated ent challenge with ZK proofs
* @param entChallengeSignature - signature on enterprise challenge
* @param bitgoIntChallengeSignature - signature on BitGo's institutional HSM challenge
* @param bitgoNitroChallengeSignature - signature on BitGo's nitro HSM challenge
*/
static async uploadChallengesToEnterprise(
bitgo: BitGoBase,
entId: string,
entChallenge: EcdsaTypes.SerializedNtilde | EcdsaTypes.SerializedNtildeWithProofs,
entChallengeSignature: string,
bitgoIntChallengeSignature: string,
bitgoNitroChallengeSignature: string
): Promise<void> {
const body = {
enterprise: {
ntilde: entChallenge.ntilde,
h1: entChallenge.h1,
h2: entChallenge.h2,
verifiers: {
adminSignature: entChallengeSignature,
},
},
bitgoInstitutionalHsm: {
verifiers: {
adminSignature: bitgoIntChallengeSignature,
},
},
bitgoNitroHsm: {
verifiers: {
adminSignature: bitgoNitroChallengeSignature,
},
},
};
if ('ntildeProof' in entChallenge) {
body.enterprise['ntildeProof'] = entChallenge.ntildeProof;
}
await bitgo
.put(bitgo.url(`/enterprise/${entId}/tssconfig/ecdsa/challenge`, 2))
.send(body)
.result();
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!