Skip to content

Commit d705b70

Browse files
committed
descriptor-policy: add a flag for enforcing unique keypaths in policies
Also fix and test a few edge cases in ensuring substitution invariants. The relevant BIP makes this mandatory, however given it is somewhat expensive to verify, and the security concerns mentioned seem to only relate to miniscript malleation via old tx signatures, make it opt-in. It would be nice to support ensuring this for non-policy miniscript descriptors. But, given the combination of possible descriptor key types and path expressions I do not believe it is feasible for any implementation to do this correctly without exactly the other limits that policies enforce. A general solution would have to solve all possible paths and derive all combinations of allowed keys which is not trivial to prove correct and extremely compute intensive and thus not feasible on HWW.
1 parent 1e3eac7 commit d705b70

File tree

5 files changed

+109
-23
lines changed

5 files changed

+109
-23
lines changed

include/wally_descriptor.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct wally_descriptor;
1616
#define WALLY_MINISCRIPT_ONLY 0x02 /** Only allow miniscript (not descriptor) expressions */
1717
#define WALLY_MINISCRIPT_REQUIRE_CHECKSUM 0x04 /** Require a checksum to be present */
1818
#define WALLY_MINISCRIPT_POLICY_TEMPLATE 0x08 /** Only allow policy templates with @n BIP32 keys */
19+
#define WALLY_MINISCRIPT_UNIQUE_KEYPATHS 0x10 /** For policy templates, ensure BIP32 derivation paths differ for identical keys */
1920
#define WALLY_MINISCRIPT_DEPTH_MASK 0xffff0000 /** Mask for limiting maximum depth */
2021
#define WALLY_MINISCRIPT_DEPTH_SHIFT 16 /** Shift to convert maximum depth to flags */
2122

src/ctest/test_descriptor.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,10 +1008,10 @@ static const struct descriptor_test {
10081008
"ydnzkve4"
10091009
}, {
10101010
"policy - previous key reference",
1011-
"sh(multi(1,@0/**,@1/**,@0/**))", /* For testing: expresssion isn't sensible */
1011+
"sh(multi(1,@0/**,@1/**,@0/<2;3>/*))", /* For testing: expresssion isn't sensible */
10121012
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY_TEMPLATE,
1013-
"a91415b7de59bd65038744e3214a521d9d4443dc78c287",
1014-
"xwdj6ucy"
1013+
"a9146cc69bc5443e97b8a30918cb6297ba4efb3d6dc387",
1014+
"45w36ywr"
10151015
},
10161016
/*
10171017
* Misc error cases (code coverage)
@@ -1937,6 +1937,13 @@ static bool check_descriptor_to_script(const struct descriptor_test* test)
19371937

19381938
ret = wally_descriptor_parse(test->descriptor, keys, test->network,
19391939
test->flags, &descriptor);
1940+
if (is_policy && expected_ret == WALLY_OK) {
1941+
/* Re-parse with strict key checking */
1942+
wally_descriptor_free(descriptor);
1943+
ret = wally_descriptor_parse(test->descriptor, keys, test->network,
1944+
test->flags | WALLY_MINISCRIPT_UNIQUE_KEYPATHS,
1945+
&descriptor);
1946+
}
19401947
if (expected_ret == WALLY_OK || ret == expected_ret) {
19411948
/* For failure cases, we may fail when generating instead of parsing,
19421949
* we catch those cases below */

src/descriptor.c

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
#define MS_FLAGS_ALL (WALLY_MINISCRIPT_TAPSCRIPT | \
1919
WALLY_MINISCRIPT_ONLY | \
2020
WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \
21-
WALLY_MINISCRIPT_POLICY_TEMPLATE)
21+
WALLY_MINISCRIPT_POLICY_TEMPLATE | \
22+
WALLY_MINISCRIPT_UNIQUE_KEYPATHS)
2223
#define MS_FLAGS_CANONICALIZE (WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \
2324
WALLY_MINISCRIPT_POLICY_TEMPLATE)
2425

@@ -204,6 +205,8 @@ static int ctx_add_key_node(ms_ctx *ctx, ms_node *node)
204205
(unsigned char *)v, 1, true, false);
205206
}
206207

