Skip to content

Commit

Permalink
feat(abstract-utxo): allow signed descriptors in prod policy
Browse files Browse the repository at this point in the history
This allows us to widen the range of permitted descriptors in the prod
policy.

Issue: BTC-1708
  • Loading branch information
OttoAllmendinger committed Jan 3, 2025
1 parent d5b0b60 commit a19606a
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 32 deletions.
9 changes: 7 additions & 2 deletions modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { Descriptor } from '@bitgo/wasm-miniscript';
export type DescriptorMap = Map<string, Descriptor>;

/** Convert an array of descriptor name-value pairs to a descriptor map */
export function toDescriptorMap(descriptors: { name: string; value: string }[]): DescriptorMap {
return new Map(descriptors.map((d) => [d.name, Descriptor.fromString(d.value, 'derivable')]));
export function toDescriptorMap(descriptors: { name: string; value: Descriptor | string }[]): DescriptorMap {
return new Map(
descriptors.map((d) => [
d.name,
d.value instanceof Descriptor ? d.value : Descriptor.fromString(d.value, 'derivable'),
])
);
}
23 changes: 16 additions & 7 deletions modules/abstract-utxo/src/descriptor/NamedDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export const NamedDescriptor = t.intersection(
'NamedDescriptor'
);

export type NamedDescriptor = t.TypeOf<typeof NamedDescriptor>;
export type NamedDescriptor<T = string> = {
name: string;
value: T;
signatures?: string[];
};

export function createNamedDescriptorWithSignature(
name: string,
Expand All @@ -28,14 +32,19 @@ export function createNamedDescriptorWithSignature(
return { name, value, signatures: [signature] };
}

export function assertHasValidSignature(namedDescriptor: NamedDescriptor, key: BIP32Interface): void {
if (namedDescriptor.signatures === undefined) {
throw new Error(`Descriptor ${namedDescriptor.name} does not have a signature`);
export function hasValidSignature(descriptor: string | Descriptor, key: BIP32Interface, signatures: string[]): boolean {
if (typeof descriptor === 'string') {
descriptor = Descriptor.fromString(descriptor, 'derivable');
}
const isValid = namedDescriptor.signatures.some((signature) => {
return verifyMessage(namedDescriptor.value, key, Buffer.from(signature, 'hex'), networks.bitcoin);

const message = descriptor.toString();
return signatures.some((signature) => {
return verifyMessage(message, key, Buffer.from(signature, 'hex'), networks.bitcoin);
});
if (!isValid) {
}

export function assertHasValidSignature(namedDescriptor: NamedDescriptor, key: BIP32Interface): void {
if (!hasValidSignature(namedDescriptor.value, key, namedDescriptor.signatures ?? [])) {
throw new Error(`Descriptor ${namedDescriptor.name} does not have a valid signature (key=${key.toBase58()})`);
}
}
46 changes: 32 additions & 14 deletions modules/abstract-utxo/src/descriptor/validatePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import * as utxolib from '@bitgo/utxo-lib';
import { DescriptorMap, toDescriptorMap } from '../core/descriptor';

import { parseDescriptor } from './builder';
import { NamedDescriptor } from './NamedDescriptor';
import { hasValidSignature, NamedDescriptor } from './NamedDescriptor';

export type KeyTriple = Triple<utxolib.BIP32Interface>;

export interface DescriptorValidationPolicy {
name: string;
validate(d: Descriptor, walletKeys: KeyTriple): boolean;
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean;
}

export const policyAllowAll: DescriptorValidationPolicy = {
Expand All @@ -36,17 +36,17 @@ export function getValidatorDescriptorTemplate(name: string): DescriptorValidati
export function getValidatorEvery(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
return {
name: 'every(' + validators.map((v) => v.name).join(',') + ')',
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
return validators.every((v) => v.validate(d, walletKeys));
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
return validators.every((v) => v.validate(d, walletKeys, signatures));
},
};
}

export function getValidatorSome(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
return {
name: 'some(' + validators.map((v) => v.name).join(',') + ')',
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
return validators.some((v) => v.validate(d, walletKeys));
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
return validators.some((v) => v.validate(d, walletKeys, signatures));
},
};
}
Expand All @@ -55,6 +55,16 @@ export function getValidatorOneOfTemplates(names: string[]): DescriptorValidatio
return getValidatorSome(names.map(getValidatorDescriptorTemplate));
}

export function getValidatorSignedByUserKey(): DescriptorValidationPolicy {
return {
name: 'signedByUser',
validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean {
// the first key is the user key, by convention
return hasValidSignature(d, walletKeys[0], signatures);
},
};
}

export class DescriptorPolicyValidationError extends Error {
constructor(descriptor: Descriptor, policy: DescriptorValidationPolicy) {
super(`Descriptor ${descriptor.toString()} does not match policy ${policy.name}`);
Expand All @@ -64,9 +74,10 @@ export class DescriptorPolicyValidationError extends Error {
export function assertDescriptorPolicy(
descriptor: Descriptor,
policy: DescriptorValidationPolicy,
walletKeys: KeyTriple
walletKeys: KeyTriple,
signatures: string[]
): void {
if (!policy.validate(descriptor, walletKeys)) {
if (!policy.validate(descriptor, walletKeys, signatures)) {
throw new DescriptorPolicyValidationError(descriptor, policy);
}
}
Expand All @@ -76,18 +87,25 @@ export function toDescriptorMapValidate(
walletKeys: KeyTriple,
policy: DescriptorValidationPolicy
): DescriptorMap {
const map = toDescriptorMap(descriptors);
for (const descriptor of map.values()) {
assertDescriptorPolicy(descriptor, policy, walletKeys);
}
return map;
return toDescriptorMap(
descriptors.map((namedDescriptor) => {
const d = Descriptor.fromString(namedDescriptor.value, 'derivable');
assertDescriptorPolicy(d, policy, walletKeys, namedDescriptor.signatures ?? []);
return { name: namedDescriptor.name, value: d };
})
);
}

export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolicy {
switch (env) {
case 'adminProd':
case 'prod':
return getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']);
return getValidatorSome([
// allow all 2-of-3-ish descriptors where the keys match the wallet keys
getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']),
// allow all descriptors signed by the user key
getValidatorSignedByUserKey(),
]);
default:
return policyAllowAll;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import {
getPolicyForEnv,
getValidatorDescriptorTemplate,
} from '../../../src/descriptor/validatePolicy';
import { getDescriptor } from '../../core/descriptor/descriptor.utils';
import { DescriptorTemplate, getDescriptor } from '../../core/descriptor/descriptor.utils';
import { getKeyTriple } from '../../core/key.utils';
import { NamedDescriptor } from '../../../src/descriptor';
import { createNamedDescriptorWithSignature } from '../../../src/descriptor/NamedDescriptor';

function testAssertDescriptorPolicy(
d: Descriptor,
d: NamedDescriptor<string>,
p: DescriptorValidationPolicy,
k: Triple<BIP32Interface>,
expectedError: DescriptorPolicyValidationError | null
) {
const f = () => assertDescriptorPolicy(d, p, k);
const f = () => assertDescriptorPolicy(Descriptor.fromString(d.value, 'derivable'), p, k, d.signatures ?? []);
if (expectedError) {
assert.throws(f);
} else {
Expand All @@ -29,20 +31,30 @@ function testAssertDescriptorPolicy(
}

describe('assertDescriptorPolicy', function () {
const keys = getKeyTriple();
function getNamedDescriptor(name: DescriptorTemplate): NamedDescriptor {
return createNamedDescriptorWithSignature(name, getDescriptor(name), keys[0]);
}
function stripSignature(d: NamedDescriptor): NamedDescriptor {
return { ...d, signatures: undefined };
}

it('has expected result', function () {
const keys = getKeyTriple();
testAssertDescriptorPolicy(getDescriptor('Wsh2Of3', keys), getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of3'), getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);

// prod does only allow Wsh2Of3-ish descriptors
testAssertDescriptorPolicy(getDescriptor('Wsh2Of3', keys), getPolicyForEnv('prod'), keys, null);
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of3'), getPolicyForEnv('prod'), keys, null);

// prod only allows other descriptors if they are signed by the user key
testAssertDescriptorPolicy(getNamedDescriptor('Wsh2Of2'), getPolicyForEnv('prod'), keys, null);
testAssertDescriptorPolicy(
getDescriptor('Wsh2Of2', keys),
stripSignature(getNamedDescriptor('Wsh2Of2')),
getPolicyForEnv('prod'),
keys,
new DescriptorPolicyValidationError(getDescriptor('Wsh2Of2', keys), getPolicyForEnv('prod'))
new DescriptorPolicyValidationError(getDescriptor('Wsh2Of2'), getPolicyForEnv('prod'))
);

// test is very permissive by default
testAssertDescriptorPolicy(getDescriptor('Wsh2Of2', keys), getPolicyForEnv('test'), keys, null);
testAssertDescriptorPolicy(stripSignature(getNamedDescriptor('Wsh2Of2')), getPolicyForEnv('test'), keys, null);
});
});

0 comments on commit a19606a

Please sign in to comment.