@@ -3,9 +3,7 @@ import { randomBytes } from 'crypto';
33import _ from 'lodash' ;
44import * as utxolib from '@bitgo/utxo-lib' ;
55import { bip32 , BIP32Interface , bitgo , getMainnet , isMainnet , isTestnet } from '@bitgo/utxo-lib' ;
6- import * as bitcoinMessage from 'bitcoinjs-message' ;
76import debugLib from 'debug' ;
8- import BigNumber from 'bignumber.js' ;
97
108import {
119 backupKeyRecovery ,
@@ -27,7 +25,6 @@ import {
2725 BaseCoin ,
2826 BitGoBase ,
2927 CreateAddressFormat ,
30- decryptKeychainPrivateKey ,
3128 ExtraPrebuildParamsOptions ,
3229 HalfSignedUtxoTransaction ,
3330 IBaseCoin ,
@@ -70,8 +67,6 @@ import {
7067 assertValidTransactionRecipient ,
7168 explainTx ,
7269 fromExtendedAddressFormat ,
73- getPsbtTxInputs ,
74- getTxInputs ,
7570 isScriptRecipient ,
7671} from './transaction' ;
7772import { assertDescriptorWalletAddress } from './descriptor' ;
@@ -83,6 +78,8 @@ import { NamedKeychains } from './keychains';
8378const debug = debugLib ( 'bitgo:v2:utxo' ) ;
8479
8580import ScriptType2Of3 = utxolib . bitgo . outputScripts . ScriptType2Of3 ;
81+ import { verifyTransaction } from './transaction/fixedScript/verifyTransaction' ;
82+ import { verifyKeySignature , verifyUserPublicKey } from './verifyKey' ;
8683
8784type UtxoCustomSigningFunction < TNumber extends number | bigint > = {
8885 ( params : {
@@ -608,150 +605,17 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
608605 }
609606
610607 /**
611- * Decrypt the wallet's user private key and verify that the claimed public key matches
612- * @param {VerifyUserPublicKeyOptions } params
613- * @return {boolean }
614- * @protected
608+ * @deprecated - use function verifyUserPublicKey instead
615609 */
616610 protected verifyUserPublicKey ( params : VerifyUserPublicKeyOptions ) : boolean {
617- const { userKeychain, txParams, disableNetworking } = params ;
618- if ( ! userKeychain ) {
619- throw new Error ( 'user keychain is required' ) ;
620- }
621-
622- const userPub = userKeychain . pub ;
623-
624- // decrypt the user private key, so we can verify that the claimed public key is a match
625- let userPrv = userKeychain . prv ;
626- if ( ! userPrv && txParams . walletPassphrase ) {
627- userPrv = decryptKeychainPrivateKey ( this . bitgo , userKeychain , txParams . walletPassphrase ) ;
628- }
629-
630- if ( ! userPrv ) {
631- const errorMessage = 'user private key unavailable for verification' ;
632- if ( disableNetworking ) {
633- console . log ( errorMessage ) ;
634- return false ;
635- } else {
636- throw new Error ( errorMessage ) ;
637- }
638- } else {
639- const userPrivateKey = bip32 . fromBase58 ( userPrv ) ;
640- if ( userPrivateKey . toBase58 ( ) === userPrivateKey . neutered ( ) . toBase58 ( ) ) {
641- throw new Error ( 'user private key is only public' ) ;
642- }
643- if ( userPrivateKey . neutered ( ) . toBase58 ( ) !== userPub ) {
644- throw new Error ( 'user private key does not match public key' ) ;
645- }
646- }
647-
648- return true ;
611+ return verifyUserPublicKey ( this . bitgo , params ) ;
649612 }
650613
651614 /**
652- * Verify signatures produced by the user key over the backup and bitgo keys.
653- *
654- * If set, these signatures ensure that the wallet keys cannot be changed after the wallet has been created.
655- * @param {VerifyKeySignaturesOptions } params
656- * @return {{backup: boolean, bitgo: boolean} }
615+ * @deprecated - use function verifyKeySignature instead
657616 */
658617 public verifyKeySignature ( params : VerifyKeySignaturesOptions ) : boolean {
659- // first, let's verify the integrity of the user key, whose public key is used for subsequent verifications
660- const { userKeychain, keychainToVerify, keySignature } = params ;
661- if ( ! userKeychain ) {
662- throw new Error ( 'user keychain is required' ) ;
663- }
664-
665- if ( ! keychainToVerify ) {
666- throw new Error ( 'keychain to verify is required' ) ;
667- }
668-
669- if ( ! keySignature ) {
670- throw new Error ( 'key signature is required' ) ;
671- }
672-
673- // verify the signature against the user public key
674- assert ( userKeychain . pub ) ;
675- const publicKey = bip32 . fromBase58 ( userKeychain . pub ) . publicKey ;
676- // Due to interface of `bitcoinMessage`, we need to convert the public key to an address.
677- // Note that this address has no relationship to on-chain transactions. We are
678- // only interested in the address as a representation of the public key.
679- const signingAddress = utxolib . address . toBase58Check (
680- utxolib . crypto . hash160 ( publicKey ) ,
681- utxolib . networks . bitcoin . pubKeyHash ,
682- // we do not pass `this.network` here because it would fail for zcash
683- // the bitcoinMessage library decodes the address and throws away the first byte
684- // because zcash has a two-byte prefix, verify() decodes zcash addresses to an invalid pubkey hash
685- utxolib . networks . bitcoin
686- ) ;
687-
688- // BG-5703: use BTC mainnet prefix for all key signature operations
689- // (this means do not pass a prefix parameter, and let it use the default prefix instead)
690- assert ( keychainToVerify . pub ) ;
691- try {
692- return bitcoinMessage . verify ( keychainToVerify . pub , signingAddress , Buffer . from ( keySignature , 'hex' ) ) ;
693- } catch ( e ) {
694- debug ( 'error thrown from bitcoinmessage while verifying key signature' , e ) ;
695- return false ;
696- }
697- }
698-
699- /**
700- * Verify signatures against the user private key over the change wallet extended keys
701- * @param {ParsedTransaction } tx
702- * @param {Keychain } userKeychain
703- * @return {boolean }
704- * @protected
705- */
706- protected verifyCustomChangeKeySignatures < TNumber extends number | bigint > (
707- tx : ParsedTransaction < TNumber > ,
708- userKeychain : Keychain
709- ) : boolean {
710- if ( ! tx . customChange ) {
711- throw new Error ( 'parsed transaction is missing required custom change verification data' ) ;
712- }
713-
714- if ( ! Array . isArray ( tx . customChange . keys ) || ! Array . isArray ( tx . customChange . signatures ) ) {
715- throw new Error ( 'customChange property is missing keys or signatures' ) ;
716- }
717-
718- for ( const keyIndex of [ KeyIndices . USER , KeyIndices . BACKUP , KeyIndices . BITGO ] ) {
719- const keychainToVerify = tx . customChange . keys [ keyIndex ] ;
720- const keySignature = tx . customChange . signatures [ keyIndex ] ;
721- if ( ! keychainToVerify ) {
722- throw new Error ( `missing required custom change ${ KeyIndices [ keyIndex ] . toLowerCase ( ) } keychain public key` ) ;
723- }
724- if ( ! keySignature ) {
725- throw new Error ( `missing required custom change ${ KeyIndices [ keyIndex ] . toLowerCase ( ) } keychain signature` ) ;
726- }
727- if (
728- ! this . verifyKeySignature ( {
729- userKeychain : userKeychain as { pub : string } ,
730- keychainToVerify : keychainToVerify as { pub : string } ,
731- keySignature,
732- } )
733- ) {
734- debug ( 'failed to verify custom change %s key signature!' , KeyIndices [ keyIndex ] . toLowerCase ( ) ) ;
735- return false ;
736- }
737- }
738-
739- return true ;
740- }
741-
742- /**
743- * Get the maximum percentage limit for pay-as-you-go outputs
744- *
745- * @protected
746- */
747- protected getPayGoLimit ( allowPaygoOutput ?: boolean ) : number {
748- // allowing paygo outputs needs to be the default behavior, so only disallow paygo outputs if the
749- // relevant verification option is both set and false
750- if ( ! _ . isNil ( allowPaygoOutput ) && ! allowPaygoOutput ) {
751- return 0 ;
752- }
753- // 150 basis points is the absolute permitted maximum if paygo outputs are allowed
754- return 0.015 ;
618+ return verifyKeySignature ( params ) ;
755619 }
756620
757621 /**
@@ -771,142 +635,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
771635 async verifyTransaction < TNumber extends number | bigint = number > (
772636 params : VerifyTransactionOptions < TNumber >
773637 ) : Promise < boolean > {
774- const { txParams, txPrebuild, wallet, verification = { allowPaygoOutput : true } , reqId } = params ;
775- const isPsbt = txPrebuild . txHex && bitgo . isPsbt ( txPrebuild . txHex ) ;
776- if ( isPsbt && txPrebuild . txInfo ?. unspents ) {
777- throw new Error ( 'should not have unspents in txInfo for psbt' ) ;
778- }
779-
780- const disableNetworking = ! ! verification . disableNetworking ;
781- const parsedTransaction : ParsedTransaction < TNumber > = await this . parseTransaction < TNumber > ( {
782- txParams,
783- txPrebuild,
784- wallet,
785- verification,
786- reqId,
787- } ) ;
788-
789- const keychains = parsedTransaction . keychains ;
790-
791- // verify that the claimed user public key corresponds to the wallet's user private key
792- let userPublicKeyVerified = false ;
793- try {
794- // verify the user public key matches the private key - this will throw if there is no match
795- userPublicKeyVerified = this . verifyUserPublicKey ( { userKeychain : keychains . user , disableNetworking, txParams } ) ;
796- } catch ( e ) {
797- debug ( 'failed to verify user public key!' , e ) ;
798- }
799-
800- // let's verify these keychains
801- const keySignatures = parsedTransaction . keySignatures ;
802- if ( ! _ . isEmpty ( keySignatures ) ) {
803- const verify = ( key , pub ) => {
804- if ( ! keychains . user || ! keychains . user . pub ) {
805- throw new Error ( 'missing user keychain' ) ;
806- }
807- return this . verifyKeySignature ( {
808- userKeychain : keychains . user as { pub : string } ,
809- keychainToVerify : key ,
810- keySignature : pub ,
811- } ) ;
812- } ;
813- const isBackupKeySignatureValid = verify ( keychains . backup , keySignatures . backupPub ) ;
814- const isBitgoKeySignatureValid = verify ( keychains . bitgo , keySignatures . bitgoPub ) ;
815- if ( ! isBackupKeySignatureValid || ! isBitgoKeySignatureValid ) {
816- throw new Error ( 'secondary public key signatures invalid' ) ;
817- }
818- debug ( 'successfully verified backup and bitgo key signatures' ) ;
819- } else if ( ! disableNetworking ) {
820- // these keys were obtained online and their signatures were not verified
821- // this could be dangerous
822- console . log ( 'unsigned keys obtained online are being used for address verification' ) ;
823- }
824-
825- if ( parsedTransaction . needsCustomChangeKeySignatureVerification ) {
826- if ( ! keychains . user || ! userPublicKeyVerified ) {
827- throw new Error ( 'transaction requires verification of user public key, but it was unable to be verified' ) ;
828- }
829- const customChangeKeySignaturesVerified = this . verifyCustomChangeKeySignatures ( parsedTransaction , keychains . user ) ;
830- if ( ! customChangeKeySignaturesVerified ) {
831- throw new Error (
832- 'transaction requires verification of custom change key signatures, but they were unable to be verified'
833- ) ;
834- }
835- debug ( 'successfully verified user public key and custom change key signatures' ) ;
836- }
837-
838- const missingOutputs = parsedTransaction . missingOutputs ;
839- if ( missingOutputs . length !== 0 ) {
840- // there are some outputs in the recipients list that have not made it into the actual transaction
841- throw new Error ( 'expected outputs missing in transaction prebuild' ) ;
842- }
843-
844- const intendedExternalSpend = parsedTransaction . explicitExternalSpendAmount ;
845-
846- // this is a limit we impose for the total value that is amended to the transaction beyond what was originally intended
847- const payAsYouGoLimit = new BigNumber ( this . getPayGoLimit ( verification . allowPaygoOutput ) ) . multipliedBy (
848- intendedExternalSpend . toString ( )
849- ) ;
850-
851- /*
852- Some explanation for why we're doing what we're doing:
853- Some customers will have an output to BitGo's PAYGo wallet added to their transaction, and we need to account for
854- it here. To protect someone tampering with the output to make it send more than it should to BitGo, we define a
855- threshold for the output's value above which we'll throw an error, because the paygo output should never be that
856- high.
857- */
858-
859- // make sure that all the extra addresses are change addresses
860- // get all the additional external outputs the server added and calculate their values
861- const nonChangeAmount = new BigNumber ( parsedTransaction . implicitExternalSpendAmount . toString ( ) ) ;
862-
863- debug (
864- 'Intended spend is %s, Non-change amount is %s, paygo limit is %s' ,
865- intendedExternalSpend . toString ( ) ,
866- nonChangeAmount . toString ( ) ,
867- payAsYouGoLimit . toString ( )
868- ) ;
869-
870- // There are two instances where we will get into this point here
871- if ( nonChangeAmount . gt ( payAsYouGoLimit ) ) {
872- if ( isPsbt && parsedTransaction . customChange ) {
873- // In the case that we have a custom change address on a wallet and we are building the transaction
874- // with a PSBT, we do not have the metadata to verify the address from the custom change wallet, nor
875- // can we fetch that information from the other wallet because we may not have the credentials. Therefore,
876- // we will not throw an error here, but we will log a warning.
877- debug ( `cannot verify some of the addresses because it belongs to a separate wallet` ) ;
878- } else {
879- // the additional external outputs can only be BitGo's pay-as-you-go fee, but we cannot verify the wallet address
880- // there are some addresses that are outside the scope of intended recipients that are not change addresses
881- throw new Error ( 'prebuild attempts to spend to unintended external recipients' ) ;
882- }
883- }
884-
885- const allOutputs = parsedTransaction . outputs ;
886- if ( ! txPrebuild . txHex ) {
887- throw new Error ( `txPrebuild.txHex not set` ) ;
888- }
889- const inputs = isPsbt
890- ? getPsbtTxInputs ( txPrebuild . txHex , this . network ) . map ( ( v ) => ( {
891- ...v ,
892- value : bitgo . toTNumber ( v . value , this . amountType ) ,
893- } ) )
894- : await getTxInputs ( { txPrebuild, bitgo : this . bitgo , coin : this , disableNetworking, reqId } ) ;
895- // coins (doge) that can exceed number limits (and thus will use bigint) will have the `valueString` field
896- const inputAmount = inputs . reduce (
897- ( sum : bigint , i ) => sum + BigInt ( this . amountType === 'bigint' ? i . valueString : i . value ) ,
898- BigInt ( 0 )
899- ) ;
900- const outputAmount = allOutputs . reduce ( ( sum : bigint , o : Output ) => sum + BigInt ( o . amount ) , BigInt ( 0 ) ) ;
901- const fee = inputAmount - outputAmount ;
902-
903- if ( fee < 0 ) {
904- throw new Error (
905- `attempting to spend ${ outputAmount } satoshis, which exceeds the input amount (${ inputAmount } satoshis) by ${ - fee } `
906- ) ;
907- }
908-
909- return true ;
638+ return verifyTransaction ( this , this . bitgo , params ) ;
910639 }
911640
912641 /**
0 commit comments