diff --git a/include/wally.hpp b/include/wally.hpp index 4e699a454..1dd7adbc4 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -677,6 +677,12 @@ inline int ec_public_key_bip341_tweak(const PUB_KEY& pub_key, const MERKLE_ROOT& return detail::check_ret(__FUNCTION__, ret); } +template +inline int ec_public_key_compress(const PUB_KEY& pub_key, BYTES_OUT& bytes_out) { + int ret = ::wally_ec_public_key_compress(pub_key.data(), pub_key.size(), bytes_out.data(), bytes_out.size()); + return detail::check_ret(__FUNCTION__, ret); +} + template inline int ec_public_key_decompress(const PUB_KEY& pub_key, BYTES_OUT& bytes_out) { int ret = ::wally_ec_public_key_decompress(pub_key.data(), pub_key.size(), bytes_out.data(), bytes_out.size()); diff --git a/include/wally_crypto.h b/include/wally_crypto.h index 30df68019..34fe783d7 100644 --- a/include/wally_crypto.h +++ b/include/wally_crypto.h @@ -427,11 +427,25 @@ WALLY_CORE_API int wally_ec_public_key_from_private_key( size_t len); /** - * Create an uncompressed public key from a compressed public key. + * Create a compressed public key from a public key. + * + * :param pub_key: The public key to compress. + * :param pub_key_len: The length of ``pub_key`` in bytes. Must be `EC_PUBLIC_KEY_LEN` or `EC_PUBLIC_KEY_UNCOMPRESSED_LEN`. + * :param bytes_out: Destination for the resulting compressed public key. + * FIXED_SIZED_OUTPUT(len, bytes_out, EC_PUBLIC_KEY_LEN) + */ +WALLY_CORE_API int wally_ec_public_key_compress( + const unsigned char *pub_key, + size_t pub_key_len, + unsigned char *bytes_out, + size_t len); + +/** + * Create an uncompressed public key from a public key. * * :param pub_key: The public key to decompress. - * :param pub_key_len: The length of ``pub_key`` in bytes. Must be `EC_PUBLIC_KEY_LEN`. - * :param bytes_out: Destination for the resulting public key. + * :param pub_key_len: The length of ``pub_key`` in bytes. Must be `EC_PUBLIC_KEY_LEN` or `EC_PUBLIC_KEY_UNCOMPRESSED_LEN`. + * :param bytes_out: Destination for the resulting uncompressed public key. * FIXED_SIZED_OUTPUT(len, bytes_out, EC_PUBLIC_KEY_UNCOMPRESSED_LEN) */ WALLY_CORE_API int wally_ec_public_key_decompress( diff --git a/src/sign.c b/src/sign.c index e803b0906..f47f71ead 100644 --- a/src/sign.c +++ b/src/sign.c @@ -93,25 +93,47 @@ int wally_ec_public_key_from_private_key(const unsigned char *priv_key, size_t p return ok ? WALLY_OK : WALLY_EINVAL; } -int wally_ec_public_key_decompress(const unsigned char *pub_key, size_t pub_key_len, - unsigned char *bytes_out, size_t len) +/* Serialize a public key of either format into the format implied by `len`. + * Keys are always parsed, so that invalid keys are rejected even when the + * input and output formats are the same. + */ +static int pk_convert_impl(const unsigned char *pub_key, size_t pub_key_len, + unsigned char *bytes_out, size_t len) { secp256k1_pubkey pub; - size_t len_in_out = EC_PUBLIC_KEY_UNCOMPRESSED_LEN; + unsigned int flags = len == EC_PUBLIC_KEY_LEN ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED; + size_t len_in_out = len; bool ok; - ok = pub_key && pub_key_len == EC_PUBLIC_KEY_LEN && - bytes_out && len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN && - pubkey_parse(&pub, pub_key, pub_key_len) && - pubkey_serialize(bytes_out, &len_in_out, &pub, PUBKEY_UNCOMPRESSED) && - len_in_out == EC_PUBLIC_KEY_UNCOMPRESSED_LEN; + ok = pub_key && + (pub_key_len == EC_PUBLIC_KEY_LEN || + pub_key_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN) && + bytes_out && pubkey_parse(&pub, pub_key, pub_key_len) && + pubkey_serialize(bytes_out, &len_in_out, &pub, flags) && + len_in_out == len; - if (!ok && bytes_out) + if (!ok && bytes_out && len) wally_clear(bytes_out, len); wally_clear(&pub, sizeof(pub)); return ok ? WALLY_OK : WALLY_EINVAL; } +int wally_ec_public_key_compress(const unsigned char *pub_key, size_t pub_key_len, + unsigned char *bytes_out, size_t len) +{ + if (len != EC_PUBLIC_KEY_LEN) + return WALLY_EINVAL; + return pk_convert_impl(pub_key, pub_key_len, bytes_out, len); +} + +int wally_ec_public_key_decompress(const unsigned char *pub_key, size_t pub_key_len, + unsigned char *bytes_out, size_t len) +{ + if (len != EC_PUBLIC_KEY_UNCOMPRESSED_LEN) + return WALLY_EINVAL; + return pk_convert_impl(pub_key, pub_key_len, bytes_out, len); +} + int wally_ec_public_key_negate(const unsigned char *pub_key, size_t pub_key_len, unsigned char *bytes_out, size_t len) { @@ -295,10 +317,15 @@ int wally_ec_sig_from_der(const unsigned char *bytes, size_t bytes_len, const secp256k1_context *ctx = secp256k1_context_static; bool ok; - ok = bytes && bytes_len && bytes_out && len == EC_SIGNATURE_LEN && + ok = bytes && bytes_len && bytes_len <= EC_SIGNATURE_DER_MAX_LEN && + bytes_out && len == EC_SIGNATURE_LEN && secp256k1_ecdsa_signature_parse_der(ctx, &sig_secp, bytes, bytes_len) && secp256k1_ecdsa_signature_serialize_compact(ctx, bytes_out, &sig_secp); + if (ok && (mem_is_zero(bytes_out, EC_SIGNATURE_LEN / 2) || + mem_is_zero(bytes_out + EC_SIGNATURE_LEN / 2, EC_SIGNATURE_LEN / 2))) + ok = false; /* R or S are 0: this signature is invalid */ + if (!ok && bytes_out) wally_clear(bytes_out, len); wally_clear(&sig_secp, sizeof(sig_secp)); diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 8b310ecbd..cf2b89918 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -601,6 +601,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_array_(wally_ec_private_key_bip341_tweak, 6, 7, EC_PRIVATE_KEY_LEN); %returns_void__(wally_ec_private_key_verify); %returns_array_(wally_ec_public_key_bip341_tweak, 6, 7, EC_PUBLIC_KEY_LEN); +%returns_array_(wally_ec_public_key_compress, 3, 4, EC_PUBLIC_KEY_LEN); %returns_array_(wally_ec_public_key_decompress, 3, 4, EC_PUBLIC_KEY_UNCOMPRESSED_LEN); %returns_array_(wally_ec_public_key_from_private_key, 3, 4, EC_PUBLIC_KEY_LEN); %returns_array_(wally_ec_public_key_negate, 3, 4, EC_PUBLIC_KEY_LEN); diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index 566d10a1e..3a05b8f95 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -152,6 +152,7 @@ descriptor_get_key_origin_fingerprint = _wrap_bin(descriptor_get_key_origin_fing 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) +ec_public_key_compress = _wrap_bin(ec_public_key_compress, EC_PUBLIC_KEY_LEN) ec_public_key_decompress = _wrap_bin(ec_public_key_decompress, EC_PUBLIC_KEY_UNCOMPRESSED_LEN) ec_public_key_from_private_key = _wrap_bin(ec_public_key_from_private_key, EC_PUBLIC_KEY_LEN) ec_public_key_negate = _wrap_bin(ec_public_key_negate, EC_PUBLIC_KEY_LEN) diff --git a/src/test/test_sign.py b/src/test/test_sign.py index 63199357d..c85394f23 100755 --- a/src/test/test_sign.py +++ b/src/test/test_sign.py @@ -40,6 +40,8 @@ def sign(self, priv_key, msg, flags, out_buf, out_len=None, aux=None, aux_len=No def test_sign_and_verify(self): sig, sig2, sig_low_r = self.cbufferize(['00' * EC_SIGNATURE_LEN] * 3) der, der_len = make_cbuffer('00' * EC_SIGNATURE_DER_MAX_LEN) + pub_key, pub_key2 = make_cbuffer('00' * 33)[0], make_cbuffer('00' * 33)[0] + pub_unc, pub_unc2 = make_cbuffer('00' * 65)[0], make_cbuffer('00' * 65)[0] for case in self.get_sign_cases(): priv_key, msg, nonce, r, s = case @@ -74,7 +76,6 @@ def test_sign_and_verify(self): self.assertEqual((ret, h(sig)), (WALLY_OK, h(sig2))) # All sigs low-s # Verify - pub_key, _ = make_cbuffer('00' * 33) ret = wally_ec_public_key_from_private_key(priv_key, len(priv_key), pub_key, len(pub_key)) self.assertEqual(ret, WALLY_OK) @@ -83,15 +84,53 @@ def test_sign_and_verify(self): FLAG_ECDSA, s, len(s)) self.assertEqual(ret, WALLY_OK) - # Validate public key - pub_unc, _ = make_cbuffer('00' * 65) + # Validate public key, Check pubkey compression/decompression + self.assertEqual(wally_ec_public_key_verify(pub_key, len(pub_key)), WALLY_OK) + ret = wally_ec_public_key_decompress(pub_key, len(pub_key), pub_unc, len(pub_unc)) self.assertEqual(ret, WALLY_OK) - self.assertEqual(wally_ec_public_key_verify(pub_key, len(pub_key)), WALLY_OK) self.assertEqual(wally_ec_public_key_verify(pub_unc, len(pub_unc)), WALLY_OK) + ret = wally_ec_public_key_decompress(pub_unc, len(pub_unc), pub_unc2, len(pub_unc2)) + self.assertEqual(ret, WALLY_OK) + self.assertEqual(pub_unc, pub_unc2) + + for p, l in [(pub_key, len(pub_key)),(pub_unc, len(pub_unc))]: + ret = wally_ec_public_key_compress(p, l, pub_key2, len(pub_key2)) + self.assertEqual(ret, WALLY_OK) + self.assertEqual(pub_key, pub_key2) + ret = wally_ec_public_key_verify(pub_key2, len(pub_key2)) + self.assertEqual(ret, WALLY_OK) + + # Invalid keys are rejected, including when no conversion is needed + bad_key, bad_key_len = make_cbuffer('02' + 'ff' * 32) + bad_unc, bad_unc_len = make_cbuffer('04' + 'ff' * 64) + for p, l in [(bad_key, bad_key_len), (bad_unc, bad_unc_len)]: + ret = wally_ec_public_key_compress(p, l, pub_key2, len(pub_key2)) + self.assertEqual(ret, WALLY_EINVAL) + ret = wally_ec_public_key_decompress(p, l, pub_unc2, len(pub_unc2)) + self.assertEqual(ret, WALLY_EINVAL) + + # Invalid lengths are rejected + for fn, out in [(wally_ec_public_key_compress, pub_key2), + (wally_ec_public_key_decompress, pub_unc2)]: + self.assertEqual(fn(None, 33, out, len(out)), WALLY_EINVAL) + self.assertEqual(fn(pub_key, 32, out, len(out)), WALLY_EINVAL) + self.assertEqual(fn(pub_key, len(pub_key), None, len(out)), WALLY_EINVAL) + self.assertEqual(fn(pub_key, len(pub_key), out, len(out) - 1), WALLY_EINVAL) set_fake_ec_nonce(None) + def test_der_convert(self): + out_buf, out_len = make_cbuffer('00' * EC_SIGNATURE_LEN) + long_der = '3081c4026001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa026001bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + for bad_der in [ + long_der, # R or S overflow + '3006020100020101', # R = 0 + '3006020101020100' # S = 0 + ]: + der, der_len = make_cbuffer(bad_der) + ret = wally_ec_sig_from_der(der, der_len, out_buf, out_len) + self.assertEqual(ret, WALLY_EINVAL) def test_invalid_inputs(self): out_buf, out_len = make_cbuffer('00' * EC_SIGNATURE_LEN) diff --git a/src/test/util.py b/src/test/util.py index ddbc5ca2d..a69741472 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -345,6 +345,7 @@ class wally_psbt(Structure): ('wally_ec_private_key_bip341_tweak', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), ('wally_ec_private_key_verify', c_int, [c_void_p, c_size_t]), ('wally_ec_public_key_bip341_tweak', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_uint32, c_void_p, c_size_t]), + ('wally_ec_public_key_compress', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), ('wally_ec_public_key_decompress', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), ('wally_ec_public_key_from_private_key', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), ('wally_ec_public_key_negate', c_int, [c_void_p, c_size_t, c_void_p, c_size_t]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 5c2ccda0e..e6f51317e 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -185,6 +185,7 @@ export const descriptor_to_script_get_maximum_length = wrap('wally_descriptor_to export const ec_private_key_bip341_tweak = wrap('wally_ec_private_key_bip341_tweak', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.EC_PRIVATE_KEY_LEN)]); export const ec_private_key_verify = wrap('wally_ec_private_key_verify', [T.Bytes]); export const ec_public_key_bip341_tweak = wrap('wally_ec_public_key_bip341_tweak', [T.Bytes, T.Bytes, T.Int32, T.DestPtrSized(T.Bytes, C.EC_PUBLIC_KEY_LEN)]); +export const ec_public_key_compress = wrap('wally_ec_public_key_compress', [T.Bytes, T.DestPtrSized(T.Bytes, C.EC_PUBLIC_KEY_LEN)]); export const ec_public_key_decompress = wrap('wally_ec_public_key_decompress', [T.Bytes, T.DestPtrSized(T.Bytes, C.EC_PUBLIC_KEY_UNCOMPRESSED_LEN)]); export const ec_public_key_from_private_key = wrap('wally_ec_public_key_from_private_key', [T.Bytes, T.DestPtrSized(T.Bytes, C.EC_PUBLIC_KEY_LEN)]); export const ec_public_key_negate = wrap('wally_ec_public_key_negate', [T.Bytes, T.DestPtrSized(T.Bytes, C.EC_PUBLIC_KEY_LEN)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index f213c335a..b9c5a8eb1 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -145,6 +145,7 @@ export function descriptor_to_script_get_maximum_length(descriptor: Ref_wally_de export function ec_private_key_bip341_tweak(priv_key: Buffer|Uint8Array|null, merkle_root: Buffer|Uint8Array|null, flags: number): Buffer; export function ec_private_key_verify(priv_key: Buffer|Uint8Array|null): void; export function ec_public_key_bip341_tweak(pub_key: Buffer|Uint8Array|null, merkle_root: Buffer|Uint8Array|null, flags: number): Buffer; +export function ec_public_key_compress(pub_key: Buffer|Uint8Array|null): Buffer; export function ec_public_key_decompress(pub_key: Buffer|Uint8Array|null): Buffer; export function ec_public_key_from_private_key(priv_key: Buffer|Uint8Array|null): Buffer; export function ec_public_key_negate(pub_key: Buffer|Uint8Array|null): Buffer; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 9d8ab1d17..6c45e2dcf 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -111,6 +111,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_ec_private_key_bip341_tweak' \ ,'_wally_ec_private_key_verify' \ ,'_wally_ec_public_key_bip341_tweak' \ +,'_wally_ec_public_key_compress' \ ,'_wally_ec_public_key_decompress' \ ,'_wally_ec_public_key_from_private_key' \ ,'_wally_ec_public_key_negate' \