Skip to content

Commit 037efc8

Browse files
Merge pull request #5262 from BitGo/BTC-1450.validate-descriptors
feat(abstract-utxo): validate descriptors depending on env
2 parents e9eb691 + dcd9793 commit 037efc8

File tree

8 files changed

+134
-33
lines changed

8 files changed

+134
-33
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,13 @@ import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptor
7575

7676
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
7777
import { CustomChangeOptions } from './transaction/fixedScript';
78-
import { NamedKeychains } from './keychains';
78+
import { NamedKeychains, toBip32Triple } from './keychains';
7979

8080
const debug = debugLib('bitgo:v2:utxo');
8181

8282
import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
8383
import { verifyKeySignature, verifyUserPublicKey } from './verifyKey';
84+
import { getPolicyForEnv } from './descriptor/validatePolicy';
8485

8586
type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
8687
(params: {
@@ -678,7 +679,17 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
678679
}
679680

680681
if (wallet && isDescriptorWallet(wallet)) {
681-
assertDescriptorWalletAddress(this.network, params, getDescriptorMapFromWallet(wallet));
682+
if (!keychains) {
683+
throw new Error('missing required param keychains');
684+
}
685+
if (!isTriple(keychains)) {
686+
throw new Error('keychains must be a triple');
687+
}
688+
assertDescriptorWalletAddress(
689+
this.network,
690+
params,
691+
getDescriptorMapFromWallet(wallet, toBip32Triple(keychains), getPolicyForEnv(this.bitgo.env))
692+
);
682693
return true;
683694
}
684695

modules/abstract-utxo/src/descriptor/descriptorWallet.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as t from 'io-ts';
2+
import { IWallet, WalletCoinSpecific } from '@bitgo/sdk-core';
3+
24
import { NamedDescriptor } from './NamedDescriptor';
5+
import { DescriptorMap } from '../core/descriptor';
36
import { AbstractUtxoCoinWalletData } from '../abstractUtxoCoin';
4-
import { DescriptorMap, toDescriptorMap } from '../core/descriptor';
5-
import { IWallet, WalletCoinSpecific } from '@bitgo/sdk-core';
7+
import { DescriptorValidationPolicy, KeyTriple, toDescriptorMapValidate } from './validatePolicy';
68

79
type DescriptorWalletCoinSpecific = {
810
descriptors: NamedDescriptor[];
@@ -30,10 +32,10 @@ export function isDescriptorWallet(obj: IWallet): obj is IDescriptorWallet {
3032
return isDescriptorWalletCoinSpecific(obj.coinSpecific());
3133
}
3234

33-
export function getDescriptorMapFromWalletData(wallet: DescriptorWalletData): DescriptorMap {
34-
return toDescriptorMap(wallet.coinSpecific.descriptors);
35-
}
36-
37-
export function getDescriptorMapFromWallet(wallet: IDescriptorWallet): DescriptorMap {
38-
return toDescriptorMap(wallet.coinSpecific().descriptors);
35+
export function getDescriptorMapFromWallet(
36+
wallet: IDescriptorWallet,
37+
walletKeys: KeyTriple,
38+
policy: DescriptorValidationPolicy
39+
): DescriptorMap {
40+
return toDescriptorMapValidate(wallet.coinSpecific().descriptors, walletKeys, policy);
3941
}
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
export { Miniscript, Descriptor } from '@bitgo/wasm-miniscript';
22
export { assertDescriptorWalletAddress } from './assertDescriptorWalletAddress';
33
export { NamedDescriptor } from './NamedDescriptor';
4-
export {
5-
isDescriptorWallet,
6-
isDescriptorWalletData,
7-
getDescriptorMapFromWallet,
8-
getDescriptorMapFromWalletData,
9-
} from './descriptorWallet';
4+
export { isDescriptorWallet, getDescriptorMapFromWallet } from './descriptorWallet';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Descriptor } from '@bitgo/wasm-miniscript';
2+
import { EnvironmentName, Triple } from '@bitgo/sdk-core';
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
5+
import { DescriptorBuilder, parseDescriptor } from './builder';
6+
import { NamedDescriptor } from './NamedDescriptor';
7+
import { DescriptorMap, toDescriptorMap } from '../core/descriptor';
8+
9+
export type DescriptorValidationPolicy = { allowedTemplates: DescriptorBuilder['name'][] } | 'allowAll';
10+
11+
export type KeyTriple = Triple<utxolib.BIP32Interface>;
12+
13+
function isDescriptorWithTemplate(
14+
d: Descriptor,
15+
name: DescriptorBuilder['name'],
16+
walletKeys: Triple<utxolib.BIP32Interface>
17+
): boolean {
18+
const parsed = parseDescriptor(d);
19+
if (parsed.name !== name) {
20+
return false;
21+
}
22+
if (parsed.keys.length !== walletKeys.length) {
23+
return false;
24+
}
25+
return parsed.keys.every((k, i) => k.toBase58() === walletKeys[i].toBase58());
26+
}
27+
28+
export function assertDescriptorPolicy(
29+
descriptor: Descriptor,
30+
policy: DescriptorValidationPolicy,
31+
walletKeys: Triple<utxolib.BIP32Interface>
32+
): void {
33+
if (policy === 'allowAll') {
34+
return;
35+
}
36+
37+
if ('allowedTemplates' in policy) {
38+
const allowed = policy.allowedTemplates;
39+
if (!allowed.some((t) => isDescriptorWithTemplate(descriptor, t, walletKeys))) {
40+
throw new Error(`Descriptor ${descriptor.toString()} does not match any allowed template`);
41+
}
42+
}
43+
44+
throw new Error(`Unknown descriptor validation policy: ${policy}`);
45+
}
46+
47+
export function toDescriptorMapValidate(
48+
descriptors: NamedDescriptor[],
49+
walletKeys: KeyTriple,
50+
policy: DescriptorValidationPolicy
51+
): DescriptorMap {
52+
const map = toDescriptorMap(descriptors);
53+
for (const descriptor of map.values()) {
54+
assertDescriptorPolicy(descriptor, policy, walletKeys);
55+
}
56+
return map;
57+
}
58+
59+
export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolicy {
60+
switch (env) {
61+
case 'adminProd':
62+
case 'prod':
63+
return {
64+
allowedTemplates: ['Wsh2Of3', 'ShWsh2Of3CltvDrop'],
65+
};
66+
default:
67+
return 'allowAll';
68+
}
69+
}

modules/abstract-utxo/src/keychains.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { AbstractUtxoCoin } from './abstractUtxoCoin';
1+
import * as utxolib from '@bitgo/utxo-lib';
22
import { IRequestTracer, IWallet, Keychain, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
33

4+
import { AbstractUtxoCoin } from './abstractUtxoCoin';
5+
46
export type NamedKeychains = {
57
user?: Keychain;
68
backup?: Keychain;
@@ -15,6 +17,13 @@ export function toKeychainTriple(keychains: NamedKeychains): Triple<Keychain> {
1517
return [user, backup, bitgo];
1618
}
1719

20+
export function toBip32Triple(keychains: Triple<{ pub: string }> | Triple<string>): Triple<utxolib.BIP32Interface> {
21+
return keychains.map((keychain: { pub: string } | string) => {
22+
const v = typeof keychain === 'string' ? keychain : keychain.pub;
23+
return utxolib.bip32.fromBase58(v);
24+
}) as Triple<utxolib.BIP32Interface>;
25+
}
26+
1827
export async function fetchKeychains(
1928
coin: AbstractUtxoCoin,
2029
wallet: IWallet,

modules/abstract-utxo/test/core/descriptor/descriptor.utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { Triple } from '@bitgo/sdk-core';
12
import { Descriptor } from '@bitgo/wasm-miniscript';
3+
import { BIP32Interface } from '@bitgo/utxo-lib';
24

35
import { DescriptorMap, PsbtParams } from '../../../src/core/descriptor';
46
import { getKeyTriple, KeyTriple } from '../key.utils';
5-
import { BIP32Interface } from '@bitgo/utxo-lib';
67

7-
export function getDefaultXPubs(seed?: string): string[] {
8-
return getKeyTriple(seed).map((k) => k.neutered().toBase58());
8+
export function getDefaultXPubs(seed?: string): Triple<string> {
9+
return getKeyTriple(seed).map((k) => k.neutered().toBase58()) as Triple<string>;
910
}
1011

1112
function toDescriptorMap(v: Record<string, string>): DescriptorMap {
Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import assert from 'assert';
2-
import { getDescriptorMapFromWalletData, isDescriptorWalletData } from '../../src/descriptor';
3-
import { AbstractUtxoCoinWalletData } from '../../src';
4-
import { getDescriptorMap } from '../core/descriptor/descriptor.utils';
2+
import { getDescriptorMapFromWallet, isDescriptorWallet } from '../../src/descriptor';
3+
import { AbstractUtxoCoinWallet } from '../../src';
4+
import { getDefaultXPubs, getDescriptorMap } from '../core/descriptor/descriptor.utils';
5+
import { toBip32Triple } from '../../src/keychains';
56

67
describe('isDescriptorWalletData', function () {
78
const descriptorMap = getDescriptorMap('Wsh2Of3');
89
it('should return true for valid DescriptorWalletData', function () {
9-
const walletData: AbstractUtxoCoinWalletData = {
10-
coinSpecific: {
11-
descriptors: [...descriptorMap.entries()].map(([name, descriptor]) => ({
12-
name,
13-
value: descriptor.toString(),
14-
})),
10+
const wallet: AbstractUtxoCoinWallet = {
11+
coinSpecific() {
12+
return {
13+
descriptors: [...descriptorMap.entries()].map(([name, descriptor]) => ({
14+
name,
15+
value: descriptor.toString(),
16+
})),
17+
};
1518
},
16-
} as unknown as AbstractUtxoCoinWalletData;
19+
} as unknown as AbstractUtxoCoinWallet;
1720

18-
assert(isDescriptorWalletData(walletData));
19-
assert.strictEqual(getDescriptorMapFromWalletData(walletData).size, descriptorMap.size);
21+
assert(isDescriptorWallet(wallet));
22+
assert.strictEqual(
23+
getDescriptorMapFromWallet(wallet, toBip32Triple(getDefaultXPubs()), 'allowAll').size,
24+
descriptorMap.size
25+
);
2026
});
2127
});

modules/bitgo/test/v2/unit/coins/utxo/descriptorAddress.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ describe('descriptor wallets', function () {
6565
it(`should return ${expected} for address ${address} with index ${index} and descriptor ${descriptorName} with checksum ${descriptorChecksum}`, async function () {
6666
const wallet = getIWalletWithDescriptors([descFoo, descBar]);
6767
async function f() {
68-
return coin.isWalletAddress({ address, index, coinSpecific: { descriptorName, descriptorChecksum } }, wallet);
68+
return coin.isWalletAddress(
69+
{
70+
address,
71+
index,
72+
coinSpecific: { descriptorName, descriptorChecksum },
73+
keychains: xpubs.map((pub) => ({ pub })),
74+
},
75+
wallet
76+
);
6977
}
7078
if (expected === true) {
7179
assert.equal(await f(), expected);

0 commit comments

Comments
 (0)