Skip to content

Commit 98f9908

Browse files
authored
Merge pull request #7960 from BitGo/create-address-issue/WP-7507
fix: address validation for SMC wallets
2 parents 5180249 + 2730531 commit 98f9908

7 files changed

Lines changed: 410 additions & 3 deletions

File tree

.iyarc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@
44
# - This CVE affects archive EXTRACTION (unpacking malicious symlinks/hardlinks)
55
# - Lerna only uses tar for PACKING
66
GHSA-8qq5-rm4j-mr97
7+
8+
# Excluded because:
9+
# - Transitive dependency through lerna and yeoman-generator, which currently pin tar to a
10+
# < 7.5.4 range; We only use their tar integration for
11+
# archive PACKING, not extraction,
712
GHSA-r6q2-hw4h-h46w
13+

modules/sdk-coin-sol/test/unit/sol.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3478,6 +3478,21 @@ describe('SOL:', function () {
34783478
message: `invalid address: ${invalidAddress}`,
34793479
});
34803480
});
3481+
3482+
it('should verify address with derivation prefix for SMC wallets', async function () {
3483+
// Address derived with derivation prefix m/999999/148242355/239336845/1
3484+
const address = 'CC715Q92C8vuEFT5xujE55Do6BkWRdpDvr7Vh8iUNUBw';
3485+
const commonKeychain =
3486+
'8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd';
3487+
const index = '1';
3488+
const derivedFromParentWithSeed = 'smc-test-seed-123';
3489+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
3490+
3491+
// This test verifies that derivedFromParentWithSeed is accepted as a parameter
3492+
// and the verification function correctly computes the derivation prefix internally
3493+
const result = await basecoin.isWalletAddress({ keychains, address, index, derivedFromParentWithSeed });
3494+
result.should.equal(true);
3495+
});
34813496
});
34823497

