diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 522c212b31..9c566610b1 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -281,9 +281,9 @@ export interface RecoverFromWrongChainOptions { } export interface VerifyKeySignaturesOptions { - userKeychain?: Keychain; - keychainToVerify?: Keychain; - keySignature?: string; + userKeychain: { pub?: string }; + keychainToVerify: { pub?: string }; + keySignature: string; } export interface VerifyUserPublicKeyOptions { @@ -744,7 +744,13 @@ export abstract class AbstractUtxoCoin extends BaseCoin { if (!keySignature) { throw new Error(`missing required custom change ${KeyIndices[keyIndex].toLowerCase()} keychain signature`); } - if (!this.verifyKeySignature({ userKeychain, keychainToVerify, keySignature })) { + if ( + !this.verifyKeySignature({ + userKeychain: userKeychain as { pub: string }, + keychainToVerify: keychainToVerify as { pub: string }, + keySignature, + }) + ) { debug('failed to verify custom change %s key signature!', KeyIndices[keyIndex].toLowerCase()); return false; } @@ -814,8 +820,16 @@ export abstract class AbstractUtxoCoin extends BaseCoin { // let's verify these keychains const keySignatures = parsedTransaction.keySignatures; if (!_.isEmpty(keySignatures)) { - const verify = (key, pub) => - this.verifyKeySignature({ userKeychain: keychains.user, keychainToVerify: key, keySignature: pub }); + const verify = (key, pub) => { + if (!keychains.user || !keychains.user.pub) { + throw new Error('missing user keychain'); + } + return this.verifyKeySignature({ + userKeychain: keychains.user as { pub: string }, + keychainToVerify: key, + keySignature: pub, + }); + }; const isBackupKeySignatureValid = verify(keychains.backup, keySignatures.backupPub); const isBitgoKeySignatureValid = verify(keychains.bitgo, keySignatures.bitgoPub); if (!isBackupKeySignatureValid || !isBitgoKeySignatureValid) { diff --git a/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts b/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts index 5fecaa8db1..0f42a0bdf5 100644 --- a/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts +++ b/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts @@ -1,7 +1,7 @@ import * as utxolib from '@bitgo/utxo-lib'; import * as should from 'should'; import * as sinon from 'sinon'; -import { Keychain, UnexpectedAddressError, VerificationOptions } from '@bitgo/sdk-core'; +import { UnexpectedAddressError, VerificationOptions } from '@bitgo/sdk-core'; import { TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../../src/bitgo'; import { @@ -247,7 +247,7 @@ describe('Abstract UTXO Coin:', () => { }; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const sign = async (key, keychain, coinToSignFor = coin) => (await coinToSignFor.signMessage(keychain, key.pub!)).toString('hex'); + const sign = async (key, keychain) => (await coin.signMessage(keychain, key.pub!)).toString('hex'); const signUser = (key) => sign(key, userKeychain); const signOther = (key) => sign(key, otherKeychain); const passphrase = 'test_passphrase'; @@ -526,18 +526,5 @@ describe('Abstract UTXO Coin:', () => { coinMock.restore(); bitcoinMock.restore(); }); - - it('should verify key signature of ZEC', async () => { - const zecCoin = bitgo.coin('tzec') as AbstractUtxoCoin; - const userKeychain = await zecCoin.keychains().create(); - const otherKeychain = await zecCoin.keychains().create(); - - await zecCoin.verifyKeySignature({ - userKeychain: (userKeychain as unknown) as Keychain, - keychainToVerify: (otherKeychain as unknown) as Keychain, - keySignature: await sign(userKeychain, otherKeychain, zecCoin), - }).should.be.true(); - }); - }); }); diff --git a/modules/bitgo/test/v2/unit/coins/utxo/keySignatures.ts b/modules/bitgo/test/v2/unit/coins/utxo/keySignatures.ts new file mode 100644 index 0000000000..46b26613f4 --- /dev/null +++ b/modules/bitgo/test/v2/unit/coins/utxo/keySignatures.ts @@ -0,0 +1,31 @@ +import * as assert from 'assert'; +import { AbstractUtxoCoin } from '@bitgo/abstract-utxo'; +import { Keychain } from '@bitgo/sdk-core'; + +import { utxoCoins } from './util'; + +function describeWithCoin(coin: AbstractUtxoCoin) { + describe(`verifyKeySignatures for ${coin.getChain()}`, function () { + it('should verify key signature of ZEC', async () => { + const userKeychain = await coin.keychains().create(); + const backupKeychain = await coin.keychains().create(); + const bitgoKeychain = await coin.keychains().create(); + + const signatures = await coin.createKeySignatures( + userKeychain.prv, + { pub: backupKeychain.pub as string }, + { pub: bitgoKeychain.pub as string }, + ); + + assert.ok( + await coin.verifyKeySignature({ + userKeychain: (userKeychain as unknown) as Keychain, + keychainToVerify: (backupKeychain as unknown) as Keychain, + keySignature: signatures.backup, + })); + }); + }); +} + +utxoCoins.forEach(coin => describeWithCoin(coin)); + diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 420f4473a0..078a65e4e4 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -471,4 +471,19 @@ export abstract class BaseCoin implements IBaseCoin { getHashFunction(): Hash { throw new NotImplementedError('getHashFunction is not supported for this coin'); } + + // `AbstractUtxoCoin` implements and uses the complementary `verifyKeySignature` method. + public async createKeySignatures( + prv: string, + backupKeychain: { pub: string }, + bitgoKeychain: { pub: string } + ): Promise<{ + backup: string; + bitgo: string; + }> { + return { + backup: (await this.signMessage({ prv }, backupKeychain.pub)).toString('hex'), + bitgo: (await this.signMessage({ prv }, bitgoKeychain.pub)).toString('hex'), + }; + } } diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 0ed4f37c38..22a45424c9 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -416,61 +416,123 @@ export type MPCAlgorithm = 'ecdsa' | 'eddsa'; export interface IBaseCoin { type: string; tokenConfig?: BaseTokenConfig; + url(suffix: string): string; + wallets(): IWallets; + enterprises(): IEnterprises; + keychains(): IKeychains; + webhooks(): IWebhooks; + pendingApprovals(): IPendingApprovals; + markets(): IMarkets; + getChain(): string; + getFamily(): string; + getFullName(): string; + valuelessTransferAllowed(): boolean; + sweepWithSendMany(): boolean; + transactionDataAllowed(): boolean; + allowsAccountConsolidations(): boolean; + getTokenEnablementConfig(): TokenEnablementConfig; + supportsTss(): boolean; + isEVM(): boolean; + supportsBlsDkg(): boolean; + getBaseFactor(): number | string; + baseUnitsToBigUnits(baseUnits: string | number): string; + bigUnitsToBaseUnits(bigUnits: string | number): string; + signMessage(key: { prv: string }, message: string): Promise; + explainTransaction(options: Record): Promise | undefined>; + verifyTransaction(params: VerifyTransactionOptions): Promise; + verifyAddress(params: VerifyAddressOptions): Promise; + isWalletAddress(params: VerifyAddressOptions): Promise; + canonicalAddress(address: string, format: unknown): string; + supportsBlockTarget(): boolean; + supportsLightning(): boolean; + supportsMessageSigning(): boolean; + supportsSigningTypedData(): boolean; + supplementGenerateWallet(walletParams: SupplementGenerateWalletOptions, keychains: KeychainsTriplet): Promise; + getExtraPrebuildParams(buildParams: ExtraPrebuildParamsOptions): Promise>; + postProcessPrebuild(prebuildResponse: TransactionPrebuild): Promise; + presignTransaction(params: PresignTransactionOptions): Promise; + signWithCustomSigningFunction?( customSigningFunction: CustomSigningFunction, signTransactionParams: { txPrebuild: TransactionPrebuild; pubs?: string[] } ): Promise; + newWalletObject(walletParams: any): IWallet; + feeEstimate(params: FeeEstimateOptions): Promise; + deriveKeyWithSeed(params: DeriveKeyWithSeedOptions): { key: string; derivationPath: string }; + keyIdsForSigning(): number[]; + preCreateBitGo(params: PrecreateBitGoOptions): void; + initiateRecovery(params: InitiateRecoveryOptions): never; + parseTransaction(params: ParseTransactionOptions): Promise; + generateKeyPair(seed?: Buffer): KeyPair; + isValidPub(pub: string): boolean; + isValidMofNSetup(params: ValidMofNOptions): boolean; + isValidAddress(address: string): boolean; + signTransaction(params: SignTransactionOptions): Promise; + getSignablePayload(serializedTx: string): Promise; + getMPCAlgorithm(): MPCAlgorithm; + // TODO - this only belongs in eth coins recoverToken(params: RecoverWalletTokenOptions): Promise; + getInscriptionBuilder(wallet: Wallet): IInscriptionBuilder; + getHashFunction(): Hash; + + createKeySignatures( + prv: string, + backupKeychain: { pub: string }, + bitgoKeychain: { pub: string } + ): Promise<{ + backup: string; + bitgo: string; + }>; }