From aed824104876bdd06d2efd4fbd9a067207e7eb77 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Tue, 4 Aug 2026 10:40:49 +1200 Subject: [PATCH 1/6] java: fix output type for psbt_get_output_amount Reported-by: Efstratios Kaplanellis (@5tratan) of Almamater Technologies --- src/swig_java/swig.i | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index cf2b89918..92b3f33e8 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -845,7 +845,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_psbt_get_locktime); %returns_size_t(wally_psbt_get_num_inputs); %returns_size_t(wally_psbt_get_num_outputs); -%returns_size_t(wally_psbt_get_output_amount); +%returns_uint64(wally_psbt_get_output_amount); %returns_size_t(wally_psbt_get_output_asset); %returns_size_t(wally_psbt_get_output_asset_len); %returns_size_t(wally_psbt_get_output_asset_blinding_surjectionproof); From e9fed8a4939c922f1f1428cfd4442043d06ff431 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Tue, 4 Aug 2026 10:42:40 +1200 Subject: [PATCH 2/6] aes: add missing length check for multi-chunk encrypt/decrypt, with tests Reported-by: Efstratios Kaplanellis (@5tratan) of Almamater Technologies --- src/aes.c | 5 ++--- src/test/test_aes.py | 42 +++++++++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/aes.c b/src/aes.c index 291946f26..323a49062 100644 --- a/src/aes.c +++ b/src/aes.c @@ -92,9 +92,8 @@ int wally_aes(const unsigned char *key, size_t key_len, AES256_ctx ctx; size_t written; - if (!are_valid_args(key, key_len, bytes, bytes_len, flags, &written) || - len % AES_BLOCK_LEN || !bytes_len || bytes_len % AES_BLOCK_LEN || - !bytes_out || !len) + if (wally_aes_len(key, key_len, bytes, bytes_len, flags, &written) != WALLY_OK || + !bytes_out || !len || len < bytes_len) return WALLY_EINVAL; if (flags & AES_FLAG_ENCRYPT) diff --git a/src/test/test_aes.py b/src/test/test_aes.py index 577c4c4a8..19f114160 100755 --- a/src/test/test_aes.py +++ b/src/test/test_aes.py @@ -51,9 +51,10 @@ "23304b7a39f9f3ff067d8d8f9e24ecc7" ], ] -class AESTests(unittest.TestCase): +ENCRYPT, DECRYPT = 1, 2 + - ENCRYPT, DECRYPT = 1, 2 +class AESTests(unittest.TestCase): def test_aes(self): @@ -62,14 +63,37 @@ def test_aes(self): key_bytes = { 128: 16, 192: 24, 256: 32}[c[0]] self.assertEqual(len(key), key_bytes) - for p, f, o in [(plain, self.ENCRYPT, cypher), - (cypher, self.DECRYPT, plain)]: + for p, f, o in [(plain, ENCRYPT, cypher), + (cypher, DECRYPT, plain)]: out_buf, out_len = make_cbuffer('00' * len(o)) ret = wally_aes(key, len(key), p, len(p), f, out_buf, out_len) - self.assertEqual(ret, 0) + self.assertEqual(ret, WALLY_OK) self.assertEqual(h(out_buf), h(o)) + # Invalid args + key = make_cbuffer('2b7e151628aed2a6abf7158809cf4f3c')[0] + plain = make_cbuffer('ae2d8a571e03ac9c9eb76fac45af8e51ae2d8a571e03ac9c9eb76fac45af8e51')[0] + out, out_len = make_cbuffer('00' * len(plain)) + invalid_cases = [ + (None, len(key), plain, len(plain), ENCRYPT, out, out_len), # NULL key + (key, 0, plain, len(plain), ENCRYPT, out, out_len), # Empty key + (key, 15, plain, len(plain), ENCRYPT, out, out_len), # Invalid key len + (key, len(key), None, len(plain), ENCRYPT, out, out_len), # NULL plaintext + (key, len(key), plain, 0, ENCRYPT, out, out_len), # Empty plaintext + (key, len(key), plain, 15, ENCRYPT, out, out_len), # Non-blocksize plaintext + (key, len(key), plain, len(plain), 0, out, out_len), # No flags + (key, len(key), plain, len(plain), ENCRYPT | \ + DECRYPT, out, out_len), # Conflicting flags + (key, len(key), plain, len(plain), 4, out, out_len), # Unknown flags + (key, len(key), plain, len(plain), ENCRYPT, None, out_len), # NULL output + (key, len(key), plain, len(plain), ENCRYPT, out, 0), # Empty output + (key, len(key), plain, len(plain), ENCRYPT, out, 16), # Too short output + ] + for c in invalid_cases: + ret = wally_aes(*c) + self.assertEqual(ret, WALLY_EINVAL) + def get_cbc_cases(self): lines = [] @@ -81,12 +105,11 @@ def get_cbc_cases(self): def test_aes_cbc(self): out_buf, out_len = make_cbuffer('00' * 80) - E, D = self.ENCRYPT, self.DECRYPT # Encryption/decryption cases for c in self.get_cbc_cases(): plain, key, iv, cypher = [make_cbuffer(s)[0] for s in c] - for p, f, o in [(plain, E, cypher), (cypher, D, plain)]: + for p, f, o in [(plain, ENCRYPT, cypher), (cypher, DECRYPT, plain)]: ret, written = wally_aes_cbc(key, len(key), iv, len(iv), p or None, len(p), f, out_buf, out_len) self.assertEqual((ret, written), (0, len(o))) @@ -100,10 +123,11 @@ def test_aes_cbc(self): # number of bytes required. ret, max_len = wally_aes_cbc_get_maximum_length(key, len(key), iv, len(iv), p or None, len(p), f) - self.assertEqual(ret, 0) + self.assertEqual(ret, WALLY_OK) self.assertTrue(max_len >= written and max_len % 16 == 0) # Invalid args + D = DECRYPT invalid_cases = [ # NULL key (None, len(key), iv, len(iv), cypher, len(cypher), D, out_buf, out_len), @@ -130,7 +154,6 @@ def test_aes_cbc(self): self.assertEqual((ret, written), (WALLY_EINVAL, 0)) def test_aes_cbc_with_ecdh_key(self): - ENCRYPT, DECRYPT, _ = 1, 2, True a_priv = make_cbuffer('1c6a837d1ac663fdc7f1002327ca38452766eaf4fe3b80ce620bf7cd3f584cf6')[0] a_pub = make_cbuffer('03e581be89d1ef8ce11d60746d08e4f8aedf934d1d861dd436042ee2e3b16db918')[0] b_priv = make_cbuffer('0b6b3dc90d203d854100110788ac87d43aa00620c9cdb361b281b09022ef4b53')[0] @@ -148,6 +171,7 @@ def test_aes_cbc_with_ecdh_key(self): self.assertEqual(ret, WALLY_OK) # Make sure good args work encrypted = make_cbuffer(buf[:written].hex())[0] + _ = True invalid_cases = [ (None, _, _, _, _, _, _, _, _, _, _, _, _), # NULL privkey (_, 0, _, _, _, _, _, _, _, _, _, _, _), # Empty privkey From c68f66837789f8f248c99608d5bb1d1a985a9cec Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Tue, 4 Aug 2026 10:44:40 +1200 Subject: [PATCH 3/6] java: use len fn to allocate wrapper buffer for aes encrypt/decrypt Reported-by: Efstratios Kaplanellis (@5tratan) of Almamater Technologies --- src/swig_java/jni_extra.java_in | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/swig_java/jni_extra.java_in b/src/swig_java/jni_extra.java_in index 9be288598..51c6ca2eb 100644 --- a/src/swig_java/jni_extra.java_in +++ b/src/swig_java/jni_extra.java_in @@ -31,7 +31,9 @@ } public final static byte[] aes(byte[] jarg1, byte[] jarg2, long jarg3) { - return aes(jarg1, jarg2, jarg3, null); + final int len = aes_len(jarg1, jarg2, jarg3); + final byte[] ret = new byte[len]; + return aes(jarg1, jarg2, jarg3, ret); } public final static String base58check_from_bytes(byte[] bytes) { From 078714063fc41c47f4324ce65be0618b2e3c37e2 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Sun, 9 Aug 2026 16:42:32 +1200 Subject: [PATCH 4/6] psbt: return an error if fetching output_amount when not present Note this is an ABI change. --- src/psbt.c | 12 ++++++++++- src/swig_python/contrib/psbt.py | 36 +++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/psbt.c b/src/psbt.c index 5806aab88..c419eda3e 100644 --- a/src/psbt.c +++ b/src/psbt.c @@ -5994,7 +5994,17 @@ PSBT_FIELD(output, redeem_script, PSBT_0) PSBT_FIELD(output, witness_script, PSBT_0) PSBT_GET_M(output, keypath) PSBT_GET_M(output, unknown) -PSBT_GET_I(output, amount, uint64_t, PSBT_2) +int wally_psbt_get_output_amount(const struct wally_psbt *psbt, size_t index, + uint64_t *written) +{ + struct wally_psbt_output *p = psbt_get_output(psbt, index); + if (written) *written = 0; + if (!p || !written || psbt->version != PSBT_2 || !p->has_amount) + return WALLY_EINVAL; + *written = p->amount; + return WALLY_OK; +} + int wally_psbt_has_output_amount(const struct wally_psbt *psbt, size_t index, size_t *written) { struct wally_psbt_output *p = psbt_get_output(psbt, index); if (written) *written = 0; diff --git a/src/swig_python/contrib/psbt.py b/src/swig_python/contrib/psbt.py index 2a12a7315..305d8dc99 100644 --- a/src/swig_python/contrib/psbt.py +++ b/src/swig_python/contrib/psbt.py @@ -53,9 +53,12 @@ def _try_set(self, func, psbt, valid_value, null_value=None, mandatory=False, al self._throws(func, psbt, 0, null_value) self._try_invalid(func, psbt, valid_value) - def _try_get_set_i(self, setfn, clearfn, getfn, psbt, valid_value, invalid_value=None, mandatory=False): + def _try_get_set_i(self, setfn, clearfn, getfn, hasfn, psbt, + valid_value, invalid_value=None, mandatory=False): self._try_invalid(setfn, psbt, valid_value) setfn(psbt, 0, valid_value) # Set + if hasfn: + self.assertTrue(hasfn(psbt, 0)) self._round_trip(psbt) self._try_invalid(getfn, psbt) ret = getfn(psbt, 0) # Get @@ -63,6 +66,9 @@ def _try_get_set_i(self, setfn, clearfn, getfn, psbt, valid_value, invalid_value if clearfn: self._try_invalid(clearfn, psbt) clearfn(psbt, 0) + if hasfn: + self.assertFalse(hasfn(psbt, 0)) + self._throws(getfn, psbt, 0) if mandatory: setfn(psbt, 0, valid_value) # Set Again else: @@ -195,7 +201,7 @@ def test_add_remove_tx_items(self): # txout has blinded value/asset, expect no values # and the commitments set in the PSET self.assertEqual(psbt_has_output_amount(pset2, 0), 0) - self.assertEqual(psbt_get_output_amount(pset2, 0), 0) + self._throws(psbt_get_output_amount, pset2, 0) self.assertEqual(psbt_get_output_value_commitment_len(pset2, 0), len(blinded_value)) self.assertEqual(psbt_get_output_value_commitment(pset2, 0), blinded_value) self.assertEqual(psbt_get_output_script(pset2, 0), script) @@ -504,7 +510,7 @@ def test_psbt(self): p, dummy_unknowns, dummy_unknown_key) psbt_set_input_signatures(p, 0, empty_signatures) self._try_get_set_i(psbt_set_input_sighash, None, - psbt_get_input_sighash, p, 0xff) # FIXME 0x100 as invalid_value should fail + psbt_get_input_sighash, None, p, 0xff) # FIXME 0x100 as invalid_value should fail for sig_type in [dummy_sig_tap_default, dummy_sig_tap_all, dummy_sig_tap_single]: psbt_set_input_taproot_signature(p, 0, sig_type) self.assertEqual(psbt_get_input_taproot_signature(p, 0), sig_type) @@ -542,9 +548,9 @@ def test_psbt(self): # V2: Output Index self._throws(psbt_set_input_output_index, psbt, 0, 1234) # Non v2 PSBT - self._try_get_set_i(psbt_set_input_output_index, - None, - psbt_get_input_output_index, psbt2, 1234) + self._try_get_set_i(psbt_set_input_output_index, None, + psbt_get_input_output_index, None, + psbt2, 1234) # For v0 PSBTs, fetching returns the value from the global tx out_idx = tx_get_input_index(global_tx, 0) self.assertEqual(psbt_get_input_output_index(psbt, 0), out_idx) @@ -552,9 +558,9 @@ def test_psbt(self): # V2: Sequence self._throws(psbt_set_input_sequence, psbt, 0, 1234) # Non v2 PSBT self._throws(psbt_clear_input_sequence, psbt, 0) # Non v2 PSBT - self._try_get_set_i(psbt_set_input_sequence, - psbt_clear_input_sequence, - psbt_get_input_sequence, psbt2, 1234) + self._try_get_set_i(psbt_set_input_sequence, psbt_clear_input_sequence, + psbt_get_input_sequence, None, + psbt2, 1234) # If no sequence is present, it defaults to final (0xffffffff) psbt_clear_input_sequence(psbt2, 0) self.assertEqual(psbt_get_input_sequence(psbt2, 0), 0xffffffff) @@ -573,7 +579,7 @@ def test_psbt(self): self._throws(g_fn, psbt, 0) # Non v2 PSBT self._throws(h_fn, psbt, 0) # Non v2 PSBT self._throws(c_fn, psbt, 0) # Non v2 PSBT - self._try_get_set_i(s_fn, c_fn, g_fn, psbt2, v) + self._try_get_set_i(s_fn, c_fn, g_fn, h_fn, psbt2, v) # # Inputs: PSET @@ -587,7 +593,7 @@ def test_psbt(self): (psbt_set_input_pegin_amount, psbt_get_input_pegin_amount)]: self._throws(setfn, psbt, 0, 1234) # Non v2 PSBT self._throws(getfn, psbt, 0) # Non v2 PSBT - self._try_get_set_i(setfn, None, getfn, pset2, 1234) + self._try_get_set_i(setfn, None, getfn, None, pset2, 1234) # Explicit amount self._throws(psbt_clear_input_amount, psbt, 0) # Non v2 PSBT @@ -687,9 +693,9 @@ def test_psbt(self): self._throws(psbt_has_output_amount, psbt2, 1) # Invalid Index self.assertEqual(psbt_has_output_amount(psbt2, 0), 1) # Non v2 PSBT self._throws(psbt_clear_output_amount, psbt, 0) # Non v2 PSBT - self._try_get_set_i(psbt_set_output_amount, - psbt_clear_output_amount, - psbt_get_output_amount, psbt2, 1234, mandatory=True) + self._try_get_set_i(psbt_set_output_amount, psbt_clear_output_amount, + psbt_get_output_amount, psbt_has_output_amount, + psbt2, 1234, mandatory=True) # V2: Script self._throws(psbt_set_output_script, psbt, 0, dummy_bytes) # Non v2 PSBT @@ -711,7 +717,7 @@ def test_psbt(self): (psbt_set_output_blinder_index, psbt_get_output_blinder_index)]: self._throws(setfn, psbt, 0, 1234) # Non v2 PSBT self._throws(getfn, psbt, 0) # Non v2 PSBT - self._try_get_set_i(setfn, None, getfn, pset2, 1234) + self._try_get_set_i(setfn, None, getfn, None, pset2, 1234) cases = [ ('value_commitment', dummy_blind_value, dummy_blind_asset), From fa3a58591ee1b6db9741cd09b9710f8270b28020 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Sun, 9 Aug 2026 23:17:06 +1200 Subject: [PATCH 5/6] psbt: return an error if fetching input_amount when not present Add has_input_amount to allow callers to determine field presence in the same way as output_amount. Also remove non-existent input_value accessors from the SWIG Java wrapper. Note this is an ABI change. --- include/wally_psbt_members.h | 1 + src/psbt.c | 28 ++++++++++++++++++++++++++++ src/swig_java/swig.i | 5 +---- src/swig_python/contrib/psbt.py | 16 ++++++++++------ 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, 44 insertions(+), 10 deletions(-) diff --git a/include/wally_psbt_members.h b/include/wally_psbt_members.h index 45651a685..4919e449b 100644 --- a/include/wally_psbt_members.h +++ b/include/wally_psbt_members.h @@ -142,6 +142,7 @@ WALLY_CORE_API int wally_psbt_get_input_utxo_rangeproof_len(const struct wally_p WALLY_CORE_API int wally_psbt_set_input_amount(struct wally_psbt *psbt, size_t index, uint64_t amount); WALLY_CORE_API int wally_psbt_clear_input_amount(struct wally_psbt *psbt, size_t index); +WALLY_CORE_API int wally_psbt_has_input_amount(const struct wally_psbt *psbt, size_t index, size_t *written); WALLY_CORE_API int wally_psbt_set_input_amount_rangeproof(struct wally_psbt *psbt, size_t index, const unsigned char *rangeproof, size_t rangeproof_len); WALLY_CORE_API int wally_psbt_clear_input_amount_rangeproof(struct wally_psbt *psbt, size_t index); WALLY_CORE_API int wally_psbt_set_input_asset(struct wally_psbt *psbt, size_t index, const unsigned char *asset, size_t asset_len); diff --git a/src/psbt.c b/src/psbt.c index c419eda3e..195ed58ef 100644 --- a/src/psbt.c +++ b/src/psbt.c @@ -5932,7 +5932,35 @@ int wally_psbt_clear_input_required_lockheight(struct wally_psbt *psbt, size_t i PSBT_FIELD(output, taproot_internal_key, PSBT_0) #ifndef WALLY_ABI_NO_ELEMENTS +#ifndef BUILD_ELEMENTS PSBT_GET_I_PSET(input, amount, uint64_t, PSBT_2) +int wally_psbt_has_input_amount(const struct wally_psbt *psbt, size_t index, size_t* written) +{ + if (written) + *written = 0; + return WALLY_EINVAL; +} +#else +int wally_psbt_get_input_amount(const struct wally_psbt *psbt, size_t index, + uint64_t *written) +{ + struct wally_psbt_input *p = psbt_get_input(psbt, index); + if (written) *written = 0; + if (!p || !written || psbt->version != PSBT_2 || !p->has_amount) + return WALLY_EINVAL; + *written = p->amount; + return WALLY_OK; +} +int wally_psbt_has_input_amount(const struct wally_psbt *psbt, size_t index, size_t* written) +{ + struct wally_psbt_input *p = psbt_get_input(psbt, index); + if (written) *written = 0; + if (!p || !written || psbt->version != PSBT_2) + return WALLY_EINVAL; + *written = p->has_amount ? 1 : 0; + return WALLY_OK; +} +#endif int wally_psbt_clear_input_amount(struct wally_psbt *psbt, size_t index) { if (!psbt || psbt->version != PSBT_2) return WALLY_EINVAL; return wally_psbt_input_clear_amount(psbt_get_input(psbt, index)); diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 92b3f33e8..633eb0c30 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -723,7 +723,6 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_void__(wally_psbt_clear_input_required_locktime); %returns_void__(wally_psbt_clear_input_sequence); %returns_void__(wally_psbt_clear_input_utxo_rangeproof); -%returns_void__(wally_psbt_clear_input_value); %returns_void__(wally_psbt_clear_output_amount); %returns_void__(wally_psbt_clear_output_asset); %returns_void__(wally_psbt_clear_output_asset_blinding_surjectionproof); @@ -835,7 +834,6 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %rename("psbt_get_input_utxo") wally_psbt_get_input_utxo_alloc; %returns_size_t(wally_psbt_get_input_utxo_rangeproof); %returns_size_t(wally_psbt_get_input_utxo_rangeproof_len); -%returns_uint64(wally_psbt_get_input_value); %returns_size_t(wally_psbt_get_input_witness_script); %returns_size_t(wally_psbt_get_input_witness_script_len); %returns_struct(wally_psbt_get_input_witness_utxo_alloc, wally_tx_output); @@ -887,7 +885,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_psbt_has_global_genesis_blockhash); %returns_size_t(wally_psbt_has_input_required_lockheight); %returns_size_t(wally_psbt_has_input_required_locktime); -%returns_size_t(wally_psbt_has_input_value); +%returns_size_t(wally_psbt_has_input_amount); %returns_size_t(wally_psbt_has_output_amount); %returns_size_t(wally_psbt_has_output_asset); %returns_size_t(wally_psbt_has_output_asset_blinding_surjectionproof); @@ -945,7 +943,6 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_void__(wally_psbt_set_input_unknowns); %returns_void__(wally_psbt_set_input_utxo); %returns_void__(wally_psbt_set_input_utxo_rangeproof); -%returns_void__(wally_psbt_set_input_value); %returns_void__(wally_psbt_set_input_witness_script); %returns_void__(wally_psbt_set_input_witness_utxo); %returns_void__(wally_psbt_set_input_witness_utxo_from_tx); diff --git a/src/swig_python/contrib/psbt.py b/src/swig_python/contrib/psbt.py index 305d8dc99..70d198805 100644 --- a/src/swig_python/contrib/psbt.py +++ b/src/swig_python/contrib/psbt.py @@ -586,14 +586,18 @@ def test_psbt(self): # if is_elements_build(): # PSET: Explicit amount/issuance amount/inflation keys/pegin amount - for setfn, getfn in [ - (psbt_set_input_amount, psbt_get_input_amount), - (psbt_set_input_issuance_amount, psbt_get_input_issuance_amount), - (psbt_set_input_inflation_keys, psbt_get_input_inflation_keys), - (psbt_set_input_pegin_amount, psbt_get_input_pegin_amount)]: + for setfn, clearfn, getfn, hasfn in [ + (psbt_set_input_amount, psbt_clear_input_amount, + psbt_get_input_amount, psbt_has_input_amount), + (psbt_set_input_issuance_amount, None, + psbt_get_input_issuance_amount, None), + (psbt_set_input_inflation_keys, None, + psbt_get_input_inflation_keys, None), + (psbt_set_input_pegin_amount, None, + psbt_get_input_pegin_amount, None)]: self._throws(setfn, psbt, 0, 1234) # Non v2 PSBT self._throws(getfn, psbt, 0) # Non v2 PSBT - self._try_get_set_i(setfn, None, getfn, None, pset2, 1234) + self._try_get_set_i(setfn, clearfn, getfn, hasfn, pset2, 1234) # Explicit amount self._throws(psbt_clear_input_amount, psbt, 0) # Non v2 PSBT diff --git a/src/test/util.py b/src/test/util.py index a69741472..36dccfcf7 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -917,6 +917,7 @@ class wally_psbt(Structure): ('wally_psbt_get_tx_modifiable_flags', c_int, [POINTER(wally_psbt), c_size_t_p]), ('wally_psbt_get_version', c_int, [POINTER(wally_psbt), c_size_t_p]), ('wally_psbt_has_fallback_locktime', c_int, [POINTER(wally_psbt), c_size_t_p]), + ('wally_psbt_has_input_amount', c_int, [POINTER(wally_psbt), c_size_t, c_size_t_p]), ('wally_psbt_has_input_required_lockheight', c_int, [POINTER(wally_psbt), c_size_t, c_size_t_p]), ('wally_psbt_has_input_required_locktime', c_int, [POINTER(wally_psbt), c_size_t, c_size_t_p]), ('wally_psbt_has_output_amount', c_int, [POINTER(wally_psbt), c_size_t, c_size_t_p]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index e6f51317e..e2f0ee340 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -416,6 +416,7 @@ export const psbt_get_tx_version = wrap('wally_psbt_get_tx_version', [T.OpaqueRe export const psbt_get_version = wrap('wally_psbt_get_version', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const psbt_has_fallback_locktime = wrap('wally_psbt_has_fallback_locktime', [T.OpaqueRef, T.DestPtr(T.Int32)]); export const psbt_has_global_genesis_blockhash = wrap('wally_psbt_has_global_genesis_blockhash', [T.OpaqueRef, T.DestPtr(T.Int32)]); +export const psbt_has_input_amount = wrap('wally_psbt_has_input_amount', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const psbt_has_input_required_lockheight = wrap('wally_psbt_has_input_required_lockheight', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const psbt_has_input_required_locktime = wrap('wally_psbt_has_input_required_locktime', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); export const psbt_has_output_amount = wrap('wally_psbt_has_output_amount', [T.OpaqueRef, T.Int32, T.DestPtr(T.Int32)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index b9c5a8eb1..c2d76c209 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -376,6 +376,7 @@ export function psbt_get_tx_version(psbt: Ref_wally_psbt): number; export function psbt_get_version(psbt: Ref_wally_psbt): number; export function psbt_has_fallback_locktime(psbt: Ref_wally_psbt): number; export function psbt_has_global_genesis_blockhash(psbt: Ref_wally_psbt): number; +export function psbt_has_input_amount(psbt: Ref_wally_psbt, index: number): number; export function psbt_has_input_required_lockheight(psbt: Ref_wally_psbt, index: number): number; export function psbt_has_input_required_locktime(psbt: Ref_wally_psbt, index: number): number; export function psbt_has_output_amount(psbt: Ref_wally_psbt, index: number): number; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 6c45e2dcf..8aa077686 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -638,6 +638,7 @@ if [ -z "$DISABLE_ELEMENTS" ]; then ,'_wally_psbt_get_output_value_rangeproof_len' \ ,'_wally_psbt_get_pset_modifiable_flags' \ ,'_wally_psbt_has_global_genesis_blockhash' \ +,'_wally_psbt_has_input_amount' \ ,'_wally_psbt_has_output_blinder_index' \ ,'_wally_psbt_input_clear_amount_rangeproof' \ ,'_wally_psbt_input_clear_asset' \ From 3bf543cd06a67fdd877688a6304808f270351aee Mon Sep 17 00:00:00 2001 From: KY-U Date: Thu, 6 Aug 2026 17:55:35 -0300 Subject: [PATCH 6/6] psbt: reject trailing data when parsing if WALLY_PSBT_PARSE_FLAG_COMPLETE is used Set the flag unconditionally for base64 inputs. Add PSBT v0 and v2 regression test cases with a trailing byte. --- include/wally_psbt.h | 5 +++-- src/ctest/psbts.h | 6 ++++++ src/data/psbt.json | 8 ++++++++ src/psbt.c | 11 ++++++++--- src/wasm_package/src/const.js | 1 + 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/include/wally_psbt.h b/include/wally_psbt.h index cd01d1500..71334d4af 100644 --- a/include/wally_psbt.h +++ b/include/wally_psbt.h @@ -29,8 +29,9 @@ extern "C" { #define WALLY_PSBT_TXMOD_SINGLE 0x4 /* SIGHASH_SINGLE signature is present */ #define WALLY_PSET_TXMOD_RESERVED 0x1 /* Elements: Reserved: not used and ignored if set */ -#define WALLY_PSBT_PARSE_FLAG_STRICT 0x1 /* Parse strictly according to the PSBT/PSET spec */ -#define WALLY_PSBT_PARSE_FLAG_LOOSE 0x2 /* Allow disallowed and missing mandatory fields */ +#define WALLY_PSBT_PARSE_FLAG_STRICT 0x1 /* Parse strictly according to the PSBT/PSET spec */ +#define WALLY_PSBT_PARSE_FLAG_LOOSE 0x2 /* Allow disallowed and missing mandatory fields */ +#define WALLY_PSBT_PARSE_FLAG_COMPLETE 0x4 /* Require parsing to consume the entire input */ /** Include redundant information to match some buggy PSBT implementations */ #define WALLY_PSBT_SERIALIZE_FLAG_REDUNDANT 0x1 diff --git a/src/ctest/psbts.h b/src/ctest/psbts.h index 4a3907419..751aaef38 100644 --- a/src/ctest/psbts.h +++ b/src/ctest/psbts.h @@ -91,6 +91,9 @@ static const struct psbt_test invalid_psbts[] = /* PSBT v0 with duplicate 0 length script */ {"cHNidP8BADMBAAAAAREREREREREREREREREREREREfrK3hERERERERERERERfwAAAAD/////AAAAAAAAAQEJAOH1BQAAAAAAAQgBAAEIAwEBUQA=", false, true}, + /* PSBT v0 with one P2PKH input. Outputs are empty. With trailing byte 0x00 */ + {"cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAAAA==", false, true}, + /* PSBT v2 With Global tx version number < 2 */ {"cHNidP8B+wQCAAAAAQIEAQAAAAEDBAECAwQBBAEAAQUBAAA=", false, true}, @@ -103,6 +106,9 @@ static const struct psbt_test invalid_psbts[] = /* PSBT v2 missing tx version */ {"cHNidP8B+wQCAAAAAQQBAAEFAQAA", false, true}, + /* PSBT v2 (no inputs, no outputs) with trailing byte 0x00 */ + {"cHNidP8BAgQCAAAAAQMEAAAAAAEEAQABBQEAAfsEAgAAAAAA", false, true}, + /* PSBT v3 - invalid version number */ {"cHNidP8B+wQDAAAAAQIEAgAAAAEFAQAA", false, true}, diff --git a/src/data/psbt.json b/src/data/psbt.json index 55859450a..d7baed78b 100644 --- a/src/data/psbt.json +++ b/src/data/psbt.json @@ -112,6 +112,10 @@ "comment": "PSBT v0 with duplicate 0 length script", "psbt": "cHNidP8BADMBAAAAAREREREREREREREREREREREREfrK3hERERERERERERERfwAAAAD/////AAAAAAAAAQEJAOH1BQAAAAAAAQgBAAEIAwEBUQA=" }, + { + "comment": "PSBT v0 with one P2PKH input. Outputs are empty. With trailing byte 0x00", + "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAAAA==" + }, { "comment": "PSBT v2 With Global tx version number < 2", "psbt": "cHNidP8B+wQCAAAAAQIEAQAAAAEDBAECAwQBBAEAAQUBAAA=" @@ -128,6 +132,10 @@ "comment": "PSBT v2 missing tx version", "psbt": "cHNidP8B+wQCAAAAAQQBAAEFAQAA" }, + { + "comment": "PSBT v2 (no inputs, no outputs) with trailing byte 0x00", + "psbt": "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQABBQEAAfsEAgAAAAAA" + }, { "comment": "PSBT v3 - invalid version number", "psbt": "cHNidP8B+wQDAAAAAQIEAgAAAAEFAQAA" diff --git a/src/psbt.c b/src/psbt.c index 195ed58ef..4be9a324c 100644 --- a/src/psbt.c +++ b/src/psbt.c @@ -31,7 +31,9 @@ #define PSBT_ID_ALL_FLAGS (WALLY_PSBT_ID_AS_V2 | WALLY_PSBT_ID_USE_LOCKTIME) /* All allowed flags for wally_psbt_from_[bytes|base64]() */ -#define PSBT_ALL_PARSE_FLAGS (WALLY_PSBT_PARSE_FLAG_STRICT|WALLY_PSBT_PARSE_FLAG_LOOSE) +#define PSBT_ALL_PARSE_FLAGS (WALLY_PSBT_PARSE_FLAG_STRICT | \ + WALLY_PSBT_PARSE_FLAG_LOOSE | \ + WALLY_PSBT_PARSE_FLAG_COMPLETE) static const uint8_t PSBT_MAGIC[5] = {'p', 's', 'b', 't', 0xff}; static const uint8_t PSET_MAGIC[5] = {'p', 's', 'e', 't', 0xff}; @@ -2830,6 +2832,8 @@ int wally_psbt_from_bytes(const unsigned char *bytes, size_t len, if (ret == WALLY_OK && !*cursor) ret = WALLY_EINVAL; /* Ran out of data */ + else if (ret == WALLY_OK && *max && (flags & WALLY_PSBT_PARSE_FLAG_COMPLETE)) + ret = WALLY_EINVAL; /* Trailing data */ if (ret != WALLY_OK) { wally_psbt_free(*output); @@ -3523,8 +3527,9 @@ int wally_psbt_from_base64_n(const char *str_in, size_t str_len, uint32_t flags, goto done; } - /* decode the psbt */ - ret = wally_psbt_from_bytes(decoded, written, flags, output); + /* Parse the psbt. For base64, require all bytes to be consumed. */ + ret = wally_psbt_from_bytes(decoded, written, + flags | WALLY_PSBT_PARSE_FLAG_COMPLETE, output); done: clear_and_free(decoded, max_len); diff --git a/src/wasm_package/src/const.js b/src/wasm_package/src/const.js index 34c86bca3..eafc5171e 100755 --- a/src/wasm_package/src/const.js +++ b/src/wasm_package/src/const.js @@ -165,6 +165,7 @@ export const WALLY_PSBT_ID_AS_V2 = 0x1; /* Compute PSBT v0 IDs like v2 by settin export const WALLY_PSBT_ID_BIP370 = 0x0; /* BIP370 compatible */ export const WALLY_PSBT_ID_USE_LOCKTIME = 0x2; /* Do not set locktime to 0 before calculating id */ export const WALLY_PSBT_INIT_PSET = 0x1; +export const WALLY_PSBT_PARSE_FLAG_COMPLETE = 0x4; /* Require parsing to consume the entire input */ export const WALLY_PSBT_PARSE_FLAG_LOOSE = 0x2; /* Allow disallowed and missing mandatory fields */ export const WALLY_PSBT_PARSE_FLAG_STRICT = 0x1; /* Parse strictly according to the PSBT/PSET spec */ export const WALLY_PSBT_PROPRIETARY_TYPE = 0xFC;