34833498
describe('getAddressFromPublicKey', () => {

modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ export interface VerifyAddressOptions {
154154
error?: string;
155155
coinSpecific?: AddressCoinSpecific;
156156
impliedForwarderVersion?: number;
157+
/**
158+
* Optional seed value from user keychain's derivedFromParentWithSeed field.
159+
* For SMC (Self-Managed Custodial) TSS wallets, this is used to compute the derivation prefix.
160+
*/
161+
derivedFromParentWithSeed?: string;
157162
}
158163

159164
/**
@@ -173,8 +178,15 @@ export interface TssVerifyAddressOptions {
173178
/**
174179
* Derivation index for the address.
175180
* Used to derive child addresses from the root keychain via HD derivation path: m/{index}
181+
* For SMC (Self-Managed Custodial) wallets, the path includes a prefix: m/{derivationPrefix}/{index}
176182
*/
177183
index: number | string;
184+
/**
185+
* Optional seed value from user keychain's derivedFromParentWithSeed field.
186+
* For SMC (Self-Managed Custodial) wallets, this is used to compute the derivation prefix.
187+
* The derivation path becomes {computedPrefix}/{index} instead of m/{index}.
188+
*/
189+
derivedFromParentWithSeed?: string;
178190
}
179191

180192
export function isTssVerifyAddressOptions<T extends VerifyAddressOptions | TssVerifyAddressOptions>(

modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
12
import { Ecdsa } from '../../../account-lib/mpc';
23
import { TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin';
34
import { InvalidAddressError } from '../../errors';
@@ -65,15 +66,21 @@ export async function verifyMPCWalletAddress(
6566
isValidAddress: (address: string) => boolean,
6667
getAddressFromPublicKey: (publicKey: string) => string
6768
): Promise<boolean> {
68-
const { keychains, address, index } = params;
69+
const { keychains, address, index, derivedFromParentWithSeed } = params;
6970

7071
if (!isValidAddress(address)) {
7172
throw new InvalidAddressError(`invalid address: ${address}`);
7273
}
7374

7475
const MPC = params.keyCurve === 'secp256k1' ? new Ecdsa() : await EDDSAMethods.getInitializedMpcInstance();
7576
const commonKeychain = extractCommonKeychain(keychains);
76-
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, 'm/' + index);
77+
78+
// Compute derivation path:
79+
// - For SMC wallets with derivedFromParentWithSeed, compute prefix and use: {prefix}/{index}
80+
// - For other wallets, use simple path: m/{index}
81+
const prefix = derivedFromParentWithSeed ? getDerivationPath(derivedFromParentWithSeed.toString()) : undefined;
82+
const derivationPath = prefix ? `${prefix}/${index}` : `m/${index}`;
83+
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath);
7784

7885
// secp256k1 expects 33 bytes; ed25519 expects 32 bytes
7986
const publicKeySize = params.keyCurve === 'secp256k1' ? 33 : 32;

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { SubmitTransactionResponse } from '../inscriptionBuilder';
3333
import { drawKeycard } from '../internal';
3434
import * as internal from '../internal/internal';
35-
import { decryptKeychainPrivateKey, Keychain, KeychainWithEncryptedPrv } from '../keychain';
35+
import { decryptKeychainPrivateKey, Keychain, KeychainWithEncryptedPrv, KeyIndices } from '../keychain';
3636
import { getLightningAuthKey } from '../lightning/lightningWalletUtil';
3737
import { IPendingApproval, PendingApproval, PendingApprovals } from '../pendingApproval';
3838
import { GoStakingWallet, StakingWallet } from '../staking';
@@ -1408,6 +1408,15 @@ export class Wallet implements IWallet {
14081408
}
14091409

14101410
verificationData.impliedForwarderVersion = forwarderVersion ?? verificationData.coinSpecific?.forwarderVersion;
1411+
1412+
// For SMC (Self-Managed Custodial) wallets, pass derivedFromParentWithSeed from user keychain
1413+
// The verification function will compute the derivation prefix internally
1414+
// Custodial wallets don't need this as their commonKeychain already accounts for the prefix
1415+
if (this.multisigType() === 'tss' && this.type() === 'cold' && this._wallet.keys.length > KeyIndices.USER) {
1416+
const userKeychain = keychains[KeyIndices.USER] as Keychain | undefined;
1417+
verificationData.derivedFromParentWithSeed = userKeychain?.derivedFromParentWithSeed;
1418+
}
1419+
14111420
// This condition was added in first place because in celo, when verifyAddress method was called on addresses which were having pendingChainInitialization as true, it used to throw some error
14121421
// In case of forwarder version 1 eth addresses, addresses need to be verified even if the pendingChainInitialization flag is true
14131422
if (
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as assert from 'assert';
2+
import 'should';
3+
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
4+
5+
function getAddressVerificationModule() {
6+
return require('../../../../../src/bitgo/utils/tss/addressVerification');
7+
}
8+
9+
const getExtractCommonKeychain = () => getAddressVerificationModule().extractCommonKeychain;
10+
11+
describe('TSS Address Verification - Derivation Path with Prefix', function () {
12+
const commonKeychain =
13+
'8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd';
14+
15+
describe('extractCommonKeychain', function () {
16+
it('should extract commonKeychain from keychains array', function () {
17+
const extractCommonKeychain = getExtractCommonKeychain();
18+
const keychains = [{ commonKeychain }, { commonKeychain }, { commonKeychain }];
19+
20+
const result = extractCommonKeychain(keychains);
21+
result.should.equal(commonKeychain);
22+
});
23+
24+
it('should throw error if keychains are missing', function () {
25+
const extractCommonKeychain = getExtractCommonKeychain();
26+
assert.throws(() => extractCommonKeychain([]), /missing required param keychains/);
27+
assert.throws(() => extractCommonKeychain(undefined as any), /missing required param keychains/);
28+
});
29+
30+
it('should throw error if commonKeychain is missing', function () {
31+
const extractCommonKeychain = getExtractCommonKeychain();
32+
const keychains = [{ id: '1' }];
33+
assert.throws(() => extractCommonKeychain(keychains as any), /missing required param commonKeychain/);
34+
});
35+
36+
it('should throw error if keychains have mismatched commonKeychains', function () {
37+
const extractCommonKeychain = getExtractCommonKeychain();
38+
const keychains = [{ commonKeychain }, { commonKeychain: 'different-keychain' }];
39+
assert.throws(() => extractCommonKeychain(keychains), /all keychains must have the same commonKeychain/);
40+
});
41+
});
42+
43+
describe('Derivation Path Format Validation', function () {
44+
it('should produce correct derivation path format', function () {
45+
const testSeeds = ['seed1', 'seed-2', '12345', 'test_seed_123'];
46+
47+
testSeeds.forEach((seed) => {
48+
const path = getDerivationPath(seed);
49+
50+
// Expected format: "m/999999/{part1}/{part2}"
51+
path.should.match(/^m\/999999\/\d+\/\d+$/);
52+
53+
path.should.startWith('m/');
54+
55+
// Should have exactly 3 parts
56+
const parts = path.split('/');
57+
parts.length.should.equal(4);
58+
parts[0].should.equal('m');
59+
parts[1].should.equal('999999');
60+
});
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)