diff --git a/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts b/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts index d726a9935f..053a6e7476 100644 --- a/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts +++ b/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts @@ -4,6 +4,11 @@ import { Descriptor } from '@bitgo/wasm-miniscript'; export type DescriptorMap = Map; /** 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'), + ]) + ); } diff --git a/modules/abstract-utxo/src/descriptor/NamedDescriptor.ts b/modules/abstract-utxo/src/descriptor/NamedDescriptor.ts index 6df54a02e7..7dcd661669 100644 --- a/modules/abstract-utxo/src/descriptor/NamedDescriptor.ts +++ b/modules/abstract-utxo/src/descriptor/NamedDescriptor.ts @@ -16,7 +16,11 @@ export const NamedDescriptor = t.intersection( 'NamedDescriptor' ); -export type NamedDescriptor = t.TypeOf; +export type NamedDescriptor = { + name: string; + value: T; + signatures?: string[]; +}; export function createNamedDescriptorWithSignature( name: string, @@ -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()})`); } } diff --git a/modules/abstract-utxo/src/descriptor/validatePolicy.ts b/modules/abstract-utxo/src/descriptor/validatePolicy.ts index 976c2d44b1..ee24e22b0a 100644 --- a/modules/abstract-utxo/src/descriptor/validatePolicy.ts +++ b/modules/abstract-utxo/src/descriptor/validatePolicy.ts @@ -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; export interface DescriptorValidationPolicy { name: string; - validate(d: Descriptor, walletKeys: KeyTriple): boolean; + validate(d: Descriptor, walletKeys: KeyTriple, signatures: string[]): boolean; } export const policyAllowAll: DescriptorValidationPolicy = { @@ -36,8 +36,8 @@ 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)); }, }; } @@ -45,8 +45,8 @@ export function getValidatorEvery(validators: DescriptorValidationPolicy[]): Des 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)); }, }; } @@ -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}`); @@ -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); } } @@ -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; } diff --git a/modules/abstract-utxo/test/transaction/descriptor/validatePolicy.ts b/modules/abstract-utxo/test/transaction/descriptor/validatePolicy.ts index 8838875f86..1303a65920 100644 --- a/modules/abstract-utxo/test/transaction/descriptor/validatePolicy.ts +++ b/modules/abstract-utxo/test/transaction/descriptor/validatePolicy.ts @@ -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, p: DescriptorValidationPolicy, k: Triple, 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 { @@ -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); }); });