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
81 changes: 81 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
TssSignTxRequestParamsWithPrv,
TxRequest,
TxRequestVersion,
isV2Envelope,
} from './baseTypes';
import { GShare, SignShare } from '../../../account-lib/mpc/tss';
import { RequestTracer } from '../util';
Expand Down Expand Up @@ -650,4 +651,84 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
const { apiVersion, state } = txRequest;
return apiVersion === 'full' && 'pendingApproval' === state;
}

/**
* Get the signable hex, derivation path, and serialized tx hex from a full single-transaction request.
* @param {TxRequest} txRequest - the transaction request object
* @returns {{ signableHex: string; derivationPath: string; serializedTxHex: string | undefined }} - the signable hex, derivation path, and serialized tx hex
*/
protected getSignableHexAndDerivationPath(
txRequest: TxRequest,
missingTransactionsMessage = 'createOfflineShare requires exactly one transaction in txRequest'
): {
signableHex: string;
derivationPath: string;
serializedTxHex: string | undefined;
} {
assert(txRequest.transactions && txRequest.transactions.length === 1, missingTransactionsMessage);
const unsignedTx = txRequest.transactions[0].unsignedTx;
assert(unsignedTx, 'Missing unsignedTx in transactions');
assert(unsignedTx.signableHex, 'Missing signableHex in unsignedTx');
assert(unsignedTx.derivationPath, 'Missing derivationPath in unsignedTx');
return {
signableHex: unsignedTx.signableHex,
derivationPath: unsignedTx.derivationPath,
serializedTxHex: unsignedTx.serializedTxHex,
};
}

/**
* Gets the BitGo and user GPG keys from the BitGo public GPG key and the encrypted user GPG private key.
* @param {string} bitgoPublicGpgKey - the BitGo public GPG key
* @param {string} encryptedUserGpgPrvKey - the encrypted user GPG private key
* @param {string} walletPassphrase - the wallet passphrase
* @param {string} adata - the additional data to validate the GPG keys
* @param {string} userGpgKeyDomainSeparator - the domain separator expected in the encrypted GPG key adata
* @returns {Promise<{ bitgoGpgKey: openpgp.Key; userGpgPrvKey: openpgp.PrivateKey }>} - the BitGo and user GPG keys
*/
protected async getBitgoAndUserGpgKeys(
bitgoPublicGpgKey: string,
encryptedUserGpgPrvKey: string,
walletPassphrase: string,
adata: string,
userGpgKeyDomainSeparator: string
): Promise<{
bitgoGpgKey: openpgp.Key;
userGpgPrvKey: openpgp.PrivateKey;
}> {
const bitgoGpgKey = await openpgp.readKey({ armoredKey: bitgoPublicGpgKey });

const decryptedGpgPrvKey = isV2Envelope(encryptedUserGpgPrvKey)
? await this.bitgo.decryptAsync({ input: encryptedUserGpgPrvKey, password: walletPassphrase })
: this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase });

if (adata) {
this.validateAdata(adata, encryptedUserGpgPrvKey, userGpgKeyDomainSeparator);
}
const userGpgPrvKey = await openpgp.readPrivateKey({ armoredKey: decryptedGpgPrvKey });
return { bitgoGpgKey, userGpgPrvKey };
}

/**
* Validates encryption additional authenticated data against the ciphertext envelope.
* @param adata string
* @param cyphertext string
* @param roundDomainSeparator string
* @throws {Error} if the adata or cyphertext is invalid
*/
protected validateAdata(adata: string, cyphertext: string, roundDomainSeparator: string): void {
let cypherJson;
try {
cypherJson = JSON.parse(cyphertext);
} catch (e) {
throw new Error('Failed to parse cyphertext to JSON, got: ' + cyphertext);
}
// using decodeURIComponent to handle special characters
if (
decodeURIComponent(cypherJson.adata) !== decodeURIComponent(`${roundDomainSeparator}:${adata}`) &&
decodeURIComponent(cypherJson.adata) !== decodeURIComponent(adata)
) {
throw new Error('Adata does not match cyphertext adata');
}
}
}
87 changes: 18 additions & 69 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,10 +1015,10 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
let derivationPath: string;
let serializedTxHex: string | undefined;
if (requestType === RequestType.tx) {
assert(txRequest.transactions && txRequest.transactions.length === 1, 'Unable to find transactions in txRequest');
txToSign = txRequest.transactions[0].unsignedTx.signableHex;
derivationPath = txRequest.transactions[0].unsignedTx.derivationPath;
serializedTxHex = txRequest.transactions[0].unsignedTx.serializedTxHex;
const signableTx = this.getSignableHexAndDerivationPath(txRequest, 'Unable to find transactions in txRequest');
txToSign = signableTx.signableHex;
derivationPath = signableTx.derivationPath;
Comment thread
vibhavgo marked this conversation as resolved.
serializedTxHex = signableTx.serializedTxHex;
} else if (requestType === RequestType.message) {
// TODO(WP-2176): Add support for message signing
throw new Error('MPCv2 message signing not supported yet.');
Expand Down Expand Up @@ -1051,67 +1051,6 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
return { hashBuffer, derivationPath };
}

