Skip to content

Commit a19606a

Browse files
feat(abstract-utxo): allow signed descriptors in prod policy
This allows us to widen the range of permitted descriptors in the prod policy. Issue: BTC-1708
1 parent d5b0b60 commit a19606a

File tree

4 files changed

+76
-32
lines changed

4 files changed

+76
-32
lines changed

modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { Descriptor } from '@bitgo/wasm-miniscript';
44
export type DescriptorMap = Map<string, Descriptor>;
55

66
/** Convert an array of descriptor name-value pairs to a descriptor map */
7-
export function toDescriptorMap(descriptors: { name: string; value: string }[]): DescriptorMap {
8-
return new Map(descriptors.map((d) => [d.name, Descriptor.fromString(d.value, 'derivable')]));
7+
export function toDescriptorMap(descriptors: { name: string; value: Descriptor | string }[]): DescriptorMap {
8+
return new Map(
9+
descriptors.map((d) => [
10+
d.name,
11+
d.value instanceof Descriptor ? d.value : Descriptor.fromString(d.value, 'derivable'),
12+
])
13+
);
914
}

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ export const NamedDescriptor = t.intersection(
1616
'NamedDescriptor'
1717
);
1818

19-
export type NamedDescriptor = t.TypeOf<typeof NamedDescriptor>;
19+
export type NamedDescriptor<T = string> = {
20+
name: string;
21+
value: T;
22+
signatures?: string[];
23+
};
2024

2125
export function createNamedDescriptorWithSignature(
2226
name: string,
@@ -28,14 +32,19 @@ export function createNamedDescriptorWithSignature(
2832
return { name, value, signatures: [signature] };
2933
}
3034

31-
export function assertHasValidSignature(namedDescriptor: NamedDescriptor, key: BIP32Interface): void {
32-
if (namedDescriptor.signatures === undefined) {
33-
throw new Error(`Descriptor ${namedDescriptor.name} does not have a signature`);
35+
export function hasValidSignature(descriptor: string | Descriptor, key: BIP32Interface, signatures: string[]): boolean {
36+
if (typeof descriptor === 'string') {
37+
descriptor = Descriptor.fromString(descriptor, 'derivable');
3438
}
35-
const isValid = namedDescriptor.signatures.some((signature) => {
36-
return verifyMessage(namedDescriptor.value, key, Buffer.from(signature, 'hex'), networks.bitcoin);
39+
40+
const message = descriptor.toString();
41+
return signatures.some((signature) => {
42+
return verifyMessage(message, key, Buffer.from(signature, 'hex'), networks.bitcoin);
3743
});
38-
if (!isValid) {
44+
}
45+
46+
export function assertHasValidSignature(namedDescriptor: NamedDescriptor, key: BIP32Interface): void {
47+
if (!hasValidSignature(namedDescriptor.value, key, namedDescriptor.signatures ?? [])) {
3948
throw new Error(`Descriptor ${namedDescriptor.name} does not have a valid signature (key=${key.toBase58()})`);
4049
}
4150
}

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

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import * as utxolib from '@bitgo/utxo-lib';
55
import { DescriptorMap, toDescriptorMap } from '../core/descriptor';
66

77
import { parseDescriptor } from './builder';
8-
import { NamedDescriptor } from './NamedDescriptor';
8+
import { hasValidSignature, NamedDescriptor } from './NamedDescriptor';
99

1010
export type KeyTriple = Triple<utxolib.BIP32Interface>;
1111

1212
export interface DescriptorValidationPolicy {
1313
name: string;
14-
validate(d: Descriptor, walletKeys: KeyTriple): boolean;
14+
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean;
1515
}
1616

1717
export const policyAllowAll: DescriptorValidationPolicy = {
@@ -36,17 +36,17 @@ export function getValidatorDescriptorTemplate(name: string): DescriptorValidati
3636
export function getValidatorEvery(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
3737
return {
3838
name: 'every(' + validators.map((v) => v.name).join(',') + ')',
39-
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
40-
return validators.every((v) => v.validate(d, walletKeys));
39+
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
40+
return validators.every((v) => v.validate(d, walletKeys, signatures));
4141
},
4242
};
4343
}
4444

4545
export function getValidatorSome(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
4646
return {
4747
name: 'some(' + validators.map((v) => v.name).join(',') + ')',
48-
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
49-
return validators.some((v) => v.validate(d, walletKeys));
48+
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
49+
return validators.some((v) => v.validate(d, walletKeys, signatures));
5050
},
5151
};
5252
}
@@ -55,6 +55,16 @@ export function getValidatorOneOfTemplates(names: string[]): DescriptorValidatio
5555
return getValidatorSome(names.map(getValidatorDescriptorTemplate));
5656
}
5757

58+
export function getValidatorSignedByUserKey(): DescriptorValidationPolicy {
59+
return {
60+
name: 'signedByUser',
61+
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
62+
// the first key is the user key, by convention
63+
return hasValidSignature(d, walletKeys[0], signatures);
64+
},
65+
};
66+
}
67+
5868
export class DescriptorPolicyValidationError extends Error {
5969
constructor(descriptor: Descriptor, policy: DescriptorValidationPolicy) {
6070
super(`Descriptor ${descriptor.toString()} does not match policy ${policy.name}`);
@@ -64,9 +74,10 @@ export class DescriptorPolicyValidationError extends Error {
6474
export function assertDescriptorPolicy(
6575
descriptor: Descriptor,
6676
policy: DescriptorValidationPolicy,
67-
walletKeys: KeyTriple
77+
walletKeys: KeyTriple,
78+
signatures: string[]
6879
): void {
69-
if (!policy.validate(descriptor, walletKeys)) {
80+
if (!policy.validate(descriptor, walletKeys, signatures)) {
7081
throw new DescriptorPolicyValidationError(descriptor, policy);
7182
}
7283
}
@@ -76,18 +87,25 @@ export function toDescriptorMapValidate(
7687
walletKeys: KeyTriple,
7788
policy: DescriptorValidationPolicy
7889
): DescriptorMap {
79-
const map = toDescriptorMap(descriptors);
80-
for (const descriptor of map.values()) {
81-
assertDescriptorPolicy(descriptor, policy, walletKeys);
82-
}
83-
return map;
90+
return toDescriptorMap(
91+
descriptors.map((namedDescriptor) => {
92+
const d = Descriptor.fromString(namedDescriptor.value, 'derivable');
93+
assertDescriptorPolicy(d, policy, walletKeys, namedDescriptor.signatures ?? []);
94+
return { name: namedDescriptor.name, value: d };
95+
})
96+
);
8497
}
8598

8699
export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolicy {
87100
switch (env) {
88101
case 'adminProd':
89102
case 'prod':
90-
return getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']);
103+
return getValidatorSome([
104+
// allow all 2-of-3-ish descriptors where the keys match the wallet keys
105+
getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']),
106+
// allow all descriptors signed by the user key
107+
getValidatorSignedByUserKey(),
108+
]);
91109
default:
92110
return policyAllowAll;
93111
}

modules/abstract-utxo/test/transaction/descriptor/validatePolicy.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ import {
1111
getPolicyForEnv,
1212
getValidatorDescriptorTemplate,
1313
} from '../../../src/descriptor/validatePolicy';
14-
import { getDescriptor } from '../../core/descriptor/descriptor.utils';
14+
import { DescriptorTemplate, getDescriptor } from '../../core/descriptor/descriptor.utils';
1515
import { getKeyTriple } from '../../core/key.utils';
16+
import { NamedDescriptor } from '../../../src/descriptor';
17+
import { createNamedDescriptorWithSignature } from '../../../src/descriptor/NamedDescriptor';
1618

1719
function testAssertDescriptorPolicy(
18-
d: Descriptor,
20+
d: NamedDescriptor<string>,
1921
p: DescriptorValidationPolicy,
2022
k: Triple<BIP32Interface>,
2123
expectedError: DescriptorPolicyValidationError | null
2224
) {
23-
const f = () => assertDescriptorPolicy(d, p, k);
25+
const f = () => assertDescriptorPolicy(Descriptor.fromString(d.value, 'derivable'), p, k, d.signatures ?? []);
2426
if (expectedError) {
2527
assert.throws(f);
2628
} else {
@@ -29,20 +31,30 @@ function testAssertDescriptorPolicy(
2931
}
3032

3133
describe('assertDescriptorPolicy', function () {
34+
const keys = getKeyTriple();
35+
function getNamedDescriptor(name: DescriptorTemplate): NamedDescriptor {
36+
return createNamedDescriptorWithSignature(name, getDescriptor(name), keys[0]);
37+
}
38+
function stripSignature(d: NamedDescriptor): NamedDescriptor {
39+
return { ...d, signatures: undefined };
40+
}
41+
3242
it('has expected result', function () {
33-
const keys = getKeyTriple();
34-
testAssertDescriptorPolicy(getDescriptor('Wsh2Of3', keys), getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);
43+
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of3'), getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);
3544

3645
// prod does only allow Wsh2Of3-ish descriptors
37-
testAssertDescriptorPolicy(getDescriptor('Wsh2Of3', keys), getPolicyForEnv('prod'), keys, null);
46+
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of3'), getPolicyForEnv('prod'), keys, null);
47+
48+
// prod only allows other descriptors if they are signed by the user key
49+
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of2'), getPolicyForEnv('prod'), keys, null);
3850
testAssertDescriptorPolicy(
39-
getDescriptor('Wsh2Of2', keys),
51+
stripSignature(getNamedDescriptor('Wsh2Of2')),
4052
getPolicyForEnv('prod'),
4153
keys,
42-
new DescriptorPolicyValidationError(getDescriptor('Wsh2Of2', keys), getPolicyForEnv('prod'))
54+
new DescriptorPolicyValidationError(getDescriptor('Wsh2Of2'), getPolicyForEnv('prod'))
4355
);
4456

4557
// test is very permissive by default
46-
testAssertDescriptorPolicy(getDescriptor('Wsh2Of2', keys), getPolicyForEnv('test'), keys, null);
58+
testAssertDescriptorPolicy(stripSignature(getNamedDescriptor('Wsh2Of2')), getPolicyForEnv('test'), keys, null);
4759
});
4860
});

0 commit comments

Comments
 (0)