From 75b881a6d3788ca345b07b38d96432e793dcd4ac Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 15 Sep 2026 10:51:03 +0000 Subject: [PATCH] keygen: move unsafe pubkey generation backends to test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Though the unsafe implementations in this commit and the preceding ones are/were only used for testing, their presence in the code base triggers certain code vulnerability checkers, so best to move them out of harm’s way. --- MANIFEST.in | 1 + mmgen/cfg.py | 2 +- mmgen/keygen.py | 10 +-- mmgen/proto/secp256k1/keygen.py | 43 ++----------- mmgen/proto/xmr/keygen.py | 50 +-------------- mmgen/proto/zec/keygen.py | 2 - test/include/ecc.py | 4 +- test/modtest_d/rune.py | 1 + test/overlay/fakemods/mmgen/keygen.py | 4 ++ .../fakemods/mmgen/proto/secp256k1/keygen.py | 45 ++++++++++++++ .../fakemods/mmgen/proto/xmr/keygen.py | 61 +++++++++++++++++++ 11 files changed, 125 insertions(+), 98 deletions(-) create mode 100644 test/overlay/fakemods/mmgen/keygen.py create mode 100644 test/overlay/fakemods/mmgen/proto/secp256k1/keygen.py create mode 100644 test/overlay/fakemods/mmgen/proto/xmr/keygen.py diff --git a/MANIFEST.in b/MANIFEST.in index 5e34928a..868d6d57 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,6 +22,7 @@ include test/ref/*/*/* include test/ref/*/*/*/* include test/overlay/fakemods/mmgen/*.py include test/overlay/fakemods/mmgen/*/*.py +include test/overlay/fakemods/mmgen/*/*/*.py include test/overlay/fakemods/mmgen/*/*/*/*.py include test/test-release.sh diff --git a/mmgen/cfg.py b/mmgen/cfg.py index fac3ade3..e6b90238 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -260,7 +260,7 @@ class Config(Lockable): xmrwallet_compat = False priority = 0 - test_suite = False # 25 references + test_suite = False # 22 references # external use: _opts = None diff --git a/mmgen/keygen.py b/mmgen/keygen.py index ce8ef7e0..7e050ae9 100755 --- a/mmgen/keygen.py +++ b/mmgen/keygen.py @@ -33,11 +33,7 @@ keygen_public_data = namedtuple( class keygen_base: def __init__(self, cfg): - if not (self.production_safe or cfg.test_suite): - from .util import die - die(2, - f'Public key generator {type(self).__name__!r} is not safe from timing attacks ' - 'and may only be used in a testing environment') + pass def gen_data(self, privkey): assert isinstance(privkey, PrivKey) @@ -56,10 +52,10 @@ class keygen_base: backend_data = { 'std': { - 'backends': ('libsecp256k1', 'python-ecdsa'), + 'backends': ('libsecp256k1',), 'package': 'secp256k1'}, 'monero': { - 'backends': ('nacl', 'ed25519ll-djbec', 'ed25519'), + 'backends': ('nacl',), 'package': 'xmr'}, 'zcash_z': { 'backends': ('nacl',), diff --git a/mmgen/proto/secp256k1/keygen.py b/mmgen/proto/secp256k1/keygen.py index 6dde7b66..438a9d74 100755 --- a/mmgen/proto/secp256k1/keygen.py +++ b/mmgen/proto/secp256k1/keygen.py @@ -38,40 +38,9 @@ class backend: @classmethod def get_clsname(cls, cfg, *, silent=False): - try: - from .secp256k1 import pubkey_gen - if not pubkey_gen(bytes.fromhex('deadbeef'*8), 1): - from ...util import die - die('ExtensionModuleError', - 'Unable to execute pubkey_gen() from secp256k1 extension module') - return cls.__name__ - except ImportError as e: - if not silent: - from ...util import ymsg - ymsg(str(e)) - cfg._util.qmsg('Using (slow) native Python ECDSA library for public key generation') - return 'python_ecdsa' - - class python_ecdsa(keygen_base): - - production_safe = False - - def __init__(self, cfg): - super().__init__(cfg) - import ecdsa - self.ecdsa = ecdsa - - def to_pubkey(self, privkey): - """ - devdoc/guide_wallets.md: - Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or - 0x02 depending on whether they're greater or less than the midpoint of the curve. - """ - def privnum2pubkey(numpriv, *, compressed=False): - pk = self.ecdsa.SigningKey.from_secret_exponent(numpriv, curve=self.ecdsa.SECP256k1) - # vk_bytes = x (32 bytes) + y (32 bytes) (unsigned big-endian) - return pubkey_format(pk.verifying_key.to_string(), compressed) - - return PubKey( - s = privnum2pubkey(int.from_bytes(privkey, 'big'), compressed=privkey.compressed), - compressed = privkey.compressed) + from .secp256k1 import pubkey_gen + if not pubkey_gen(bytes.fromhex('deadbeef'*8), 1): + from ...util import die + die('ExtensionModuleError', + 'Unable to execute pubkey_gen() from secp256k1 extension module') + return cls.__name__ diff --git a/mmgen/proto/xmr/keygen.py b/mmgen/proto/xmr/keygen.py index ded6760f..176db4ca 100755 --- a/mmgen/proto/xmr/keygen.py +++ b/mmgen/proto/xmr/keygen.py @@ -34,8 +34,6 @@ class backend: class nacl(base): - production_safe = True - def __init__(self, cfg): super().__init__(cfg) from nacl.bindings import crypto_scalarmult_ed25519_base_noclamp @@ -45,50 +43,4 @@ class backend: return PubKey( self.scalarmultbase(privkey) + self.scalarmultbase(self.to_viewkey(privkey)), - compressed = privkey.compressed - ) - - class ed25519(base): - - production_safe = False - - def __init__(self, cfg): - super().__init__(cfg) - from ...contrib.ed25519 import edwards, encodepoint, B, scalarmult - self.edwards = edwards - self.encodepoint = encodepoint - self.B = B - self.scalarmult = scalarmult - - def scalarmultbase(self, privnum): - """ - Source and license for scalarmultbase function: - https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py - Copyright (c) 2014-2016, The Monero Project - All rights reserved. - """ - if privnum == 0: - return [0, 1] - Q = self.scalarmult(self.B, privnum//2) - Q = self.edwards(Q, Q) - if privnum & 1: - Q = self.edwards(Q, self.B) - return Q - - @staticmethod - def rev_bytes2int(in_bytes): - return int.from_bytes(in_bytes[::-1], 'big') - - def to_pubkey(self, privkey): - return PubKey( - self.encodepoint(self.scalarmultbase(self.rev_bytes2int(privkey))) + - self.encodepoint(self.scalarmultbase(self.rev_bytes2int(self.to_viewkey(privkey)))), - compressed = privkey.compressed - ) - - class ed25519ll_djbec(ed25519): - - def __init__(self, cfg): - super().__init__(cfg) - from ...contrib.ed25519ll_djbec import scalarmult - self.scalarmult = scalarmult + compressed = privkey.compressed) diff --git a/mmgen/proto/zec/keygen.py b/mmgen/proto/zec/keygen.py index b9efcb49..f8f10cd1 100755 --- a/mmgen/proto/zec/keygen.py +++ b/mmgen/proto/zec/keygen.py @@ -19,8 +19,6 @@ class backend: class nacl(keygen_base): - production_safe = True - def __init__(self, cfg): super().__init__(cfg) from nacl.bindings import crypto_scalarmult_base diff --git a/test/include/ecc.py b/test/include/ecc.py index 1b9ef781..2f502539 100755 --- a/test/include/ecc.py +++ b/test/include/ecc.py @@ -13,7 +13,7 @@ test.include.ecc: elliptic curve utilities for the MMGen test suite """ import ecdsa, hashlib -from mmgen.proto.secp256k1.keygen import pubkey_format +from mmgen.proto.secp256k1.keygen import overlay_fake_pubkey_format def _pubkey_to_pub_point(vk_bytes): try: @@ -38,7 +38,7 @@ def pubkey_tweak_add_pyecdsa(vk_bytes, pk_addend_bytes): ecdsa.SigningKey.from_secret_exponent(pk_addend, curve=ecdsa.SECP256k1).verifying_key.pubkey.point ) _check_pub_point(point_sum, vk_bytes, pk_addend_bytes) - return pubkey_format( + return overlay_fake_pubkey_format( ecdsa.VerifyingKey.from_public_point(point_sum, curve=ecdsa.curves.SECP256k1).to_string(), compressed = len(vk_bytes) == 33) diff --git a/test/modtest_d/rune.py b/test/modtest_d/rune.py index b866509f..11437eef 100755 --- a/test/modtest_d/rune.py +++ b/test/modtest_d/rune.py @@ -163,6 +163,7 @@ def test_tx(src, cfg, vec): vmsg('\n TX info:\n ' + '\n '.join(tx_info(tx, proto)) + '\n') tx.verify_sig(cfg, proto, parms.account_number) + tx.verify_sig(cfg, proto, parms.account_number, backend='ecdsa') pubkey = tx.authInfo.signerInfos[0].publicKey.key.data vec_txid2 = getattr(vec, 'txid2', None) diff --git a/test/overlay/fakemods/mmgen/keygen.py b/test/overlay/fakemods/mmgen/keygen.py new file mode 100644 index 00000000..24a8e919 --- /dev/null +++ b/test/overlay/fakemods/mmgen/keygen.py @@ -0,0 +1,4 @@ +from .keygen_orig import * + +backend_data['std']['backends'] = ('libsecp256k1', 'python-ecdsa') +backend_data['monero']['backends'] = ('nacl', 'ed25519ll-djbec', 'ed25519') diff --git a/test/overlay/fakemods/mmgen/proto/secp256k1/keygen.py b/test/overlay/fakemods/mmgen/proto/secp256k1/keygen.py new file mode 100644 index 00000000..5c08d16c --- /dev/null +++ b/test/overlay/fakemods/mmgen/proto/secp256k1/keygen.py @@ -0,0 +1,45 @@ +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2026 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +test.overlay.fakemods.mmgen.proto.secp256k1.keygen: + testing secp256k1 public key generation backends for the MMGen suite +""" + +from .keygen_orig import * + +def overlay_fake_pubkey_format(vk_bytes, compressed): + # if compressed, discard Y coord, replace with appropriate version byte + # even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0 + return (b'\x02', b'\x03')[vk_bytes[-1] & 1] + vk_bytes[:32] if compressed else b'\x04' + vk_bytes + +class overlay_fake_backend: + + class python_ecdsa(keygen_base): + + def __init__(self, cfg): + super().__init__(cfg) + import ecdsa + self.ecdsa = ecdsa + + def to_pubkey(self, privkey): + """ + devdoc/guide_wallets.md: + Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or + 0x02 depending on whether they're greater or less than the midpoint of the curve. + """ + def privnum2pubkey(numpriv, *, compressed=False): + pk = self.ecdsa.SigningKey.from_secret_exponent(numpriv, curve=self.ecdsa.SECP256k1) + # vk_bytes = x (32 bytes) + y (32 bytes) (unsigned big-endian) + return overlay_fake_pubkey_format(pk.verifying_key.to_string(), compressed) + + return PubKey( + s = privnum2pubkey(int.from_bytes(privkey, 'big'), compressed=privkey.compressed), + compressed = privkey.compressed) + +backend.python_ecdsa = overlay_fake_backend.python_ecdsa diff --git a/test/overlay/fakemods/mmgen/proto/xmr/keygen.py b/test/overlay/fakemods/mmgen/proto/xmr/keygen.py new file mode 100644 index 00000000..2505d250 --- /dev/null +++ b/test/overlay/fakemods/mmgen/proto/xmr/keygen.py @@ -0,0 +1,61 @@ +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2026 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +test.overlay.fakemods.mmgen.proto.xmr.keygen: + testing Monero public key generation backends for the MMGen suite +""" + +from .keygen_orig import * + +class overlay_fake_backend: + + class ed25519(backend.base): + + def __init__(self, cfg): + super().__init__(cfg) + from ...contrib.ed25519 import edwards, encodepoint, B, scalarmult + self.edwards = edwards + self.encodepoint = encodepoint + self.B = B + self.scalarmult = scalarmult + + def scalarmultbase(self, privnum): + """ + Source and license for scalarmultbase function: + https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py + Copyright (c) 2014-2016, The Monero Project + All rights reserved. + """ + if privnum == 0: + return [0, 1] + Q = self.scalarmult(self.B, privnum//2) + Q = self.edwards(Q, Q) + if privnum & 1: + Q = self.edwards(Q, self.B) + return Q + + @staticmethod + def rev_bytes2int(in_bytes): + return int.from_bytes(in_bytes[::-1], 'big') + + def to_pubkey(self, privkey): + return PubKey( + self.encodepoint(self.scalarmultbase(self.rev_bytes2int(privkey))) + + self.encodepoint(self.scalarmultbase(self.rev_bytes2int(self.to_viewkey(privkey)))), + compressed = privkey.compressed) + + class ed25519ll_djbec(ed25519): + + def __init__(self, cfg): + super().__init__(cfg) + from ...contrib.ed25519ll_djbec import scalarmult + self.scalarmult = scalarmult + +backend.ed25519 = overlay_fake_backend.ed25519 +backend.ed25519ll_djbec = overlay_fake_backend.ed25519ll_djbec