208+
static int ensure_unique_policy_keys(const ms_ctx *ctx);
209+
207210
/* Built-in miniscript expressions */
208211
typedef int (*node_verify_fn_t)(ms_ctx *ctx, ms_node *node);
209212
typedef int (*node_gen_fn_t)(ms_ctx *ctx, ms_node *node,
@@ -381,7 +384,7 @@ static int canonicalize(const char *descriptor,
381384
int key_index_hwm = -1;
382385
const char *p = descriptor, *start;
383386
char *out;
384-
bool found_policy_key = false, found_policy_single = false, found_policy_multi = false;;
387+
bool found_policy_single = false, found_policy_multi = false;;
385388

386389
*output = NULL;
387390
*num_substitutions = 0;
@@ -427,7 +430,6 @@ static int canonicalize(const char *descriptor,
427430
return WALLY_EINVAL; /* Must be ordered with no gaps */
428431
if (key_index > key_index_hwm)
429432
key_index_hwm = key_index;
430-
found_policy_key = true;
431433
if (*p++ != '/')
432434
return WALLY_EINVAL;
433435
++required_len;
@@ -453,10 +455,10 @@ static int canonicalize(const char *descriptor,
453455
if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM))
454456
return WALLY_EINVAL; /* Checksum required but not present */
455457
if (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE) {
456-
if (!found_policy_key)
457-
return WALLY_EINVAL; /* At least one key expression must be present */
458458
if (found_policy_single && found_policy_multi)
459459
return WALLY_EINVAL; /* Cannot mix cardinality of policy keys */
460+
if (key_index_hwm == -1 || key_index_hwm != (int)vars_in->num_items - 1)
461+
return WALLY_EINVAL; /* One or more keys wasn't substituted */
460462
}
461463
if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1)))
462464
return WALLY_ENOMEM;
@@ -2633,9 +2635,13 @@ int wally_descriptor_parse(const char *miniscript,
26332635
ret = node_generation_size(ctx->top_node, &ctx->script_len);
26342636
if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE)) {
26352637
if (ctx->keys.num_items != num_substitutions)
2636-
ret = WALLY_EINVAL; /* A non-substituted key was present */
2638+
ret = WALLY_EINVAL; /* non-substituted key in the expression */
2639+
else if (vars_in && ctx->keys.num_items < vars_in->num_items)
2640+
ret = WALLY_EINVAL; /* non-substituted key in substitutions */
26372641
else if (ctx->num_variants > 1 || ctx->num_multipaths > 2)
26382642
ret = WALLY_EINVAL; /* Solved cardinality must be 1 or 2 */
2643+
else if (flags & WALLY_MINISCRIPT_UNIQUE_KEYPATHS)
2644+
ret = ensure_unique_policy_keys(ctx);
26392645
}
26402646
}
26412647
if (ret != WALLY_OK) {
@@ -2973,3 +2979,64 @@ int wally_descriptor_get_key_child_path_str(
29732979
return WALLY_ENOMEM;
29742980
return WALLY_OK;
29752981
}
2982+
2983+
static const char *get_multipath_child(const char* p, uint32_t *v)
2984+
{
2985+
*v = 0;
2986+
if (*p != '<' && *p != ';')
2987+
return NULL;
2988+
else {
2989+
++p;
2990+
while (*p >= '0' && *p <= '9') {
2991+
*v *= 10;
2992+
*v += (*p++ - '0');
2993+
}
2994+
if (*p == '\'' || *p == 'h' || *p == 'H') {
2995+
*v |= BIP32_INITIAL_HARDENED_CHILD;
2996+
++p;
2997+
}
2998+
}
2999+
return p;
3000+
}
3001+
3002+
static int are_keys_overlapped(const ms_ctx *ctx,
3003+
const ms_node *lhs, const ms_node *rhs)
3004+
{
3005+
const char *p;
3006+
uint32_t l1, l2, r1, r2;
3007+
3008+
if (lhs->data_len != rhs->data_len ||
3009+
memcmp(lhs->data, rhs->data, lhs->data_len))
3010+
return WALLY_OK; /* Different root keys */
3011+
if (lhs->child_path_len == rhs->child_path_len &&
3012+
!memcmp(lhs->child_path, rhs->child_path, lhs->child_path_len))
3013+
return WALLY_EINVAL; /* Identical paths */
3014+
if (!(lhs->flags & WALLY_MS_IS_MULTIPATH))
3015+
return WALLY_OK; /* Non-identical ranged, non-multipath keys */
3016+
if (ctx->max_path_elems != 2 || !(rhs->flags & WALLY_MS_IS_MULTIPATH))
3017+
return WALLY_ERROR; /* Should never happen! */
3018+
/* Check the set of multi-path indices is disjoint */
3019+
if (!(p = get_multipath_child(strchr(lhs->child_path, '<'), &l1)) ||
3020+
!get_multipath_child(p, &l2) ||
3021+
!(p = get_multipath_child(strchr(rhs->child_path, '<'), &r1)) ||
3022+
!get_multipath_child(p, &r2))
3023+
return WALLY_ERROR; /* Should never happen! */
3024+
if (l1 == r1 || l1 == r2 || l2 == r1 || l2 == r2)
3025+
return WALLY_EINVAL; /* indices are not disjoint */
3026+
return WALLY_OK;
3027+
}
3028+
3029+
static int ensure_unique_policy_keys(const ms_ctx *ctx)
3030+
{
3031+
size_t i, j;
3032+
3033+
for (i = 0; i < ctx->keys.num_items; ++i) {
3034+
const ms_node *node = descriptor_get_key(ctx, i);
3035+
for (j = i + 1; j < ctx->keys.num_items; ++j) {
3036+
int ret = are_keys_overlapped(ctx, node, descriptor_get_key(ctx, j));
3037+
if (ret != WALLY_OK)
3038+
return ret;
3039+
}
3040+
}
3041+
return WALLY_OK;
3042+
}

src/test/test_descriptor.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
NETWORK_LIQUID = 0x03
1111
NETWORK_LIQUID_REG = 0x04
1212

13-
MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT
14-
MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY
15-
REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM
16-
POLICY = 0x08 # WALLY_MINISCRIPT_POLICY_TEMPLATE
13+
MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT
14+
MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY
15+
REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM
16+
POLICY = 0x08 # WALLY_MINISCRIPT_POLICY_TEMPLATE
17+
UNIQUE_KEYPATHS = 0x10 # WALLY_MINISCRIPT_UNIQUE_KEYPATHS
1718

1819
MS_IS_RANGED = 0x1
1920
MS_IS_MULTIPATH = 0x2
@@ -291,26 +292,35 @@ def make_keys(xpubs):
291292
keys = {f'@{i}': xpub for i,xpub in enumerate(xpubs)}
292293
return wally_map_from_dict(keys)
293294

295+
P, K = POLICY, UNIQUE_KEYPATHS
294296
bad_args = [
295297
# Raw pubkey
296-
[POLICY, ['038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048']],
298+
[P, 'pkh(@0/*)', ['038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048']],
297299
# Bip32 private key
298-
[POLICY, [xpriv]],
300+
[P, 'pkh(@0/*)', [xpriv]],
299301
# Keys must be in the form of @N
300-
[POLICY, {'foo': xpub1}],
302+
[P, 'pkh(@0/*)', {'foo': xpub1}],
301303
# Keys must start from 0
302-
[POLICY, {'@1': xpub1}],
304+
[P, 'pkh(@0/*)', {'@1': xpub1}],
303305
# Keys must be successive integers
304-
[POLICY, {'@0': xpub1, '@2': xpub2}],
306+
[P, 'pkh(@0/*)', [xpub1, xpub2]],
307+
# Keys must all be substituted
308+
[P, 'pkh(@0/*)', {'@0': xpub1, '@1': xpub2}],
305309
# Keys cannot have child paths
306-
[POLICY, {'@0': f'{xpub1}/0'}],
307-
# Keys must be unique
308-
[POLICY, [xpub1, xpub1]],
310+
[P, 'pkh(@0/*)', {'@0': f'{xpub1}/0'}],
311+
# Keys must be unique in the substitution list (always)
312+
[P, 'sh(multi(1, @0/*,@1/*))', [xpub1, xpub1]],
313+
# Keys must be unique in the final expression (with flag)
314+
[P|K, 'sh(multi(1,@0/*,@0/*))', [xpub1]],
315+
[P|K, 'sh(multi(1,@0/**,@0/**))', [xpub1]],
316+
# Key multi-paths must be disjoint sets
317+
[P|K, 'sh(multi(1,@0/<0;1>/*,@0/<1;2>/*))', [xpub1]],
318+
[P|K, 'sh(multi(1,@0/<1;0>/*,@0/<2;1>/*))', [xpub1]],
309319
]
310320
d = c_void_p()
311-
for flags, key_items in bad_args:
321+
for flags, policy, key_items in bad_args:
312322
keys = wally_map_from_dict(key_items) if type(key_items) is dict else make_keys(key_items)
313-
ret = wally_descriptor_parse('pkh(@0/*)', keys, NETWORK_BTC_MAIN, POLICY, d)
323+
ret = wally_descriptor_parse(policy, keys, NETWORK_BTC_MAIN, flags, d)
314324
self.assertEqual(ret, WALLY_EINVAL)
315325
wally_map_free(keys)
316326

src/wasm_package/src/const.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export const WALLY_MINISCRIPT_ONLY = 0x02; /** Only allow miniscript (not descri
124124
export const WALLY_MINISCRIPT_POLICY_TEMPLATE = 0x08; /** Only allow policy templates with @n BIP32 keys */
125125
export const WALLY_MINISCRIPT_REQUIRE_CHECKSUM = 0x04; /** Require a checksum to be present */
126126
export const WALLY_MINISCRIPT_TAPSCRIPT = 0x01; /** Tapscript, use x-only pubkeys */
127+
export const WALLY_MINISCRIPT_UNIQUE_KEYPATHS = 0x10; /** For policy templates, ensure BIP32 derivation paths differ for identical keys */
127128
export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */
128129
export const WALLY_MS_IS_DESCRIPTOR = 0x20; /** Contains only descriptor expressions (no miniscript) */
129130
export const WALLY_MS_IS_MULTIPATH = 0x02; /** Allows multiple paths via ``<a;b;c>`` */

0 commit comments

Comments
 (0)