keygen: move unsafe pubkey generation backends to test suite

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.
This commit is contained in:
The MMGen Project 2026-09-15 10:51:03 +00:00
commit 75b881a6d3
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
11 changed files with 125 additions and 98 deletions

View file

@ -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

View file

@ -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

View file

@ -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',),

View file

@ -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__

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,4 @@
from .keygen_orig import *
backend_data['std']['backends'] = ('libsecp256k1', 'python-ecdsa')
backend_data['monero']['backends'] = ('nacl', 'ed25519ll-djbec', 'ed25519')

View file

@ -0,0 +1,45 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# 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

View file

@ -0,0 +1,61 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# 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