From 8e4fc85ab53883505ed7d65aee4eec25478d98bc Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 00:39:38 +1200 Subject: [PATCH 01/25] descriptor-policy: initial implementation of wallet-policies See https://github.com/bitcoin/bips/pull/1389 Policies are a restriction on the general variable substitution mechanism that wally already supports, combined with a shorthand notation for path derivation. 1 - Key variables can only be named @n where n is an integer 2 - Key variable names must increase sequentially from 0 to n 3 - Key variables names must be followed by '/*', '/**', or '//*' 4 - Key variables can only be serialized BIP32 public keys without paths 5 - All key expressions to substitute must be unique 6 - Al least one key expression must be present 7 - Key variables must appear in the policy in order from 0 to n (back-references are allowed for repeated keys) 8 - All key expressions in a policy must be in the form of Key variables 9 - All key expression must share the same solved cardinality (keys using '/*', cannot be mixed with those using '/**' or '//*') 10 - The solved cardinality of a policy must be 1 or 2 (e.g. no combo())`. 11 - All repeated references to the same key must use distinct derivations. This initial change implements and tests points 1-4. This implementation will ignore the whitelisted expression lists given in the BIP, and instead accept any valid descriptor that doesn't have a solved cardinality greater than 2. See the above linked github PR discussion for the rationale behind this decision. --- include/wally_descriptor.h | 1 + src/ctest/test_descriptor.c | 63 ++++++++++++++++++- src/descriptor.c | 110 +++++++++++++++++++++++++++++----- src/test/test_descriptor.py | 44 ++++++++++++-- src/wasm_package/src/const.js | 1 + 5 files changed, 199 insertions(+), 20 deletions(-) diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index d98abf32a..39ad8a32c 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -15,6 +15,7 @@ struct wally_descriptor; #define WALLY_MINISCRIPT_TAPSCRIPT 0x01 /** Tapscript, use x-only pubkeys */ #define WALLY_MINISCRIPT_ONLY 0x02 /** Only allow miniscript (not descriptor) expressions */ #define WALLY_MINISCRIPT_REQUIRE_CHECKSUM 0x04 /** Require a checksum to be present */ +#define WALLY_MINISCRIPT_POLICY 0x08 /** Only allow policy @n variable substitution */ #define WALLY_MINISCRIPT_DEPTH_MASK 0xffff0000 /** Mask for limiting maximum depth */ #define WALLY_MINISCRIPT_DEPTH_SHIFT 16 /** Shift to convert maximum depth to flags */ diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 3445e1c80..9a55569e4 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -51,6 +51,17 @@ static const struct wally_map g_key_map = { NULL }; +static struct wally_map_item g_policy_map_items[] = { + { B("@0"), B("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") } +}; + +static const struct wally_map g_policy_map = { + g_policy_map_items, + NUM_ELEMS(g_policy_map_items), + NUM_ELEMS(g_policy_map_items), + NULL +}; + static const uint32_t g_miniscript_index_0 = 0; static const uint32_t g_miniscript_index_16 = 0x10; @@ -968,6 +979,32 @@ static const struct descriptor_test { "5221038145454b87fc9ec3557478d6eadc2aea290b50f3c469b828abeb542ae8f8849d2102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f052ae", "y5pky4r2" }, + /* Wallet policies https://github.com/bitcoin/bips/pull/1389 */ + { + "policy - single asterisk reconciliation", + "pkh(mainnet_xpub/*)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, + "76a914bb57ca9e62c7084081edc68d2cbc9524a523784288ac", + "cp8r8rlg" + }, { + "policy - single asterisk", + "pkh(@0/*)", // Becomes "pkh(mainnet_xpub/*)" i.e. the test case above this + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + "76a914bb57ca9e62c7084081edc68d2cbc9524a523784288ac", + "cp8r8rlg" + }, { + "policy - double asterisk", + "pkh(@0/**)", // Becomes "pkh(mainnet_xpub/<0;1>/*)" + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + "76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac", + "ydnzkve4" + }, { + "policy - multi-path", + "pkh(@0/<0;1>/*)", // Becomes "pkh(mainnet_xpub/<0;1>/*)" + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + "76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac", + "ydnzkve4" + }, /* * Misc error cases (code coverage) */ @@ -1375,6 +1412,28 @@ static const struct descriptor_test { "descriptor - hardened xpub multi-path", /* TODO: Allow setting an xpriv into the descriptor */ "pkh(mainnet_xpub/<0';1>)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + }, + /* Wallet policy error cases */ + { + "policy errchk - key with path", + "pkh(@0/0/*)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - missing key postfix", + "pkh(@0)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - terminal key postfix", + "@0", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - missing key number", + "@", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" } }; @@ -1823,10 +1882,12 @@ static bool check_descriptor_to_script(const struct descriptor_test* test) int expected_ret, ret, len_ret; uint32_t multi_index = 0; uint32_t child_num = test->child_num ? *test->child_num : 0, features; + const bool is_policy = test->flags & WALLY_MINISCRIPT_POLICY; + const struct wally_map *keys = is_policy ? &g_policy_map : &g_key_map; expected_ret = test->script ? WALLY_OK : WALLY_EINVAL; - ret = wally_descriptor_parse(test->descriptor, &g_key_map, test->network, + ret = wally_descriptor_parse(test->descriptor, keys, test->network, test->flags, &descriptor); if (expected_ret == WALLY_OK || ret == expected_ret) { /* For failure cases, we may fail when generating instead of parsing, diff --git a/src/descriptor.c b/src/descriptor.c index 7fedb5c95..3aeced794 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -17,7 +17,10 @@ #define NUM_ELEMS(a) (sizeof(a) / sizeof(a[0])) #define MS_FLAGS_ALL (WALLY_MINISCRIPT_TAPSCRIPT | \ WALLY_MINISCRIPT_ONLY | \ - WALLY_MINISCRIPT_REQUIRE_CHECKSUM) + WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \ + WALLY_MINISCRIPT_POLICY) +#define MS_FLAGS_CANONICALIZE (WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \ + WALLY_MINISCRIPT_POLICY) /* Properties and expressions definition */ #define TYPE_NONE 0x00 @@ -270,6 +273,7 @@ static const struct addr_ver_t *addr_ver_from_family( static const struct ms_builtin_t *builtin_get(const ms_node *node); static int generate_script(ms_ctx *ctx, ms_node *node, unsigned char *script, size_t script_len, size_t *written); +static bool is_valid_policy_map(const struct wally_map *map_in); /* Wrapper for strtoll */ static bool strtoll_n(const char *str, size_t str_len, int64_t *v) @@ -355,16 +359,21 @@ static int generate_checksum(const char *str, size_t str_len, char *checksum_out return WALLY_OK; } -static inline bool is_identifer_char(char c) +typedef bool (*is_identifer_fn)(char c); + +static bool is_identifer_char(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; } +static bool is_policy_start_char(char c) { return c == '@'; } +static bool is_policy_identifer_char(char c) { return c >= '0' && c <= '9'; } static int canonicalize(const char *descriptor, const struct wally_map *vars_in, uint32_t flags, char **output) { const size_t VAR_MAX_NAME_LEN = 16; + is_identifer_fn is_id_start = is_identifer_char, is_id_char = is_identifer_char; size_t required_len = 0; const char *p = descriptor, *start; char *out; @@ -372,17 +381,26 @@ static int canonicalize(const char *descriptor, if (output) *output = NULL; - if (!descriptor || (flags & ~WALLY_MINISCRIPT_REQUIRE_CHECKSUM) || !output) + if (!descriptor || (flags & ~MS_FLAGS_CANONICALIZE) || !output) return WALLY_EINVAL; + if (flags & WALLY_MINISCRIPT_POLICY) { + if (!is_valid_policy_map(vars_in)) + return WALLY_EINVAL; /* Invalid policy variables given */ + is_id_start = is_policy_start_char; + is_id_char = is_policy_identifer_char; + } + /* First, find the length of the canonicalized descriptor */ while (*p && *p != '#') { - while (*p && *p != '#' && !is_identifer_char(*p)) { + while (*p && *p != '#' && !is_id_start(*p)) { ++required_len; ++p; } - start = p; - while (is_identifer_char(*p)) + if (!is_id_start(*p)) + break; + start = p++; + while (is_id_char(*p)) ++p; if (p != start) { const bool starts_with_digit = *start >= '0' && *start <= '9'; @@ -394,36 +412,60 @@ static int canonicalize(const char *descriptor, const struct wally_map_item *item; item = wally_map_get(vars_in, (unsigned char*)start, lookup_len); required_len += item ? item->value_len : lookup_len; + if (item && flags & WALLY_MINISCRIPT_POLICY) { + if (*p++ != '/') + return WALLY_EINVAL; + ++required_len; + if (*p == '<') + continue; + if (*p++ != '*') + return WALLY_EINVAL; + if (*p == '*') { + ++p; + required_len += strlen("<0;1>/*"); + } else { + required_len += 1; + } + } } } } if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM)) return WALLY_EINVAL; /* Checksum required but not present */ - if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1))) return WALLY_ENOMEM; p = descriptor; out = *output; while (*p && *p != '#') { - while (*p && *p != '#' && !is_identifer_char(*p)) { + while (*p && *p != '#' && !is_id_start(*p)) { *out++ = *p++; } - start = p; - while (is_identifer_char(*p)) + if (!is_id_start(*p)) + break; + start = p++; + while (is_id_char(*p)) ++p; if (p != start) { const bool is_number = *start >= '0' && *start <= '9'; size_t lookup_len = p - start; - if (!vars_in || lookup_len > VAR_MAX_NAME_LEN || is_number) { + if (!vars_in || lookup_len > VAR_MAX_NAME_LEN || is_number) memcpy(out, start, lookup_len); - } else { + else { /* Lookup the potential identifier */ const struct wally_map_item *item; item = wally_map_get(vars_in, (unsigned char*)start, lookup_len); lookup_len = item ? item->value_len : lookup_len; memcpy(out, item ? (char *)item->value : start, lookup_len); + if (item && flags & WALLY_MINISCRIPT_POLICY) { + if (p[1] == '*' && p[2] == '*') { + out += lookup_len; + lookup_len = strlen("/<0;1>/*"); + memcpy(out, "/<0;1>/*", lookup_len); + p += strlen("/**"); + } + } } out += lookup_len; } @@ -2455,6 +2497,47 @@ static uint32_t get_max_depth(const char *miniscript, size_t miniscript_len) return depth == 1 ? max_depth : 0xffffffff; } +static bool is_valid_policy_map(const struct wally_map *map_in) +{ + ms_ctx ctx; + ms_node* node; + int64_t v; + size_t i; + int ret = WALLY_OK; + + if (!map_in || !map_in->num_items) + return WALLY_EINVAL; /* Must contain at least one key expression */ + + memset(&ctx, 0, sizeof(ctx)); + + for (i = 0; ret == WALLY_OK && i < map_in->num_items; ++i) { + const struct wally_map_item *item = &map_in->items[i]; + if (!item->key || item->key_len < 2 || item->key[0] != '@' || + !strtoll_n((const char *)item->key + 1, item->key_len - 1, &v) || v < 0) + ret = WALLY_EINVAL; /* Policy keys can only be @n */ + else if ((size_t)v != i) + ret = WALLY_EINVAL; /* Must be sorted in order from 0-n */ + else if (!item->value || !item->value_len) + ret = WALLY_EINVAL; /* No key value */ + else if (!(node = wally_calloc(sizeof(*node)))) + ret = WALLY_EINVAL; + else { + node->data = (const char*)item->value; + node->data_len = item->value_len; + if (analyze_miniscript_key(&ctx, 0, node, NULL) != WALLY_OK || + node->kind != KIND_BIP32_PUBLIC_KEY || + node->child_path_len) { + ret = WALLY_EINVAL; /* Only BIP32 xpubs are allowed */ + } else if (ctx.features & (WALLY_MS_IS_MULTIPATH | WALLY_MS_IS_RANGED)) { + /* Range or multipath must be part of the expression, not the key */ + ret = WALLY_EINVAL; + } + } + node_free(node); + } + return ret == WALLY_OK; +} + int wally_descriptor_parse(const char *miniscript, const struct wally_map *vars_in, uint32_t network, uint32_t flags, @@ -2480,8 +2563,7 @@ int wally_descriptor_parse(const char *miniscript, ctx->addr_ver = addr_ver; ctx->num_variants = 1; ctx->num_multipaths = 1; - ret = canonicalize(miniscript, vars_in, - flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM, + ret = canonicalize(miniscript, vars_in, flags & MS_FLAGS_CANONICALIZE, &ctx->src); if (ret == WALLY_OK) { ctx->src_len = strlen(ctx->src); diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index f4e28dbd1..5a3a77632 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -12,6 +12,8 @@ MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY +REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM +POLICY = 0x08 # WALLY_MINISCRIPT_POLICY MS_IS_RANGED = 0x1 MS_IS_MULTIPATH = 0x2 @@ -26,7 +28,7 @@ def wally_map_from_dict(d): m = pointer(wally_map()) assert(wally_map_init_alloc(len(d.keys()), None, m) == WALLY_OK) for k,v in d.items(): - assert(wally_map_add(m, k, len(k), v, len(v)) == WALLY_OK) + assert(wally_map_add(m, utf8(k), len(k), utf8(v), len(v)) == WALLY_OK) return m @@ -35,10 +37,10 @@ class DescriptorTests(unittest.TestCase): def test_parse_and_to_script(self): """Test parsing and script generation""" keys = wally_map_from_dict({ - utf8('key_local'): utf8('038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048'), - utf8('key_remote'): utf8('03a22745365f673e658f0d25eb0afa9aaece858c6a48dfe37a67210c2e23da8ce7'), - utf8('key_revocation'): utf8('03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284'), - utf8('H'): utf8('d0721279e70d39fb4aa409b52839a0056454e3b5'), # HASH160(key_local) + 'key_local': '038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048', + 'key_remote': '03a22745365f673e658f0d25eb0afa9aaece858c6a48dfe37a67210c2e23da8ce7', + 'key_revocation': '03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284', + 'H': 'd0721279e70d39fb4aa409b52839a0056454e3b5', # HASH160(key_local) }) script, script_len = make_cbuffer('00' * 256 * 2) @@ -278,5 +280,37 @@ def test_features_and_depth(self): flags | (5 << 16), d) self.assertEqual(ret, WALLY_EINVAL) + def test_policy(self): + """Test policy parsing""" + # Substitution variables + xpriv = 'xprvA2YKGLieCs6cWCiczALiH1jzk3VCCS5M1pGQfWPkamCdR9UpBgE2Gb8AKAyVjKHkz8v37avcfRjdcnP19dVAmZrvZQfvTcXXSAiFNQ6tTtU' + xpub1 = 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL' + xpub2 = 'xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU' + + def make_keys(xpubs): + keys = {f'@{i}': xpub for i,xpub in enumerate(xpubs)} + return wally_map_from_dict(keys) + + bad_args = [ + # Raw pubkey + [POLICY, ['038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048']], + # Bip32 private key + [POLICY, [xpriv]], + # Keys must be in the form of @N + [POLICY, {'foo': xpub1}], + # Keys must start from 0 + [POLICY, {'@1': xpub1}], + # Keys must be successive integers + [POLICY, {'@0': xpub1, '@2': xpub2}], + # Keys cannot have child paths + [POLICY, {'@0': f'{xpub1}/0'}], + ] + d = c_void_p() + for flags, key_items in bad_args: + keys = wally_map_from_dict(key_items) if type(key_items) is dict else make_keys(key_items) + ret = wally_descriptor_parse('pkh(@0/*)', keys, NETWORK_BTC_MAIN, POLICY, d) + self.assertEqual(ret, WALLY_EINVAL) + wally_map_free(keys) + if __name__ == '__main__': unittest.main() diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index f8eb3545b..9f15607d7 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -121,6 +121,7 @@ export const WALLY_MAX_OP_RETURN_LEN = 80; /* Maximum length of OP_RETURN data p export const WALLY_MINISCRIPT_DEPTH_MASK = 0xffff0000; /** Mask for limiting maximum depth */ export const WALLY_MINISCRIPT_DEPTH_SHIFT = 16; /** Shift to convert maximum depth to flags */ export const WALLY_MINISCRIPT_ONLY = 0x02; /** Only allow miniscript (not descriptor) expressions */ +export const WALLY_MINISCRIPT_POLICY = 0x08; /** Only allow policy @n variable substitution */ export const WALLY_MINISCRIPT_REQUIRE_CHECKSUM = 0x04; /** Require a checksum to be present */ export const WALLY_MINISCRIPT_TAPSCRIPT = 0x01; /** Tapscript, use x-only pubkeys */ export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */ From 2e78e3d6272d54fa06c64b095c7bfa3f90d44104 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 11:16:57 +1200 Subject: [PATCH 02/25] descriptor-policy: ensure policy key expressions are unique Point 5 of the policy key requirements. --- src/descriptor.c | 9 ++++++++- src/test/test_descriptor.py | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/descriptor.c b/src/descriptor.c index 3aeced794..2379b7229 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2499,6 +2499,7 @@ static uint32_t get_max_depth(const char *miniscript, size_t miniscript_len) static bool is_valid_policy_map(const struct wally_map *map_in) { + struct wally_map keys; ms_ctx ctx; ms_node* node; int64_t v; @@ -2509,6 +2510,7 @@ static bool is_valid_policy_map(const struct wally_map *map_in) return WALLY_EINVAL; /* Must contain at least one key expression */ memset(&ctx, 0, sizeof(ctx)); + ret = wally_map_init(map_in->num_items, NULL, &keys); for (i = 0; ret == WALLY_OK && i < map_in->num_items; ++i) { const struct wally_map_item *item = &map_in->items[i]; @@ -2531,10 +2533,15 @@ static bool is_valid_policy_map(const struct wally_map *map_in) } else if (ctx.features & (WALLY_MS_IS_MULTIPATH | WALLY_MS_IS_RANGED)) { /* Range or multipath must be part of the expression, not the key */ ret = WALLY_EINVAL; + } else { + ret = wally_map_add(&keys, item->value, item->value_len, NULL, 0); } + node_free(node); } - node_free(node); } + if (ret == WALLY_OK && keys.num_items != map_in->num_items) + ret = WALLY_EINVAL; /* One of more keys is not unique */ + wally_map_clear(&keys); return ret == WALLY_OK; } diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index 5a3a77632..be2b1a029 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -304,6 +304,8 @@ def make_keys(xpubs): [POLICY, {'@0': xpub1, '@2': xpub2}], # Keys cannot have child paths [POLICY, {'@0': f'{xpub1}/0'}], + # Keys must be unique + [POLICY, [xpub1, xpub1]], ] d = c_void_p() for flags, key_items in bad_args: From defa31b4506e163f51ac919e367317d7db5318fd Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 11:25:45 +1200 Subject: [PATCH 03/25] descriptor-policy: ensure at least one policy key is present Point 6 of the policy key requirements. --- src/ctest/test_descriptor.c | 5 +++++ src/descriptor.c | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 9a55569e4..9afecb992 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -1415,6 +1415,11 @@ static const struct descriptor_test { }, /* Wallet policy error cases */ { + "policy errchk - no key expression", + "raw()", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" + }, { "policy errchk - key with path", "pkh(@0/0/*)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, diff --git a/src/descriptor.c b/src/descriptor.c index 2379b7229..e5785eaa2 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -377,6 +377,7 @@ static int canonicalize(const char *descriptor, size_t required_len = 0; const char *p = descriptor, *start; char *out; + bool found_policy_key = false; if (output) *output = NULL; @@ -413,6 +414,7 @@ static int canonicalize(const char *descriptor, item = wally_map_get(vars_in, (unsigned char*)start, lookup_len); required_len += item ? item->value_len : lookup_len; if (item && flags & WALLY_MINISCRIPT_POLICY) { + found_policy_key = true; if (*p++ != '/') return WALLY_EINVAL; ++required_len; @@ -433,6 +435,8 @@ static int canonicalize(const char *descriptor, if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM)) return WALLY_EINVAL; /* Checksum required but not present */ + if (!found_policy_key && flags & WALLY_MINISCRIPT_POLICY) + return WALLY_EINVAL; /* At least one key expression must be present */ if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1))) return WALLY_ENOMEM; From 6cb042e237f454ad705389e35979ea90fe19b393 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 13:57:52 +1200 Subject: [PATCH 04/25] descriptor-policy: ensure key ordering is sequential Point 7 of the policy key requirements. --- src/ctest/test_descriptor.c | 14 +++++++++++++- src/descriptor.c | 6 ++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 9afecb992..3ac982450 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -52,7 +52,8 @@ static const struct wally_map g_key_map = { }; static struct wally_map_item g_policy_map_items[] = { - { B("@0"), B("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") } + { B("@0"), B("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") }, + { B("@1"), B("xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU") } }; static const struct wally_map g_policy_map = { @@ -1004,6 +1005,12 @@ static const struct descriptor_test { WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, "76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac", "ydnzkve4" + }, { + "policy - previous key reference", + "sh(multi(1,@0/**,@1/**,@0/**))", /* For testing: expresssion isn't sensible */ + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + "a91415b7de59bd65038744e3214a521d9d4443dc78c287", + "xwdj6ucy" }, /* * Misc error cases (code coverage) @@ -1439,6 +1446,11 @@ static const struct descriptor_test { "@", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - mis-ordered keys", + "sh(multi(1,@1/**,@0/**))", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" } }; diff --git a/src/descriptor.c b/src/descriptor.c index e5785eaa2..700321669 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -375,6 +375,7 @@ static int canonicalize(const char *descriptor, const size_t VAR_MAX_NAME_LEN = 16; is_identifer_fn is_id_start = is_identifer_char, is_id_char = is_identifer_char; size_t required_len = 0; + int key_index_hwm = -1; const char *p = descriptor, *start; char *out; bool found_policy_key = false; @@ -414,6 +415,11 @@ static int canonicalize(const char *descriptor, item = wally_map_get(vars_in, (unsigned char*)start, lookup_len); required_len += item ? item->value_len : lookup_len; if (item && flags & WALLY_MINISCRIPT_POLICY) { + int key_index = (int)(item - vars_in->items); + if (key_index > key_index_hwm + 1) + return WALLY_EINVAL; /* Must be ordered with no gaps */ + if (key_index > key_index_hwm) + key_index_hwm = key_index; found_policy_key = true; if (*p++ != '/') return WALLY_EINVAL; From 8ef414d051e829cb6b2da71ded3702e0efd695ef Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 20:58:02 +1200 Subject: [PATCH 05/25] descriptor: store and expose the number of keys in a descriptor --- include/wally.hpp | 6 ++++ include/wally_descriptor.h | 12 ++++++++ src/descriptor.c | 46 +++++++++++++++++++------------ src/swig_java/swig.i | 1 + src/test/util.py | 1 + src/wasm_package/src/functions.js | 1 + src/wasm_package/src/index.d.ts | 1 + tools/wasm_exports.sh | 1 + 8 files changed, 52 insertions(+), 17 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index 193bf4705..dd0746ea4 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -510,6 +510,12 @@ inline int descriptor_get_network(const DESCRIPTOR& descriptor, uint32_t* value_ return ret; } +template +inline int descriptor_get_num_keys(const DESCRIPTOR& descriptor, uint32_t* value_out) { + int ret = ::wally_descriptor_get_num_keys(detail::get_p(descriptor), value_out); + return ret; +} + template inline int descriptor_get_num_paths(const DESCRIPTOR& descriptor, uint32_t* value_out) { int ret = ::wally_descriptor_get_num_paths(detail::get_p(descriptor), value_out); diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index 39ad8a32c..108107a60 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -131,6 +131,18 @@ WALLY_CORE_API int wally_descriptor_get_features( const struct wally_descriptor *descriptor, uint32_t *value_out); +/** + * Get the number of keys in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param value_out: Destination for the number of keys. + * +* .. note:: Repeated keys are counted once for each time they appear. + */ +WALLY_CORE_API int wally_descriptor_get_num_keys( + const struct wally_descriptor *descriptor, + uint32_t *value_out); + /** * Get the number of variants in a parsed output descriptor or miniscript expression. * diff --git a/src/descriptor.c b/src/descriptor.c index 700321669..7995dbf9d 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -190,6 +190,7 @@ typedef struct wally_descriptor { ms_node *top_node; /* The first node of the parse tree */ const struct addr_ver_t *addr_ver; uint32_t features; /* Features present in the parsed tree */ + uint32_t num_keys; /* Number of keys in the expression */ uint32_t num_variants; /* Number of script variants in the expression */ uint32_t num_multipaths; /* Number of multi-path items in the expression */ size_t script_len; /* Max script length generatable from this expression */ @@ -2034,6 +2035,7 @@ static bool analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, node->flags |= NF_IS_XONLY; node->kind = KIND_PUBLIC_KEY; ctx->features |= WALLY_MS_IS_RAW; + ++ctx->num_keys; return true; } @@ -2091,6 +2093,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, node->data_len = EC_PRIVATE_KEY_LEN; node->kind = KIND_PRIVATE_KEY; ctx->features |= (WALLY_MS_IS_PRIVATE | WALLY_MS_IS_RAW); + ++ctx->num_keys; } wally_clear(privkey, sizeof(privkey)); return ret; @@ -2153,8 +2156,11 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, ret = WALLY_EINVAL; /* Mismatched main/test network */ } - if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_TAPSCRIPT)) - node->flags |= NF_IS_XONLY; + if (ret == WALLY_OK) { + if (flags & WALLY_MINISCRIPT_TAPSCRIPT) + node->flags |= NF_IS_XONLY; + ++ctx->num_keys; + } wally_clear(&extkey, sizeof(extkey)); return ret; } @@ -2782,37 +2788,43 @@ int wally_descriptor_set_network(struct wally_descriptor *descriptor, return descriptor->addr_ver ? WALLY_OK : WALLY_EINVAL; } -int wally_descriptor_get_features(const struct wally_descriptor *descriptor, - uint32_t *value_out) +static int descriptor_uint32(const void *descriptor, + uint32_t *value_out, size_t offset) { if (value_out) *value_out = 0; if (!descriptor || !value_out) return WALLY_EINVAL; - *value_out = descriptor->features; + memcpy(value_out, (char*)descriptor + offset, sizeof(uint32_t)); return WALLY_OK; } +int wally_descriptor_get_features(const struct wally_descriptor *descriptor, + uint32_t *value_out) +{ + return descriptor_uint32(descriptor, value_out, + offsetof(struct wally_descriptor, features)); +} + +int wally_descriptor_get_num_keys(const struct wally_descriptor *descriptor, + uint32_t *value_out) +{ + return descriptor_uint32(descriptor, value_out, + offsetof(struct wally_descriptor, num_keys)); +} + int wally_descriptor_get_num_variants(const struct wally_descriptor *descriptor, uint32_t *value_out) { - if (value_out) - *value_out = 0; - if (!descriptor || !value_out) - return WALLY_EINVAL; - *value_out = descriptor->num_variants; - return WALLY_OK; + return descriptor_uint32(descriptor, value_out, + offsetof(struct wally_descriptor, num_variants)); } int wally_descriptor_get_num_paths(const struct wally_descriptor *descriptor, uint32_t *value_out) { - if (value_out) - *value_out = 0; - if (!descriptor || !value_out) - return WALLY_EINVAL; - *value_out = descriptor->num_multipaths; - return WALLY_OK; + return descriptor_uint32(descriptor, value_out, + offsetof(struct wally_descriptor, num_multipaths)); } static uint32_t node_get_depth(const ms_node *node) diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index b36c2cbbd..04335f93f 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -561,6 +561,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_descriptor_get_depth); %returns_size_t(wally_descriptor_get_features); %returns_size_t(wally_descriptor_get_network); +%returns_size_t(wally_descriptor_get_num_keys); %returns_size_t(wally_descriptor_get_num_paths); %returns_size_t(wally_descriptor_get_num_variants); %returns_void__(wally_descriptor_set_network); diff --git a/src/test/util.py b/src/test/util.py index edfe9ff5b..22b354ee5 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -314,6 +314,7 @@ class wally_psbt(Structure): ('wally_descriptor_get_depth', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_features', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_network', c_int, [c_void_p, c_uint32_p]), + ('wally_descriptor_get_num_keys', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_paths', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_variants', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_parse', c_int, [c_char_p, POINTER(wally_map), c_uint32, c_uint32, POINTER(c_void_p)]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 2b7557126..82bc174cd 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -172,6 +172,7 @@ export const descriptor_get_checksum = wrap('wally_descriptor_get_checksum', [T. export const descriptor_get_depth = wrap('wally_descriptor_get_depth', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_features = wrap('wally_descriptor_get_features', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_network = wrap('wally_descriptor_get_network', [T.OpaqueRef, T.DestPtr(T.Int32)]); +export const descriptor_get_num_keys = wrap('wally_descriptor_get_num_keys', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_paths = wrap('wally_descriptor_get_num_paths', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_variants = wrap('wally_descriptor_get_num_variants', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_parse = wrap('wally_descriptor_parse', [T.String, T.OpaqueRef, T.Int32, T.Int32, T.DestPtrPtr(T.OpaqueRef)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 121b9566b..69bddea94 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -124,6 +124,7 @@ export function descriptor_get_checksum(descriptor: Ref_wally_descriptor, flags: export function descriptor_get_depth(descriptor: Ref_wally_descriptor): number; export function descriptor_get_features(descriptor: Ref_wally_descriptor): number; export function descriptor_get_network(descriptor: Ref_wally_descriptor): number; +export function descriptor_get_num_keys(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_paths(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_variants(descriptor: Ref_wally_descriptor): number; export function descriptor_parse(descriptor: string, vars_in: Ref_wally_map, network: number, flags: number): Ref_wally_descriptor; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index bf18a3edb..540fa7ea3 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -82,6 +82,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_descriptor_get_depth' \ ,'_wally_descriptor_get_features' \ ,'_wally_descriptor_get_network' \ +,'_wally_descriptor_get_num_keys' \ ,'_wally_descriptor_get_num_paths' \ ,'_wally_descriptor_get_num_variants' \ ,'_wally_descriptor_parse' \ From 4b24d1a4cb7b212d77a56db3ac5bb10e314d9fdf Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 22:36:46 +1200 Subject: [PATCH 06/25] descriptor-policy: ensure all policy keys are substituted key expressions Point 8 of the policy key requirements. --- src/ctest/test_descriptor.c | 5 +++++ src/descriptor.c | 24 ++++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 3ac982450..e7d0e3864 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -1451,6 +1451,11 @@ static const struct descriptor_test { "sh(multi(1,@1/**,@0/**))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - non-policy keys", + "sh(multi(1,@0/**,xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU))", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" } }; diff --git a/src/descriptor.c b/src/descriptor.c index 7995dbf9d..09d60d9cf 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -371,7 +371,7 @@ static bool is_policy_identifer_char(char c) { return c >= '0' && c <= '9'; } static int canonicalize(const char *descriptor, const struct wally_map *vars_in, uint32_t flags, - char **output) + char **output, size_t *num_substitutions) { const size_t VAR_MAX_NAME_LEN = 16; is_identifer_fn is_id_start = is_identifer_char, is_id_char = is_identifer_char; @@ -381,10 +381,9 @@ static int canonicalize(const char *descriptor, char *out; bool found_policy_key = false; - if (output) - *output = NULL; - - if (!descriptor || (flags & ~MS_FLAGS_CANONICALIZE) || !output) + *output = NULL; + *num_substitutions = 0; + if (!descriptor || (flags & ~MS_FLAGS_CANONICALIZE)) return WALLY_EINVAL; if (flags & WALLY_MINISCRIPT_POLICY) { @@ -414,8 +413,13 @@ static int canonicalize(const char *descriptor, /* Lookup the potential identifier */ const struct wally_map_item *item; item = wally_map_get(vars_in, (unsigned char*)start, lookup_len); - required_len += item ? item->value_len : lookup_len; - if (item && flags & WALLY_MINISCRIPT_POLICY) { + if (!item) { + required_len += lookup_len; + continue; + } + required_len += item->value_len; + ++*num_substitutions; + if (flags & WALLY_MINISCRIPT_POLICY) { int key_index = (int)(item - vars_in->items); if (key_index > key_index_hwm + 1) return WALLY_EINVAL; /* Must be ordered with no gaps */ @@ -2567,6 +2571,7 @@ int wally_descriptor_parse(const char *miniscript, ms_ctx **output) { const struct addr_ver_t *addr_ver = addr_ver_from_network(network); + size_t num_substitutions; uint32_t kind = KIND_MINISCRIPT | (flags & WALLY_MINISCRIPT_ONLY ? 0 : KIND_DESCRIPTOR); uint32_t max_depth = flags >> WALLY_MINISCRIPT_DEPTH_SHIFT; ms_ctx *ctx; @@ -2587,7 +2592,7 @@ int wally_descriptor_parse(const char *miniscript, ctx->num_variants = 1; ctx->num_multipaths = 1; ret = canonicalize(miniscript, vars_in, flags & MS_FLAGS_CANONICALIZE, - &ctx->src); + &ctx->src, &num_substitutions); if (ret == WALLY_OK) { ctx->src_len = strlen(ctx->src); ctx->features = WALLY_MS_IS_DESCRIPTOR; /* Un-set if miniscript found */ @@ -2599,6 +2604,9 @@ int wally_descriptor_parse(const char *miniscript, flags, NULL, NULL, &ctx->top_node); if (ret == WALLY_OK) ret = node_generation_size(ctx->top_node, &ctx->script_len); + if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY) && + ctx->num_keys != num_substitutions) + ret = WALLY_EINVAL; } if (ret != WALLY_OK) { wally_descriptor_free(ctx); From 1874a8ed6d728cd1d4b08e20d387fd68983b4aaf Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 22:46:49 +1200 Subject: [PATCH 07/25] descriptor-policy: disallow mixed cardinality key expressions Point 9 of the policy key requirements. --- src/ctest/test_descriptor.c | 10 ++++++++++ src/descriptor.c | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index e7d0e3864..5afad4920 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -1456,6 +1456,16 @@ static const struct descriptor_test { "sh(multi(1,@0/**,xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - mismatched key cardinalities (1)", + "sh(multi(1,@0/**,@1/*))", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - mismatched key cardinalities (2)", + "sh(multi(1,@0/<0;1>/*,@1/*))", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" } }; diff --git a/src/descriptor.c b/src/descriptor.c index 09d60d9cf..29916cc1a 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -379,7 +379,7 @@ static int canonicalize(const char *descriptor, int key_index_hwm = -1; const char *p = descriptor, *start; char *out; - bool found_policy_key = false; + bool found_policy_key = false, found_policy_single = false, found_policy_multi = false;; *output = NULL; *num_substitutions = 0; @@ -429,14 +429,18 @@ static int canonicalize(const char *descriptor, if (*p++ != '/') return WALLY_EINVAL; ++required_len; - if (*p == '<') + if (*p == '<') { + found_policy_multi = true; continue; + } if (*p++ != '*') return WALLY_EINVAL; if (*p == '*') { + found_policy_multi = true; ++p; required_len += strlen("<0;1>/*"); } else { + found_policy_single = true; required_len += 1; } } @@ -446,8 +450,12 @@ static int canonicalize(const char *descriptor, if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM)) return WALLY_EINVAL; /* Checksum required but not present */ - if (!found_policy_key && flags & WALLY_MINISCRIPT_POLICY) - return WALLY_EINVAL; /* At least one key expression must be present */ + if (flags & WALLY_MINISCRIPT_POLICY) { + if (!found_policy_key) + return WALLY_EINVAL; /* At least one key expression must be present */ + if (found_policy_single && found_policy_multi) + return WALLY_EINVAL; /* Cannot mix cardinality of policy keys */ + } if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1))) return WALLY_ENOMEM; From 41efd7c3b38119b22cc4135d3421037f78de9fc3 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 6 Sep 2023 23:01:23 +1200 Subject: [PATCH 08/25] descriptor-policy: disallow solved cardinalities other than 1 and 2 Point 10 of the policy key requirements --- src/ctest/test_descriptor.c | 10 ++++++++++ src/descriptor.c | 9 ++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 5afad4920..7c28f7411 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -1466,6 +1466,16 @@ static const struct descriptor_test { "sh(multi(1,@0/<0;1>/*,@1/*))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - invalid key cardinality (key path)", + "pkh(@0/<0;1;2>/*)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" + }, { + "policy errchk - invalid key cardinality (variant)", + "combo(@0/**)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, + WALLY_MINISCRIPT_POLICY, NULL, "" } }; diff --git a/src/descriptor.c b/src/descriptor.c index 29916cc1a..21c6ee47d 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2612,9 +2612,12 @@ int wally_descriptor_parse(const char *miniscript, flags, NULL, NULL, &ctx->top_node); if (ret == WALLY_OK) ret = node_generation_size(ctx->top_node, &ctx->script_len); - if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY) && - ctx->num_keys != num_substitutions) - ret = WALLY_EINVAL; + if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY)) { + if (ctx->num_keys != num_substitutions) + ret = WALLY_EINVAL; /* A non-substituted key was present */ + else if (ctx->num_variants > 1 || ctx->num_multipaths > 2) + ret = WALLY_EINVAL; /* Solved cardinality must be 1 or 2 */ + } } if (ret != WALLY_OK) { wally_descriptor_free(ctx); From df75323099fe7e92517e71b066bd0a2e90b72d81 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 7 Sep 2023 22:34:28 +1200 Subject: [PATCH 09/25] descriptor: store key nodes when parsing --- src/descriptor.c | 57 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/descriptor.c b/src/descriptor.c index 21c6ee47d..e8963cf14 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -190,7 +190,6 @@ typedef struct wally_descriptor { ms_node *top_node; /* The first node of the parse tree */ const struct addr_ver_t *addr_ver; uint32_t features; /* Features present in the parsed tree */ - uint32_t num_keys; /* Number of keys in the expression */ uint32_t num_variants; /* Number of script variants in the expression */ uint32_t num_multipaths; /* Number of multi-path items in the expression */ size_t script_len; /* Max script length generatable from this expression */ @@ -200,8 +199,16 @@ typedef struct wally_descriptor { uint32_t multi_index; /* Multi-path index for derivation */ uint32_t *path_buff; /* Path buffer for deriving keys */ uint32_t max_path_elems; /* Max path length seen in the descriptor */ + struct wally_map keys; } ms_ctx; +static int ctx_add_key_node(ms_ctx *ctx, ms_node *node) +{ + const char *v = (char *)node; + return map_add(&ctx->keys, NULL, ctx->keys.num_items, + (unsigned char *)v, 1, false, false); +} + /* Built-in miniscript expressions */ typedef int (*node_verify_fn_t)(ms_ctx *ctx, ms_node *node); typedef int (*node_gen_fn_t)(ms_ctx *ctx, ms_node *node, @@ -557,6 +564,10 @@ static bool has_two_different_lock_states(uint32_t primary, uint32_t secondary) int wally_descriptor_free(ms_ctx *ctx) { if (ctx) { + /* Just clear the item storage, the actual items are owned by + * the tree of nodes */ + clear_and_free(ctx->keys.items, + ctx->keys.num_items * sizeof(*ctx->keys.items)); wally_free_string(ctx->src); node_free(ctx->top_node); clear_and_free(ctx, sizeof(*ctx)); @@ -2015,29 +2026,29 @@ static int analyze_address(ms_ctx *ctx, const char *str, size_t str_len, return ret; } -static bool analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, - uint32_t flags, ms_node *node) +static int analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, + uint32_t flags, ms_node *node, bool *is_hex) { unsigned char pubkey[EC_PUBLIC_KEY_UNCOMPRESSED_LEN + 1]; size_t offset = flags & WALLY_MINISCRIPT_TAPSCRIPT ? 1 : 0; size_t written; - (void)ctx; + *is_hex = false; if (offset) { if (str_len != EC_XONLY_PUBLIC_KEY_LEN * 2) - return false; /* Only X-only pubkeys allowed under tapscript */ + return WALLY_OK; /* Only X-only pubkeys allowed under tapscript */ pubkey[0] = 2; /* Non-X-only pubkey prefix, for validation below */ } else { if (str_len != EC_PUBLIC_KEY_LEN * 2 && str_len != EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) - return false; /* Unknown public key size */ + return WALLY_OK; /* Unknown public key size */ } if (wally_hex_n_to_bytes(str, str_len, pubkey + offset, sizeof(pubkey) - offset, &written) != WALLY_OK || wally_ec_public_key_verify(pubkey, written + offset) != WALLY_OK) - return false; + return WALLY_OK; /* Not hex, or not a pubkey */ if (!clone_bytes((unsigned char **)&node->data, pubkey + offset, written)) - return false; /* FIXME: This needs to return ENOMEM, not continue checking */ + return WALLY_ENOMEM; node->data_len = str_len / 2; if (str_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) { node->flags |= NF_IS_UNCOMPRESSED; @@ -2047,8 +2058,8 @@ static bool analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, node->flags |= NF_IS_XONLY; node->kind = KIND_PUBLIC_KEY; ctx->features |= WALLY_MS_IS_RAW; - ++ctx->num_keys; - return true; + *is_hex = true; + return ctx_add_key_node(ctx, node); } static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, @@ -2058,6 +2069,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, struct ext_key extkey; size_t privkey_len, size; int ret; + bool is_hex; if (!node || (parent && !parent->builtin)) return WALLY_EINVAL; @@ -2079,7 +2091,8 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, } /* check key (public key) */ - if (analyze_pubkey_hex(ctx, node->data, node->data_len, flags, node)) + ret = analyze_pubkey_hex(ctx, node->data, node->data_len, flags, node, &is_hex); + if (ret == WALLY_OK && is_hex) return WALLY_OK; /* check key (private key(wif)) */ @@ -2105,7 +2118,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, node->data_len = EC_PRIVATE_KEY_LEN; node->kind = KIND_PRIVATE_KEY; ctx->features |= (WALLY_MS_IS_PRIVATE | WALLY_MS_IS_RAW); - ++ctx->num_keys; + ret = ctx_add_key_node(ctx, node); } wally_clear(privkey, sizeof(privkey)); return ret; @@ -2171,7 +2184,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, if (ret == WALLY_OK) { if (flags & WALLY_MINISCRIPT_TAPSCRIPT) node->flags |= NF_IS_XONLY; - ++ctx->num_keys; + ret = ctx_add_key_node(ctx, node); } wally_clear(&extkey, sizeof(extkey)); return ret; @@ -2569,6 +2582,8 @@ static bool is_valid_policy_map(const struct wally_map *map_in) } if (ret == WALLY_OK && keys.num_items != map_in->num_items) ret = WALLY_EINVAL; /* One of more keys is not unique */ + clear_and_free(ctx.keys.items, + ctx.keys.num_items * sizeof(*ctx.keys.items)); wally_map_clear(&keys); return ret == WALLY_OK; } @@ -2599,8 +2614,10 @@ int wally_descriptor_parse(const char *miniscript, ctx->addr_ver = addr_ver; ctx->num_variants = 1; ctx->num_multipaths = 1; - ret = canonicalize(miniscript, vars_in, flags & MS_FLAGS_CANONICALIZE, - &ctx->src, &num_substitutions); + ret = wally_map_init(vars_in ? vars_in->num_items : 1, NULL, &ctx->keys); + if (ret == WALLY_OK) + ret = canonicalize(miniscript, vars_in, flags & MS_FLAGS_CANONICALIZE, + &ctx->src, &num_substitutions); if (ret == WALLY_OK) { ctx->src_len = strlen(ctx->src); ctx->features = WALLY_MS_IS_DESCRIPTOR; /* Un-set if miniscript found */ @@ -2613,7 +2630,7 @@ int wally_descriptor_parse(const char *miniscript, if (ret == WALLY_OK) ret = node_generation_size(ctx->top_node, &ctx->script_len); if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY)) { - if (ctx->num_keys != num_substitutions) + if (ctx->keys.num_items != num_substitutions) ret = WALLY_EINVAL; /* A non-substituted key was present */ else if (ctx->num_variants > 1 || ctx->num_multipaths > 2) ret = WALLY_EINVAL; /* Solved cardinality must be 1 or 2 */ @@ -2828,8 +2845,12 @@ int wally_descriptor_get_features(const struct wally_descriptor *descriptor, int wally_descriptor_get_num_keys(const struct wally_descriptor *descriptor, uint32_t *value_out) { - return descriptor_uint32(descriptor, value_out, - offsetof(struct wally_descriptor, num_keys)); + if (value_out) + *value_out = 0; + if (!descriptor || !value_out) + return WALLY_EINVAL; + *value_out = (uint32_t)descriptor->keys.num_items; + return WALLY_OK; } int wally_descriptor_get_num_variants(const struct wally_descriptor *descriptor, From 625fb62f02972e51883ba8c44ab7eb55f9bd1fe7 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 8 Sep 2023 00:21:07 +1200 Subject: [PATCH 10/25] descriptor: allow iterating keys --- include/wally.hpp | 6 ++++ include/wally_descriptor.h | 42 +++++++++++++++------- src/descriptor.c | 60 ++++++++++++++++++++++++------- src/swig_java/swig.i | 1 + src/test/test_descriptor.py | 30 ++++++++++++++++ src/test/util.py | 1 + src/wasm_package/src/functions.js | 1 + src/wasm_package/src/index.d.ts | 1 + tools/wasm_exports.sh | 1 + 9 files changed, 119 insertions(+), 24 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index dd0746ea4..5e743e5bc 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -504,6 +504,12 @@ inline int descriptor_get_features(const DESCRIPTOR& descriptor, uint32_t* value return ret; } +template +inline int descriptor_get_key(const DESCRIPTOR& descriptor, size_t index, char** output) { + int ret = ::wally_descriptor_get_key(detail::get_p(descriptor), index, output); + return ret; +} + template inline int descriptor_get_network(const DESCRIPTOR& descriptor, uint32_t* value_out) { int ret = ::wally_descriptor_get_network(detail::get_p(descriptor), value_out); diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index 108107a60..b3492af96 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -131,18 +131,6 @@ WALLY_CORE_API int wally_descriptor_get_features( const struct wally_descriptor *descriptor, uint32_t *value_out); -/** - * Get the number of keys in a parsed output descriptor or miniscript expression. - * - * :param descriptor: Parsed output descriptor or miniscript expression. - * :param value_out: Destination for the number of keys. - * -* .. note:: Repeated keys are counted once for each time they appear. - */ -WALLY_CORE_API int wally_descriptor_get_num_keys( - const struct wally_descriptor *descriptor, - uint32_t *value_out); - /** * Get the number of variants in a parsed output descriptor or miniscript expression. * @@ -193,6 +181,36 @@ WALLY_CORE_API int wally_descriptor_get_depth( const struct wally_descriptor *descriptor, uint32_t *value_out); +/** + * Get the number of keys in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param value_out: Destination for the number of keys. + * + * .. note:: Repeated keys are counted once for each time they appear. + */ +WALLY_CORE_API int wally_descriptor_get_num_keys( + const struct wally_descriptor *descriptor, + uint32_t *value_out); + +/** + * Get the string representation of a key in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key to get. + * :param output: Destination for the resulting string representation. + *| The string returned should be freed using `wally_free_string`. + * + * .. note:: Keys may be BIP32 xpub/xpriv, WIF or hex pubkeys. The caller + *| can use `wally_descriptor_get_features` to determine which key types + *| are present. + * + */ +WALLY_CORE_API int wally_descriptor_get_key( + const struct wally_descriptor *descriptor, + size_t index, + char **output); + /** * Get the maximum length of a script corresponding to an output descriptor. * diff --git a/src/descriptor.c b/src/descriptor.c index e8963cf14..5c4b6d3cf 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -206,7 +206,7 @@ static int ctx_add_key_node(ms_ctx *ctx, ms_node *node) { const char *v = (char *)node; return map_add(&ctx->keys, NULL, ctx->keys.num_items, - (unsigned char *)v, 1, false, false); + (unsigned char *)v, 1, true, false); } /* Built-in miniscript expressions */ @@ -2842,17 +2842,6 @@ int wally_descriptor_get_features(const struct wally_descriptor *descriptor, offsetof(struct wally_descriptor, features)); } -int wally_descriptor_get_num_keys(const struct wally_descriptor *descriptor, - uint32_t *value_out) -{ - if (value_out) - *value_out = 0; - if (!descriptor || !value_out) - return WALLY_EINVAL; - *value_out = (uint32_t)descriptor->keys.num_items; - return WALLY_OK; -} - int wally_descriptor_get_num_variants(const struct wally_descriptor *descriptor, uint32_t *value_out) { @@ -2889,3 +2878,50 @@ int wally_descriptor_get_depth(const struct wally_descriptor *descriptor, *value_out = node_get_depth(descriptor->top_node) - 1; return WALLY_OK; } + +int wally_descriptor_get_num_keys(const struct wally_descriptor *descriptor, + uint32_t *value_out) +{ + if (value_out) + *value_out = 0; + if (!descriptor || !value_out) + return WALLY_EINVAL; + *value_out = (uint32_t)descriptor->keys.num_items; + return WALLY_OK; +} + +/* Ignore incorrect warnings from the ms_node cast below */ +#pragma GCC diagnostic ignored "-Wcast-align" +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wcast-align" +#endif + +int wally_descriptor_get_key(const struct wally_descriptor *descriptor, + size_t index, char **output) +{ + const ms_node *node; + + if (output) + *output = 0; + if (!descriptor || index >= descriptor->keys.num_items || !output) + return WALLY_EINVAL; + + node = (ms_node *)descriptor->keys.items[index].value; + if (node->kind == KIND_PUBLIC_KEY) { + return wally_hex_from_bytes((const unsigned char *)node->data, + node->data_len, output); + } + if (node->kind == KIND_PRIVATE_KEY) { + uint32_t flags = node->flags & NF_IS_UNCOMPRESSED ? WALLY_WIF_FLAG_UNCOMPRESSED : 0; + if (!descriptor->addr_ver) + return WALLY_EINVAL; /* Must have a network to fetch private keys */ + return wally_wif_from_bytes((const unsigned char *)node->data, node->data_len, + descriptor->addr_ver->version_wif, + flags, output); + } + if ((node->kind & KIND_BIP32) != KIND_BIP32) + return WALLY_ERROR; /* Unknown key type, should not happen */ + if (!(*output = wally_strdup_n(node->data, node->data_len))) + return WALLY_ENOMEM; + return WALLY_OK; +} diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 04335f93f..2a526b5fc 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -561,6 +561,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_descriptor_get_depth); %returns_size_t(wally_descriptor_get_features); %returns_size_t(wally_descriptor_get_network); +%returns_string(wally_descriptor_get_key); %returns_size_t(wally_descriptor_get_num_keys); %returns_size_t(wally_descriptor_get_num_paths); %returns_size_t(wally_descriptor_get_num_variants); diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index be2b1a029..7081f38ce 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -314,5 +314,35 @@ def make_keys(xpubs): self.assertEqual(ret, WALLY_EINVAL) wally_map_free(keys) + def test_key_iteration(self): + """Test iterating descriptor keys""" + k1 = 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB' + k2 = 'xprvA2YKGLieCs6cWCiczALiH1jzk3VCCS5M1pGQfWPkamCdR9UpBgE2Gb8AKAyVjKHkz8v37avcfRjdcnP19dVAmZrvZQfvTcXXSAiFNQ6tTtU' + wif = 'L1AAHuEC7XuDM7pJ7yHLEqYK1QspMo8n1kgxyZVdgvEpVC1rkUrM' + pk = '03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284' + pk_u = '0414fc03b8df87cd7b872996810db8458d61da8448e531569c8517b469a119d267be5645686309c6e6736dbd93940707cc9143d3cf29f1b877ff340e2cb2d259cf' + # Valid args + for descriptor, expected in [ + # Bip32 xpub + (f'pkh({k1})', k1), + # BIP32 xprv + (f'pkh({k2})', k2), + # WIF + (f'pkh({wif})', wif), + # Hex pubkey, compressed + (f'pk({pk})', pk), + # Hex pubkey, uncompressed + (f'pk({pk_u})', pk_u), + ]: + d = c_void_p() + ret = wally_descriptor_parse(descriptor, None, NETWORK_BTC_MAIN, 0, d) + self.assertEqual(ret, WALLY_OK) + ret, num_keys = wally_descriptor_get_num_keys(d) + self.assertEqual((ret, num_keys), (WALLY_OK, 1)) + ret, key_str = wally_descriptor_get_key(d, 0) + self.assertEqual((ret, key_str), (WALLY_OK, expected)) + wally_descriptor_free(d) + + if __name__ == '__main__': unittest.main() diff --git a/src/test/util.py b/src/test/util.py index 22b354ee5..52bc41ab7 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -313,6 +313,7 @@ class wally_psbt(Structure): ('wally_descriptor_get_checksum', c_int, [c_void_p, c_uint32, c_char_p_p]), ('wally_descriptor_get_depth', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_features', c_int, [c_void_p, c_uint32_p]), + ('wally_descriptor_get_key', c_int, [c_void_p, c_size_t, c_char_p_p]), ('wally_descriptor_get_network', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_keys', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_paths', c_int, [c_void_p, c_uint32_p]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 82bc174cd..9a5f03ffb 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -171,6 +171,7 @@ export const descriptor_free = wrap('wally_descriptor_free', [T.OpaqueRef]); export const descriptor_get_checksum = wrap('wally_descriptor_get_checksum', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); export const descriptor_get_depth = wrap('wally_descriptor_get_depth', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_features = wrap('wally_descriptor_get_features', [T.OpaqueRef, T.DestPtr(T.Int32)]); +export const descriptor_get_key = wrap('wally_descriptor_get_key', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); export const descriptor_get_network = wrap('wally_descriptor_get_network', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_keys = wrap('wally_descriptor_get_num_keys', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_paths = wrap('wally_descriptor_get_num_paths', [T.OpaqueRef, T.DestPtr(T.Int32)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 69bddea94..67ec487c0 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -123,6 +123,7 @@ export function descriptor_free(descriptor: Ref_wally_descriptor): void; export function descriptor_get_checksum(descriptor: Ref_wally_descriptor, flags: number): string; export function descriptor_get_depth(descriptor: Ref_wally_descriptor): number; export function descriptor_get_features(descriptor: Ref_wally_descriptor): number; +export function descriptor_get_key(descriptor: Ref_wally_descriptor, index: number): string; export function descriptor_get_network(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_keys(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_paths(descriptor: Ref_wally_descriptor): number; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 540fa7ea3..c65c05678 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -81,6 +81,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_descriptor_get_checksum' \ ,'_wally_descriptor_get_depth' \ ,'_wally_descriptor_get_features' \ +,'_wally_descriptor_get_key' \ ,'_wally_descriptor_get_network' \ ,'_wally_descriptor_get_num_keys' \ ,'_wally_descriptor_get_num_paths' \ From b28398d5ce225ceab23e4d5956adecdd493f2aa9 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 8 Sep 2023 07:54:01 +1200 Subject: [PATCH 11/25] descriptor: allow fetching child paths from iterated keys --- include/wally.hpp | 12 ++++++++++ include/wally_descriptor.h | 27 +++++++++++++++++++++ src/descriptor.c | 39 ++++++++++++++++++++++++++++--- src/swig_java/swig.i | 2 ++ src/test/test_descriptor.py | 29 +++++++++++++++++------ src/test/util.py | 2 ++ src/wasm_package/src/functions.js | 2 ++ src/wasm_package/src/index.d.ts | 2 ++ tools/wasm_exports.sh | 2 ++ 9 files changed, 107 insertions(+), 10 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index 5e743e5bc..62b76cdd8 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -510,6 +510,18 @@ inline int descriptor_get_key(const DESCRIPTOR& descriptor, size_t index, char** return ret; } +template +inline int descriptor_get_key_child_path_str(const DESCRIPTOR& descriptor, size_t index, char** output) { + int ret = ::wally_descriptor_get_key_child_path_str(detail::get_p(descriptor), index, output); + return ret; +} + +template +inline int descriptor_get_key_child_path_str_len(const DESCRIPTOR& descriptor, size_t index, size_t* written) { + int ret = ::wally_descriptor_get_key_child_path_str_len(detail::get_p(descriptor), index, written); + return ret; +} + template inline int descriptor_get_network(const DESCRIPTOR& descriptor, uint32_t* value_out) { int ret = ::wally_descriptor_get_network(detail::get_p(descriptor), value_out); diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index b3492af96..167fbfe15 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -211,6 +211,33 @@ WALLY_CORE_API int wally_descriptor_get_key( size_t index, char **output); +/** + * Get the length of a keys child path string in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key whose child path to get. + * :param written: Destination for the length of the keys child path string, + *| excluding the NUL terminator. + */ +WALLY_CORE_API int wally_descriptor_get_key_child_path_str_len( + const struct wally_descriptor *descriptor, + size_t index, + size_t *written); + +/** + * Get the keys child path string in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key whose child path to get. + * :param output: Destination for the resulting path string (may be empty). + *| The string returned should be freed using `wally_free_string`. + */ +WALLY_CORE_API int wally_descriptor_get_key_child_path_str( + const struct wally_descriptor *descriptor, + size_t index, + char **output); + + /** * Get the maximum length of a script corresponding to an output descriptor. * diff --git a/src/descriptor.c b/src/descriptor.c index 5c4b6d3cf..f1b0c93ae 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2895,18 +2895,24 @@ int wally_descriptor_get_num_keys(const struct wally_descriptor *descriptor, #if defined(__clang__) #pragma clang diagnostic ignored "-Wcast-align" #endif +static const ms_node *descriptor_get_key(const struct wally_descriptor *descriptor, + size_t index) +{ + if (!descriptor || index >= descriptor->keys.num_items) + return NULL; + return (ms_node *)descriptor->keys.items[index].value; +} int wally_descriptor_get_key(const struct wally_descriptor *descriptor, size_t index, char **output) { - const ms_node *node; + const ms_node *node = descriptor_get_key(descriptor, index); if (output) *output = 0; - if (!descriptor || index >= descriptor->keys.num_items || !output) + if (!node || !output) return WALLY_EINVAL; - node = (ms_node *)descriptor->keys.items[index].value; if (node->kind == KIND_PUBLIC_KEY) { return wally_hex_from_bytes((const unsigned char *)node->data, node->data_len, output); @@ -2925,3 +2931,30 @@ int wally_descriptor_get_key(const struct wally_descriptor *descriptor, return WALLY_ENOMEM; return WALLY_OK; } + +int wally_descriptor_get_key_child_path_str_len( + const struct wally_descriptor *descriptor, size_t index, size_t *written) +{ + const ms_node *node = descriptor_get_key(descriptor, index); + + if (written) + *written = 0; + if (!node || !written) + return WALLY_EINVAL; + *written = node->child_path_len; + return WALLY_OK; +} + +int wally_descriptor_get_key_child_path_str( + const struct wally_descriptor *descriptor, size_t index, char **output) +{ + const ms_node *node = descriptor_get_key(descriptor, index); + + if (output) + *output = 0; + if (!node || !output) + return WALLY_EINVAL; + if (!(*output = wally_strdup_n(node->child_path, node->child_path_len))) + return WALLY_ENOMEM; + return WALLY_OK; +} diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 2a526b5fc..9e83ec2bb 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -562,6 +562,8 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_descriptor_get_features); %returns_size_t(wally_descriptor_get_network); %returns_string(wally_descriptor_get_key); +%returns_string(wally_descriptor_get_key_child_path_str); +%returns_size_t(wally_descriptor_get_key_child_path_str_len); %returns_size_t(wally_descriptor_get_num_keys); %returns_size_t(wally_descriptor_get_num_paths); %returns_size_t(wally_descriptor_get_num_variants); diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index 7081f38ce..e83c21bcd 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -321,26 +321,41 @@ def test_key_iteration(self): wif = 'L1AAHuEC7XuDM7pJ7yHLEqYK1QspMo8n1kgxyZVdgvEpVC1rkUrM' pk = '03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284' pk_u = '0414fc03b8df87cd7b872996810db8458d61da8448e531569c8517b469a119d267be5645686309c6e6736dbd93940707cc9143d3cf29f1b877ff340e2cb2d259cf' + policy_keys = wally_map_from_dict({f'@{i}': xpub for i,xpub in enumerate([k1])}) + P = POLICY + # Valid args - for descriptor, expected in [ + for flags, descriptor, expected, child_path in [ # Bip32 xpub - (f'pkh({k1})', k1), + (0, f'pkh({k1})', k1, ''), + (0, f'pkh({k1}/*)', k1, '*'), + (0, f'pkh({k1}/0/1/2/*)', k1, '0/1/2/*'), + (0, f'pkh({k1}/<0;1>/*)', k1, '<0;1>/*'), + # Bip32 xpub (as policy) + (P, 'pkh(@0/*)', k1, '*'), + (P, 'pkh(@0/**)', k1, '<0;1>/*'), + (P, 'pkh(@0/<0;1>/*)', k1, '<0;1>/*'), # BIP32 xprv - (f'pkh({k2})', k2), + (0, f'pkh({k2})', k2, ''), # WIF - (f'pkh({wif})', wif), + (0, f'pkh({wif})', wif, ''), # Hex pubkey, compressed - (f'pk({pk})', pk), + (0, f'pk({pk})', pk, ''), # Hex pubkey, uncompressed - (f'pk({pk_u})', pk_u), + (0, f'pk({pk_u})', pk_u, ''), ]: d = c_void_p() - ret = wally_descriptor_parse(descriptor, None, NETWORK_BTC_MAIN, 0, d) + keys = policy_keys if flags & P else None + ret = wally_descriptor_parse(descriptor, keys, NETWORK_BTC_MAIN, flags, d) self.assertEqual(ret, WALLY_OK) ret, num_keys = wally_descriptor_get_num_keys(d) self.assertEqual((ret, num_keys), (WALLY_OK, 1)) ret, key_str = wally_descriptor_get_key(d, 0) self.assertEqual((ret, key_str), (WALLY_OK, expected)) + ret, path_len = wally_descriptor_get_key_child_path_str_len(d, 0) + self.assertEqual((ret, path_len), (WALLY_OK, len(child_path))) + ret, path_str = wally_descriptor_get_key_child_path_str(d, 0) + self.assertEqual((ret, path_str), (WALLY_OK, child_path)) wally_descriptor_free(d) diff --git a/src/test/util.py b/src/test/util.py index 52bc41ab7..de28f5dfe 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -314,6 +314,8 @@ class wally_psbt(Structure): ('wally_descriptor_get_depth', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_features', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_key', c_int, [c_void_p, c_size_t, c_char_p_p]), + ('wally_descriptor_get_key_child_path_str', c_int, [c_void_p, c_size_t, c_char_p_p]), + ('wally_descriptor_get_key_child_path_str_len', c_int, [c_void_p, c_size_t, c_size_t_p]), ('wally_descriptor_get_network', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_keys', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_paths', c_int, [c_void_p, c_uint32_p]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 9a5f03ffb..fed2117e8 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -172,6 +172,7 @@ export const descriptor_get_checksum = wrap('wally_descriptor_get_checksum', [T. export const descriptor_get_depth = wrap('wally_descriptor_get_depth', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_features = wrap('wally_descriptor_get_features', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_key = wrap('wally_descriptor_get_key', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); +export const descriptor_get_key_child_path_str_len = wrap('wally_descriptor_get_key_child_path_str_len', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const descriptor_get_network = wrap('wally_descriptor_get_network', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_keys = wrap('wally_descriptor_get_num_keys', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_paths = wrap('wally_descriptor_get_num_paths', [T.OpaqueRef, T.DestPtr(T.Int32)]); @@ -773,6 +774,7 @@ export const asset_surjectionproof = wrap('wally_asset_surjectionproof', [T.Byte export const base58_n_to_bytes = wrap('wally_base58_n_to_bytes', [T.String, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, base58_n_to_bytes_len, true)]); export const base58_to_bytes = wrap('wally_base58_to_bytes', [T.String, T.Int32, T.DestPtrVarLen(T.Bytes, base58_to_bytes_len, true)]); export const base64_to_bytes = wrap('wally_base64_to_bytes', [T.String, T.Int32, T.DestPtrVarLen(T.Bytes, base64_get_maximum_length, true)]); +export const descriptor_get_key_child_path_str = wrap('wally_descriptor_get_key_child_path_str', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); export const descriptor_to_script = wrap('wally_descriptor_to_script', [T.OpaqueRef, T.Int32, T.Int32, T.Int32, T.Int32, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, descriptor_to_script_get_maximum_length, true)]); export const ec_sig_from_bytes = wrap('wally_ec_sig_from_bytes', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, ec_sig_from_bytes_len, false)]); export const ec_sig_from_bytes_aux = wrap('wally_ec_sig_from_bytes_aux', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, ec_sig_from_bytes_aux_len, false)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 67ec487c0..09f0bb035 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -124,6 +124,7 @@ export function descriptor_get_checksum(descriptor: Ref_wally_descriptor, flags: export function descriptor_get_depth(descriptor: Ref_wally_descriptor): number; export function descriptor_get_features(descriptor: Ref_wally_descriptor): number; export function descriptor_get_key(descriptor: Ref_wally_descriptor, index: number): string; +export function descriptor_get_key_child_path_str_len(descriptor: Ref_wally_descriptor, index: number): number; export function descriptor_get_network(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_keys(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_paths(descriptor: Ref_wally_descriptor): number; @@ -725,6 +726,7 @@ export function asset_surjectionproof(output_asset: Buffer|Uint8Array, output_ab export function base58_n_to_bytes(str_in: string, str_len: number, flags: number): Buffer; export function base58_to_bytes(str_in: string, flags: number): Buffer; export function base64_to_bytes(str_in: string, flags: number): Buffer; +export function descriptor_get_key_child_path_str(descriptor: Ref_wally_descriptor, index: number): string; export function descriptor_to_script(descriptor: Ref_wally_descriptor, depth: number, index: number, variant: number, multi_index: number, child_num: number, flags: number): Buffer; export function ec_sig_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; export function ec_sig_from_bytes_aux(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, aux_rand: Buffer|Uint8Array, flags: number): Buffer; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index c65c05678..cc815d078 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -82,6 +82,8 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_descriptor_get_depth' \ ,'_wally_descriptor_get_features' \ ,'_wally_descriptor_get_key' \ +,'_wally_descriptor_get_key_child_path_str' \ +,'_wally_descriptor_get_key_child_path_str_len' \ ,'_wally_descriptor_get_network' \ ,'_wally_descriptor_get_num_keys' \ ,'_wally_descriptor_get_num_paths' \ From 2656858c93a45867c38474fef345c4a90f9b4648 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 8 Sep 2023 08:39:13 +1200 Subject: [PATCH 12/25] descriptor: expose whether x-only keys are present in features Also align and re-use the context feature flags for node features (so we can later expose the node features to callers). --- include/wally_descriptor.h | 1 + src/descriptor.c | 51 +++++++++++++++++------------------ src/wasm_package/src/const.js | 1 + 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index 167fbfe15..1cabdf3e6 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -26,6 +26,7 @@ struct wally_descriptor; #define WALLY_MS_IS_UNCOMPRESSED 0x08 /** Contains at least one uncompressed key */ #define WALLY_MS_IS_RAW 0x10 /** Contains at least one raw key */ #define WALLY_MS_IS_DESCRIPTOR 0x20 /** Contains only descriptor expressions (no miniscript) */ +#define WALLY_MS_IS_X_ONLY 0x40 /** Contains at least one x-only key */ /*** ms-canonicalization-flags Miniscript/Descriptor canonicalization flags */ #define WALLY_MS_CANONICAL_NO_CHECKSUM 0x01 /** Do not include a checksum */ diff --git a/src/descriptor.c b/src/descriptor.c index f1b0c93ae..f7f385fc9 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -162,11 +162,6 @@ static const struct addr_ver_t g_address_versions[] = { }, }; -#define NF_IS_UNCOMPRESSED 0x01 -#define NF_IS_XONLY 0x02 -#define NF_IS_RANGED 0x04 -#define NF_IS_MULTI 0x08 - /* A node in a parsed miniscript expression */ typedef struct ms_node_t { struct ms_node_t *next; @@ -181,7 +176,7 @@ typedef struct ms_node_t { uint32_t child_path_len; char wrapper_str[12]; unsigned char builtin; - unsigned char flags; /* NF_ flags */ + unsigned char flags; /* WALLY_MS_IS_ flags */ } ms_node; typedef struct wally_descriptor { @@ -526,7 +521,7 @@ static bool node_has_uncompressed_key(const ms_ctx *ctx, const ms_node *node) if (ctx->features & WALLY_MS_IS_UNCOMPRESSED) { const ms_node *child; for (child = node->child; child; child = child->next) - if ((child->flags & NF_IS_UNCOMPRESSED) || node_has_uncompressed_key(ctx, child)) + if ((child->flags & WALLY_MS_IS_UNCOMPRESSED) || node_has_uncompressed_key(ctx, child)) return true; } return false; @@ -1148,7 +1143,7 @@ static int generate_pk_h(ms_ctx *ctx, ms_node *node, if (script_len >= WALLY_SCRIPTPUBKEY_P2PKH_LEN - 1) { ret = generate_pk_k(ctx, node, buff+3, sizeof(buff)-3, written); if (ret == WALLY_OK) { - if (node->child->flags & NF_IS_XONLY) + if (node->child->flags & WALLY_MS_IS_X_ONLY) return WALLY_EINVAL; script[0] = OP_DUP; script[1] = OP_HASH160; @@ -1902,13 +1897,13 @@ static int generate_script(ms_ctx *ctx, ms_node *node, ret = wally_ec_public_key_from_private_key((const unsigned char*)node->data, node->data_len, pubkey, sizeof(pubkey)); if (ret == WALLY_OK) { - if (node->flags & NF_IS_UNCOMPRESSED) { + if (node->flags & WALLY_MS_IS_UNCOMPRESSED) { output_len = EC_PUBLIC_KEY_UNCOMPRESSED_LEN; if (output_len <= script_len) ret = wally_ec_public_key_decompress(pubkey, sizeof(pubkey), script, EC_PUBLIC_KEY_UNCOMPRESSED_LEN); } else { - if (node->flags & NF_IS_XONLY) { + if (node->flags & WALLY_MS_IS_X_ONLY) { output_len = EC_XONLY_PUBLIC_KEY_LEN; if (output_len <= script_len) memcpy(script, &pubkey[1], EC_XONLY_PUBLIC_KEY_LEN); @@ -1920,7 +1915,7 @@ static int generate_script(ms_ctx *ctx, ms_node *node, } } } else if ((node->kind & KIND_BIP32) == KIND_BIP32) { - output_len = node->flags & NF_IS_XONLY ? EC_XONLY_PUBLIC_KEY_LEN : EC_PUBLIC_KEY_LEN; + output_len = node->flags & WALLY_MS_IS_X_ONLY ? EC_XONLY_PUBLIC_KEY_LEN : EC_PUBLIC_KEY_LEN; if (output_len > script_len) { ret = WALLY_OK; /* Return required length without writing */ } else { @@ -1934,8 +1929,8 @@ static int generate_script(ms_ctx *ctx, ms_node *node, BIP32_FLAG_STR_MULTIPATH; const uint32_t derive_flags = BIP32_FLAG_SKIP_HASH | BIP32_FLAG_KEY_PUBLIC; - const bool is_ranged = node->flags & NF_IS_RANGED; - const bool is_multi = node->flags & NF_IS_MULTI; + const bool is_ranged = node->flags & WALLY_MS_IS_RANGED; + const bool is_multi = node->flags & WALLY_MS_IS_MULTIPATH; struct ext_key derived; ret = bip32_path_from_str_n(node->child_path, node->child_path_len, @@ -1950,7 +1945,7 @@ static int generate_script(ms_ctx *ctx, ms_node *node, memcpy(&master, &derived, sizeof(master)); } if (ret == WALLY_OK) - memcpy(script, master.pub_key + ((node->flags & NF_IS_XONLY) ? 1 : 0), output_len); + memcpy(script, master.pub_key + ((node->flags & WALLY_MS_IS_X_ONLY) ? 1 : 0), output_len); wally_clear(&master, sizeof(master)); } } @@ -2051,11 +2046,13 @@ static int analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, return WALLY_ENOMEM; node->data_len = str_len / 2; if (str_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) { - node->flags |= NF_IS_UNCOMPRESSED; + node->flags |= WALLY_MS_IS_UNCOMPRESSED; ctx->features |= WALLY_MS_IS_UNCOMPRESSED; } - if (str_len == EC_XONLY_PUBLIC_KEY_LEN * 2) - node->flags |= NF_IS_XONLY; + if (str_len == EC_XONLY_PUBLIC_KEY_LEN * 2) { + node->flags |= WALLY_MS_IS_X_ONLY; + ctx->features |= WALLY_MS_IS_X_ONLY; + } node->kind = KIND_PUBLIC_KEY; ctx->features |= WALLY_MS_IS_RAW; *is_hex = true; @@ -2104,13 +2101,13 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, if (privkey_len == EC_PRIVATE_KEY_LEN + 1) { if (flags & WALLY_MINISCRIPT_TAPSCRIPT) return WALLY_EINVAL; /* Tapscript only allows x-only keys */ - node->flags |= NF_IS_UNCOMPRESSED; + node->flags |= WALLY_MS_IS_UNCOMPRESSED; ctx->features |= WALLY_MS_IS_UNCOMPRESSED; } else if (privkey_len != EC_PRIVATE_KEY_LEN + 2 || privkey[EC_PRIVATE_KEY_LEN + 1] != 1) return WALLY_EINVAL; /* Unknown WIF format */ - node->flags |= (flags & WALLY_MINISCRIPT_TAPSCRIPT) ? NF_IS_XONLY : 0; + node->flags |= (flags & WALLY_MINISCRIPT_TAPSCRIPT) ? WALLY_MS_IS_X_ONLY : 0; ret = wally_ec_private_key_verify(&privkey[1], EC_PRIVATE_KEY_LEN); if (ret == WALLY_OK && !clone_bytes((unsigned char **)&node->data, &privkey[1], EC_PRIVATE_KEY_LEN)) ret = WALLY_EINVAL; @@ -2146,14 +2143,14 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, return WALLY_EINVAL; /* Different multi-path lengths */ ctx->num_multipaths = num_multi; ctx->features |= WALLY_MS_IS_MULTIPATH; - node->flags |= NF_IS_MULTI; + node->flags |= WALLY_MS_IS_MULTIPATH; } if (features & BIP32_PATH_IS_WILDCARD) { wildcard_pos = (features & BIP32_PATH_WILDCARD_MASK) >> BIP32_PATH_WILDCARD_SHIFT; if (wildcard_pos != num_elems - 1) return WALLY_EINVAL; /* Must be the last element */ ctx->features |= WALLY_MS_IS_RANGED; - node->flags |= NF_IS_RANGED; + node->flags |= WALLY_MS_IS_RANGED; } if (num_elems > ctx->max_path_elems) ctx->max_path_elems = num_elems; @@ -2182,8 +2179,10 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, } if (ret == WALLY_OK) { - if (flags & WALLY_MINISCRIPT_TAPSCRIPT) - node->flags |= NF_IS_XONLY; + if (flags & WALLY_MINISCRIPT_TAPSCRIPT) { + node->flags |= WALLY_MS_IS_X_ONLY; + ctx->features |= WALLY_MS_IS_X_ONLY; + } ret = ctx_add_key_node(ctx, node); } wally_clear(&extkey, sizeof(extkey)); @@ -2468,9 +2467,9 @@ static int node_generation_size(const ms_node *node, size_t *total) } else if (node->kind & (KIND_RAW | KIND_ADDRESS) || node->kind == KIND_PUBLIC_KEY) { *total += node->data_len; } else if (node->kind == KIND_PRIVATE_KEY || (node->kind & KIND_BIP32) == KIND_BIP32) { - if (node->flags & NF_IS_UNCOMPRESSED) + if (node->flags & WALLY_MS_IS_UNCOMPRESSED) *total += EC_PUBLIC_KEY_UNCOMPRESSED_LEN; - else if (node->flags & NF_IS_XONLY) + else if (node->flags & WALLY_MS_IS_X_ONLY) *total += EC_XONLY_PUBLIC_KEY_LEN; else *total += EC_PUBLIC_KEY_LEN; @@ -2918,7 +2917,7 @@ int wally_descriptor_get_key(const struct wally_descriptor *descriptor, node->data_len, output); } if (node->kind == KIND_PRIVATE_KEY) { - uint32_t flags = node->flags & NF_IS_UNCOMPRESSED ? WALLY_WIF_FLAG_UNCOMPRESSED : 0; + uint32_t flags = node->flags & WALLY_MS_IS_UNCOMPRESSED ? WALLY_WIF_FLAG_UNCOMPRESSED : 0; if (!descriptor->addr_ver) return WALLY_EINVAL; /* Must have a network to fetch private keys */ return wally_wif_from_bytes((const unsigned char *)node->data, node->data_len, diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index 9f15607d7..efee82b87 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -131,6 +131,7 @@ export const WALLY_MS_IS_PRIVATE = 0x04; /** Contains at least one private key * export const WALLY_MS_IS_RANGED = 0x01; /** Allows key ranges via ``*`` */ export const WALLY_MS_IS_RAW = 0x10; /** Contains at least one raw key */ export const WALLY_MS_IS_UNCOMPRESSED = 0x08; /** Contains at least one uncompressed key */ +export const WALLY_MS_IS_X_ONLY = 0x40; /** Contains at least one x-only key */ export const WALLY_NETWORK_BITCOIN_MAINNET = 0x01; /** Bitcoin mainnet */ export const WALLY_NETWORK_BITCOIN_REGTEST = 0xff ; /** Bitcoin regtest: Behaves as testnet except for segwit */ export const WALLY_NETWORK_BITCOIN_TESTNET = 0x02; /** Bitcoin testnet */ From fa9b984270b5c6b62c815eb0f811efa3cb16b772 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Sun, 10 Sep 2023 20:24:39 +1200 Subject: [PATCH 13/25] descriptor: store all features in the node flags as well as the context --- src/descriptor.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/descriptor.c b/src/descriptor.c index f7f385fc9..064025ee9 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2053,8 +2053,9 @@ static int analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, node->flags |= WALLY_MS_IS_X_ONLY; ctx->features |= WALLY_MS_IS_X_ONLY; } - node->kind = KIND_PUBLIC_KEY; ctx->features |= WALLY_MS_IS_RAW; + node->kind = KIND_PUBLIC_KEY; + node->flags |= WALLY_MS_IS_RAW; *is_hex = true; return ctx_add_key_node(ctx, node); } @@ -2115,6 +2116,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, node->data_len = EC_PRIVATE_KEY_LEN; node->kind = KIND_PRIVATE_KEY; ctx->features |= (WALLY_MS_IS_PRIVATE | WALLY_MS_IS_RAW); + node->flags |= (WALLY_MS_IS_PRIVATE | WALLY_MS_IS_RAW); ret = ctx_add_key_node(ctx, node); } wally_clear(privkey, sizeof(privkey)); @@ -2166,6 +2168,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, if (extkey.priv_key[0] == BIP32_FLAG_KEY_PRIVATE) { node->kind = KIND_BIP32_PRIVATE_KEY; ctx->features |= WALLY_MS_IS_PRIVATE; + node->flags |= WALLY_MS_IS_PRIVATE; } else node->kind = KIND_BIP32_PUBLIC_KEY; From f298a449bb93493c08ac450f9104253e6b048861 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Sun, 10 Sep 2023 20:27:15 +1200 Subject: [PATCH 14/25] descriptor: allow caller to fetch node features for keys This is required e.g. to differentiate x-only keys. --- include/wally.hpp | 6 ++++++ include/wally_descriptor.h | 19 +++++++++++++++---- src/descriptor.c | 13 +++++++++++++ src/swig_java/swig.i | 1 + src/test/test_descriptor.py | 26 ++++++++++++++------------ src/test/util.py | 1 + src/wasm_package/src/functions.js | 1 + src/wasm_package/src/index.d.ts | 1 + tools/wasm_exports.sh | 1 + 9 files changed, 53 insertions(+), 16 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index 62b76cdd8..01ded53e4 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -522,6 +522,12 @@ inline int descriptor_get_key_child_path_str_len(const DESCRIPTOR& descriptor, s return ret; } +template +inline int descriptor_get_key_features(const DESCRIPTOR& descriptor, size_t index, uint32_t* value_out) { + int ret = ::wally_descriptor_get_key_features(detail::get_p(descriptor), index, value_out); + return ret; +} + template inline int descriptor_get_network(const DESCRIPTOR& descriptor, uint32_t* value_out) { int ret = ::wally_descriptor_get_network(detail::get_p(descriptor), value_out); diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index 1cabdf3e6..d583df98a 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -202,16 +202,27 @@ WALLY_CORE_API int wally_descriptor_get_num_keys( * :param output: Destination for the resulting string representation. *| The string returned should be freed using `wally_free_string`. * - * .. note:: Keys may be BIP32 xpub/xpriv, WIF or hex pubkeys. The caller - *| can use `wally_descriptor_get_features` to determine which key types - *| are present. - * + * .. note:: Keys may be BIP32 xpub/xpriv, WIF or hex pubkeys, and may be + *| x-only. The caller can use `wally_descriptor_get_key_features` to + *| determine the type of a given key. */ WALLY_CORE_API int wally_descriptor_get_key( const struct wally_descriptor *descriptor, size_t index, char **output); +/** + * Get the features of a key in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key to get. + * :param value_out: Destination for the resulting :ref:`miniscript-features`. + */ +WALLY_CORE_API int wally_descriptor_get_key_features( + const struct wally_descriptor *descriptor, + size_t index, + uint32_t *value_out); + /** * Get the length of a keys child path string in a parsed output descriptor or miniscript expression. * diff --git a/src/descriptor.c b/src/descriptor.c index 064025ee9..2b11cbe8d 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2934,6 +2934,19 @@ int wally_descriptor_get_key(const struct wally_descriptor *descriptor, return WALLY_OK; } +int wally_descriptor_get_key_features(const struct wally_descriptor *descriptor, + size_t index, uint32_t *value_out) +{ + const ms_node *node = descriptor_get_key(descriptor, index); + + if (value_out) + *value_out = 0; + if (!node || !value_out) + return WALLY_EINVAL; + *value_out = node->flags; + return WALLY_OK; +} + int wally_descriptor_get_key_child_path_str_len( const struct wally_descriptor *descriptor, size_t index, size_t *written) { diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 9e83ec2bb..d7bd9db30 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -564,6 +564,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_string(wally_descriptor_get_key); %returns_string(wally_descriptor_get_key_child_path_str); %returns_size_t(wally_descriptor_get_key_child_path_str_len); +%returns_size_t(wally_descriptor_get_key_features); %returns_size_t(wally_descriptor_get_num_keys); %returns_size_t(wally_descriptor_get_num_paths); %returns_size_t(wally_descriptor_get_num_variants); diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index e83c21bcd..4788ff558 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -325,24 +325,24 @@ def test_key_iteration(self): P = POLICY # Valid args - for flags, descriptor, expected, child_path in [ + for flags, descriptor, expected, child_path, expected_features in [ # Bip32 xpub - (0, f'pkh({k1})', k1, ''), - (0, f'pkh({k1}/*)', k1, '*'), - (0, f'pkh({k1}/0/1/2/*)', k1, '0/1/2/*'), - (0, f'pkh({k1}/<0;1>/*)', k1, '<0;1>/*'), + (0, f'pkh({k1})', k1, '', 0), + (0, f'pkh({k1}/*)', k1, '*', MS_IS_RANGED), + (0, f'pkh({k1}/0/1/2/*)', k1, '0/1/2/*', MS_IS_RANGED), + (0, f'pkh({k1}/<0;1>/*)', k1, '<0;1>/*', MS_IS_RANGED|MS_IS_MULTIPATH), # Bip32 xpub (as policy) - (P, 'pkh(@0/*)', k1, '*'), - (P, 'pkh(@0/**)', k1, '<0;1>/*'), - (P, 'pkh(@0/<0;1>/*)', k1, '<0;1>/*'), + (P, 'pkh(@0/*)', k1, '*', MS_IS_RANGED), + (P, 'pkh(@0/**)', k1, '<0;1>/*', MS_IS_RANGED|MS_IS_MULTIPATH), + (P, 'pkh(@0/<0;1>/*)', k1, '<0;1>/*', MS_IS_RANGED|MS_IS_MULTIPATH), # BIP32 xprv - (0, f'pkh({k2})', k2, ''), + (0, f'pkh({k2})', k2, '', MS_IS_PRIVATE), # WIF - (0, f'pkh({wif})', wif, ''), + (0, f'pkh({wif})', wif, '', MS_IS_RAW|MS_IS_PRIVATE), # Hex pubkey, compressed - (0, f'pk({pk})', pk, ''), + (0, f'pk({pk})', pk, '', MS_IS_RAW), # Hex pubkey, uncompressed - (0, f'pk({pk_u})', pk_u, ''), + (0, f'pk({pk_u})', pk_u, '', MS_IS_RAW|MS_IS_UNCOMPRESSED), ]: d = c_void_p() keys = policy_keys if flags & P else None @@ -356,6 +356,8 @@ def test_key_iteration(self): self.assertEqual((ret, path_len), (WALLY_OK, len(child_path))) ret, path_str = wally_descriptor_get_key_child_path_str(d, 0) self.assertEqual((ret, path_str), (WALLY_OK, child_path)) + ret, features = wally_descriptor_get_key_features(d, 0) + self.assertEqual((ret, features), (WALLY_OK, expected_features)) wally_descriptor_free(d) diff --git a/src/test/util.py b/src/test/util.py index de28f5dfe..6b2c4b88d 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -316,6 +316,7 @@ class wally_psbt(Structure): ('wally_descriptor_get_key', c_int, [c_void_p, c_size_t, c_char_p_p]), ('wally_descriptor_get_key_child_path_str', c_int, [c_void_p, c_size_t, c_char_p_p]), ('wally_descriptor_get_key_child_path_str_len', c_int, [c_void_p, c_size_t, c_size_t_p]), + ('wally_descriptor_get_key_features', c_int, [c_void_p, c_size_t, c_uint32_p]), ('wally_descriptor_get_network', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_keys', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_paths', c_int, [c_void_p, c_uint32_p]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index fed2117e8..a1a5c0aa0 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -173,6 +173,7 @@ export const descriptor_get_depth = wrap('wally_descriptor_get_depth', [T.Opaque export const descriptor_get_features = wrap('wally_descriptor_get_features', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_key = wrap('wally_descriptor_get_key', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); export const descriptor_get_key_child_path_str_len = wrap('wally_descriptor_get_key_child_path_str_len', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); +export const descriptor_get_key_features = wrap('wally_descriptor_get_key_features', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const descriptor_get_network = wrap('wally_descriptor_get_network', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_keys = wrap('wally_descriptor_get_num_keys', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_paths = wrap('wally_descriptor_get_num_paths', [T.OpaqueRef, T.DestPtr(T.Int32)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 09f0bb035..4a0bdba74 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -125,6 +125,7 @@ export function descriptor_get_depth(descriptor: Ref_wally_descriptor): number; export function descriptor_get_features(descriptor: Ref_wally_descriptor): number; export function descriptor_get_key(descriptor: Ref_wally_descriptor, index: number): string; export function descriptor_get_key_child_path_str_len(descriptor: Ref_wally_descriptor, index: number): number; +export function descriptor_get_key_features(descriptor: Ref_wally_descriptor, index: number): number; export function descriptor_get_network(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_keys(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_paths(descriptor: Ref_wally_descriptor): number; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index cc815d078..3b9aa9886 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -84,6 +84,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_descriptor_get_key' \ ,'_wally_descriptor_get_key_child_path_str' \ ,'_wally_descriptor_get_key_child_path_str_len' \ +,'_wally_descriptor_get_key_features' \ ,'_wally_descriptor_get_network' \ ,'_wally_descriptor_get_num_keys' \ ,'_wally_descriptor_get_num_paths' \ From ef7220b0b487e24ec1d2761e722706875fd7d4fd Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Mon, 11 Sep 2023 13:09:26 +1200 Subject: [PATCH 15/25] descriptor-policy: rename flag to disambiguate from miniscript policy --- include/wally_descriptor.h | 2 +- src/ctest/test_descriptor.c | 32 ++++++++++++++++---------------- src/descriptor.c | 14 +++++++------- src/test/test_descriptor.py | 2 +- src/wasm_package/src/const.js | 2 +- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index d583df98a..8f85db941 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -15,7 +15,7 @@ struct wally_descriptor; #define WALLY_MINISCRIPT_TAPSCRIPT 0x01 /** Tapscript, use x-only pubkeys */ #define WALLY_MINISCRIPT_ONLY 0x02 /** Only allow miniscript (not descriptor) expressions */ #define WALLY_MINISCRIPT_REQUIRE_CHECKSUM 0x04 /** Require a checksum to be present */ -#define WALLY_MINISCRIPT_POLICY 0x08 /** Only allow policy @n variable substitution */ +#define WALLY_MINISCRIPT_POLICY_TEMPLATE 0x08 /** Only allow policy templates with @n BIP32 keys */ #define WALLY_MINISCRIPT_DEPTH_MASK 0xffff0000 /** Mask for limiting maximum depth */ #define WALLY_MINISCRIPT_DEPTH_SHIFT 16 /** Shift to convert maximum depth to flags */ diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 7c28f7411..6a0556ef9 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -990,25 +990,25 @@ static const struct descriptor_test { }, { "policy - single asterisk", "pkh(@0/*)", // Becomes "pkh(mainnet_xpub/*)" i.e. the test case above this - WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY_TEMPLATE, "76a914bb57ca9e62c7084081edc68d2cbc9524a523784288ac", "cp8r8rlg" }, { "policy - double asterisk", "pkh(@0/**)", // Becomes "pkh(mainnet_xpub/<0;1>/*)" - WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY_TEMPLATE, "76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac", "ydnzkve4" }, { "policy - multi-path", "pkh(@0/<0;1>/*)", // Becomes "pkh(mainnet_xpub/<0;1>/*)" - WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY_TEMPLATE, "76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac", "ydnzkve4" }, { "policy - previous key reference", "sh(multi(1,@0/**,@1/**,@0/**))", /* For testing: expresssion isn't sensible */ - WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY, + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY_TEMPLATE, "a91415b7de59bd65038744e3214a521d9d4443dc78c287", "xwdj6ucy" }, @@ -1425,57 +1425,57 @@ static const struct descriptor_test { "policy errchk - no key expression", "raw()", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - key with path", "pkh(@0/0/*)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - missing key postfix", "pkh(@0)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - terminal key postfix", "@0", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - missing key number", "@", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - mis-ordered keys", "sh(multi(1,@1/**,@0/**))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - non-policy keys", "sh(multi(1,@0/**,xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - mismatched key cardinalities (1)", "sh(multi(1,@0/**,@1/*))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - mismatched key cardinalities (2)", "sh(multi(1,@0/<0;1>/*,@1/*))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - invalid key cardinality (key path)", "pkh(@0/<0;1;2>/*)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" }, { "policy errchk - invalid key cardinality (variant)", "combo(@0/**)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, - WALLY_MINISCRIPT_POLICY, NULL, "" + WALLY_MINISCRIPT_POLICY_TEMPLATE, NULL, "" } }; @@ -1924,7 +1924,7 @@ static bool check_descriptor_to_script(const struct descriptor_test* test) int expected_ret, ret, len_ret; uint32_t multi_index = 0; uint32_t child_num = test->child_num ? *test->child_num : 0, features; - const bool is_policy = test->flags & WALLY_MINISCRIPT_POLICY; + const bool is_policy = test->flags & WALLY_MINISCRIPT_POLICY_TEMPLATE; const struct wally_map *keys = is_policy ? &g_policy_map : &g_key_map; expected_ret = test->script ? WALLY_OK : WALLY_EINVAL; diff --git a/src/descriptor.c b/src/descriptor.c index 2b11cbe8d..d786c0d80 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -18,9 +18,9 @@ #define MS_FLAGS_ALL (WALLY_MINISCRIPT_TAPSCRIPT | \ WALLY_MINISCRIPT_ONLY | \ WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \ - WALLY_MINISCRIPT_POLICY) + WALLY_MINISCRIPT_POLICY_TEMPLATE) #define MS_FLAGS_CANONICALIZE (WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \ - WALLY_MINISCRIPT_POLICY) + WALLY_MINISCRIPT_POLICY_TEMPLATE) /* Properties and expressions definition */ #define TYPE_NONE 0x00 @@ -388,7 +388,7 @@ static int canonicalize(const char *descriptor, if (!descriptor || (flags & ~MS_FLAGS_CANONICALIZE)) return WALLY_EINVAL; - if (flags & WALLY_MINISCRIPT_POLICY) { + if (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE) { if (!is_valid_policy_map(vars_in)) return WALLY_EINVAL; /* Invalid policy variables given */ is_id_start = is_policy_start_char; @@ -421,7 +421,7 @@ static int canonicalize(const char *descriptor, } required_len += item->value_len; ++*num_substitutions; - if (flags & WALLY_MINISCRIPT_POLICY) { + if (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE) { int key_index = (int)(item - vars_in->items); if (key_index > key_index_hwm + 1) return WALLY_EINVAL; /* Must be ordered with no gaps */ @@ -452,7 +452,7 @@ static int canonicalize(const char *descriptor, if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM)) return WALLY_EINVAL; /* Checksum required but not present */ - if (flags & WALLY_MINISCRIPT_POLICY) { + if (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE) { if (!found_policy_key) return WALLY_EINVAL; /* At least one key expression must be present */ if (found_policy_single && found_policy_multi) @@ -483,7 +483,7 @@ static int canonicalize(const char *descriptor, item = wally_map_get(vars_in, (unsigned char*)start, lookup_len); lookup_len = item ? item->value_len : lookup_len; memcpy(out, item ? (char *)item->value : start, lookup_len); - if (item && flags & WALLY_MINISCRIPT_POLICY) { + if (item && flags & WALLY_MINISCRIPT_POLICY_TEMPLATE) { if (p[1] == '*' && p[2] == '*') { out += lookup_len; lookup_len = strlen("/<0;1>/*"); @@ -2631,7 +2631,7 @@ int wally_descriptor_parse(const char *miniscript, flags, NULL, NULL, &ctx->top_node); if (ret == WALLY_OK) ret = node_generation_size(ctx->top_node, &ctx->script_len); - if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY)) { + if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE)) { if (ctx->keys.num_items != num_substitutions) ret = WALLY_EINVAL; /* A non-substituted key was present */ else if (ctx->num_variants > 1 || ctx->num_multipaths > 2) diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index 4788ff558..99b1084eb 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -13,7 +13,7 @@ MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM -POLICY = 0x08 # WALLY_MINISCRIPT_POLICY +POLICY = 0x08 # WALLY_MINISCRIPT_POLICY_TEMPLATE MS_IS_RANGED = 0x1 MS_IS_MULTIPATH = 0x2 diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index efee82b87..31b18daf1 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -121,7 +121,7 @@ export const WALLY_MAX_OP_RETURN_LEN = 80; /* Maximum length of OP_RETURN data p export const WALLY_MINISCRIPT_DEPTH_MASK = 0xffff0000; /** Mask for limiting maximum depth */ export const WALLY_MINISCRIPT_DEPTH_SHIFT = 16; /** Shift to convert maximum depth to flags */ export const WALLY_MINISCRIPT_ONLY = 0x02; /** Only allow miniscript (not descriptor) expressions */ -export const WALLY_MINISCRIPT_POLICY = 0x08; /** Only allow policy @n variable substitution */ +export const WALLY_MINISCRIPT_POLICY_TEMPLATE = 0x08; /** Only allow policy templates with @n BIP32 keys */ export const WALLY_MINISCRIPT_REQUIRE_CHECKSUM = 0x04; /** Require a checksum to be present */ export const WALLY_MINISCRIPT_TAPSCRIPT = 0x01; /** Tapscript, use x-only pubkeys */ export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */ From 1e3eac7c8bf69343d2562d76f993a5a58abf4d3f Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Tue, 12 Sep 2023 10:05:26 +1200 Subject: [PATCH 16/25] descriptor-policy: pass correctly sized key information map for tests --- src/ctest/test_descriptor.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 6a0556ef9..24e47c218 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -56,11 +56,12 @@ static struct wally_map_item g_policy_map_items[] = { { B("@1"), B("xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU") } }; -static const struct wally_map g_policy_map = { - g_policy_map_items, - NUM_ELEMS(g_policy_map_items), - NUM_ELEMS(g_policy_map_items), - NULL +/* 1 and 2 element key variable maps for policy testing. + * The bip refers to these as "key information vectors". + */ +static const struct wally_map g_policy_maps[2] = { + { g_policy_map_items, 1, 1, NULL }, + { g_policy_map_items, NUM_ELEMS(g_policy_map_items), NUM_ELEMS(g_policy_map_items), NULL } }; static const uint32_t g_miniscript_index_0 = 0; @@ -1925,9 +1926,14 @@ static bool check_descriptor_to_script(const struct descriptor_test* test) uint32_t multi_index = 0; uint32_t child_num = test->child_num ? *test->child_num : 0, features; const bool is_policy = test->flags & WALLY_MINISCRIPT_POLICY_TEMPLATE; - const struct wally_map *keys = is_policy ? &g_policy_map : &g_key_map; + const struct wally_map *keys = is_policy ? &g_policy_maps[0] : &g_key_map; expected_ret = test->script ? WALLY_OK : WALLY_EINVAL; + if (is_policy && expected_ret == WALLY_OK) { + const char *p = strchr(test->descriptor, '@'); + if (p && strchr(p + 1, '@')) + keys = &g_policy_maps[1]; /* 2-Element policy key map */ + } ret = wally_descriptor_parse(test->descriptor, keys, test->network, test->flags, &descriptor); From d705b70d0be88304dac0392daad577de2371d3be Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Tue, 12 Sep 2023 10:07:26 +1200 Subject: [PATCH 17/25] 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. --- include/wally_descriptor.h | 1 + src/ctest/test_descriptor.c | 13 ++++-- src/descriptor.c | 79 ++++++++++++++++++++++++++++++++--- src/test/test_descriptor.py | 38 ++++++++++------- src/wasm_package/src/const.js | 1 + 5 files changed, 109 insertions(+), 23 deletions(-) diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index 8f85db941..af01139a8 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -16,6 +16,7 @@ struct wally_descriptor; #define WALLY_MINISCRIPT_ONLY 0x02 /** Only allow miniscript (not descriptor) expressions */ #define WALLY_MINISCRIPT_REQUIRE_CHECKSUM 0x04 /** Require a checksum to be present */ #define WALLY_MINISCRIPT_POLICY_TEMPLATE 0x08 /** Only allow policy templates with @n BIP32 keys */ +#define WALLY_MINISCRIPT_UNIQUE_KEYPATHS 0x10 /** For policy templates, ensure BIP32 derivation paths differ for identical keys */ #define WALLY_MINISCRIPT_DEPTH_MASK 0xffff0000 /** Mask for limiting maximum depth */ #define WALLY_MINISCRIPT_DEPTH_SHIFT 16 /** Shift to convert maximum depth to flags */ diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 24e47c218..0ceb1da72 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -1008,10 +1008,10 @@ static const struct descriptor_test { "ydnzkve4" }, { "policy - previous key reference", - "sh(multi(1,@0/**,@1/**,@0/**))", /* For testing: expresssion isn't sensible */ + "sh(multi(1,@0/**,@1/**,@0/<2;3>/*))", /* For testing: expresssion isn't sensible */ WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY_TEMPLATE, - "a91415b7de59bd65038744e3214a521d9d4443dc78c287", - "xwdj6ucy" + "a9146cc69bc5443e97b8a30918cb6297ba4efb3d6dc387", + "45w36ywr" }, /* * Misc error cases (code coverage) @@ -1937,6 +1937,13 @@ static bool check_descriptor_to_script(const struct descriptor_test* test) ret = wally_descriptor_parse(test->descriptor, keys, test->network, test->flags, &descriptor); + if (is_policy && expected_ret == WALLY_OK) { + /* Re-parse with strict key checking */ + wally_descriptor_free(descriptor); + ret = wally_descriptor_parse(test->descriptor, keys, test->network, + test->flags | WALLY_MINISCRIPT_UNIQUE_KEYPATHS, + &descriptor); + } if (expected_ret == WALLY_OK || ret == expected_ret) { /* For failure cases, we may fail when generating instead of parsing, * we catch those cases below */ diff --git a/src/descriptor.c b/src/descriptor.c index d786c0d80..dc4b98630 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -18,7 +18,8 @@ #define MS_FLAGS_ALL (WALLY_MINISCRIPT_TAPSCRIPT | \ WALLY_MINISCRIPT_ONLY | \ WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \ - WALLY_MINISCRIPT_POLICY_TEMPLATE) + WALLY_MINISCRIPT_POLICY_TEMPLATE | \ + WALLY_MINISCRIPT_UNIQUE_KEYPATHS) #define MS_FLAGS_CANONICALIZE (WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \ WALLY_MINISCRIPT_POLICY_TEMPLATE) @@ -204,6 +205,8 @@ static int ctx_add_key_node(ms_ctx *ctx, ms_node *node) (unsigned char *)v, 1, true, false); } +static int ensure_unique_policy_keys(const ms_ctx *ctx); + /* Built-in miniscript expressions */ typedef int (*node_verify_fn_t)(ms_ctx *ctx, ms_node *node); typedef int (*node_gen_fn_t)(ms_ctx *ctx, ms_node *node, @@ -381,7 +384,7 @@ static int canonicalize(const char *descriptor, int key_index_hwm = -1; const char *p = descriptor, *start; char *out; - bool found_policy_key = false, found_policy_single = false, found_policy_multi = false;; + bool found_policy_single = false, found_policy_multi = false;; *output = NULL; *num_substitutions = 0; @@ -427,7 +430,6 @@ static int canonicalize(const char *descriptor, return WALLY_EINVAL; /* Must be ordered with no gaps */ if (key_index > key_index_hwm) key_index_hwm = key_index; - found_policy_key = true; if (*p++ != '/') return WALLY_EINVAL; ++required_len; @@ -453,10 +455,10 @@ static int canonicalize(const char *descriptor, if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM)) return WALLY_EINVAL; /* Checksum required but not present */ if (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE) { - if (!found_policy_key) - return WALLY_EINVAL; /* At least one key expression must be present */ if (found_policy_single && found_policy_multi) return WALLY_EINVAL; /* Cannot mix cardinality of policy keys */ + if (key_index_hwm == -1 || key_index_hwm != (int)vars_in->num_items - 1) + return WALLY_EINVAL; /* One or more keys wasn't substituted */ } if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1))) return WALLY_ENOMEM; @@ -2633,9 +2635,13 @@ int wally_descriptor_parse(const char *miniscript, ret = node_generation_size(ctx->top_node, &ctx->script_len); if (ret == WALLY_OK && (flags & WALLY_MINISCRIPT_POLICY_TEMPLATE)) { if (ctx->keys.num_items != num_substitutions) - ret = WALLY_EINVAL; /* A non-substituted key was present */ + ret = WALLY_EINVAL; /* non-substituted key in the expression */ + else if (vars_in && ctx->keys.num_items < vars_in->num_items) + ret = WALLY_EINVAL; /* non-substituted key in substitutions */ else if (ctx->num_variants > 1 || ctx->num_multipaths > 2) ret = WALLY_EINVAL; /* Solved cardinality must be 1 or 2 */ + else if (flags & WALLY_MINISCRIPT_UNIQUE_KEYPATHS) + ret = ensure_unique_policy_keys(ctx); } } if (ret != WALLY_OK) { @@ -2973,3 +2979,64 @@ int wally_descriptor_get_key_child_path_str( return WALLY_ENOMEM; return WALLY_OK; } + +static const char *get_multipath_child(const char* p, uint32_t *v) +{ + *v = 0; + if (*p != '<' && *p != ';') + return NULL; + else { + ++p; + while (*p >= '0' && *p <= '9') { + *v *= 10; + *v += (*p++ - '0'); + } + if (*p == '\'' || *p == 'h' || *p == 'H') { + *v |= BIP32_INITIAL_HARDENED_CHILD; + ++p; + } + } + return p; +} + +static int are_keys_overlapped(const ms_ctx *ctx, + const ms_node *lhs, const ms_node *rhs) +{ + const char *p; + uint32_t l1, l2, r1, r2; + + if (lhs->data_len != rhs->data_len || + memcmp(lhs->data, rhs->data, lhs->data_len)) + return WALLY_OK; /* Different root keys */ + if (lhs->child_path_len == rhs->child_path_len && + !memcmp(lhs->child_path, rhs->child_path, lhs->child_path_len)) + return WALLY_EINVAL; /* Identical paths */ + if (!(lhs->flags & WALLY_MS_IS_MULTIPATH)) + return WALLY_OK; /* Non-identical ranged, non-multipath keys */ + if (ctx->max_path_elems != 2 || !(rhs->flags & WALLY_MS_IS_MULTIPATH)) + return WALLY_ERROR; /* Should never happen! */ + /* Check the set of multi-path indices is disjoint */ + if (!(p = get_multipath_child(strchr(lhs->child_path, '<'), &l1)) || + !get_multipath_child(p, &l2) || + !(p = get_multipath_child(strchr(rhs->child_path, '<'), &r1)) || + !get_multipath_child(p, &r2)) + return WALLY_ERROR; /* Should never happen! */ + if (l1 == r1 || l1 == r2 || l2 == r1 || l2 == r2) + return WALLY_EINVAL; /* indices are not disjoint */ + return WALLY_OK; +} + +static int ensure_unique_policy_keys(const ms_ctx *ctx) +{ + size_t i, j; + + for (i = 0; i < ctx->keys.num_items; ++i) { + const ms_node *node = descriptor_get_key(ctx, i); + for (j = i + 1; j < ctx->keys.num_items; ++j) { + int ret = are_keys_overlapped(ctx, node, descriptor_get_key(ctx, j)); + if (ret != WALLY_OK) + return ret; + } + } + return WALLY_OK; +} diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index 99b1084eb..991a046c6 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -10,10 +10,11 @@ NETWORK_LIQUID = 0x03 NETWORK_LIQUID_REG = 0x04 -MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT -MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY -REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM -POLICY = 0x08 # WALLY_MINISCRIPT_POLICY_TEMPLATE +MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT +MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY +REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM +POLICY = 0x08 # WALLY_MINISCRIPT_POLICY_TEMPLATE +UNIQUE_KEYPATHS = 0x10 # WALLY_MINISCRIPT_UNIQUE_KEYPATHS MS_IS_RANGED = 0x1 MS_IS_MULTIPATH = 0x2 @@ -291,26 +292,35 @@ def make_keys(xpubs): keys = {f'@{i}': xpub for i,xpub in enumerate(xpubs)} return wally_map_from_dict(keys) + P, K = POLICY, UNIQUE_KEYPATHS bad_args = [ # Raw pubkey - [POLICY, ['038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048']], + [P, 'pkh(@0/*)', ['038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048']], # Bip32 private key - [POLICY, [xpriv]], + [P, 'pkh(@0/*)', [xpriv]], # Keys must be in the form of @N - [POLICY, {'foo': xpub1}], + [P, 'pkh(@0/*)', {'foo': xpub1}], # Keys must start from 0 - [POLICY, {'@1': xpub1}], + [P, 'pkh(@0/*)', {'@1': xpub1}], # Keys must be successive integers - [POLICY, {'@0': xpub1, '@2': xpub2}], + [P, 'pkh(@0/*)', [xpub1, xpub2]], + # Keys must all be substituted + [P, 'pkh(@0/*)', {'@0': xpub1, '@1': xpub2}], # Keys cannot have child paths - [POLICY, {'@0': f'{xpub1}/0'}], - # Keys must be unique - [POLICY, [xpub1, xpub1]], + [P, 'pkh(@0/*)', {'@0': f'{xpub1}/0'}], + # Keys must be unique in the substitution list (always) + [P, 'sh(multi(1, @0/*,@1/*))', [xpub1, xpub1]], + # Keys must be unique in the final expression (with flag) + [P|K, 'sh(multi(1,@0/*,@0/*))', [xpub1]], + [P|K, 'sh(multi(1,@0/**,@0/**))', [xpub1]], + # Key multi-paths must be disjoint sets + [P|K, 'sh(multi(1,@0/<0;1>/*,@0/<1;2>/*))', [xpub1]], + [P|K, 'sh(multi(1,@0/<1;0>/*,@0/<2;1>/*))', [xpub1]], ] d = c_void_p() - for flags, key_items in bad_args: + for flags, policy, key_items in bad_args: keys = wally_map_from_dict(key_items) if type(key_items) is dict else make_keys(key_items) - ret = wally_descriptor_parse('pkh(@0/*)', keys, NETWORK_BTC_MAIN, POLICY, d) + ret = wally_descriptor_parse(policy, keys, NETWORK_BTC_MAIN, flags, d) self.assertEqual(ret, WALLY_EINVAL) wally_map_free(keys) diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index 31b18daf1..9cea9103a 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -124,6 +124,7 @@ export const WALLY_MINISCRIPT_ONLY = 0x02; /** Only allow miniscript (not descri export const WALLY_MINISCRIPT_POLICY_TEMPLATE = 0x08; /** Only allow policy templates with @n BIP32 keys */ export const WALLY_MINISCRIPT_REQUIRE_CHECKSUM = 0x04; /** Require a checksum to be present */ export const WALLY_MINISCRIPT_TAPSCRIPT = 0x01; /** Tapscript, use x-only pubkeys */ +export const WALLY_MINISCRIPT_UNIQUE_KEYPATHS = 0x10; /** For policy templates, ensure BIP32 derivation paths differ for identical keys */ export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */ export const WALLY_MS_IS_DESCRIPTOR = 0x20; /** Contains only descriptor expressions (no miniscript) */ export const WALLY_MS_IS_MULTIPATH = 0x02; /** Allows multiple paths via ```` */ From ad635b5732747b89fcbd8a235aa4f2fc6fc68735 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Tue, 12 Sep 2023 22:36:12 +1200 Subject: [PATCH 18/25] descriptor: allow fetching key origin info from keys --- include/wally.hpp | 18 ++++++++ include/wally_descriptor.h | 46 +++++++++++++++++++- src/descriptor.c | 58 +++++++++++++++++++++++++- src/swig_java/swig.i | 3 ++ src/swig_python/python_extra.py_in | 1 + src/test/test_descriptor.py | 67 +++++++++++++++++++++--------- src/test/util.py | 3 ++ src/wasm_package/src/const.js | 1 + src/wasm_package/src/functions.js | 3 ++ src/wasm_package/src/index.d.ts | 3 ++ tools/wasm_exports.sh | 3 ++ 11 files changed, 184 insertions(+), 22 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index 01ded53e4..9a196b932 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -528,6 +528,24 @@ inline int descriptor_get_key_features(const DESCRIPTOR& descriptor, size_t inde return ret; } +template +inline int descriptor_get_key_origin_fingerprint(const DESCRIPTOR& descriptor, size_t index, BYTES_OUT& bytes_out) { + int ret = ::wally_descriptor_get_key_origin_fingerprint(detail::get_p(descriptor), index, bytes_out.data(), bytes_out.size()); + return ret; +} + +template +inline int descriptor_get_key_origin_path_str(const DESCRIPTOR& descriptor, size_t index, char** output) { + int ret = ::wally_descriptor_get_key_origin_path_str(detail::get_p(descriptor), index, output); + return ret; +} + +template +inline int descriptor_get_key_origin_path_str_len(const DESCRIPTOR& descriptor, size_t index, size_t* written) { + int ret = ::wally_descriptor_get_key_origin_path_str_len(detail::get_p(descriptor), index, written); + return ret; +} + template inline int descriptor_get_network(const DESCRIPTOR& descriptor, uint32_t* value_out) { int ret = ::wally_descriptor_get_network(detail::get_p(descriptor), value_out); diff --git a/include/wally_descriptor.h b/include/wally_descriptor.h index af01139a8..9d16300c0 100644 --- a/include/wally_descriptor.h +++ b/include/wally_descriptor.h @@ -28,6 +28,7 @@ struct wally_descriptor; #define WALLY_MS_IS_RAW 0x10 /** Contains at least one raw key */ #define WALLY_MS_IS_DESCRIPTOR 0x20 /** Contains only descriptor expressions (no miniscript) */ #define WALLY_MS_IS_X_ONLY 0x40 /** Contains at least one x-only key */ +#define WALLY_MS_IS_PARENTED 0x80 /** Contains at least one key key with a parent key origin */ /*** ms-canonicalization-flags Miniscript/Descriptor canonicalization flags */ #define WALLY_MS_CANONICAL_NO_CHECKSUM 0x01 /** Do not include a checksum */ @@ -230,7 +231,7 @@ WALLY_CORE_API int wally_descriptor_get_key_features( * :param descriptor: Parsed output descriptor or miniscript expression. * :param index: The zero-based index of the key whose child path to get. * :param written: Destination for the length of the keys child path string, - *| excluding the NUL terminator. + *| excluding the NUL terminator (zero if not present). */ WALLY_CORE_API int wally_descriptor_get_key_child_path_str_len( const struct wally_descriptor *descriptor, @@ -242,7 +243,7 @@ WALLY_CORE_API int wally_descriptor_get_key_child_path_str_len( * * :param descriptor: Parsed output descriptor or miniscript expression. * :param index: The zero-based index of the key whose child path to get. - * :param output: Destination for the resulting path string (may be empty). + * :param output: Destination for the resulting path string (empty if not present). *| The string returned should be freed using `wally_free_string`. */ WALLY_CORE_API int wally_descriptor_get_key_child_path_str( @@ -250,6 +251,47 @@ WALLY_CORE_API int wally_descriptor_get_key_child_path_str( size_t index, char **output); +/** + * Get the keys parent BIP32 fingerprint in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key whose parent fingerprint to get. + * :param bytes_out: Destination for the fingerprint. + * FIXED_SIZED_OUTPUT(len, bytes_out, BIP32_KEY_FINGERPRINT_LEN) + * + * If the key does not contain key origin information then `WALLY_EINVAL` is returned. + */ +WALLY_CORE_API int wally_descriptor_get_key_origin_fingerprint( + const struct wally_descriptor *descriptor, + size_t index, + unsigned char *bytes_out, + size_t len); + +/** + * Get the length of a keys parent path string in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key whose parent path to get. + * :param written: Destination for the length of the keys parent path string, + *| excluding the NUL terminator (zero if not present). + */ +WALLY_CORE_API int wally_descriptor_get_key_origin_path_str_len( + const struct wally_descriptor *descriptor, + size_t index, + size_t *written); + +/** + * Get the keys parent path string in a parsed output descriptor or miniscript expression. + * + * :param descriptor: Parsed output descriptor or miniscript expression. + * :param index: The zero-based index of the key whose parent path to get. + * :param output: Destination for the resulting path string (empty if not present). + *| The string returned should be freed using `wally_free_string`. + */ +WALLY_CORE_API int wally_descriptor_get_key_origin_path_str( + const struct wally_descriptor *descriptor, + size_t index, + char **output); /** * Get the maximum length of a script corresponding to an output descriptor. diff --git a/src/descriptor.c b/src/descriptor.c index dc4b98630..8a6f10b0e 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2085,7 +2085,12 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, (node->data[9] != ']' && node->data[9] != '/')) return WALLY_EINVAL; /* Invalid key origin fingerprint */ size = end - node->data + 1; - /* cut parent path */ + /* Store offset and length of any origin info in the number field */ + node->number = (uint64_t)(node->data - ctx->src) << 32u; + node->number |= size; + ctx->features |= WALLY_MS_IS_PARENTED; + node->flags |= WALLY_MS_IS_PARENTED; + /* Remove the key origin info from the key data */ node->data = end + 1; node->data_len -= size; } @@ -2980,6 +2985,57 @@ int wally_descriptor_get_key_child_path_str( return WALLY_OK; } +int wally_descriptor_get_key_origin_fingerprint( + const struct wally_descriptor *descriptor, size_t index, + unsigned char *bytes_out, size_t len) +{ + const ms_node *node = descriptor_get_key(descriptor, index); + const char *fingerprint; + size_t written; + int ret; + + if (!node || !bytes_out || len != BIP32_KEY_FINGERPRINT_LEN || + !(node->flags & WALLY_MS_IS_PARENTED)) + return WALLY_EINVAL; + fingerprint = descriptor->src + (((uint64_t)node->number) >> 32u) + 1; + ret = wally_hex_n_to_bytes(fingerprint, BIP32_KEY_FINGERPRINT_LEN * 2, + bytes_out, len, &written); + return ret == WALLY_OK && written != BIP32_KEY_FINGERPRINT_LEN ? WALLY_EINVAL : ret; +} + +int wally_descriptor_get_key_origin_path_str_len( + const struct wally_descriptor *descriptor, size_t index, size_t *written) +{ + const ms_node *node = descriptor_get_key(descriptor, index); + + if (written) + *written = 0; + if (!node || !written) + return WALLY_EINVAL; + *written = node->flags & WALLY_MS_IS_PARENTED ? node->number & 0xffffffff : 0; + *written = *written < 11u ? 0 : *written - 11u; + return WALLY_OK; +} + +int wally_descriptor_get_key_origin_path_str( + const struct wally_descriptor *descriptor, size_t index, char **output) +{ + const ms_node *node = descriptor_get_key(descriptor, index); + const char *path; + size_t path_len; + + if (output) + *output = NULL; + if (!node || !output) + return WALLY_EINVAL; + path_len = node->flags & WALLY_MS_IS_PARENTED ? node->number & 0xffffffff : 0; + path_len = path_len < 11u ? 0 : path_len - 11u; + path = descriptor->src + (((uint64_t)node->number) >> 32u) + 10u; + if (!(*output = wally_strdup_n(path, path_len))) + return WALLY_ENOMEM; + return WALLY_OK; +} + static const char *get_multipath_child(const char* p, uint32_t *v) { *v = 0; diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index d7bd9db30..93962678a 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -565,6 +565,9 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_string(wally_descriptor_get_key_child_path_str); %returns_size_t(wally_descriptor_get_key_child_path_str_len); %returns_size_t(wally_descriptor_get_key_features); +%returns_array_(wally_descriptor_get_key_origin_fingerprint, 3, 4, BIP32_KEY_FINGERPRINT_LEN); +%returns_string(wally_descriptor_get_key_origin_path_str); +%returns_size_t(wally_descriptor_get_key_origin_path_str_len); %returns_size_t(wally_descriptor_get_num_keys); %returns_size_t(wally_descriptor_get_num_paths); %returns_size_t(wally_descriptor_get_num_variants); diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index 26d2894b6..2948b3de2 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -150,6 +150,7 @@ bip38_to_private_key = _wrap_bin(bip38_to_private_key, EC_PRIVATE_KEY_LEN) bip39_mnemonic_to_bytes = _wrap_bin(bip39_mnemonic_to_bytes, BIP39_ENTROPY_MAX_LEN, resize=True) bip39_mnemonic_to_seed512 = _wrap_bin(bip39_mnemonic_to_seed512, BIP39_SEED_LEN_512) bip85_get_bip39_entropy = _wrap_bin(bip85_get_bip39_entropy, HMAC_SHA512_LEN, resize=True) +descriptor_get_key_origin_fingerprint = _wrap_bin(descriptor_get_key_origin_fingerprint, BIP32_KEY_FINGERPRINT_LEN) descriptor_to_script = _wrap_bin(descriptor_to_script, descriptor_to_script_get_maximum_length, resize=True) ec_private_key_bip341_tweak = _wrap_bin(ec_private_key_bip341_tweak, EC_PRIVATE_KEY_LEN) ec_public_key_bip341_tweak = _wrap_bin(ec_public_key_bip341_tweak, EC_PUBLIC_KEY_LEN) diff --git a/src/test/test_descriptor.py b/src/test/test_descriptor.py index 991a046c6..3d92bbd48 100644 --- a/src/test/test_descriptor.py +++ b/src/test/test_descriptor.py @@ -16,12 +16,14 @@ POLICY = 0x08 # WALLY_MINISCRIPT_POLICY_TEMPLATE UNIQUE_KEYPATHS = 0x10 # WALLY_MINISCRIPT_UNIQUE_KEYPATHS -MS_IS_RANGED = 0x1 -MS_IS_MULTIPATH = 0x2 -MS_IS_PRIVATE = 0x4 +MS_IS_RANGED = 0x1 +MS_IS_MULTIPATH = 0x2 +MS_IS_PRIVATE = 0x4 MS_IS_UNCOMPRESSED = 0x08 -MS_IS_RAW = 0x010 -MS_IS_DESCRIPTOR = 0x20 +MS_IS_RAW = 0x010 +MS_IS_DESCRIPTOR = 0x20 +MS_IS_X_ONLY = 0x40 +MS_IS_PARENTED = 0x80 NO_CHECKSUM = 0x1 # WALLY_MS_CANONICAL_NO_CHECKSUM @@ -326,12 +328,16 @@ def make_keys(xpubs): def test_key_iteration(self): """Test iterating descriptor keys""" + origin_fp = 'd34db33f' + origin_path = "44'/0'/0'" + origin = f'[{origin_fp}/{origin_path}]' k1 = 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB' k2 = 'xprvA2YKGLieCs6cWCiczALiH1jzk3VCCS5M1pGQfWPkamCdR9UpBgE2Gb8AKAyVjKHkz8v37avcfRjdcnP19dVAmZrvZQfvTcXXSAiFNQ6tTtU' wif = 'L1AAHuEC7XuDM7pJ7yHLEqYK1QspMo8n1kgxyZVdgvEpVC1rkUrM' pk = '03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284' pk_u = '0414fc03b8df87cd7b872996810db8458d61da8448e531569c8517b469a119d267be5645686309c6e6736dbd93940707cc9143d3cf29f1b877ff340e2cb2d259cf' policy_keys = wally_map_from_dict({f'@{i}': xpub for i,xpub in enumerate([k1])}) + policy_keys_with_origins = wally_map_from_dict({f'@{i}': f'{origin}{xpub}' for i,xpub in enumerate([k1])}) P = POLICY # Valid args @@ -355,20 +361,43 @@ def test_key_iteration(self): (0, f'pk({pk_u})', pk_u, '', MS_IS_RAW|MS_IS_UNCOMPRESSED), ]: d = c_void_p() - keys = policy_keys if flags & P else None - ret = wally_descriptor_parse(descriptor, keys, NETWORK_BTC_MAIN, flags, d) - self.assertEqual(ret, WALLY_OK) - ret, num_keys = wally_descriptor_get_num_keys(d) - self.assertEqual((ret, num_keys), (WALLY_OK, 1)) - ret, key_str = wally_descriptor_get_key(d, 0) - self.assertEqual((ret, key_str), (WALLY_OK, expected)) - ret, path_len = wally_descriptor_get_key_child_path_str_len(d, 0) - self.assertEqual((ret, path_len), (WALLY_OK, len(child_path))) - ret, path_str = wally_descriptor_get_key_child_path_str(d, 0) - self.assertEqual((ret, path_str), (WALLY_OK, child_path)) - ret, features = wally_descriptor_get_key_features(d, 0) - self.assertEqual((ret, features), (WALLY_OK, expected_features)) - wally_descriptor_free(d) + buf, buf_len = make_cbuffer('0' * 8) + + for with_origin in [False, True] if expected == k1 else [False]: + keys = None + if flags & P: + keys = policy_keys_with_origins if with_origin else policy_keys + elif with_origin: + continue + ret = wally_descriptor_parse(descriptor, keys, NETWORK_BTC_MAIN, flags, d) + self.assertEqual(ret, WALLY_OK) + ret, num_keys = wally_descriptor_get_num_keys(d) + self.assertEqual((ret, num_keys), (WALLY_OK, 1)) + ret, key_str = wally_descriptor_get_key(d, 0) + self.assertEqual((ret, key_str), (WALLY_OK, expected)) + ret, path_len = wally_descriptor_get_key_child_path_str_len(d, 0) + self.assertEqual((ret, path_len), (WALLY_OK, len(child_path))) + ret, path_str = wally_descriptor_get_key_child_path_str(d, 0) + self.assertEqual((ret, path_str), (WALLY_OK, child_path)) + ret, features = wally_descriptor_get_key_features(d, 0) + if with_origin: + expected_features |= MS_IS_PARENTED + self.assertEqual((ret, features), (WALLY_OK, expected_features)) + ret = wally_descriptor_get_key_origin_fingerprint(d, 0, buf, buf_len) + # Ensure the key origin matches if present + if with_origin: + self.assertEqual(ret, WALLY_OK) + ret, fp = wally_hex_from_bytes(buf, buf_len) + self.assertEqual((ret, fp), (WALLY_OK, origin_fp)) + else: + self.assertEqual(ret, WALLY_EINVAL) + ret, path_len = wally_descriptor_get_key_origin_path_str_len(d, 0) + expected_len = len(origin_path) if with_origin else 0 + self.assertEqual((ret, path_len), (WALLY_OK, expected_len)) + ret, path_str = wally_descriptor_get_key_origin_path_str(d, 0) + expected_path = origin_path if with_origin else '' + self.assertEqual((ret, path_str), (WALLY_OK, expected_path)) + wally_descriptor_free(d) if __name__ == '__main__': diff --git a/src/test/util.py b/src/test/util.py index 6b2c4b88d..ef0f87047 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -317,6 +317,9 @@ class wally_psbt(Structure): ('wally_descriptor_get_key_child_path_str', c_int, [c_void_p, c_size_t, c_char_p_p]), ('wally_descriptor_get_key_child_path_str_len', c_int, [c_void_p, c_size_t, c_size_t_p]), ('wally_descriptor_get_key_features', c_int, [c_void_p, c_size_t, c_uint32_p]), + ('wally_descriptor_get_key_origin_fingerprint', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), + ('wally_descriptor_get_key_origin_path_str', c_int, [c_void_p, c_size_t, c_char_p_p]), + ('wally_descriptor_get_key_origin_path_str_len', c_int, [c_void_p, c_size_t, c_size_t_p]), ('wally_descriptor_get_network', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_keys', c_int, [c_void_p, c_uint32_p]), ('wally_descriptor_get_num_paths', c_int, [c_void_p, c_uint32_p]), diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index 9cea9103a..8331da509 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -128,6 +128,7 @@ export const WALLY_MINISCRIPT_UNIQUE_KEYPATHS = 0x10; /** For policy templates, export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */ export const WALLY_MS_IS_DESCRIPTOR = 0x20; /** Contains only descriptor expressions (no miniscript) */ export const WALLY_MS_IS_MULTIPATH = 0x02; /** Allows multiple paths via ```` */ +export const WALLY_MS_IS_PARENTED = 0x80; /** Contains at least one key key with a parent key origin */ export const WALLY_MS_IS_PRIVATE = 0x04; /** Contains at least one private key */ export const WALLY_MS_IS_RANGED = 0x01; /** Allows key ranges via ``*`` */ export const WALLY_MS_IS_RAW = 0x10; /** Contains at least one raw key */ diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index a1a5c0aa0..952769abf 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -174,6 +174,8 @@ export const descriptor_get_features = wrap('wally_descriptor_get_features', [T. export const descriptor_get_key = wrap('wally_descriptor_get_key', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); export const descriptor_get_key_child_path_str_len = wrap('wally_descriptor_get_key_child_path_str_len', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const descriptor_get_key_features = wrap('wally_descriptor_get_key_features', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); +export const descriptor_get_key_origin_fingerprint = wrap('wally_descriptor_get_key_origin_fingerprint', [T.OpaqueRef, T.Int32, T.DestPtrSized(T.Bytes, C.BIP32_KEY_FINGERPRINT_LEN)]); +export const descriptor_get_key_origin_path_str_len = wrap('wally_descriptor_get_key_origin_path_str_len', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const descriptor_get_network = wrap('wally_descriptor_get_network', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_keys = wrap('wally_descriptor_get_num_keys', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const descriptor_get_num_paths = wrap('wally_descriptor_get_num_paths', [T.OpaqueRef, T.DestPtr(T.Int32)]); @@ -776,6 +778,7 @@ export const base58_n_to_bytes = wrap('wally_base58_n_to_bytes', [T.String, T.In export const base58_to_bytes = wrap('wally_base58_to_bytes', [T.String, T.Int32, T.DestPtrVarLen(T.Bytes, base58_to_bytes_len, true)]); export const base64_to_bytes = wrap('wally_base64_to_bytes', [T.String, T.Int32, T.DestPtrVarLen(T.Bytes, base64_get_maximum_length, true)]); export const descriptor_get_key_child_path_str = wrap('wally_descriptor_get_key_child_path_str', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); +export const descriptor_get_key_origin_path_str = wrap('wally_descriptor_get_key_origin_path_str', [T.OpaqueRef, T.Int32, T.DestPtrPtr(T.String)]); export const descriptor_to_script = wrap('wally_descriptor_to_script', [T.OpaqueRef, T.Int32, T.Int32, T.Int32, T.Int32, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, descriptor_to_script_get_maximum_length, true)]); export const ec_sig_from_bytes = wrap('wally_ec_sig_from_bytes', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, ec_sig_from_bytes_len, false)]); export const ec_sig_from_bytes_aux = wrap('wally_ec_sig_from_bytes_aux', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, ec_sig_from_bytes_aux_len, false)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 4a0bdba74..4783a0955 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -126,6 +126,8 @@ export function descriptor_get_features(descriptor: Ref_wally_descriptor): numbe export function descriptor_get_key(descriptor: Ref_wally_descriptor, index: number): string; export function descriptor_get_key_child_path_str_len(descriptor: Ref_wally_descriptor, index: number): number; export function descriptor_get_key_features(descriptor: Ref_wally_descriptor, index: number): number; +export function descriptor_get_key_origin_fingerprint(descriptor: Ref_wally_descriptor, index: number): Buffer; +export function descriptor_get_key_origin_path_str_len(descriptor: Ref_wally_descriptor, index: number): number; export function descriptor_get_network(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_keys(descriptor: Ref_wally_descriptor): number; export function descriptor_get_num_paths(descriptor: Ref_wally_descriptor): number; @@ -728,6 +730,7 @@ export function base58_n_to_bytes(str_in: string, str_len: number, flags: number export function base58_to_bytes(str_in: string, flags: number): Buffer; export function base64_to_bytes(str_in: string, flags: number): Buffer; export function descriptor_get_key_child_path_str(descriptor: Ref_wally_descriptor, index: number): string; +export function descriptor_get_key_origin_path_str(descriptor: Ref_wally_descriptor, index: number): string; export function descriptor_to_script(descriptor: Ref_wally_descriptor, depth: number, index: number, variant: number, multi_index: number, child_num: number, flags: number): Buffer; export function ec_sig_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; export function ec_sig_from_bytes_aux(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, aux_rand: Buffer|Uint8Array, flags: number): Buffer; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 3b9aa9886..270abe51e 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -85,6 +85,9 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_descriptor_get_key_child_path_str' \ ,'_wally_descriptor_get_key_child_path_str_len' \ ,'_wally_descriptor_get_key_features' \ +,'_wally_descriptor_get_key_origin_fingerprint' \ +,'_wally_descriptor_get_key_origin_path_str' \ +,'_wally_descriptor_get_key_origin_path_str_len' \ ,'_wally_descriptor_get_network' \ ,'_wally_descriptor_get_num_keys' \ ,'_wally_descriptor_get_num_paths' \ From 5fcce1299229d0451eabecdcaa460db5b7b28863 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 20 Sep 2023 07:57:44 +1200 Subject: [PATCH 19/25] aes: check explicitly for encryption or decryption flags --- src/aes.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aes.c b/src/aes.c index a9a761d85..d8aff6238 100644 --- a/src/aes.c +++ b/src/aes.c @@ -17,7 +17,7 @@ static bool are_valid_args(const unsigned char *key, size_t key_len, { return key && is_valid_key_len(key_len) && (bytes != NULL || (bytes == NULL && bytes_len == 0 && (flags & AES_FLAG_ENCRYPT))) && - (flags & ALL_OPS) != ALL_OPS; + (flags == AES_FLAG_ENCRYPT || flags == AES_FLAG_DECRYPT); } static void aes_enc(AES256_ctx *ctx, @@ -79,7 +79,7 @@ int wally_aes(const unsigned char *key, size_t key_len, if (!are_valid_args(key, key_len, bytes, bytes_len, flags) || len % AES_BLOCK_LEN || !bytes_len || bytes_len % AES_BLOCK_LEN || - flags & ~ALL_OPS || !bytes_out || !len) + !bytes_out || !len) return WALLY_EINVAL; if (flags & AES_FLAG_ENCRYPT) @@ -109,7 +109,7 @@ int wally_aes_cbc(const unsigned char *key, size_t key_len, if (!are_valid_args(key, key_len, bytes, bytes_len, flags) || ((flags & AES_FLAG_ENCRYPT) && (len % AES_BLOCK_LEN)) || ((flags & AES_FLAG_DECRYPT) && (bytes_len % AES_BLOCK_LEN)) || - !iv || iv_len != AES_BLOCK_LEN || flags & ~ALL_OPS || !written) + !iv || iv_len != AES_BLOCK_LEN || !written) return WALLY_EINVAL; blocks = bytes_len / AES_BLOCK_LEN; From b5aed770b9adb02aeb7187e356a434d18b7b0dce Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 20 Sep 2023 08:25:41 +1200 Subject: [PATCH 20/25] aes: provide aes_len and remove manual wrapper hacks for it --- include/wally.hpp | 6 ++++++ include/wally_crypto.h | 20 ++++++++++++++++++++ src/aes.c | 13 +++++++++++++ src/swig_java/swig.i | 1 + src/swig_python/python_extra.py_in | 3 --- src/test/util.py | 1 + src/wasm_package/src/functions.js | 7 ++----- src/wasm_package/src/index.d.ts | 3 ++- tools/build_wrappers.py | 1 - tools/wasm_exports.sh | 1 + 10 files changed, 46 insertions(+), 10 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index 9a196b932..2f258b5cf 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -395,6 +395,12 @@ inline int aes_cbc(const KEY& key, const IV& iv, const BYTES& bytes, uint32_t fl return written || ret != WALLY_OK ? ret : n == static_cast(bytes_out.size()) ? WALLY_OK : WALLY_EINVAL; } +template +inline int aes_len(const KEY& key, const BYTES& bytes, uint32_t flags, size_t* written) { + int ret = ::wally_aes_len(key.data(), key.size(), bytes.data(), bytes.size(), flags, written); + return ret; +} + template inline int base58_from_bytes(const BYTES& bytes, uint32_t flags, char** output) { int ret = ::wally_base58_from_bytes(bytes.data(), bytes.size(), flags, output); diff --git a/include/wally_crypto.h b/include/wally_crypto.h index 8cdcacac4..ff84a3626 100644 --- a/include/wally_crypto.h +++ b/include/wally_crypto.h @@ -46,6 +46,26 @@ WALLY_CORE_API int wally_scrypt( #define AES_FLAG_ENCRYPT 1 /** Encrypt */ #define AES_FLAG_DECRYPT 2 /** Decrypt */ +/** + * Get the length of encrypted/decrypted data using AES (ECB mode, no padding). + * + * :param key: Key material for initialisation. + * :param key_len: Length of ``key`` in bytes. Must be one of the :ref:`aes-key-length`. + * :param bytes: Bytes to encrypt/decrypt. + * :param bytes_len: Length of ``bytes`` in bytes. Must be a multiple of `AES_BLOCK_LEN`. + * :param flags: :ref:`aes-operation-flag` indicating the desired behavior. + * :param written: Destination for the length of the encrypted/decrypted data. + * + * This function returns ``bytes_len`` assuming its arguments are valid. + */ +WALLY_CORE_API int wally_aes_len( + const unsigned char *key, + size_t key_len, + const unsigned char *bytes, + size_t bytes_len, + uint32_t flags, + size_t *written); + /** * Encrypt/decrypt data using AES (ECB mode, no padding). * diff --git a/src/aes.c b/src/aes.c index d8aff6238..0c79047fc 100644 --- a/src/aes.c +++ b/src/aes.c @@ -70,6 +70,19 @@ static void aes_dec(AES256_ctx *ctx, } } +int wally_aes_len(const unsigned char *key, size_t key_len, + const unsigned char *bytes, size_t bytes_len, + uint32_t flags, size_t *written) +{ + if (!written) + *written = 0; + if (!are_valid_args(key, key_len, bytes, bytes_len, flags) || + !bytes_len || bytes_len % AES_BLOCK_LEN || !written) + return WALLY_EINVAL; + *written = bytes_len; + return WALLY_OK; +} + int wally_aes(const unsigned char *key, size_t key_len, const unsigned char *bytes, size_t bytes_len, uint32_t flags, diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 93962678a..31827168f 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -520,6 +520,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_addr_segwit_to_bytes); %returns_size_t(wally_address_to_scriptpubkey); %returns_array_(wally_aes, 6, 7, AES_BLOCK_LEN); +%returns_size_t(wally_aes_len); %returns_size_t(wally_aes_cbc); %returns_array_(wally_asset_final_vbf, 8, 9, ASSET_TAG_LEN); %returns_array_(wally_asset_generator_from_bytes, 5, 6, ASSET_GENERATOR_LEN); diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index 2948b3de2..0c0bf28a8 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -92,9 +92,6 @@ def scriptsig_multisig_from_bytes_len(script, sigs, sighashes, flags): def wif_to_public_key_len(wif, prefix): return EC_PUBLIC_KEY_UNCOMPRESSED_LEN if wif_is_uncompressed(wif) else EC_PUBLIC_KEY_LEN -def aes_len(key, src_bytes, flags): - return len(src_bytes) - def aes_cbc_len(key, iv, src_bytes, flags): bytes_len = len(src_bytes) return (bytes_len // AES_BLOCK_LEN + 1) * AES_BLOCK_LEN diff --git a/src/test/util.py b/src/test/util.py index ef0f87047..345746fcb 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -268,6 +268,7 @@ class wally_psbt(Structure): ('wally_ae_verify', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_aes', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_aes_cbc', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), + ('wally_aes_len', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_size_t_p]), ('wally_asset_blinding_key_from_seed', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), ('wally_asset_blinding_key_to_abf', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_asset_blinding_key_to_abf_vbf', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 952769abf..b0cb7372e 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -9,10 +9,6 @@ const hex_n_to_bytes_len = (_hex, hex_len) => Math.floor(hex_len / 2) const base58_to_bytes_len = (base58, flags) => base58_get_length(base58) const base58_n_to_bytes_len = (base58, n, flags) => base58_n_get_length(base58, n) -const aes_len = (_key, bytes, _flags) => - // ECB mode with no padding - output size is always exactly the same as the input - bytes.length - const aes_cbc_len = (_key, iv, bytes, _flags) => // CBC mode with PKCS#7 padding - output must be padded to the next block size multiply (for the pad length byte) (Math.floor(bytes.length/C.AES_BLOCK_LEN) + 1) * C.AES_BLOCK_LEN @@ -78,8 +74,8 @@ export const ae_host_commit_from_bytes = wrap('wally_ae_host_commit_from_bytes', export const ae_sig_from_bytes = wrap('wally_ae_sig_from_bytes', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.EC_SIGNATURE_LEN)]); export const ae_signer_commit_from_bytes = wrap('wally_ae_signer_commit_from_bytes', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.WALLY_S2C_OPENING_LEN)]); export const ae_verify = wrap('wally_ae_verify', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Int32, T.Bytes]); -export const aes = wrap('wally_aes', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, aes_len, false)]); export const aes_cbc = wrap('wally_aes_cbc', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, aes_cbc_len, true)]); +export const aes_len = wrap('wally_aes_len', [T.Bytes, T.Bytes, T.Int32, T.DestPtr(T.Int32)]); export const asset_blinding_key_from_seed = wrap('wally_asset_blinding_key_from_seed', [T.Bytes, T.DestPtrSized(T.Bytes, C.HMAC_SHA512_LEN)]); export const asset_blinding_key_to_abf = wrap('wally_asset_blinding_key_to_abf', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.BLINDING_FACTOR_LEN)]); export const asset_blinding_key_to_abf_vbf = wrap('wally_asset_blinding_key_to_abf_vbf', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.WALLY_ABF_VBF_LEN)]); @@ -772,6 +768,7 @@ export const witness_p2wpkh_from_der = wrap('wally_witness_p2wpkh_from_der', [T. export const witness_p2wpkh_from_sig = wrap('wally_witness_p2wpkh_from_sig', [T.Bytes, T.Bytes, T.Int32, T.DestPtrPtr(T.OpaqueRef)]); export const witness_program_from_bytes = wrap('wally_witness_program_from_bytes', [T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_WITNESSSCRIPT_MAX_LEN, true)]); export const witness_program_from_bytes_and_version = wrap('wally_witness_program_from_bytes_and_version', [T.Bytes, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_WITNESSSCRIPT_MAX_LEN, true)]); +export const aes = wrap('wally_aes', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, aes_len, false)]); export const asset_pak_whitelistproof = wrap('wally_asset_pak_whitelistproof', [T.Bytes, T.Bytes, T.Int32, T.Bytes, T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, asset_pak_whitelistproof_len, false)]); export const asset_surjectionproof = wrap('wally_asset_surjectionproof', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, asset_surjectionproof_len, false)]); export const base58_n_to_bytes = wrap('wally_base58_n_to_bytes', [T.String, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, base58_n_to_bytes_len, true)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 4783a0955..d6a0edef7 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -30,8 +30,8 @@ export function ae_host_commit_from_bytes(entropy: Buffer|Uint8Array, flags: num export function ae_sig_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, entropy: Buffer|Uint8Array, flags: number): Buffer; export function ae_signer_commit_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, commitment: Buffer|Uint8Array, flags: number): Buffer; export function ae_verify(pub_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, entropy: Buffer|Uint8Array, s2c_opening: Buffer|Uint8Array, flags: number, sig: Buffer|Uint8Array): void; -export function aes(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; export function aes_cbc(key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; +export function aes_len(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): number; export function asset_blinding_key_from_seed(bytes: Buffer|Uint8Array): Buffer; export function asset_blinding_key_to_abf(bytes: Buffer|Uint8Array, hash_prevouts: Buffer|Uint8Array, output_index: number): Buffer; export function asset_blinding_key_to_abf_vbf(bytes: Buffer|Uint8Array, hash_prevouts: Buffer|Uint8Array, output_index: number): Buffer; @@ -724,6 +724,7 @@ export function witness_p2wpkh_from_der(pub_key: Buffer|Uint8Array, sig: Buffer| export function witness_p2wpkh_from_sig(pub_key: Buffer|Uint8Array, sig: Buffer|Uint8Array, sighash: number): Ref_wally_tx_witness_stack; export function witness_program_from_bytes(bytes: Buffer|Uint8Array, flags: number): Buffer; export function witness_program_from_bytes_and_version(bytes: Buffer|Uint8Array, version: number, flags: number): Buffer; +export function aes(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; export function asset_pak_whitelistproof(online_keys: Buffer|Uint8Array, offline_keys: Buffer|Uint8Array, key_index: number, sub_pubkey: Buffer|Uint8Array, online_priv_key: Buffer|Uint8Array, summed_key: Buffer|Uint8Array): Buffer; export function asset_surjectionproof(output_asset: Buffer|Uint8Array, output_abf: Buffer|Uint8Array, output_generator: Buffer|Uint8Array, bytes: Buffer|Uint8Array, asset: Buffer|Uint8Array, abf: Buffer|Uint8Array, generator: Buffer|Uint8Array): Buffer; export function base58_n_to_bytes(str_in: string, str_len: number, flags: number): Buffer; diff --git a/tools/build_wrappers.py b/tools/build_wrappers.py index 7aea69e2f..5f1f9ccb8 100755 --- a/tools/build_wrappers.py +++ b/tools/build_wrappers.py @@ -26,7 +26,6 @@ # The boolean is whether the length function is a maximum length, # True = Yes, False = Exact length MISSING_LEN_FUNCS = { - 'wally_aes': False, 'wally_aes_cbc': True, # is_upper_bound=true only needed for the case of decryption 'wally_base58_to_bytes': True, 'wally_base58_n_to_bytes': True, diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 270abe51e..b090183c9 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -63,6 +63,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_ae_verify' \ ,'_wally_aes' \ ,'_wally_aes_cbc' \ +,'_wally_aes_len' \ ,'_wally_base58_from_bytes' \ ,'_wally_base58_get_length' \ ,'_wally_base58_n_get_length' \ From 28c72d9aaa9da271176140de0d54c67044d81952 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 20 Sep 2023 08:54:46 +1200 Subject: [PATCH 21/25] aes: provide aes_cbc_get_maximum_length and remove manual wrapper hacks for it --- include/wally.hpp | 6 ++++++ include/wally_crypto.h | 22 ++++++++++++++++++++++ src/aes.c | 18 ++++++++++++++++++ src/swig_java/swig.i | 1 + src/swig_python/python_extra.py_in | 6 +----- src/test/util.py | 1 + src/wasm_package/src/functions.js | 7 ++----- src/wasm_package/src/index.d.ts | 3 ++- tools/build_wrappers.py | 1 - tools/wasm_exports.sh | 1 + 10 files changed, 54 insertions(+), 12 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index 2f258b5cf..78659b863 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -395,6 +395,12 @@ inline int aes_cbc(const KEY& key, const IV& iv, const BYTES& bytes, uint32_t fl return written || ret != WALLY_OK ? ret : n == static_cast(bytes_out.size()) ? WALLY_OK : WALLY_EINVAL; } +template +inline int aes_cbc_get_maximum_length(const KEY& key, const IV& iv, const BYTES& bytes, uint32_t flags, size_t* written) { + int ret = ::wally_aes_cbc_get_maximum_length(key.data(), key.size(), iv.data(), iv.size(), bytes.data(), bytes.size(), flags, written); + return ret; +} + template inline int aes_len(const KEY& key, const BYTES& bytes, uint32_t flags, size_t* written) { int ret = ::wally_aes_len(key.data(), key.size(), bytes.data(), bytes.size(), flags, written); diff --git a/include/wally_crypto.h b/include/wally_crypto.h index ff84a3626..91ce9bc1f 100644 --- a/include/wally_crypto.h +++ b/include/wally_crypto.h @@ -86,6 +86,28 @@ WALLY_CORE_API int wally_aes( unsigned char *bytes_out, size_t len); +/** + * Get the maximum length of encrypted/decrypted data using AES (CBC mode, PKCS#7 padding). + * + * :param key: Key material for initialisation. + * :param key_len: Length of ``key`` in bytes. Must be one of the :ref:`aes-key-length`. + * :param iv: Initialisation vector. + * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN`. + * :param bytes: Bytes to encrypt/decrypt. + * :param bytes_len: Length of ``bytes`` in bytes. Can be of any length for encryption, must be a multiple of `AES_BLOCK_LEN` for decryption. + * :param flags: :ref:`aes-operation-flag` indicating the desired behavior. + * :param written: Destination for the maximum length of the encrypted/decrypted data. + */ +WALLY_CORE_API int wally_aes_cbc_get_maximum_length( + const unsigned char *key, + size_t key_len, + const unsigned char *iv, + size_t iv_len, + const unsigned char *bytes, + size_t bytes_len, + uint32_t flags, + size_t *written); + /** * Encrypt/decrypt data using AES (CBC mode, PKCS#7 padding). * diff --git a/src/aes.c b/src/aes.c index 0c79047fc..b7c3c4c79 100644 --- a/src/aes.c +++ b/src/aes.c @@ -104,6 +104,24 @@ int wally_aes(const unsigned char *key, size_t key_len, return WALLY_OK; } +int wally_aes_cbc_get_maximum_length(const unsigned char *key, size_t key_len, + const unsigned char *iv, size_t iv_len, + const unsigned char *bytes, size_t bytes_len, + uint32_t flags, + size_t *written) +{ + if (written) + *written = 0; + + if (!are_valid_args(key, key_len, bytes, bytes_len, flags) || + ((flags & AES_FLAG_DECRYPT) && (bytes_len % AES_BLOCK_LEN)) || + !iv || iv_len != AES_BLOCK_LEN || !written) + return WALLY_EINVAL; + + *written = ((bytes_len / AES_BLOCK_LEN) + 1) * AES_BLOCK_LEN; + return WALLY_OK; +} + int wally_aes_cbc(const unsigned char *key, size_t key_len, const unsigned char *iv, size_t iv_len, const unsigned char *bytes, size_t bytes_len, diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 31827168f..e68a3a6c6 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -522,6 +522,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_array_(wally_aes, 6, 7, AES_BLOCK_LEN); %returns_size_t(wally_aes_len); %returns_size_t(wally_aes_cbc); +%returns_size_t(wally_aes_cbc_get_maximum_length); %returns_array_(wally_asset_final_vbf, 8, 9, ASSET_TAG_LEN); %returns_array_(wally_asset_generator_from_bytes, 5, 6, ASSET_GENERATOR_LEN); %returns_size_t(wally_asset_rangeproof_get_maximum_len); diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index 0c0bf28a8..77c582670 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -92,10 +92,6 @@ def scriptsig_multisig_from_bytes_len(script, sigs, sighashes, flags): def wif_to_public_key_len(wif, prefix): return EC_PUBLIC_KEY_UNCOMPRESSED_LEN if wif_is_uncompressed(wif) else EC_PUBLIC_KEY_LEN -def aes_cbc_len(key, iv, src_bytes, flags): - bytes_len = len(src_bytes) - return (bytes_len // AES_BLOCK_LEN + 1) * AES_BLOCK_LEN - if is_elements_build(): def elements_pegout_script_from_bytes_len(bh, mcs, pk, whl, flags): return elements_pegout_script_size(len(bh), len(mcs), len(pk), len(whl)) @@ -119,7 +115,7 @@ ae_host_commit_from_bytes = _wrap_bin(ae_host_commit_from_bytes, WALLY_HOST_COMM ae_sig_from_bytes = _wrap_bin(ae_sig_from_bytes, EC_SIGNATURE_LEN) ae_signer_commit_from_bytes = _wrap_bin(ae_signer_commit_from_bytes, WALLY_S2C_OPENING_LEN) aes = _wrap_bin(aes, aes_len) -aes_cbc = _wrap_bin(aes_cbc, aes_cbc_len, resize=True) +aes_cbc = _wrap_bin(aes_cbc, aes_cbc_get_maximum_length, resize=True) base58_n_to_bytes = _wrap_bin(base58_n_to_bytes, base58_n_to_bytes_len, resize=True) base58_to_bytes = _wrap_bin(base58_to_bytes, base58_to_bytes_len, resize=True) base64_to_bytes = _wrap_bin(base64_to_bytes, base64_get_maximum_length, resize=True) diff --git a/src/test/util.py b/src/test/util.py index 345746fcb..2c9c332fc 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -268,6 +268,7 @@ class wally_psbt(Structure): ('wally_ae_verify', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_aes', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_aes_cbc', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), + ('wally_aes_cbc_get_maximum_length', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_size_t_p]), ('wally_aes_len', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_size_t_p]), ('wally_asset_blinding_key_from_seed', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), ('wally_asset_blinding_key_to_abf', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index b0cb7372e..46b8d322e 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -9,10 +9,6 @@ const hex_n_to_bytes_len = (_hex, hex_len) => Math.floor(hex_len / 2) const base58_to_bytes_len = (base58, flags) => base58_get_length(base58) const base58_n_to_bytes_len = (base58, n, flags) => base58_n_get_length(base58, n) -const aes_cbc_len = (_key, iv, bytes, _flags) => - // CBC mode with PKCS#7 padding - output must be padded to the next block size multiply (for the pad length byte) - (Math.floor(bytes.length/C.AES_BLOCK_LEN) + 1) * C.AES_BLOCK_LEN - const wif_to_public_key_len = (wif, _prefix) => wif_is_uncompressed(wif) ? C.EC_PUBLIC_KEY_UNCOMPRESSED_LEN : C.EC_PUBLIC_KEY_LEN @@ -74,7 +70,7 @@ export const ae_host_commit_from_bytes = wrap('wally_ae_host_commit_from_bytes', export const ae_sig_from_bytes = wrap('wally_ae_sig_from_bytes', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.EC_SIGNATURE_LEN)]); export const ae_signer_commit_from_bytes = wrap('wally_ae_signer_commit_from_bytes', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.WALLY_S2C_OPENING_LEN)]); export const ae_verify = wrap('wally_ae_verify', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Int32, T.Bytes]); -export const aes_cbc = wrap('wally_aes_cbc', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, aes_cbc_len, true)]); +export const aes_cbc_get_maximum_length = wrap('wally_aes_cbc_get_maximum_length', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtr(T.Int32)]); export const aes_len = wrap('wally_aes_len', [T.Bytes, T.Bytes, T.Int32, T.DestPtr(T.Int32)]); export const asset_blinding_key_from_seed = wrap('wally_asset_blinding_key_from_seed', [T.Bytes, T.DestPtrSized(T.Bytes, C.HMAC_SHA512_LEN)]); export const asset_blinding_key_to_abf = wrap('wally_asset_blinding_key_to_abf', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.BLINDING_FACTOR_LEN)]); @@ -769,6 +765,7 @@ export const witness_p2wpkh_from_sig = wrap('wally_witness_p2wpkh_from_sig', [T. export const witness_program_from_bytes = wrap('wally_witness_program_from_bytes', [T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_WITNESSSCRIPT_MAX_LEN, true)]); export const witness_program_from_bytes_and_version = wrap('wally_witness_program_from_bytes_and_version', [T.Bytes, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_WITNESSSCRIPT_MAX_LEN, true)]); export const aes = wrap('wally_aes', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, aes_len, false)]); +export const aes_cbc = wrap('wally_aes_cbc', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, aes_cbc_get_maximum_length, true)]); export const asset_pak_whitelistproof = wrap('wally_asset_pak_whitelistproof', [T.Bytes, T.Bytes, T.Int32, T.Bytes, T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, asset_pak_whitelistproof_len, false)]); export const asset_surjectionproof = wrap('wally_asset_surjectionproof', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, asset_surjectionproof_len, false)]); export const base58_n_to_bytes = wrap('wally_base58_n_to_bytes', [T.String, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, base58_n_to_bytes_len, true)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index d6a0edef7..313fd04d4 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -30,7 +30,7 @@ export function ae_host_commit_from_bytes(entropy: Buffer|Uint8Array, flags: num export function ae_sig_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, entropy: Buffer|Uint8Array, flags: number): Buffer; export function ae_signer_commit_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, commitment: Buffer|Uint8Array, flags: number): Buffer; export function ae_verify(pub_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, entropy: Buffer|Uint8Array, s2c_opening: Buffer|Uint8Array, flags: number, sig: Buffer|Uint8Array): void; -export function aes_cbc(key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; +export function aes_cbc_get_maximum_length(key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): number; export function aes_len(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): number; export function asset_blinding_key_from_seed(bytes: Buffer|Uint8Array): Buffer; export function asset_blinding_key_to_abf(bytes: Buffer|Uint8Array, hash_prevouts: Buffer|Uint8Array, output_index: number): Buffer; @@ -725,6 +725,7 @@ export function witness_p2wpkh_from_sig(pub_key: Buffer|Uint8Array, sig: Buffer| export function witness_program_from_bytes(bytes: Buffer|Uint8Array, flags: number): Buffer; export function witness_program_from_bytes_and_version(bytes: Buffer|Uint8Array, version: number, flags: number): Buffer; export function aes(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; +export function aes_cbc(key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; export function asset_pak_whitelistproof(online_keys: Buffer|Uint8Array, offline_keys: Buffer|Uint8Array, key_index: number, sub_pubkey: Buffer|Uint8Array, online_priv_key: Buffer|Uint8Array, summed_key: Buffer|Uint8Array): Buffer; export function asset_surjectionproof(output_asset: Buffer|Uint8Array, output_abf: Buffer|Uint8Array, output_generator: Buffer|Uint8Array, bytes: Buffer|Uint8Array, asset: Buffer|Uint8Array, abf: Buffer|Uint8Array, generator: Buffer|Uint8Array): Buffer; export function base58_n_to_bytes(str_in: string, str_len: number, flags: number): Buffer; diff --git a/tools/build_wrappers.py b/tools/build_wrappers.py index 5f1f9ccb8..4c57e20fd 100755 --- a/tools/build_wrappers.py +++ b/tools/build_wrappers.py @@ -26,7 +26,6 @@ # The boolean is whether the length function is a maximum length, # True = Yes, False = Exact length MISSING_LEN_FUNCS = { - 'wally_aes_cbc': True, # is_upper_bound=true only needed for the case of decryption 'wally_base58_to_bytes': True, 'wally_base58_n_to_bytes': True, 'wally_elements_pegin_contract_script_from_bytes': True, diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index b090183c9..963b97d47 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -63,6 +63,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_ae_verify' \ ,'_wally_aes' \ ,'_wally_aes_cbc' \ +,'_wally_aes_cbc_get_maximum_length' \ ,'_wally_aes_len' \ ,'_wally_base58_from_bytes' \ ,'_wally_base58_get_length' \ From de6316dca8a3db7271835edfcafbcccd363662cd Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 20 Sep 2023 12:09:34 +1200 Subject: [PATCH 22/25] python: remove py2 support from SHA test and clean it up. --- src/swig_python/contrib/sha.py | 89 +++++++++++++--------------------- 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/src/swig_python/contrib/sha.py b/src/swig_python/contrib/sha.py index cc897890e..2285602a9 100644 --- a/src/swig_python/contrib/sha.py +++ b/src/swig_python/contrib/sha.py @@ -1,62 +1,39 @@ -"""Tests for shas incl wrong types passed""" -import sys import unittest -from wallycore import * - -b2h = hex_from_bytes -h2b = hex_to_bytes - -class SHA_tests(unittest.TestCase): - - def test_sha256(self): - self.assertEqual(b2h(sha256("This is a test message to hash".encode())), "726ca2c10e9d8b76e5b79f2961c3069a09fdd0a3b9bf8650e091e39b3c6c35be") - - self.assertEqual(b2h(sha256(h2b("3e8379862d658e168c71f083bc05169b3b58ca3212e11c838b08629c5ca48a42"))), "2f7d292595788655c5288b6e1dc698440d9c12559e3bc1e3cc38005a4add132f") - - - def test_sha256d(self): - self.assertEqual(b2h(sha256d("This is a test message to hash".encode())), "29e04e90a1075caaa06573ea701913148d99fb0b7d6928e33f1aabe6032761a0") - - self.assertEqual(b2h(sha256d(h2b("3e8379862d658e168c71f083bc05169b3b58ca3212e11c838b08629c5ca48a42"))), "26e30f19dc2b29d8c220766fd5835d8256c87c32804d19b8307e21d6685c9d3e") - - - def test_sha512(self): - self.assertEqual(b2h(sha512("This is a test message to hash".encode())), "2ed34644ddfcf76ca4de13e4632aa61376fbce813fecc5a043a479daaab17b2f8c3f376468d4637cb2e7c9e2b99ad08b8cb56fe6e724e476826f2aa210872c32") - - self.assertEqual(b2h(sha512(h2b("3e8379862d658e168c71f083bc05169b3b58ca3212e11c838b08629c5ca48a42"))), "d51342efcb114c11045c12f7fede6f9a5fdb11051032bd520a99d79023423f4ac3ab706ce5fa88c0aac46bbbf15bde720cf49eae5be0def3b39e6d3abb29a67b") - - - def _test_wrong_types_py2(self): - # Python2 implicitly converts/decodes - self.assertEqual(b2h(sha256('not bytes')), "b6cb5f25b258630497a18528fb8f73a64034e94e1ead857a8151e3f30a9835ae") - - self.assertEqual(b2h(sha256d('not bytes')), "878eb992aeb736646ecf2c76f562c5d411a487d62ac172d098a83afb023d1b53") - - self.assertEqual(b2h(sha512('not bytes')), "981e82b6ccc079c455cd3fd37b9e04f52f084ffb268a07c47b0447910e2d6280ccbaa5be3f8f062e3e284c98f52039bbddee150a06183ff8d9cb243ef35e3f57") - - - def _test_wrong_types_py3(self): - # Python3 raises TypeError - for shaX in [sha256, sha256d, sha512]: +from wallycore import init, cleanup, sha256, sha256d, sha512 + + +class SHATests(unittest.TestCase): + + def test_sha_functions(self): + """Test python wrappers for SHA hash functions""" + # Ensure empty and valid buffers work correctly + msg1 = 'This is a test message to hash'.encode() + msg2 = bytes.fromhex('3e8379862d658e168c71f083bc05169b3b58ca3212e11c838b08629c5ca48a42') + cases = [ + (sha256, None, 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'), + (sha256, msg1, '726ca2c10e9d8b76e5b79f2961c3069a09fdd0a3b9bf8650e091e39b3c6c35be'), + (sha256, msg2, '2f7d292595788655c5288b6e1dc698440d9c12559e3bc1e3cc38005a4add132f'), + (sha256d, None, '5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456'), + (sha256d, msg1, '29e04e90a1075caaa06573ea701913148d99fb0b7d6928e33f1aabe6032761a0'), + (sha256d, msg2, '26e30f19dc2b29d8c220766fd5835d8256c87c32804d19b8307e21d6685c9d3e'), + (sha512, None, 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce' + '47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e'), + (sha512, msg1, '2ed34644ddfcf76ca4de13e4632aa61376fbce813fecc5a043a479daaab17b2f' + '8c3f376468d4637cb2e7c9e2b99ad08b8cb56fe6e724e476826f2aa210872c32'), + (sha512, msg2, 'd51342efcb114c11045c12f7fede6f9a5fdb11051032bd520a99d79023423f4a' + 'c3ab706ce5fa88c0aac46bbbf15bde720cf49eae5be0def3b39e6d3abb29a67b'), + ] + for sha_fn, src, expected in cases: + result = sha_fn(src) + self.assertEqual(result.hex(), expected) + + # Ensure passing an incorrect type (a) throws and (b) doesn't crash + for sha_fn in [sha256, sha256d, sha512]: with self.assertRaises(TypeError): - shaX('not bytes') - - - def test_wrong_types(self): - if sys.version_info.major < 3: - # Python2 implicitly converts/decodes - self._test_wrong_types_py2() - else: - # Python3 raises TypeError - self._test_wrong_types_py3() - - - def test_pass_none(self): - self.assertEqual(b2h(sha256(None)), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") - self.assertEqual(b2h(sha256d(None)), "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456") - self.assertEqual(b2h(sha512(None)), "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + sha_fn('not bytes') if __name__ == '__main__': + init(0) unittest.main() - + cleanup(0) From 375983c8cd0562592f032531dcf173c8dec0426a Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 20 Sep 2023 13:31:54 +1200 Subject: [PATCH 23/25] python: make the auto-buffer assertion more helpful --- src/swig_python/python_extra.py_in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index 77c582670..9752d1c5d 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -9,7 +9,7 @@ def _wrap_bin(fn, length, resize=False): if resize: # Truncate buf to bytes written if needed. Also assert the # wrapper allocated enough space for the returned value to fit. - assert ret <= n + assert ret <= n, f'{fn.__name__}: {ret} > {n}' return buf[0:ret] if ret != n else buf if ret is not None: assert ret == n From 06ce5e709fdcbc3b2adf474abe2440c8f95cb96c Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Wed, 20 Sep 2023 23:11:28 +1200 Subject: [PATCH 24/25] aes: add aes_cbc_with_ecdh_key for ephemeral request/response encryption --- include/wally.hpp | 13 +++ include/wally_crypto.h | 75 +++++++++++++++++ src/Makefile.am | 1 + src/aes.c | 130 +++++++++++++++++++++++++++++ src/swig_java/swig.i | 2 + src/swig_python/contrib/aes.py | 43 ++++++++++ src/swig_python/python_extra.py_in | 1 + src/test/test_aes.py | 79 ++++++++++++++++++ src/test/util.py | 2 + src/wasm_package/src/functions.js | 2 + src/wasm_package/src/index.d.ts | 2 + tools/wasm_exports.sh | 2 + 12 files changed, 352 insertions(+) create mode 100644 src/swig_python/contrib/aes.py diff --git a/include/wally.hpp b/include/wally.hpp index 78659b863..26524e61f 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -401,6 +401,19 @@ inline int aes_cbc_get_maximum_length(const KEY& key, const IV& iv, const BYTES& return ret; } +template +inline int aes_cbc_with_ecdh_key(const PRIV_KEY& priv_key, const IV& iv, const BYTES& bytes, const PUB_KEY& pub_key, const LABEL& label, uint32_t flags, BYTES_OUT& bytes_out, size_t* written = 0) { + size_t n; + int ret = ::wally_aes_cbc_with_ecdh_key(priv_key.data(), priv_key.size(), iv.data(), iv.size(), bytes.data(), bytes.size(), pub_key.data(), pub_key.size(), label.data(), label.size(), flags, bytes_out.data(), bytes_out.size(), written ? written : &n); + return written || ret != WALLY_OK ? ret : n == static_cast(bytes_out.size()) ? WALLY_OK : WALLY_EINVAL; +} + +template +inline int aes_cbc_with_ecdh_key_get_maximum_length(const PRIV_KEY& priv_key, const IV& iv, const BYTES& bytes, const PUB_KEY& pub_key, const LABEL& label, uint32_t flags, size_t* written) { + int ret = ::wally_aes_cbc_with_ecdh_key_get_maximum_length(priv_key.data(), priv_key.size(), iv.data(), iv.size(), bytes.data(), bytes.size(), pub_key.data(), pub_key.size(), label.data(), label.size(), flags, written); + return ret; +} + template inline int aes_len(const KEY& key, const BYTES& bytes, uint32_t flags, size_t* written) { int ret = ::wally_aes_len(key.data(), key.size(), bytes.data(), bytes.size(), flags, written); diff --git a/include/wally_crypto.h b/include/wally_crypto.h index 91ce9bc1f..7a3fc0c2c 100644 --- a/include/wally_crypto.h +++ b/include/wally_crypto.h @@ -891,6 +891,81 @@ WALLY_CORE_API int wally_s2c_commitment_verify( size_t s2c_opening_len, uint32_t flags); +/** + * Get the maximum length of data encrypted/decrypted using `wally_aes_cbc_with_ecdh_key`. + * + * :param priv_key: The callers private key used for Diffie-Helman exchange. + * :param priv_key_len: The length of ``priv_key`` in bytes. Must be `EC_PRIVATE_KEY_LEN`. + * :param iv: Initialisation vector. Only required when encrypting, otherwise pass NULL. + * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN`. + * :param bytes: Bytes to encrypt/decrypt. + * :param bytes_len: Length of ``bytes`` in bytes. + * :param pub_key: The other parties public key used for Diffie-Helman exchange. + * :param pub_key_len: Length of ``pub_key`` in bytes. Must be `EC_PUBLIC_KEY_LEN`. + * :param label: A non-empty array of bytes for internal key generation. Must + *| be the same (fixed) value when encrypting and decrypting. + * :param label_len: Length of ``label`` in bytes. + * :param flags: :ref:`aes-operation-flag` indicating the desired behavior. + * :param written: Destination for the maximum length of the encrypted/decrypted data. + */ +WALLY_CORE_API int wally_aes_cbc_with_ecdh_key_get_maximum_length( + const unsigned char *priv_key, + size_t priv_key_len, + const unsigned char *iv, + size_t iv_len, + const unsigned char *bytes, + size_t bytes_len, + const unsigned char *pub_key, + size_t pub_key_len, + const unsigned char *label, + size_t label_len, + uint32_t flags, + size_t *written); + +/** + * Encrypt/decrypt data using AES-256 (CBC mode, PKCS#7 padding) and a shared Diffie-Helman secret. + * + * :param priv_key: The callers private key used for Diffie-Helman exchange. + * :param priv_key_len: The length of ``priv_key`` in bytes. Must be `EC_PRIVATE_KEY_LEN`. + * :param iv: Initialisation vector. Only required when encrypting, otherwise pass NULL. + * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN` if encrypting otherwise 0. + * :param bytes: Bytes to encrypt/decrypt. + * :param bytes_len: Length of ``bytes`` in bytes. + * :param pub_key: The other parties public key used for Diffie-Helman exchange. + * :param pub_key_len: Length of ``pub_key`` in bytes. Must be `EC_PUBLIC_KEY_LEN`. + * :param label: A non-empty array of bytes for internal key generation. Must + *| be the same (fixed) value when encrypting and decrypting. + * :param label_len: Length of ``label`` in bytes. + * :param flags: :ref:`aes-operation-flag` indicating the desired behavior. + * :param bytes_out: Destination for the encrypted/decrypted data. + * :param len: The length of ``bytes_out`` in bytes. + * :param written: Destination for the number of bytes written to ``bytes_out``. + * + * This function implements a scheme for sharing data using a derived secret. + * Alice creates an ephemeral key pair and sends her public key to Bob along + * with any request details. Bob creates an ephemeral key pair and calls this + * function with his private key and Alices public key to encrypt ``bytes`` + * (the request payload). Bob returns his public key and the encrypted data to + * Alice, who calls this function with her private key and Bobs public key + * to decrypt and authenticate the payload. The ``label`` parameter must be + * be the same for both Alice and Bob for a given request/response. + */ +WALLY_CORE_API int wally_aes_cbc_with_ecdh_key( + const unsigned char *priv_key, + size_t priv_key_len, + const unsigned char *iv, + size_t iv_len, + const unsigned char *bytes, + size_t bytes_len, + const unsigned char *pub_key, + size_t pub_key_len, + const unsigned char *label, + size_t label_len, + uint32_t flags, + unsigned char *bytes_out, + size_t len, + size_t *written); + #ifdef __cplusplus } #endif diff --git a/src/Makefile.am b/src/Makefile.am index d9ff95759..9933955a4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -319,6 +319,7 @@ endif if USE_SWIG_PYTHON check-swig-python: $(SWIG_PYTHON_TEST_DEPS) + $(AM_V_at)$(PYTHON_SWIGTEST) swig_python/contrib/aes.py $(AM_V_at)$(PYTHON_SWIGTEST) swig_python/contrib/bip32.py $(AM_V_at)$(PYTHON_SWIGTEST) swig_python/contrib/coinselection.py $(AM_V_at)$(PYTHON_SWIGTEST) swig_python/contrib/descriptor.py diff --git a/src/aes.c b/src/aes.c index b7c3c4c79..0f3245767 100644 --- a/src/aes.c +++ b/src/aes.c @@ -1,6 +1,7 @@ #include "internal.h" #include +#include "ccan/ccan/build_assert/build_assert.h" #include "ctaes/ctaes.h" #include "ctaes/ctaes.c" @@ -208,3 +209,132 @@ int wally_aes_cbc(const unsigned char *key, size_t key_len, wally_clear_2(buf, sizeof(buf), &ctx, sizeof(ctx)); return WALLY_OK; } + +int wally_aes_cbc_with_ecdh_key_get_maximum_length( + const unsigned char *priv_key, size_t priv_key_len, + const unsigned char *iv, size_t iv_len, + const unsigned char *bytes, size_t bytes_len, + const unsigned char *pub_key, size_t pub_key_len, + const unsigned char *label, size_t label_len, uint32_t flags, + size_t *written) +{ + if (written) + *written = 0; + + if (!priv_key || priv_key_len != EC_PRIVATE_KEY_LEN || !bytes || + !pub_key || pub_key_len != EC_PUBLIC_KEY_LEN || !label || !label_len || + (flags != AES_FLAG_ENCRYPT && flags != AES_FLAG_DECRYPT) || !written) + return WALLY_EINVAL; + + if (flags & AES_FLAG_ENCRYPT) { + /* Must provide IV + minimum 1 byte of payload for encryption */ + if (!iv || iv_len != AES_BLOCK_LEN || !bytes_len) + return WALLY_EINVAL; + /* Output is IV + encrypted payload + HMAC */ + *written = AES_BLOCK_LEN + HMAC_SHA256_LEN + + ((bytes_len / AES_BLOCK_LEN) + 1) * AES_BLOCK_LEN; + } else { + /* Must not provide an IV for decryption */ + if (iv || iv_len) + return WALLY_EINVAL; + /* Payload must contain IV, payload and the HMAC for decryption */ + if (bytes_len < AES_BLOCK_LEN + AES_BLOCK_LEN + HMAC_SHA256_LEN) + return WALLY_EINVAL; + /* Output is the decrypted payload without the IV and HMAC */ + bytes_len = bytes_len - AES_BLOCK_LEN - HMAC_SHA256_LEN; + if (bytes_len % AES_BLOCK_LEN) + return WALLY_EINVAL; /* Payload isn't a block size multiple */ + /* Actual bytes written may be less due to padding, but the + * caller must pass a buffer of the padded size. */ + *written = bytes_len; + } + return WALLY_OK; +} + +int wally_aes_cbc_with_ecdh_key( + const unsigned char *priv_key, size_t priv_key_len, + const unsigned char *iv, size_t iv_len, + const unsigned char *bytes, size_t bytes_len, + const unsigned char *pub_key, size_t pub_key_len, + const unsigned char *label, size_t label_len, uint32_t flags, + unsigned char *bytes_out, size_t len, size_t *written) +{ + unsigned char secret[SHA256_LEN], keys[HMAC_SHA512_LEN]; + const unsigned char *enc_key = keys, *hmac_key = keys + AES_KEY_LEN_256; + size_t expected_len; + const bool is_encrypt = flags & AES_FLAG_ENCRYPT; + int ret; + + /* Derived key sizes must match the derived keys buffer */ + BUILD_ASSERT(sizeof(keys) == AES_KEY_LEN_256 + SHA256_LEN); + + if (written) + *written = 0; + + if (!bytes_out || !len || !written) + return WALLY_EINVAL; + ret = wally_aes_cbc_with_ecdh_key_get_maximum_length( + priv_key, priv_key_len, iv, iv_len, bytes, bytes_len, + pub_key, pub_key_len, label, label_len, flags, + &expected_len); + if (ret == WALLY_OK && expected_len > len) { + *written = expected_len; + return ret; /* Tell the caller how much space is needed */ + } + if (ret != WALLY_OK) + return ret; + + if (is_encrypt) { + /* Copy the IV to the start of the encrypted output */ + memcpy(bytes_out, iv, iv_len); + } else { + /* The IV is the first AES_BLOCK_LEN bytes of the payload */ + iv = bytes; + iv_len = AES_BLOCK_LEN; + } + + /* Shared secret is ECDH(their pubkey, our private key) */ + ret = wally_ecdh(pub_key, pub_key_len, priv_key, priv_key_len, + secret, sizeof(secret)); + /* Generate encryption/HMAC keys using the shared secret and label */ + if (ret == WALLY_OK) + ret = wally_hmac_sha512(secret, sizeof(secret), label, label_len, keys, sizeof(keys)); + if (ret == WALLY_OK && !is_encrypt) { + /* Verify the IV + encrypted data's HMAC */ + unsigned char hmac[HMAC_SHA256_LEN]; + ret = wally_hmac_sha256(hmac_key, SHA256_LEN, + bytes, bytes_len - sizeof(hmac), + hmac, sizeof(hmac)); + if (ret == WALLY_OK && + memcmp(hmac, bytes + bytes_len - sizeof(hmac), sizeof(hmac))) + ret = WALLY_EINVAL; /* Invalid HMAC */ + } + + /* Encrypt/decrypt the payload */ + if (ret == WALLY_OK) { + /* Trim our output length to a block size multiple for aes_cbc */ + size_t out_len = (len - (is_encrypt ? (iv_len + HMAC_SHA256_LEN) : 0)) & ~((size_t)0xf); + if (is_encrypt) + ret = wally_aes_cbc(enc_key, AES_KEY_LEN_256, iv, iv_len, + bytes, bytes_len, flags, + bytes_out + iv_len, out_len, written); + else + ret = wally_aes_cbc(enc_key, AES_KEY_LEN_256, iv, iv_len, + bytes + iv_len, bytes_len - iv_len - HMAC_SHA256_LEN, + flags, bytes_out, out_len, written); + } + + if (ret == WALLY_OK && is_encrypt) { + /* append the HMAC of the IV + encrypted data */ + *written += AES_BLOCK_LEN; /* Include the IV */ + ret = wally_hmac_sha256(hmac_key, SHA256_LEN, + bytes_out, *written, + bytes_out + *written, HMAC_SHA256_LEN); + *written = ret == WALLY_OK ? *written + HMAC_SHA256_LEN : 0; + } + + if (ret == WALLY_OK && *written > expected_len) + ret = WALLY_ERROR; /* Should never happen! */ + wally_clear_2(secret, sizeof(secret), keys, sizeof(keys)); + return ret; +} diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index e68a3a6c6..1b0fd73fa 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -523,6 +523,8 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_aes_len); %returns_size_t(wally_aes_cbc); %returns_size_t(wally_aes_cbc_get_maximum_length); +%returns_size_t(wally_aes_cbc_with_ecdh_key); +%returns_size_t(wally_aes_cbc_with_ecdh_key_get_maximum_length); %returns_array_(wally_asset_final_vbf, 8, 9, ASSET_TAG_LEN); %returns_array_(wally_asset_generator_from_bytes, 5, 6, ASSET_GENERATOR_LEN); %returns_size_t(wally_asset_rangeproof_get_maximum_len); diff --git a/src/swig_python/contrib/aes.py b/src/swig_python/contrib/aes.py new file mode 100644 index 000000000..50280f70f --- /dev/null +++ b/src/swig_python/contrib/aes.py @@ -0,0 +1,43 @@ +import unittest +from wallycore import init, cleanup, \ + ec_public_key_from_private_key, aes_cbc_with_ecdh_key, \ + AES_FLAG_ENCRYPT, AES_FLAG_DECRYPT + + +class AESTests(unittest.TestCase): + + def test_aes_cbc_ecdh(self): + """Test python wrappers for aes_cbc_with_ecdh_key """ + # Alice generates an ephemeral keypair for her request. + alice_priv = bytes.fromhex('1c6a837d1ac663fdc7f1002327ca38452766eaf4fe3b80ce620bf7cd3f584cf6') + alice_pub = ec_public_key_from_private_key(alice_priv) + # Bob generates an ephemeral keypair for his response. + bob_priv = bytes.fromhex('0b6b3dc90d203d854100110788ac87d43aa00620c9cdb361b281b09022ef4b53') + bob_pub = ec_public_key_from_private_key(bob_priv) + # Bob also generates a secure random IV for encrypting the response. + iv = bytes.fromhex('bd5d4724243880738e7e8b0c02658700') + # Both parties must agree on a shared label to use. + label = 'a sample label'.encode() + # This is the example payload we are using. + payload = 'This is an example response/payload to encrypt'.encode() + + # Test we handle messages up to and over AES_BLOCK_LEN boundaries + for i in range(1, len(payload) + 1): + # The protocol: + # 1) Alice requests some data using her pubkey. + # 2) Bob encrypts the response (payload) with his private key, + # a random IV, a shared label and Alice's pubkey. + encrypted = aes_cbc_with_ecdh_key(bob_priv, iv, payload[0:i], + alice_pub, label, AES_FLAG_ENCRYPT) + # 3) Bob sends the encrypted data and his pubkey to Alice. + # 4) Alice decrypts the payload with her private key and Bobs pubkey. + decrypted = aes_cbc_with_ecdh_key(alice_priv, None, encrypted, + bob_pub, label, AES_FLAG_DECRYPT) + # Alice now has the unencrypted payload. + self.assertEqual(decrypted.hex(), payload[0:i].hex()) + + +if __name__ == '__main__': + init(0) + unittest.main() + cleanup(0) diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index 9752d1c5d..01e0e44b8 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -116,6 +116,7 @@ ae_sig_from_bytes = _wrap_bin(ae_sig_from_bytes, EC_SIGNATURE_LEN) ae_signer_commit_from_bytes = _wrap_bin(ae_signer_commit_from_bytes, WALLY_S2C_OPENING_LEN) aes = _wrap_bin(aes, aes_len) aes_cbc = _wrap_bin(aes_cbc, aes_cbc_get_maximum_length, resize=True) +aes_cbc_with_ecdh_key = _wrap_bin(aes_cbc_with_ecdh_key, aes_cbc_with_ecdh_key_get_maximum_length, resize=True) base58_n_to_bytes = _wrap_bin(base58_n_to_bytes, base58_n_to_bytes_len, resize=True) base58_to_bytes = _wrap_bin(base58_to_bytes, base58_to_bytes_len, resize=True) base64_to_bytes = _wrap_bin(base64_to_bytes, base64_get_maximum_length, resize=True) diff --git a/src/test/test_aes.py b/src/test/test_aes.py index 2a4a376a5..dd75de47c 100755 --- a/src/test/test_aes.py +++ b/src/test/test_aes.py @@ -92,6 +92,85 @@ def test_aes_cbc(self): self.assertEqual((ret, written), (0, len(o))) self.assertEqual(h(out_buf), h(o)) + def test_aes_cbc_with_ecdh_key(self): + ENCRYPT, DECRYPT, PUBKEY_LEN, _ = 1, 2, 33, True + a_priv = make_cbuffer('1c6a837d1ac663fdc7f1002327ca38452766eaf4fe3b80ce620bf7cd3f584cf6')[0] + a_pub = make_cbuffer('03e581be89d1ef8ce11d60746d08e4f8aedf934d1d861dd436042ee2e3b16db918')[0] + b_priv = make_cbuffer('0b6b3dc90d203d854100110788ac87d43aa00620c9cdb361b281b09022ef4b53')[0] + b_pub = make_cbuffer('03ff06999ad61c0f3a733b93fc1e6b75ecfb1439b326e840de590a56454f0eeb0d')[0] + iv = make_cbuffer('bd5d4724243880738e7e8b0c02658700')[0] + label = 'a sample label'.encode() + payload = 'This is an example response/payload to encrypt'.encode() + buf = make_cbuffer('00' * 256)[0] + + # Encryption + good_args = [b_priv, len(b_priv), iv, len(iv), payload, len(payload), + a_pub, len(a_pub), label, len(label), ENCRYPT, buf, len(buf)] + + ret, written = wally_aes_cbc_with_ecdh_key(*good_args) + self.assertEqual(ret, WALLY_OK) # Make sure good args work + encrypted = make_cbuffer(buf[:written].hex())[0] + + invalid_cases = [ + (None, _, _, _, _, _, _, _, _, _, _, _, _), # NULL privkey + (_, 0, _, _, _, _, _, _, _, _, _, _, _), # Empty privkey + (_, 9, _, _, _, _, _, _, _, _, _, _, _), # Wrong privkey length + (_, _, None, _, _, _, _, _, _, _, _, _, _), # NULL IV (enc) + (_, _, _, 0, _, _, _, _, _, _, _, _, _), # Empty IV (enc) + (_, _, _, 9, _, _, _, _, _, _, _, _, _), # Wrong IV length (enc) + (_, _, _, _, None, _, _, _, _, _, _, _, _), # NULL payload + (_, _, _, _, _, 0, _, _, _, _, _, _, _), # Empty payload + (_, _, _, _, _, _, None, _, _, _, _, _, _), # NULL pubkey + (_, _, _, _, _, _, _, 0, _, _, _, _, _), # Empty pubkey + (_, _, _, _, _, _, _, 9, _, _, _, _, _), # Wrong pubkey length + (_, _, _, _, _, _, _, _, None, _, _, _, _), # NULL label + (_, _, _, _, _, _, _, _, _, 0, _, _, _), # Empty label + (_, _, _, _, _, _, _, _, _, _, 3, _, _), # Encrypt+Decrypt flags + (_, _, _, _, _, _, _, _, _, _, 5, _, _), # Unknown flag + (_, _, _, _, _, _, _, _, _, _, _, None, _), # NULL output + (_, _, _, _, _, _, _, _, _, _, _, _, 0), # Zero-length output + ] + for case in invalid_cases: + args = [good_args[i] if a == _ else a for i, a in enumerate(case)] + self.assertEqual(wally_aes_cbc_with_ecdh_key(*args), (WALLY_EINVAL, 0)) + + # Test writing up to/beyond the output buffer size + for out_len in range(1, len(encrypted) + 16): + args = [a for a in good_args] + args[-1] = out_len + ret = wally_aes_cbc_with_ecdh_key(*args) + self.assertEqual(ret, (WALLY_OK, written)) # returns required length + + # Decryption + good_args = [a_priv, len(a_priv), None, 0, encrypted, len(encrypted), + b_pub, len(b_pub), label, len(label), DECRYPT, buf, len(buf)] + + ret, written = wally_aes_cbc_with_ecdh_key(*good_args) + self.assertEqual(ret, WALLY_OK) # Make sure good args work + self.assertEqual(buf[:written], payload) + + bad = make_cbuffer((encrypted[:-1] + b'?').hex())[0] # Corrupt the HMAC + bad_len = len(encrypted) - 1 + invalid_cases = [ + (_, _, iv, _, _, _, _, _, _, _, _, _, _), # Non-NULL IV (dec) + (_, _, _, len(iv), _, _, _, _, _, _, _, _, _), # Non-zero IV length (dec) + (_, _, _, _, bad, _, _, _, _, _, _, _, _), # Corrupt HMAC + (_, _, _, _, _, bad_len, _, _, _, _, _, _, _), # Truncated encrypted data + ] + for case in invalid_cases: + args = [good_args[i] if a == _ else a for i, a in enumerate(case)] + self.assertEqual(wally_aes_cbc_with_ecdh_key(*args), (WALLY_EINVAL, 0)) + + # Test writing up to/beyond the output buffer size + for out_len in range(1, len(payload) + 16): + args = [a for a in good_args] + args[-1] = out_len + ret, written = wally_aes_cbc_with_ecdh_key(*args) + self.assertEqual(ret, WALLY_OK) + # The output size required includes final padding which is + # stripped if the payload isn't a multiple of the AES block size. + self.assertLessEqual(len(payload), written) + if __name__ == '__main__': unittest.main() diff --git a/src/test/util.py b/src/test/util.py index 2c9c332fc..22ae6afc9 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -269,6 +269,8 @@ class wally_psbt(Structure): ('wally_aes', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_aes_cbc', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), ('wally_aes_cbc_get_maximum_length', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_size_t_p]), + ('wally_aes_cbc_with_ecdh_key', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), + ('wally_aes_cbc_with_ecdh_key_get_maximum_length', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_size_t_p]), ('wally_aes_len', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_size_t_p]), ('wally_asset_blinding_key_from_seed', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), ('wally_asset_blinding_key_to_abf', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 46b8d322e..e07023149 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -71,6 +71,7 @@ export const ae_sig_from_bytes = wrap('wally_ae_sig_from_bytes', [T.Bytes, T.Byt export const ae_signer_commit_from_bytes = wrap('wally_ae_signer_commit_from_bytes', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.WALLY_S2C_OPENING_LEN)]); export const ae_verify = wrap('wally_ae_verify', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Int32, T.Bytes]); export const aes_cbc_get_maximum_length = wrap('wally_aes_cbc_get_maximum_length', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtr(T.Int32)]); +export const aes_cbc_with_ecdh_key_get_maximum_length = wrap('wally_aes_cbc_with_ecdh_key_get_maximum_length', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtr(T.Int32)]); export const aes_len = wrap('wally_aes_len', [T.Bytes, T.Bytes, T.Int32, T.DestPtr(T.Int32)]); export const asset_blinding_key_from_seed = wrap('wally_asset_blinding_key_from_seed', [T.Bytes, T.DestPtrSized(T.Bytes, C.HMAC_SHA512_LEN)]); export const asset_blinding_key_to_abf = wrap('wally_asset_blinding_key_to_abf', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.BLINDING_FACTOR_LEN)]); @@ -766,6 +767,7 @@ export const witness_program_from_bytes = wrap('wally_witness_program_from_bytes export const witness_program_from_bytes_and_version = wrap('wally_witness_program_from_bytes_and_version', [T.Bytes, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_WITNESSSCRIPT_MAX_LEN, true)]); export const aes = wrap('wally_aes', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, aes_len, false)]); export const aes_cbc = wrap('wally_aes_cbc', [T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, aes_cbc_get_maximum_length, true)]); +export const aes_cbc_with_ecdh_key = wrap('wally_aes_cbc_with_ecdh_key', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, aes_cbc_with_ecdh_key_get_maximum_length, true)]); export const asset_pak_whitelistproof = wrap('wally_asset_pak_whitelistproof', [T.Bytes, T.Bytes, T.Int32, T.Bytes, T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, asset_pak_whitelistproof_len, false)]); export const asset_surjectionproof = wrap('wally_asset_surjectionproof', [T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, asset_surjectionproof_len, false)]); export const base58_n_to_bytes = wrap('wally_base58_n_to_bytes', [T.String, T.Int32, T.Int32, T.DestPtrVarLen(T.Bytes, base58_n_to_bytes_len, true)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 313fd04d4..70efc9c1f 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -31,6 +31,7 @@ export function ae_sig_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uin export function ae_signer_commit_from_bytes(priv_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, commitment: Buffer|Uint8Array, flags: number): Buffer; export function ae_verify(pub_key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, entropy: Buffer|Uint8Array, s2c_opening: Buffer|Uint8Array, flags: number, sig: Buffer|Uint8Array): void; export function aes_cbc_get_maximum_length(key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): number; +export function aes_cbc_with_ecdh_key_get_maximum_length(priv_key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, pub_key: Buffer|Uint8Array, label: Buffer|Uint8Array, flags: number): number; export function aes_len(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): number; export function asset_blinding_key_from_seed(bytes: Buffer|Uint8Array): Buffer; export function asset_blinding_key_to_abf(bytes: Buffer|Uint8Array, hash_prevouts: Buffer|Uint8Array, output_index: number): Buffer; @@ -726,6 +727,7 @@ export function witness_program_from_bytes(bytes: Buffer|Uint8Array, flags: numb export function witness_program_from_bytes_and_version(bytes: Buffer|Uint8Array, version: number, flags: number): Buffer; export function aes(key: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; export function aes_cbc(key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, flags: number): Buffer; +export function aes_cbc_with_ecdh_key(priv_key: Buffer|Uint8Array, iv: Buffer|Uint8Array, bytes: Buffer|Uint8Array, pub_key: Buffer|Uint8Array, label: Buffer|Uint8Array, flags: number): Buffer; export function asset_pak_whitelistproof(online_keys: Buffer|Uint8Array, offline_keys: Buffer|Uint8Array, key_index: number, sub_pubkey: Buffer|Uint8Array, online_priv_key: Buffer|Uint8Array, summed_key: Buffer|Uint8Array): Buffer; export function asset_surjectionproof(output_asset: Buffer|Uint8Array, output_abf: Buffer|Uint8Array, output_generator: Buffer|Uint8Array, bytes: Buffer|Uint8Array, asset: Buffer|Uint8Array, abf: Buffer|Uint8Array, generator: Buffer|Uint8Array): Buffer; export function base58_n_to_bytes(str_in: string, str_len: number, flags: number): Buffer; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 963b97d47..3b84e8593 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -64,6 +64,8 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_aes' \ ,'_wally_aes_cbc' \ ,'_wally_aes_cbc_get_maximum_length' \ +,'_wally_aes_cbc_with_ecdh_key' \ +,'_wally_aes_cbc_with_ecdh_key_get_maximum_length' \ ,'_wally_aes_len' \ ,'_wally_base58_from_bytes' \ ,'_wally_base58_get_length' \ From 744628370039c8c903b44dcfd6dc73d4f11c9d7f Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 21 Sep 2023 11:02:03 +1200 Subject: [PATCH 25/25] docs: minor improvements --- README.md | 2 +- include/wally_crypto.h | 18 ++++++++++-------- include/wally_transaction.h | 2 +- src/coins.c | 4 ++-- src/ctest/test_clear.c | 2 +- .../src/com/blockstream/test/test_bip32.java | 2 +- src/transaction.c | 2 +- src/wasm_package/src/const.js | 2 +- src/wasm_package/test/bip32.js | 2 +- 9 files changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0855aba61..ad53c42a7 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ $ brew install swig ### configure options - `--enable-debug`. Enables debugging information and disables compiler - optimisations (default: no). + optimizations (default: no). - `--enable-minimal`. Minimises library size and memory requirements to target embedded or resource-constrained environments (default: no). - `--enable-asm`. Enables fast assembly language implementations where available. diff --git a/include/wally_crypto.h b/include/wally_crypto.h index 7a3fc0c2c..ccf03bcce 100644 --- a/include/wally_crypto.h +++ b/include/wally_crypto.h @@ -49,7 +49,7 @@ WALLY_CORE_API int wally_scrypt( /** * Get the length of encrypted/decrypted data using AES (ECB mode, no padding). * - * :param key: Key material for initialisation. + * :param key: Encryption/decryption key. * :param key_len: Length of ``key`` in bytes. Must be one of the :ref:`aes-key-length`. * :param bytes: Bytes to encrypt/decrypt. * :param bytes_len: Length of ``bytes`` in bytes. Must be a multiple of `AES_BLOCK_LEN`. @@ -69,7 +69,7 @@ WALLY_CORE_API int wally_aes_len( /** * Encrypt/decrypt data using AES (ECB mode, no padding). * - * :param key: Key material for initialisation. + * :param key: Encryption/decryption key. * :param key_len: Length of ``key`` in bytes. Must be one of the :ref:`aes-key-length`. * :param bytes: Bytes to encrypt/decrypt. * :param bytes_len: Length of ``bytes`` in bytes. Must be a multiple of `AES_BLOCK_LEN`. @@ -89,9 +89,10 @@ WALLY_CORE_API int wally_aes( /** * Get the maximum length of encrypted/decrypted data using AES (CBC mode, PKCS#7 padding). * - * :param key: Key material for initialisation. + * :param key: Encryption/decryption key. * :param key_len: Length of ``key`` in bytes. Must be one of the :ref:`aes-key-length`. - * :param iv: Initialisation vector. + * :param iv: Initialization vector. For encryption this should be secure entropy. For + *| decryption the bytes used when encrypting must be given. * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN`. * :param bytes: Bytes to encrypt/decrypt. * :param bytes_len: Length of ``bytes`` in bytes. Can be of any length for encryption, must be a multiple of `AES_BLOCK_LEN` for decryption. @@ -111,9 +112,10 @@ WALLY_CORE_API int wally_aes_cbc_get_maximum_length( /** * Encrypt/decrypt data using AES (CBC mode, PKCS#7 padding). * - * :param key: Key material for initialisation. + * :param key: Encryption/decryption key. * :param key_len: Length of ``key`` in bytes. Must be one of the :ref:`aes-key-length`. - * :param iv: Initialisation vector. + * :param iv: Initialization vector. For encryption this should be secure entropy. For + *| decryption the bytes used when encrypting must be given. * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN`. * :param bytes: Bytes to encrypt/decrypt. * :param bytes_len: Length of ``bytes`` in bytes. Can be of any length for encryption, must be a multiple of `AES_BLOCK_LEN` for decryption. @@ -896,7 +898,7 @@ WALLY_CORE_API int wally_s2c_commitment_verify( * * :param priv_key: The callers private key used for Diffie-Helman exchange. * :param priv_key_len: The length of ``priv_key`` in bytes. Must be `EC_PRIVATE_KEY_LEN`. - * :param iv: Initialisation vector. Only required when encrypting, otherwise pass NULL. + * :param iv: Initialization vector. Only required when encrypting, otherwise pass NULL. * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN`. * :param bytes: Bytes to encrypt/decrypt. * :param bytes_len: Length of ``bytes`` in bytes. @@ -927,7 +929,7 @@ WALLY_CORE_API int wally_aes_cbc_with_ecdh_key_get_maximum_length( * * :param priv_key: The callers private key used for Diffie-Helman exchange. * :param priv_key_len: The length of ``priv_key`` in bytes. Must be `EC_PRIVATE_KEY_LEN`. - * :param iv: Initialisation vector. Only required when encrypting, otherwise pass NULL. + * :param iv: Initialization vector. Only required when encrypting, otherwise pass NULL. * :param iv_len: Length of ``iv`` in bytes. Must be `AES_BLOCK_LEN` if encrypting otherwise 0. * :param bytes: Bytes to encrypt/decrypt. * :param bytes_len: Length of ``bytes`` in bytes. diff --git a/include/wally_transaction.h b/include/wally_transaction.h index 118a9f109..6dfa65af0 100644 --- a/include/wally_transaction.h +++ b/include/wally_transaction.h @@ -26,7 +26,7 @@ extern "C" { #define WALLY_TX_FLAG_ALLOW_PARTIAL 0x4 /* Allow partially complete transactions */ /* Note: This flag encodes/parses transactions that are ambiguous to decode. Unless you have a good reason to do so you will most likely not need it */ -#define WALLY_TX_FLAG_PRE_BIP144 0x8 /* Encode/Decode using pre-BIP 144 serialisation */ +#define WALLY_TX_FLAG_PRE_BIP144 0x8 /* Encode/Decode using pre-BIP 144 serialization */ #define WALLY_TX_FLAG_BLINDED_INITIAL_ISSUANCE 0x1 diff --git a/src/coins.c b/src/coins.c index b65d58dc4..988b54cf9 100644 --- a/src/coins.c +++ b/src/coins.c @@ -53,12 +53,12 @@ typedef struct value_remaining { * the io_ratio passed in, which increases the search space and prefers to * use more inputs if a changeless solution is found. * - * We implement the core optimisations: + * We implement the core optimizations: * - Cut the search branch when it cannot reach the target * - Cut the search branch when it exceeds the target (after scoring it) * - Do not test equivalent combinations * - * We add two additional optimisations. First, we return immediately if we find + * We add two additional optimizations. First, we return immediately if we find * a single input exact match, since that represents the best match possible. * Second, we cut the search branch when the number of selected inputs * is enough that even an exact match would not score better than the current diff --git a/src/ctest/test_clear.c b/src/ctest/test_clear.c index 15bacdc56..0432396c5 100644 --- a/src/ctest/test_clear.c +++ b/src/ctest/test_clear.c @@ -36,7 +36,7 @@ extern void __asan_unpoison_memory_region(void const volatile *addr, size_t size * determine that the memory is not read afterwards. There are reports * that tricks designed to work around this including making data volatile, * calling through function pointers, dummy asm contraints etc are - * not always effective as optimisation continues to improve. + * not always effective as optimization continues to improve. * * Here we try to ensure that the wally_clear/wally_clear_n() functions work as advertised * by: diff --git a/src/swig_java/src/com/blockstream/test/test_bip32.java b/src/swig_java/src/com/blockstream/test/test_bip32.java index c17fa9d9f..f4c412786 100644 --- a/src/swig_java/src/com/blockstream/test/test_bip32.java +++ b/src/swig_java/src/com/blockstream/test/test_bip32.java @@ -48,7 +48,7 @@ public void test() { final byte[] initSerialized = Wally.bip32_key_serialize(initKey, BIP32_FLAG_KEY_PRIVATE); if (!h(initSerialized).equals(h(derivedSerialized))) - throw new RuntimeException("BIP32 initialisation by member failed"); + throw new RuntimeException("BIP32 initialization by member failed"); final byte[] message = Wally.bip32_key_get_chain_code(derivedKey); final byte[] signature = Wally.ec_sig_from_bytes(Wally.bip32_key_get_priv_key(derivedKey), diff --git a/src/transaction.c b/src/transaction.c index 0bb1e3ccf..fd1a18cb7 100644 --- a/src/transaction.c +++ b/src/transaction.c @@ -2794,7 +2794,7 @@ static int analyze_tx(const unsigned char *bytes, size_t bytes_len, if (is_elements) { if (flags & WALLY_TX_FLAG_PRE_BIP144) - return WALLY_EINVAL; /* No pre-BIP 144 serialisation for elements */ + return WALLY_EINVAL; /* No pre-BIP 144 serialization for elements */ *expect_witnesses = *p++ != 0; } else { if (!(flags & WALLY_TX_FLAG_PRE_BIP144) && *p == 0) { diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index 8331da509..2c0e012e8 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -228,7 +228,7 @@ export const WALLY_TX_DUMMY_SIG = 0x2; /* A dummy signature */ export const WALLY_TX_DUMMY_SIG_LOW_R = 0x4; /* A dummy signature created with EC_FLAG_GRIND_R */ export const WALLY_TX_FLAG_ALLOW_PARTIAL = 0x4; /* Allow partially complete transactions */ export const WALLY_TX_FLAG_BLINDED_INITIAL_ISSUANCE = 0x1; -export const WALLY_TX_FLAG_PRE_BIP144 = 0x8; /* Encode/Decode using pre-BIP 144 serialisation */ +export const WALLY_TX_FLAG_PRE_BIP144 = 0x8; /* Encode/Decode using pre-BIP 144 serialization */ export const WALLY_TX_FLAG_USE_ELEMENTS = 0x2; /* Encode/Decode as an elements transaction */ export const WALLY_TX_FLAG_USE_WITNESS = 0x1; /* Encode witness data if present */ export const WALLY_TX_INDEX_MASK = 0x3fffffff; diff --git a/src/wasm_package/test/bip32.js b/src/wasm_package/test/bip32.js index c31735765..da05e563e 100644 --- a/src/wasm_package/test/bip32.js +++ b/src/wasm_package/test/bip32.js @@ -109,7 +109,7 @@ test('BIP32 derivation', () => { const derivedSerialized = wally.bip32_key_serialize(derivedKey, wally.BIP32_FLAG_KEY_PRIVATE) const initSerialized = wally.bip32_key_serialize(initKey, wally.BIP32_FLAG_KEY_PRIVATE) - assert.deepEqual(initSerialized, derivedSerialized, 'BIP32 initialisation by member failed') + assert.deepEqual(initSerialized, derivedSerialized, 'BIP32 initialization by member failed') wally.bip32_key_free(seedKey) wally.bip32_key_free(derivedKey)