@@ -5,9 +5,11 @@ import { btcBackupKey } from './fixtures';
55import { type TestBitGoAPI , TestBitGo } from '@bitgo/sdk-test' ;
66
77import { Tbtc } from '../../src' ;
8- import { BitGoAPI } from '@bitgo/sdk-api' ;
8+ import { BitGoAPI , encrypt } from '@bitgo/sdk-api' ;
99import * as utxolib from '@bitgo/utxo-lib' ;
1010
11+ import { Wallet } from '@bitgo/sdk-core' ;
12+
1113describe ( 'BTC:' , ( ) => {
1214 let bitgo : TestBitGoAPI ;
1315
@@ -106,4 +108,153 @@ describe('BTC:', () => {
106108 ) ;
107109 } ) ;
108110 } ) ;
111+
112+ describe ( 'Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)' , ( ) => {
113+ let coin : Tbtc ;
114+ let bitgoTest : TestBitGoAPI ;
115+ before ( ( ) => {
116+ bitgoTest = TestBitGo . decorate ( BitGoAPI , { env : 'test' } ) ;
117+ bitgoTest . safeRegister ( 'tbtc' , Tbtc . createInstance ) ;
118+ bitgoTest . initializeTestVars ( ) ;
119+ coin = bitgoTest . coin ( 'tbtc' ) as Tbtc ;
120+ } ) ;
121+
122+ it ( 'should detect hex spoofing in BUILD_SIGN_SEND' , async ( ) : Promise < void > => {
123+ const keyTriple = utxolib . testutil . getKeyTriple ( 'default' ) ;
124+ const rootWalletKey = new utxolib . bitgo . RootWalletKeys ( keyTriple ) ;
125+ const [ user ] = keyTriple ;
126+
127+ const wallet = new Wallet ( bitgoTest , coin , {
128+ id : '5b34252f1bf349930e34020a' ,
129+ coin : 'tbtc' ,
130+ keys : [ 'user' , 'backup' , 'bitgo' ] ,
131+ } ) ;
132+
133+ const originalPsbt = utxolib . testutil . constructPsbt (
134+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
135+ [ { address : 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7' , value : BigInt ( 9000 ) } ] ,
136+ coin . network ,
137+ rootWalletKey ,
138+ 'unsigned' as const
139+ ) ;
140+ utxolib . bitgo . addXpubsToPsbt ( originalPsbt , rootWalletKey ) ;
141+ const spoofedPsbt = utxolib . testutil . constructPsbt (
142+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
143+ [ { address : 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn' , value : BigInt ( 9000 ) } ] ,
144+ coin . network ,
145+ rootWalletKey ,
146+ 'unsigned' as const
147+ ) ;
148+ utxolib . bitgo . addXpubsToPsbt ( spoofedPsbt , rootWalletKey ) ;
149+ const spoofedHex : string = spoofedPsbt . toHex ( ) ;
150+
151+ const bgUrl : string = ( bitgoTest as any ) . _baseUrl ;
152+ const nock = require ( 'nock' ) ;
153+
154+ nock ( bgUrl )
155+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /consolidateUnspents` )
156+ . reply ( 200 , { txHex : spoofedHex , consolidateId : 'test' } ) ;
157+
158+ nock ( bgUrl )
159+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /tx/send` )
160+ . reply ( ( requestBody : any ) => {
161+ if ( requestBody ?. txHex === spoofedHex ) {
162+ throw new Error ( 'Spoofed transaction was sent: spoofing protection failed' ) ;
163+ }
164+ return [ 200 , { txid : 'test-txid-123' , status : 'signed' } ] ;
165+ } ) ;
166+
167+ const pubs = keyTriple . map ( ( k ) => k . neutered ( ) . toBase58 ( ) ) ;
168+ const responses = [
169+ { pub : pubs [ 0 ] , encryptedPrv : encrypt ( 'pass' , user . toBase58 ( ) ) } ,
170+ { pub : pubs [ 1 ] } ,
171+ { pub : pubs [ 2 ] } ,
172+ ] ;
173+ wallet
174+ . keyIds ( )
175+ . forEach ( ( id , i ) => nock ( bgUrl ) . get ( `/api/v2/${ wallet . coin ( ) } /key/${ id } ` ) . reply ( 200 , responses [ i ] ) ) ;
176+
177+ await assert . rejects (
178+ wallet . consolidateUnspents ( { walletPassphrase : 'pass' } ) ,
179+ ( e : any ) =>
180+ typeof e ?. message === 'string' &&
181+ e . message . includes ( 'prebuild attempts to spend to unintended external recipients' )
182+ ) ;
183+ } ) ;
184+ } ) ;
185+
186+ describe ( 'Unspent management spoofability - Fanout (BUILD_SIGN_SEND)' , ( ) => {
187+ let coin : Tbtc ;
188+ let bitgoTest : TestBitGoAPI ;
189+ before ( ( ) => {
190+ bitgoTest = TestBitGo . decorate ( BitGoAPI , { env : 'test' } ) ;
191+ bitgoTest . safeRegister ( 'tbtc' , Tbtc . createInstance ) ;
192+ bitgoTest . initializeTestVars ( ) ;
193+ coin = bitgoTest . coin ( 'tbtc' ) as Tbtc ;
194+ } ) ;
195+
196+ it ( 'should detect hex spoofing in fanout BUILD_SIGN_SEND' , async ( ) : Promise < void > => {
197+ const keyTriple = utxolib . testutil . getKeyTriple ( 'default' ) ;
198+ const rootWalletKey = new utxolib . bitgo . RootWalletKeys ( keyTriple ) ;
199+ const [ user ] = keyTriple ;
200+
201+ const wallet = new Wallet ( bitgoTest , coin , {
202+ id : '5b34252f1bf349930e34020a' ,
203+ coin : 'tbtc' ,
204+ keys : [ 'user' , 'backup' , 'bitgo' ] ,
205+ } ) ;
206+
207+ const originalPsbt = utxolib . testutil . constructPsbt (
208+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
209+ [ { address : 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7' , value : BigInt ( 9000 ) } ] ,
210+ coin . network ,
211+ rootWalletKey ,
212+ 'unsigned' as const
213+ ) ;
214+ utxolib . bitgo . addXpubsToPsbt ( originalPsbt , rootWalletKey ) ;
215+
216+ const spoofedPsbt = utxolib . testutil . constructPsbt (
217+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
218+ [ { address : 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn' , value : BigInt ( 9000 ) } ] ,
219+ coin . network ,
220+ rootWalletKey ,
221+ 'unsigned' as const
222+ ) ;
223+ utxolib . bitgo . addXpubsToPsbt ( spoofedPsbt , rootWalletKey ) ;
224+ const spoofedHex : string = spoofedPsbt . toHex ( ) ;
225+
226+ const bgUrl : string = ( bitgoTest as any ) . _baseUrl ;
227+ const nock = require ( 'nock' ) ;
228+
229+ nock ( bgUrl )
230+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /fanoutUnspents` )
231+ . reply ( 200 , { txHex : spoofedHex , fanoutId : 'test' } ) ;
232+
233+ nock ( bgUrl )
234+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /tx/send` )
235+ . reply ( ( requestBody : any ) => {
236+ if ( requestBody ?. txHex === spoofedHex ) {
237+ throw new Error ( 'Spoofed transaction was sent: spoofing protection failed' ) ;
238+ }
239+ return [ 200 , { txid : 'test-txid-123' , status : 'signed' } ] ;
240+ } ) ;
241+
242+ const pubs = keyTriple . map ( ( k ) => k . neutered ( ) . toBase58 ( ) ) ;
243+ const responses = [
244+ { pub : pubs [ 0 ] , encryptedPrv : encrypt ( 'pass' , user . toBase58 ( ) ) } ,
245+ { pub : pubs [ 1 ] } ,
246+ { pub : pubs [ 2 ] } ,
247+ ] ;
248+ wallet
249+ . keyIds ( )
250+ . forEach ( ( id , i ) => nock ( bgUrl ) . get ( `/api/v2/${ wallet . coin ( ) } /key/${ id } ` ) . reply ( 200 , responses [ i ] ) ) ;
251+
252+ await assert . rejects (
253+ wallet . fanoutUnspents ( { walletPassphrase : 'pass' } ) ,
254+ ( e : any ) =>
255+ typeof e ?. message === 'string' &&
256+ e . message . includes ( 'prebuild attempts to spend to unintended external recipients' )
257+ ) ;
258+ } ) ;
259+ } ) ;
109260} ) ;
0 commit comments