Skip to content

Commit 107d33f

Browse files
Merge pull request #5574 from BitGo/BTC-1843.validate-descriptor-group
feat(abstract-utxo): enforce groupwise descriptor validation
2 parents 10a54bc + 417276e commit 107d33f

File tree

4 files changed

+84
-45
lines changed

4 files changed

+84
-45
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as t from 'io-ts';
2-
import { Descriptor } from '@bitgo/wasm-miniscript';
2+
import { Descriptor, DescriptorPkType } from '@bitgo/wasm-miniscript';
33
import { BIP32Interface, networks } from '@bitgo/utxo-lib';
44
import { signMessage, verifyMessage } from '@bitgo/sdk-core';
55

@@ -22,6 +22,8 @@ export type NamedDescriptor<T = string> = {
2222
signatures?: string[];
2323
};
2424

25+
export type NamedDescriptorNative = NamedDescriptor<Descriptor>;
26+
2527
export function createNamedDescriptorWithSignature(
2628
name: string,
2729
descriptor: string | Descriptor,
@@ -35,6 +37,10 @@ export function createNamedDescriptorWithSignature(
3537
return { name, value, signatures: [signature] };
3638
}
3739

40+
export function toNamedDescriptorNative(e: NamedDescriptor, pkType: DescriptorPkType): NamedDescriptorNative {
41+
return { ...e, value: Descriptor.fromString(e.value, pkType) };
42+
}
43+
3844
export function hasValidSignature(descriptor: string | Descriptor, key: BIP32Interface, signatures: string[]): boolean {
3945
if (typeof descriptor === 'string') {
4046
descriptor = Descriptor.fromString(descriptor, 'derivable');
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
export { Miniscript, Descriptor } from '@bitgo/wasm-miniscript';
22
export { DescriptorMap } from '@bitgo/utxo-core/descriptor';
33
export { assertDescriptorWalletAddress } from './assertDescriptorWalletAddress';
4-
export { NamedDescriptor, createNamedDescriptorWithSignature, hasValidSignature } from './NamedDescriptor';
4+
export {
5+
NamedDescriptor,
6+
createNamedDescriptorWithSignature,
7+
hasValidSignature,
8+
NamedDescriptorNative,
9+
toNamedDescriptorNative,
10+
} from './NamedDescriptor';
511
export { isDescriptorWallet, getDescriptorMapFromWallet } from './descriptorWallet';
612
export { getPolicyForEnv } from './validatePolicy';
713
export * as createWallet from './createWallet';

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

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { Descriptor } from '@bitgo/wasm-miniscript';
21
import { EnvironmentName, Triple } from '@bitgo/sdk-core';
32
import * as utxolib from '@bitgo/utxo-lib';
43
import { DescriptorMap, toDescriptorMap } from '@bitgo/utxo-core/descriptor';
54

65
import { parseDescriptor } from './builder';
7-
import { hasValidSignature, NamedDescriptor } from './NamedDescriptor';
6+
import { hasValidSignature, NamedDescriptor, NamedDescriptorNative, toNamedDescriptorNative } from './NamedDescriptor';
87

98
export type KeyTriple = Triple<utxolib.BIP32Interface>;
109

1110
export interface DescriptorValidationPolicy {
1211
name: string;
13-
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean;
12+
13+
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean;
1414
}
1515

1616
export const policyAllowAll: DescriptorValidationPolicy = {
@@ -21,31 +21,33 @@ export const policyAllowAll: DescriptorValidationPolicy = {
2121
export function getValidatorDescriptorTemplate(name: string): DescriptorValidationPolicy {
2222
return {
2323
name: 'descriptorTemplate(' + name + ')',
24-
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
25-
const parsed = parseDescriptor(d);
26-
return (
27-
parsed.name === name &&
28-
parsed.keys.length === walletKeys.length &&
29-
parsed.keys.every((k, i) => k.toBase58() === walletKeys[i].neutered().toBase58())
30-
);
24+
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
25+
return arr.every((d) => {
26+
const parsed = parseDescriptor(d.value);
27+
return (
28+
parsed.name === name &&
29+
parsed.keys.length === walletKeys.length &&
30+
parsed.keys.every((k, i) => k.toBase58() === walletKeys[i].neutered().toBase58())
31+
);
32+
});
3133
},
3234
};
3335
}
3436

3537
export function getValidatorEvery(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
3638
return {
3739
name: 'every(' + validators.map((v) => v.name).join(',') + ')',
38-
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
39-
return validators.every((v) => v.validate(d, walletKeys, signatures));
40+
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
41+
return validators.every((v) => v.validate(arr, walletKeys));
4042
},
4143
};
4244
}
4345

4446
export function getValidatorSome(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
4547
return {
4648
name: 'some(' + validators.map((v) => v.name).join(',') + ')',
47-
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
48-
return validators.some((v) => v.validate(d, walletKeys, signatures));
49+
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
50+
return validators.some((v) => v.validate(arr, walletKeys));
4951
},
5052
};
5153
}
@@ -57,27 +59,26 @@ export function getValidatorOneOfTemplates(names: string[]): DescriptorValidatio
5759
export function getValidatorSignedByUserKey(): DescriptorValidationPolicy {
5860
return {
5961
name: 'signedByUser',
60-
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
62+
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
6163
// the first key is the user key, by convention
62-
return hasValidSignature(d, walletKeys[0], signatures);
64+
return arr.every((d) => hasValidSignature(d.value, walletKeys[0], d.signatures ?? []));
6365
},
6466
};
6567
}
6668

6769
export class DescriptorPolicyValidationError extends Error {
68-
constructor(descriptor: Descriptor, policy: DescriptorValidationPolicy) {
69-
super(`Descriptor ${descriptor.toString()} does not match policy ${policy.name}`);
70+
constructor(ds: NamedDescriptorNative[], policy: DescriptorValidationPolicy) {
71+
super(`Descriptors ${ds.map((d) => d.value.toString())} does not match policy ${policy.name}`);
7072
}
7173
}
7274

7375
export function assertDescriptorPolicy(
74-
descriptor: Descriptor,
76+
descriptors: NamedDescriptorNative[],
7577
policy: DescriptorValidationPolicy,
76-
walletKeys: KeyTriple,
77-
signatures: string[]
78+
walletKeys: KeyTriple
7879
): void {
79-
if (!policy.validate(descriptor, walletKeys, signatures)) {
80-
throw new DescriptorPolicyValidationError(descriptor, policy);
80+
if (!policy.validate(descriptors, walletKeys)) {
81+
throw new DescriptorPolicyValidationError(descriptors, policy);
8182
}
8283
}
8384

@@ -86,22 +87,22 @@ export function toDescriptorMapValidate(
8687
walletKeys: KeyTriple,
8788
policy: DescriptorValidationPolicy
8889
): DescriptorMap {
89-
return toDescriptorMap(
90-
descriptors.map((namedDescriptor) => {
91-
const d = Descriptor.fromString(namedDescriptor.value, 'derivable');
92-
assertDescriptorPolicy(d, policy, walletKeys, namedDescriptor.signatures ?? []);
93-
return { name: namedDescriptor.name, value: d };
94-
})
90+
const namedDescriptorsNative: NamedDescriptorNative[] = descriptors.map((v) =>
91+
toNamedDescriptorNative(v, 'derivable')
9592
);
93+
assertDescriptorPolicy(namedDescriptorsNative, policy, walletKeys);
94+
return toDescriptorMap(namedDescriptorsNative);
9695
}
9796

9897
export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolicy {
9998
switch (env) {
10099
case 'adminProd':
101100
case 'prod':
102101
return getValidatorSome([
103-
// allow all 2-of-3-ish descriptors where the keys match the wallet keys
104-
getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']),
102+
// allow 2-of-3-ish descriptor groups where the keys match the wallet keys
103+
getValidatorDescriptorTemplate('Wsh2Of3'),
104+
// allow descriptor groups where all keys match the wallet keys plus OP_DROP (coredao staking)
105+
getValidatorDescriptorTemplate('Wsh2Of3CltvDrop'),
105106
// allow all descriptors signed by the user key
106107
getValidatorSignedByUserKey(),
107108
]);
Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
import assert from 'assert';
22

3-
import { Descriptor } from '@bitgo/wasm-miniscript';
43
import { Triple } from '@bitgo/sdk-core';
54
import { BIP32Interface } from '@bitgo/utxo-lib';
65
import { getKeyTriple } from '@bitgo/utxo-core/testutil';
76

8-
import { DescriptorTemplate, getDescriptor } from '../../../../utxo-core/src/testutil/descriptor/descriptors';
7+
import { DescriptorTemplate, getDescriptor } from '../../../../utxo-core/src/testutil/descriptor';
98
import {
109
assertDescriptorPolicy,
1110
DescriptorPolicyValidationError,
1211
DescriptorValidationPolicy,
1312
getPolicyForEnv,
1413
getValidatorDescriptorTemplate,
1514
} from '../../../src/descriptor/validatePolicy';
16-
import { NamedDescriptor, createNamedDescriptorWithSignature } from '../../../src/descriptor';
15+
import { NamedDescriptor, createNamedDescriptorWithSignature, toNamedDescriptorNative } from '../../../src/descriptor';
1716

1817
function testAssertDescriptorPolicy(
19-
d: NamedDescriptor<string>,
18+
ds: NamedDescriptor<string>[],
2019
p: DescriptorValidationPolicy,
2120
k: Triple<BIP32Interface>,
2221
expectedError: DescriptorPolicyValidationError | null
2322
) {
24-
const f = () => assertDescriptorPolicy(Descriptor.fromString(d.value, 'derivable'), p, k, d.signatures ?? []);
23+
const f = () =>
24+
assertDescriptorPolicy(
25+
ds.map((d) => toNamedDescriptorNative(d, 'derivable')),
26+
p,
27+
k
28+
);
2529
if (expectedError) {
2630
assert.throws(f);
2731
} else {
@@ -31,29 +35,51 @@ function testAssertDescriptorPolicy(
3135

3236
describe('assertDescriptorPolicy', function () {
3337
const keys = getKeyTriple();
34-
function getNamedDescriptor(name: DescriptorTemplate): NamedDescriptor {
38+
function getNamedDescriptorSigned(name: DescriptorTemplate): NamedDescriptor {
3539
return createNamedDescriptorWithSignature(name, getDescriptor(name), keys[0]);
3640
}
41+
function getNamedDescriptor(name: DescriptorTemplate): NamedDescriptor {
42+
return stripSignature(getNamedDescriptorSigned(name));
43+
}
44+
3745
function stripSignature(d: NamedDescriptor): NamedDescriptor {
3846
return { ...d, signatures: undefined };
3947
}
4048

4149
it('has expected result', function () {
42-
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of3'), getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);
50+
testAssertDescriptorPolicy([getNamedDescriptor('Wsh2Of3')], getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);
4351

4452
// prod does only allow Wsh2Of3-ish descriptors
45-
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of3'), getPolicyForEnv('prod'), keys, null);
53+
testAssertDescriptorPolicy([getNamedDescriptor('Wsh2Of3')], getPolicyForEnv('prod'), keys, null);
54+
testAssertDescriptorPolicy([getNamedDescriptor('Wsh2Of3CltvDrop')], getPolicyForEnv('prod'), keys, null);
55+
56+
// does not allow mixed descriptors
57+
testAssertDescriptorPolicy(
58+
[getNamedDescriptor('Wsh2Of3'), getNamedDescriptor('Wsh2Of3CltvDrop')],
59+
getPolicyForEnv('prod'),
60+
keys,
61+
new DescriptorPolicyValidationError(
62+
[
63+
toNamedDescriptorNative(getNamedDescriptor('Wsh2Of3'), 'derivable'),
64+
toNamedDescriptorNative(getNamedDescriptor('Wsh2Of3CltvDrop'), 'derivable'),
65+
],
66+
getPolicyForEnv('prod')
67+
)
68+
);
4669

4770
// prod only allows other descriptors if they are signed by the user key
48-
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of2'), getPolicyForEnv('prod'), keys, null);
71+
testAssertDescriptorPolicy([getNamedDescriptorSigned('Wsh2Of2')], getPolicyForEnv('prod'), keys, null);
4972
testAssertDescriptorPolicy(
50-
stripSignature(getNamedDescriptor('Wsh2Of2')),
73+
[getNamedDescriptor('Wsh2Of2')],
5174
getPolicyForEnv('prod'),
5275
keys,
53-
new DescriptorPolicyValidationError(getDescriptor('Wsh2Of2'), getPolicyForEnv('prod'))
76+
new DescriptorPolicyValidationError(
77+
[toNamedDescriptorNative(getNamedDescriptor('Wsh2Of2'), 'derivable')],
78+
getPolicyForEnv('prod')
79+
)
5480
);
5581

5682
// test is very permissive by default
57-
testAssertDescriptorPolicy(stripSignature(getNamedDescriptor('Wsh2Of2')), getPolicyForEnv('test'), keys, null);
83+
testAssertDescriptorPolicy([stripSignature(getNamedDescriptor('Wsh2Of2'))], getPolicyForEnv('test'), keys, null);
5884
});
5985
});

0 commit comments

Comments
 (0)