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