/**
* Gets the BitGo and user GPG keys from the BitGo public GPG key and the encrypted user GPG private key.
* @param {string} bitgoPublicGpgKey - the BitGo public GPG key
* @param {string} encryptedUserGpgPrvKey - the encrypted user GPG private key
* @param {string} walletPassphrase - the wallet passphrase
* @param {string} adata - the additional data to validate the GPG keys
* @returns {Promise<{ bitgoGpgKey: pgp.Key; userGpgKey: pgp.SerializedKeyPair<string> }>} - the BitGo and user GPG keys
*/
private async getBitgoAndUserGpgKeys(
bitgoPublicGpgKey: string,
encryptedUserGpgPrvKey: string,
walletPassphrase: string,
adata: string
): Promise<{
bitgoGpgKey: pgp.Key;
userGpgKey: pgp.SerializedKeyPair<string>;
}> {
const bitgoGpgKey = await pgp.readKey({ armoredKey: bitgoPublicGpgKey });
let decryptedGpgPrvKey: string;
if (isV2Envelope(encryptedUserGpgPrvKey)) {
decryptedGpgPrvKey = await this.bitgo.decryptAsync({ input: encryptedUserGpgPrvKey, password: walletPassphrase });
} else {
decryptedGpgPrvKey = this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase });
}
if (adata) {
this.validateAdata(adata, encryptedUserGpgPrvKey, EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY);
}
const userDecryptedKey = await pgp.readKey({ armoredKey: decryptedGpgPrvKey });
const userGpgKey: pgp.SerializedKeyPair<string> = {
privateKey: userDecryptedKey.armor(),
publicKey: userDecryptedKey.toPublic().armor(),
};
return {
bitgoGpgKey,
userGpgKey,
};
}

/**
* Validates the adata and cyphertext.
* @param adata string
* @param cyphertext string
* @returns void
* @throws {Error} if the adata or cyphertext is invalid
*/
private validateAdata(adata: string, cyphertext: string, roundDomainSeparator: string): void {
let cypherJson;
try {
cypherJson = JSON.parse(cyphertext);
} catch (e) {
throw new Error('Failed to parse cyphertext to JSON, got: ' + cyphertext);
}
// using decodeURIComponent to handle special characters
if (
decodeURIComponent(cypherJson.adata) !== decodeURIComponent(`${roundDomainSeparator}:${adata}`) &&
decodeURIComponent(cypherJson.adata) !== decodeURIComponent(adata)
) {
throw new Error('Adata does not match cyphertext adata');
}
}

// #endregion

// #region external signer
Expand Down Expand Up @@ -1286,12 +1225,17 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {

const useV2 = isV2Envelope(encryptedRound1Session);

const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys(
const { bitgoGpgKey, userGpgPrvKey } = await this.getBitgoAndUserGpgKeys(
bitgoPublicGpgKey,
encryptedUserGpgPrvKey,
walletPassphrase,
adata
adata,
EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY
);
const userGpgKey: pgp.SerializedKeyPair<string> = {
privateKey: userGpgPrvKey.armor(),
publicKey: userGpgPrvKey.toPublic().armor(),
};

const signatureShares = txRequest.transactions?.[0].signatureShares;
assert(signatureShares, 'Missing signature shares in round 1 txRequest');
Expand Down Expand Up @@ -1380,12 +1324,17 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {

const useV2 = isV2Envelope(encryptedRound2Session);

const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys(
const { bitgoGpgKey, userGpgPrvKey } = await this.getBitgoAndUserGpgKeys(
bitgoPublicGpgKey,
encryptedUserGpgPrvKey,
walletPassphrase,
adata
adata,
EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY
);
const userGpgKey: pgp.SerializedKeyPair<string> = {
privateKey: userGpgPrvKey.armor(),
publicKey: userGpgPrvKey.toPublic().armor(),
};

const signatureShares = txRequest.transactions?.[0].signatureShares;
assert(signatureShares, 'Missing signature shares in round 2 txRequest');
Expand Down
4 changes: 4 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import { BaseEddsaUtils } from './base';
import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender';

export class EddsaMPCv2Utils extends BaseEddsaUtils {
// TODO(WCI-378): call the MPS_DSG_SIGNING_ROUND1/2_STATE in createOfflineRoundShare handlers
// private static readonly MPS_DSG_SIGNING_ROUND1_STATE = 'MPS_DSG_SIGNING_ROUND1_STATE';
// private static readonly MPS_DSG_SIGNING_ROUND2_STATE = 'MPS_DSG_SIGNING_ROUND2_STATE';

/** @inheritdoc */
async createKeychains(params: {
passphrase: string;
Expand Down
Loading
Loading