crypto: add ec_public_key_compress to produce a compressed pubkey
What changed, and why it matters
This commit adds a new public-key compression function to a cryptographic library and updates the existing decompression function so both can accept already-compressed or already-uncompressed keys as input. It also adds tests and language bindings. There is no security vulnerability here; it is a normal feature addition with defensive input validation.
No security action required. Treat as a routine feature/API update.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces wally_ec_public_key_compress and refactors wally_ec_public_key_decompress to share a common pk_convert_impl helper. Both functions now accept either EC_PUBLIC_KEY_LEN (33-byte compressed) or EC_PUBLIC_KEY_UNCOMPRESSED_LEN (65-byte uncompressed) inputs, parse the key through secp256k1, and serialize to the requested output format. Invalid keys are rejected even when input and output formats match. Tests verify round-trip behavior, no-op behavior, and rejection of invalid keys/lengths/null pointers. Bindings are added for C++, Java SWIG, Python, WASM/JS, and TypeScript declarations.
Changed components
src/sign.cinclude/wally_crypto.hinclude/wally.hppsrc/swig_java/swig.isrc/swig_python/python_extra.py_insrc/wasm_package/src/functions.jssrc/wasm_package/src/index.d.tstools/wasm_exports.shsrc/test/test_sign.pysrc/test/util.pyInspect captured patch +92 / −16
diff --git a/include/wally.hpp b/include/wally.hpp
index 4e699a4..1dd7adb 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 <class PUB_KEY, class BYTES_OUT>
+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 <class PUB_KEY, class BYTES_OUT>
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 30df680..34fe783 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 0399102..f47f71e 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)
{
diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i
index 8b310ec..cf2b899 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 566d10a..3a05b8f 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 4aec728..c85394f 100755
--- a/src/test/test_sign.py
+++ b/src/test/test_sign.py
@@ -40,6 +40,8 @@ class SignTests(unittest.TestCase):
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 @@ class SignTests(unittest.TestCase):
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,12 +84,39 @@ class SignTests(unittest.TestCase):
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)
diff --git a/src/test/util.py b/src/test/util.py
index ddbc5ca..a697414 100755
--- a/src/test/util.py
+++ b/src/test/util.py
@@ -345,6 +345,7 @@ for f in (
('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 5c2ccda..e6f5131 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 f213c33..b9c5a8e 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 9d8ab1d..6c45e2d 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' \
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.