diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 313cab37df..74be73ca27 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -8,6 +8,7 @@ import { IPendingApprovals } from '../pendingApproval'; import { InitiateRecoveryOptions } from '../recovery'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; import EddsaUtils, { EddsaMPCv2Utils, PrebuildTransactionWithIntentOptions, TxRequest } from '../utils/tss/eddsa'; +import { RedpallasMPCv2Utils } from '../utils/tss/redpallas'; import { CreateAddressFormat, CustomSigningFunction, IWallet, IWallets, Memo, Wallet, WalletData } from '../wallet'; import { TokenEnablement } from '@bitgo/public-types'; @@ -364,7 +365,7 @@ export interface ExtraPrebuildParamsOptions { export interface PresignTransactionOptions { txPrebuild?: TransactionPrebuild; walletData: WalletData; - tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined; + tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | RedpallasMPCv2Utils | undefined; [index: string]: unknown; } @@ -628,11 +629,14 @@ export interface MessagePrep { } /** - * 'redpallas' is a DKG-only MPC algorithm (no signing support in this SDK) used for the - * Zcash Orchard shielded pool. It is additive: existing coins never return it from - * `getMPCAlgorithm()` unless explicitly implemented to do so, so this does not change - * behavior for any existing ECDSA/EdDSA coin or for ZEC's existing transparent - * (secp256k1) multisig/TSS flows. + * 'redpallas' is the MPC algorithm used for the Zcash Orchard shielded pool (RedPallas / + * "Ironwood"). Today it only supports DKG (key generation) via MPCv2 in this SDK - there is no + * online self-custody DSG (transaction signing) entrypoint yet, though the underlying + * signature-share primitives (`bitgo/tss/redpallas`) and the `sendSignatureShareV2` MPCv2 + * request type exist as groundwork for a future custodial/cold (SMC/OVC) DSG signing flow. + * It is additive: existing coins never return it from `getMPCAlgorithm()` unless explicitly + * implemented to do so, so this does not change behavior for any existing ECDSA/EdDSA coin or + * for ZEC's existing transparent (secp256k1) multisig/TSS flows. */ export type MPCAlgorithm = 'ecdsa' | 'eddsa' | 'redpallas'; diff --git a/modules/sdk-core/src/bitgo/tss/common.ts b/modules/sdk-core/src/bitgo/tss/common.ts index 97bb251f8d..cd338823ee 100644 --- a/modules/sdk-core/src/bitgo/tss/common.ts +++ b/modules/sdk-core/src/bitgo/tss/common.ts @@ -158,6 +158,8 @@ export async function sendSignatureShareV2( type = 'ecdsaMpcV2'; } else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'eddsa') { type = 'eddsaMpcV2'; + } else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'redpallas') { + type = 'redpallasMpcV2'; } else if (multisigTypeVersion === undefined && mpcAlgorithm === 'eddsa') { type = 'eddsaMpcV1'; } diff --git a/modules/sdk-core/src/bitgo/tss/redpallas/redpallasMPCv2.ts b/modules/sdk-core/src/bitgo/tss/redpallas/redpallasMPCv2.ts new file mode 100644 index 0000000000..769f832d8e --- /dev/null +++ b/modules/sdk-core/src/bitgo/tss/redpallas/redpallasMPCv2.ts @@ -0,0 +1,149 @@ +import * as openpgp from 'openpgp'; +import { RedPallasMPSComms, RedPallasMPSTypes } from '@bitgo/sdk-lib-mpc'; +import { + RedpallasMPCv2SignatureShareRound1Input, + RedpallasMPCv2SignatureShareRound1Output, + RedpallasMPCv2SignatureShareRound2Input, + RedpallasMPCv2SignatureShareRound2Output, + RedpallasMPCv2SignatureShareRound3Input, + RedpallasMPCv2SignatureShareRound3Output, +} from '@bitgo/public-types'; +import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes'; +import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2'; + +type SignerPartyId = MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP; + +function partyIdToSignatureShareType(partyId: MPCv2PartiesEnum): SignatureShareType { + switch (partyId) { + case MPCv2PartiesEnum.USER: + return SignatureShareType.USER; + case MPCv2PartiesEnum.BACKUP: + return SignatureShareType.BACKUP; + case MPCv2PartiesEnum.BITGO: + return SignatureShareType.BITGO; + } +} + +/** + * RedPallas MPS DSG signature-share helpers. + * + * Groundwork for a future custodial/cold (SMC/OVC) DSG signing flow - not yet wired up to any + * caller in this SDK. Mirrors `../eddsa/eddsaMPCv2.ts` (same 3-round shape, same PGP-signed- + * message envelope), but kept as an independent copy - built on `RedPallasMPSComms` - so + * RedPallas MPS never depends on the EdDSA MPS module, and vice versa. + */ + +/** + * Builds the round-1 signature share record. + * + * PGP-signs the WASM round-0 broadcast message with the signer's ephemeral key and + * wraps it into a SignatureShareRecord ready for `sendSignatureShareV2`. + */ +export async function getSignatureShareRoundOne( + userMsg1: RedPallasMPSTypes.DeserializedMessage, + userGpgPrivKey: openpgp.PrivateKey, + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const signedMsg1 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey); + const share: RedpallasMPCv2SignatureShareRound1Input = { + type: 'round1Input', + data: { msg1: signedMsg1 }, + }; + return { + from: partyIdToSignatureShareType(partyId), + to: partyIdToSignatureShareType(otherSignerPartyId), + share: JSON.stringify(share), + }; +} + +/** + * Verifies the peer's round-1 PGP signature and returns the raw deserialized + * message ready for `RedPallasDSG.handleIncomingMessages`. + */ +export async function verifyPeerMessageRoundOne( + parsedRound1Output: RedpallasMPCv2SignatureShareRound1Output, + peerGpgKey: openpgp.Key, + peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound1Output.data.msg1, peerGpgKey); + return { + from: peerPartyId, + payload: new Uint8Array(rawBytes), + }; +} + +/** + * Builds the round-2 signature share record. + */ +export async function getSignatureShareRoundTwo( + userMsg2: RedPallasMPSTypes.DeserializedMessage, + userGpgPrivKey: openpgp.PrivateKey, + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const signedMsg2 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey); + const share: RedpallasMPCv2SignatureShareRound2Input = { + type: 'round2Input', + data: { msg2: signedMsg2 }, + }; + return { + from: partyIdToSignatureShareType(partyId), + to: partyIdToSignatureShareType(otherSignerPartyId), + share: JSON.stringify(share), + }; +} + +/** + * Verifies the peer's round-2 PGP signature and returns the raw deserialized + * message ready for `RedPallasDSG.handleIncomingMessages`. + */ +export async function verifyPeerMessageRoundTwo( + parsedRound2Output: RedpallasMPCv2SignatureShareRound2Output, + peerGpgKey: openpgp.Key, + peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound2Output.data.msg2, peerGpgKey); + return { + from: peerPartyId, + payload: new Uint8Array(rawBytes), + }; +} + +/** + * Verifies the peer's round-3 PGP signature and returns the raw deserialized + * message ready for `RedPallasDSG.handleIncomingMessages`. + */ +export async function verifyPeerMessageRoundThree( + parsedRound3Output: RedpallasMPCv2SignatureShareRound3Output, + peerGpgKey: openpgp.Key, + peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound3Output.data.msg3, peerGpgKey); + return { from: peerPartyId, payload: new Uint8Array(rawBytes) }; +} + +/** + * Builds the round-3 signature share record (final signer message). + * + * There is no corresponding `verifyBitGoMessageRoundThree` because Wallet Platform + * finalises the signing server-side after receiving round 3; the client obtains the + * signed transaction via `sendTxRequest`. + */ +export async function getSignatureShareRoundThree( + userMsg3: RedPallasMPSTypes.DeserializedMessage, + userGpgPrivKey: openpgp.PrivateKey, + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const signedMsg3 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey); + const share: RedpallasMPCv2SignatureShareRound3Input = { + type: 'round3Input', + data: { msg3: signedMsg3 }, + }; + return { + from: partyIdToSignatureShareType(partyId), + to: partyIdToSignatureShareType(otherSignerPartyId), + share: JSON.stringify(share), + }; +} diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index fc9687cd38..ba8e7f971e 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -56,6 +56,7 @@ import { decodeWithCodec } from '../utils/codecs'; import { postWithCodec } from '../utils/postWithCodec'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa'; +import { RedpallasMPCv2Utils } from '../utils/tss/redpallas'; import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest'; import { buildParamKeys, BuildParams } from './BuildParams'; import { @@ -239,7 +240,13 @@ export class Wallet implements IWallet { public readonly baseCoin: IBaseCoin; public _wallet: WalletData; private _defi?: DefiVault; - private readonly tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined; + private readonly tssUtils: + | EcdsaUtils + | EcdsaMPCv2Utils + | EddsaUtils + | EddsaMPCv2Utils + | RedpallasMPCv2Utils + | undefined; private readonly _permissions?: string[]; /** Root keychain from passphrase preflight; consumed by getUserPrv to avoid a second GET. */ private validatedSafeRootKeychain?: KeychainWithEncryptedPrv; @@ -269,6 +276,10 @@ export class Wallet implements IWallet { this.tssUtils = new EddsaUtils(bitgo, baseCoin, this); } break; + case 'redpallas': + // RedPallas (Zcash Orchard shielded pool) is MPCv2-only; there is no MPCv1 variant. + this.tssUtils = new RedpallasMPCv2Utils(bitgo, baseCoin, this); + break; default: this.tssUtils = undefined; } diff --git a/modules/sdk-core/test/unit/bitgo/tss/common.ts b/modules/sdk-core/test/unit/bitgo/tss/common.ts new file mode 100644 index 0000000000..169523f269 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/tss/common.ts @@ -0,0 +1,63 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { sendSignatureShareV2 } from '../../../../src/bitgo/tss/common'; +import { BitGoBase, RequestType, TxRequest, MPCAlgorithm } from '../../../../src'; + +describe('sendSignatureShareV2 request type dispatch', function () { + afterEach(function () { + sinon.restore(); + }); + + async function captureRequestType( + mpcAlgorithm: MPCAlgorithm, + multisigTypeVersion: 'MPCv2' | undefined + ): Promise { + let capturedBody: { type: string } | undefined; + const send = sinon.stub().callsFake((body) => { + capturedBody = body; + return { result: sinon.stub().resolves({} as TxRequest) }; + }); + const mockBitGo = { + url: sinon.stub().returns('/mock/url'), + post: sinon.stub().returns({ send }), + setRequestTracer: sinon.stub(), + } as unknown as BitGoBase; + + await sendSignatureShareV2( + mockBitGo, + 'walletId', + 'txRequestId', + [], + RequestType.tx, + mpcAlgorithm, + 'signerGpgPublicKey', + undefined, + multisigTypeVersion + ); + + if (!capturedBody) { + throw new Error('request body should have been captured'); + } + return capturedBody.type; + } + + it('resolves ecdsaMpcV2 for MPCv2 + ecdsa', async function () { + assert.strictEqual(await captureRequestType('ecdsa', 'MPCv2'), 'ecdsaMpcV2'); + }); + + it('resolves eddsaMpcV2 for MPCv2 + eddsa', async function () { + assert.strictEqual(await captureRequestType('eddsa', 'MPCv2'), 'eddsaMpcV2'); + }); + + it('resolves redpallasMpcV2 for MPCv2 + redpallas', async function () { + assert.strictEqual(await captureRequestType('redpallas', 'MPCv2'), 'redpallasMpcV2'); + }); + + it('resolves eddsaMpcV1 for undefined multisigTypeVersion + eddsa', async function () { + assert.strictEqual(await captureRequestType('eddsa', undefined), 'eddsaMpcV1'); + }); + + it('resolves an empty type for redpallas without MPCv2 (no MPCv1 variant exists)', async function () { + assert.strictEqual(await captureRequestType('redpallas', undefined), ''); + }); +}); diff --git a/modules/sdk-core/test/unit/bitgo/tss/redpallas/redpallasMPCv2.ts b/modules/sdk-core/test/unit/bitgo/tss/redpallas/redpallasMPCv2.ts new file mode 100644 index 0000000000..6e4fdc69c2 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/tss/redpallas/redpallasMPCv2.ts @@ -0,0 +1,229 @@ +import * as assert from 'assert'; +import * as pgp from 'openpgp'; +import { RedPallasMPSComms, RedPallasMPSTypes } from '@bitgo/sdk-lib-mpc'; +import { + RedpallasMPCv2SignatureShareRound1Input, + RedpallasMPCv2SignatureShareRound1Output, + RedpallasMPCv2SignatureShareRound2Input, + RedpallasMPCv2SignatureShareRound2Output, + RedpallasMPCv2SignatureShareRound3Input, + RedpallasMPCv2SignatureShareRound3Output, +} from '@bitgo/public-types'; +import { SignatureShareRecord, SignatureShareType } from '../../../../../src'; +import { + getSignatureShareRoundOne, + getSignatureShareRoundTwo, + getSignatureShareRoundThree, + verifyPeerMessageRoundOne, + verifyPeerMessageRoundTwo, + verifyPeerMessageRoundThree, +} from '../../../../../src/bitgo/tss/redpallas/redpallasMPCv2'; +import { decodeWithCodec } from '../../../../../src/bitgo/utils/codecs'; +import { generateGPGKeyPair } from '../../../../../src/bitgo/utils/opengpgUtils'; +import { MPCv2PartiesEnum } from '../../../../../src/bitgo/utils/tss/ecdsa/typesMPCv2'; + +describe('RedPallas MPS DSG helper functions', function () { + let userGpgPrivKey: pgp.PrivateKey; + let backupGpgPrivKey: pgp.PrivateKey; + let bitgoGpgPrivKey: pgp.PrivateKey; + let bitgoGpgPubKey: pgp.Key; + + const userPayload = (round: number): RedPallasMPSTypes.DeserializedMessage => ({ + from: MPCv2PartiesEnum.USER, + payload: new Uint8Array(Buffer.from(`user-round-${round}-payload`)), + }); + const backupPayload = (round: number): RedPallasMPSTypes.DeserializedMessage => ({ + from: MPCv2PartiesEnum.BACKUP, + payload: new Uint8Array(Buffer.from(`backup-round-${round}-payload`)), + }); + const bitgoPayload = (round: number): RedPallasMPSTypes.DeserializedMessage => ({ + from: MPCv2PartiesEnum.BITGO, + payload: new Uint8Array(Buffer.from(`bitgo-round-${round}-payload`)), + }); + + before('generate GPG key pairs', async function () { + const userGpgKeyPair = await generateGPGKeyPair('ed25519'); + const backupGpgKeyPair = await generateGPGKeyPair('ed25519'); + const bitgoGpgKeyPair = await generateGPGKeyPair('ed25519'); + + userGpgPrivKey = await pgp.readPrivateKey({ armoredKey: userGpgKeyPair.privateKey }); + backupGpgPrivKey = await pgp.readPrivateKey({ armoredKey: backupGpgKeyPair.privateKey }); + bitgoGpgPrivKey = await pgp.readPrivateKey({ armoredKey: bitgoGpgKeyPair.privateKey }); + bitgoGpgPubKey = await pgp.readKey({ armoredKey: bitgoGpgKeyPair.publicKey }); + }); + + // ── Round 1 ───────────────────────────────────────────────────────────────── + + it('getSignatureShareRoundOne should build a valid round-1 share for the user', async function () { + const share: SignatureShareRecord = await getSignatureShareRoundOne(userPayload(1), userGpgPrivKey); + + assert.strictEqual(share.from, SignatureShareType.USER); + assert.strictEqual(share.to, SignatureShareType.BITGO); + + const parsed = decodeWithCodec( + RedpallasMPCv2SignatureShareRound1Input, + JSON.parse(share.share), + 'RedpallasMPCv2SignatureShareRound1Input' + ); + assert.strictEqual(parsed.type, 'round1Input'); + assert.ok(parsed.data.msg1.message, 'msg1.message should be set'); + assert.ok(parsed.data.msg1.signature, 'msg1.signature should be set'); + }); + + it('getSignatureShareRoundOne should build a valid round-1 share for the backup', async function () { + const share: SignatureShareRecord = await getSignatureShareRoundOne( + backupPayload(1), + backupGpgPrivKey, + MPCv2PartiesEnum.BACKUP + ); + + assert.strictEqual(share.from, SignatureShareType.BACKUP); + assert.strictEqual(share.to, SignatureShareType.BITGO); + }); + + it('verifyPeerMessageRoundOne should verify a valid BitGo round-1 message', async function () { + const bitgoSignedMsg1 = await RedPallasMPSComms.detachSignMpsMessage( + Buffer.from(bitgoPayload(1).payload), + bitgoGpgPrivKey + ); + const round1Output: RedpallasMPCv2SignatureShareRound1Output = { + type: 'round1Output', + data: { msg1: bitgoSignedMsg1 }, + }; + + const result = await verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey); + + assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); + assert.deepStrictEqual(Buffer.from(result.payload), Buffer.from(bitgoPayload(1).payload)); + }); + + it('verifyPeerMessageRoundOne should throw on a tampered round-1 message', async function () { + const round1Output: RedpallasMPCv2SignatureShareRound1Output = { + type: 'round1Output', + data: { + msg1: { + message: Buffer.from('tampered').toString('base64'), + signature: '-----BEGIN PGP SIGNATURE-----\n\nINVALID\n-----END PGP SIGNATURE-----\n', + }, + }, + }; + + await assert.rejects(verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey)); + }); + + // ── Round 2 ───────────────────────────────────────────────────────────────── + + it('getSignatureShareRoundTwo should build a valid round-2 share for the user', async function () { + const share: SignatureShareRecord = await getSignatureShareRoundTwo(userPayload(2), userGpgPrivKey); + + assert.strictEqual(share.from, SignatureShareType.USER); + assert.strictEqual(share.to, SignatureShareType.BITGO); + + const parsed = decodeWithCodec( + RedpallasMPCv2SignatureShareRound2Input, + JSON.parse(share.share), + 'RedpallasMPCv2SignatureShareRound2Input' + ); + assert.strictEqual(parsed.type, 'round2Input'); + assert.ok(parsed.data.msg2.message, 'msg2.message should be set'); + assert.ok(parsed.data.msg2.signature, 'msg2.signature should be set'); + }); + + it('verifyPeerMessageRoundTwo should verify a valid BitGo round-2 message', async function () { + const bitgoSignedMsg2 = await RedPallasMPSComms.detachSignMpsMessage( + Buffer.from(bitgoPayload(2).payload), + bitgoGpgPrivKey + ); + const round2Output: RedpallasMPCv2SignatureShareRound2Output = { + type: 'round2Output', + data: { msg2: bitgoSignedMsg2 }, + }; + + const result = await verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey); + + assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); + assert.deepStrictEqual(Buffer.from(result.payload), Buffer.from(bitgoPayload(2).payload)); + }); + + it('verifyPeerMessageRoundTwo should throw on a tampered round-2 message', async function () { + const round2Output: RedpallasMPCv2SignatureShareRound2Output = { + type: 'round2Output', + data: { + msg2: { + message: Buffer.from('tampered').toString('base64'), + signature: '-----BEGIN PGP SIGNATURE-----\n\nINVALID\n-----END PGP SIGNATURE-----\n', + }, + }, + }; + + await assert.rejects(verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey)); + }); + + // ── Round 3 ───────────────────────────────────────────────────────────────── + + it('getSignatureShareRoundThree should build a valid round-3 share for the backup', async function () { + const share: SignatureShareRecord = await getSignatureShareRoundThree( + backupPayload(3), + backupGpgPrivKey, + MPCv2PartiesEnum.BACKUP + ); + + assert.strictEqual(share.from, SignatureShareType.BACKUP); + assert.strictEqual(share.to, SignatureShareType.BITGO); + + const parsed = decodeWithCodec( + RedpallasMPCv2SignatureShareRound3Input, + JSON.parse(share.share), + 'RedpallasMPCv2SignatureShareRound3Input' + ); + assert.strictEqual(parsed.type, 'round3Input'); + assert.ok(parsed.data.msg3.message, 'msg3.message should be set'); + assert.ok(parsed.data.msg3.signature, 'msg3.signature should be set'); + }); + + it('verifyPeerMessageRoundThree should verify a valid BitGo round-3 message', async function () { + const bitgoSignedMsg3 = await RedPallasMPSComms.detachSignMpsMessage( + Buffer.from(bitgoPayload(3).payload), + bitgoGpgPrivKey + ); + const round3Output: RedpallasMPCv2SignatureShareRound3Output = { + type: 'round3Output', + data: { msg3: bitgoSignedMsg3 }, + }; + + const result = await verifyPeerMessageRoundThree(round3Output, bitgoGpgPubKey); + + assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); + assert.deepStrictEqual(Buffer.from(result.payload), Buffer.from(bitgoPayload(3).payload)); + }); + + it('verifyPeerMessageRoundThree should throw on a tampered round-3 message', async function () { + const round3Output: RedpallasMPCv2SignatureShareRound3Output = { + type: 'round3Output', + data: { + msg3: { + message: Buffer.from('tampered').toString('base64'), + signature: '-----BEGIN PGP SIGNATURE-----\n\nINVALID\n-----END PGP SIGNATURE-----\n', + }, + }, + }; + + await assert.rejects(verifyPeerMessageRoundThree(round3Output, bitgoGpgPubKey)); + }); + + // ── Envelope round-trip via the independent RedPallas MPS comms layer ─────── + + it('a share built by getSignatureShareRoundOne verifies against RedPallasMPSComms directly', async function () { + const share = await getSignatureShareRoundOne(userPayload(1), userGpgPrivKey); + const parsed = decodeWithCodec( + RedpallasMPCv2SignatureShareRound1Input, + JSON.parse(share.share), + 'RedpallasMPCv2SignatureShareRound1Input' + ); + + const userGpgPubKey = userGpgPrivKey.toPublic(); + const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsed.data.msg1, userGpgPubKey); + + assert.deepStrictEqual(rawBytes, Buffer.from(userPayload(1).payload)); + }); +});