diff --git a/benchmark/key.cpp b/benchmark/key.cpp index 730efe8246..f152ff784f 100644 --- a/benchmark/key.cpp +++ b/benchmark/key.cpp @@ -42,7 +42,7 @@ void k1_benchmarking() { void r1_benchmarking() { auto payload = "Test Cases"; auto digest = sha256::hash(payload, const_strlen(payload)); - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::r1); auto sign_f = [&]() { key.sign(digest); diff --git a/libraries/chain/include/sysio/chain/abi_serializer.hpp b/libraries/chain/include/sysio/chain/abi_serializer.hpp index f1e650535d..f45b14664f 100644 --- a/libraries/chain/include/sysio/chain/abi_serializer.hpp +++ b/libraries/chain/include/sysio/chain/abi_serializer.hpp @@ -647,27 +647,8 @@ namespace impl { add(mvo, "context_free_actions", trx.context_free_actions, resolver, ctx); add(mvo, "actions", trx.actions, resolver, ctx); - // process contents of block.transaction_extensions - auto exts = trx.validate_and_extract_extensions(); - // Iterate through every extension entry and serialize known ones: - for ( auto const& kv : exts ) { - auto id = kv.first; - auto const& var = kv.second; - switch (id) { - case ed_pubkey_extension::extension_id(): { - // Unpack our ED25519 pubkey extension - const auto& edext = std::get(var); - // Emit the 32-byte pubkey (you can change the field name as needed) - mvo("ed_pubkey", edext.pubkey); - break; - } - // future case XXX_extension::extension_id(): … - default: - // Should never reach this as validate_and_extract_extensions() should only return known extensions - SYS_ASSERT( false, invalid_transaction_extension, "Transaction extension with id type {} is not supported", id); - break; - } - } + // No transaction extensions are currently supported. + // When extensions are added in the future, deserialize them here. out(name, std::move(mvo)); } diff --git a/libraries/chain/include/sysio/chain/transaction.hpp b/libraries/chain/include/sysio/chain/transaction.hpp index 8ae2a8a80e..bf928c0924 100644 --- a/libraries/chain/include/sysio/chain/transaction.hpp +++ b/libraries/chain/include/sysio/chain/transaction.hpp @@ -9,14 +9,11 @@ namespace sysio { namespace chain { using account_subjective_cpu_bill_t = flat_map; using action_payers_t = flat_set; - /** - * This extension is for including an ED25519 public key in a transaction for signature verification. Generic public_key_type was used to future proof - * in the scenario where another ED25519 curve variant is added. We don't want to add another extension per variant. - */ - struct ed_pubkey_extension { - static constexpr uint16_t extension_id() { return 0x8000; } // 32768 in decimal - static constexpr bool enforce_unique() { return false; } - public_key_type pubkey; // 32-byte public key + // No transaction extensions remain. Keep the type infrastructure for future use. + // Remove placeholder when a transaction extension is needed. + struct transaction_extension_placeholder { + static constexpr uint16_t extension_id() { return 0xFFFF; } + static constexpr bool enforce_unique() { return true; } }; namespace detail { @@ -28,7 +25,7 @@ namespace sysio { namespace chain { } using transaction_extension_types = detail::transaction_extension_types< - ed_pubkey_extension + transaction_extension_placeholder >; using transaction_extension = transaction_extension_types::transaction_extension_t; @@ -214,4 +211,4 @@ FC_REFLECT_DERIVED( sysio::chain::signed_transaction, (sysio::chain::transaction FC_REFLECT_ENUM( sysio::chain::packed_transaction::compression_type, (none)(zlib)) // @ignore unpacked_trx FC_REFLECT( sysio::chain::packed_transaction, (signatures)(compression)(packed_context_free_data)(packed_trx) ) -FC_REFLECT( sysio::chain::ed_pubkey_extension, (pubkey) ) +FC_REFLECT( sysio::chain::transaction_extension_placeholder, ) diff --git a/libraries/chain/transaction.cpp b/libraries/chain/transaction.cpp index 40a97f7403..a5f40ff957 100644 --- a/libraries/chain/transaction.cpp +++ b/libraries/chain/transaction.cpp @@ -12,7 +12,6 @@ #include #include -#include #include namespace sysio { namespace chain { @@ -56,26 +55,7 @@ fc::microseconds transaction::get_signature_keys( const vector& auto start = fc::time_point::now(); recovered_pub_keys.clear(); - // 1) Extract and validate extensions - auto validated_ext = validate_and_extract_extensions(); - vector ed_pubkeys; - for ( auto const& item : validated_ext ) { - if ( auto* e = std::get_if(&item.second) ) { - ed_pubkeys.emplace_back(e->pubkey); - } - } - auto to_pk_str = [&](const auto& pk){ try { return pk.to_string([&]() {FC_CHECK_DEADLINE(deadline); }); } catch (...) { return std::string("unknown"); } }; - if( !allow_duplicate_keys ) { - flat_set seen; - for( auto& pk : ed_pubkeys ) { - auto [it, inserted] = seen.emplace(pk); - SYS_ASSERT( inserted, tx_duplicate_sig, "duplicate ED public-key extension for key {}", to_pk_str(pk) ); - } - } - - // Prepare index for public key extensions. - size_t pubkey_idx = 0; if ( !signatures.empty() ) { const digest_type digest = sig_digest(chain_id, cfd); @@ -85,35 +65,20 @@ fc::microseconds transaction::get_signature_keys( const vector& SYS_ASSERT( now < deadline, tx_cpu_usage_exceeded, "sig verification timed out {}us", now-start ); - // dynamic dispatch into the correct path sig.visit([&](auto const& shim){ using Shim = std::decay_t; if constexpr ( std::is_same_v) { SYS_THROW(fc::unsupported_exception, "BLS signatures can not be used to recover public keys."); - } else if constexpr( Shim::is_recoverable ) { - // If public key can be recovered from signature + } else { + static_assert( Shim::is_recoverable, "All non-BLS signature types must be recoverable" ); auto [itr, ok] = recovered_pub_keys.emplace(fc::crypto::public_key::recover(sig, digest)); SYS_ASSERT( allow_duplicate_keys || ok, tx_duplicate_sig, "duplicate signature for key {}", to_pk_str(*itr) ); - } else { - // If public key cannot be recovered from signature, we need to get it from transaction extensions and use verify. - SYS_ASSERT( pubkey_idx < ed_pubkeys.size(), unsatisfied_authorization, "missing ED pubkey extension for signature #{}", pubkey_idx ); - - const auto& pubkey = ed_pubkeys[pubkey_idx++]; - const auto& pubkey_shim = pubkey.template get(); - - SYS_ASSERT( shim.verify(digest, pubkey_shim), unsatisfied_authorization, "non-recoverable signature #{} failed", pubkey_idx-1 ); - - recovered_pub_keys.emplace(pubkey); } }); } } - // Ensure no extra ED pubkey extensions were provided - SYS_ASSERT( pubkey_idx == ed_pubkeys.size(), unsatisfied_authorization, - "got {} ED public-key extensions but only {} ED signatures", ed_pubkeys.size(), pubkey_idx ); - return fc::time_point::now() - start; } FC_CAPTURE_AND_RETHROW("") } diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index 9e0a710d12..857dcc2773 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -280,10 +280,12 @@ namespace sysio::chain { void transaction_context::init_for_input_trx() { const transaction& trx = packed_trx.get_transaction(); - // delayed and compressed transactions are not supported by wire + // delayed, compressed, and extension-bearing transactions are not supported by wire SYS_ASSERT( trx.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" ); SYS_ASSERT( packed_trx.get_compression() == packed_transaction::compression_type::none, tx_compression_not_allowed, "packed transaction cannot be compressed"); + SYS_ASSERT( trx.transaction_extensions.empty(), invalid_transaction_extension, + "transaction extensions are not currently supported" ); is_input = true; if (!control.skip_trx_checks()) { diff --git a/libraries/chain/webassembly/crypto.cpp b/libraries/chain/webassembly/crypto.cpp index cfe1a26b6a..94dc2a2f99 100644 --- a/libraries/chain/webassembly/crypto.cpp +++ b/libraries/chain/webassembly/crypto.cpp @@ -58,21 +58,21 @@ namespace sysio::chain::webassembly { sig_variable_size_limit_exception, "signature variable length component size greater than subjective maximum"); // Check if the signature is ED25519 if( s.contains() ) { - // a) Extract 32 raw bytes from fc::sha256 - auto sha_data = digest->data(); + // Extract 32 raw bytes from fc::sha256 + auto sha_data = digest->data(); const unsigned char* msgptr = reinterpret_cast(sha_data); - // b) Extract 64-byte signature (skip the 1-byte “which” prefix) - const unsigned char* sigptr = reinterpret_cast(sig.data()) + 1; - - // c) Extract 32-byte pubkey (skip the 1-byte “which” prefix) + // Extract 32-byte pubkey (skip the 1-byte “which” prefix) const unsigned char* pubptr = reinterpret_cast(pub.data()) + 1; + // Extract 64-byte signature (skip 1-byte variant index + 32-byte embedded pubkey) + const unsigned char* sigptr = reinterpret_cast(sig.data()) + 1 + crypto_sign_PUBLICKEYBYTES; + // d) Call libsodium’s raw ED25519 detached-verify int ok = crypto_sign_verify_detached( sigptr, - msgptr, - 32, - pubptr ); + msgptr, + 32, + pubptr ); SYS_ASSERT( ok == 0, crypto_api_exception, "ED25519 signature verify failed" ); @@ -80,7 +80,7 @@ namespace sysio::chain::webassembly { } // otherwise, fall back to the existing ECDSA‐style recover→compare path - auto check = fc::crypto::public_key::recover( s, *digest); + auto check = fc::crypto::public_key::recover( s, *digest ); SYS_ASSERT( check == p, crypto_api_exception, "Error expected key different than recovered key" ); @@ -94,7 +94,7 @@ namespace sysio::chain::webassembly { fc::raw::unpack(ds, s); using sig_type = fc::crypto::signature::sig_type; - SYS_ASSERT(s.contains_type(sig_type::k1, sig_type::r1, sig_type::wa, sig_type::em), unactivated_signature_type, + SYS_ASSERT(s.contains_type(sig_type::k1, sig_type::r1, sig_type::wa, sig_type::em, sig_type::ed), unactivated_signature_type, "Unactivated signature type used during recover_key"); if(context.control.is_speculative_block()) diff --git a/libraries/libfc/include/fc/crypto/elliptic_ed.hpp b/libraries/libfc/include/fc/crypto/elliptic_ed.hpp index e8be3ff1ec..8f57f35ce7 100644 --- a/libraries/libfc/include/fc/crypto/elliptic_ed.hpp +++ b/libraries/libfc/include/fc/crypto/elliptic_ed.hpp @@ -51,11 +51,19 @@ struct public_key_shim { }; /** - * ED25519 signature (64 bytes) + * ED25519 signature (96 bytes = 32 embedded pubkey + 64 sig) + * + * The public key is embedded directly in the signature blob, making + * ED25519 signatures self-contained and recoverable. Layout: + * [0..31] - 32-byte ED25519 public key + * [32..95] - 64-byte ED25519 signature */ struct signature_shim { - static constexpr size_t size = crypto_sign_BYTES; - static constexpr bool is_recoverable = false; + static constexpr size_t size = crypto_sign_BYTES + crypto_sign_PUBLICKEYBYTES; + // ED25519 signatures are not mathematically recoverable like ECDSA (K1/R1), + // but we embed the public key in the signature blob so recover() can extract + // and verify it, allowing uniform treatment in get_signature_keys(). + static constexpr bool is_recoverable = true; using data_type = std::array; data_type _data{}; @@ -72,9 +80,7 @@ struct signature_shim { } using public_key_type = public_key_shim; - public_key_shim recover(const sha256&) const { - FC_THROW_EXCEPTION(exception, "ED25519 signature recovery not supported"); - } + public_key_shim recover(const sha256& digest) const; bool verify(const sha256& digest, const public_key_shim& pub) const; bool verify_solana(const uint8_t* data, size_t len, const public_key_shim& pub) const; @@ -85,7 +91,7 @@ struct signature_shim { } static signature_shim from_base58_string(const std::string& str) { - constexpr size_t max_sig_len = 88; + constexpr size_t max_sig_len = 132; FC_ASSERT( str.size() <= max_sig_len, "Invalid ED25519 signature string length {}", str.size()); auto bytes = from_base58(str); FC_ASSERT(bytes.size() == size, "Invalid ED25519 signature bytes length {}", bytes.size()); @@ -206,13 +212,13 @@ DataStream& operator>>(DataStream& ds, crypto::ed::public_key_shim& pk) { template DataStream& operator<<(DataStream& ds, const crypto::ed::signature_shim& sig) { - ds.write(reinterpret_cast(sig._data.data()), crypto_sign_BYTES); + ds.write(reinterpret_cast(sig._data.data()), crypto::ed::signature_shim::size); return ds; } template DataStream& operator>>(DataStream& ds, crypto::ed::signature_shim& sig) { - ds.read(reinterpret_cast(sig._data.data()), crypto_sign_BYTES); + ds.read(reinterpret_cast(sig._data.data()), crypto::ed::signature_shim::size); return ds; } diff --git a/libraries/libfc/include/fc/crypto/private_key.hpp b/libraries/libfc/include/fc/crypto/private_key.hpp index 8411f9b948..9fbdd1c6b3 100644 --- a/libraries/libfc/include/fc/crypto/private_key.hpp +++ b/libraries/libfc/include/fc/crypto/private_key.hpp @@ -61,15 +61,7 @@ namespace fc { namespace crypto { signature sign(const sha256& digest) const; - template< typename KeyType = ecc::private_key_shim > - static private_key generate() { - return private_key(storage_type(KeyType::generate())); - } - - template< typename KeyType = r1::private_key_shim > - static private_key generate_r1() { - return private_key(storage_type(KeyType::generate())); - } + static private_key generate(key_type t = key_type::k1); template< typename KeyType = ecc::private_key_shim > static private_key regenerate( const typename KeyType::data_type& data ) { diff --git a/libraries/libfc/src/crypto/elliptic_ed.cpp b/libraries/libfc/src/crypto/elliptic_ed.cpp index 031738c257..8455ccb425 100644 --- a/libraries/libfc/src/crypto/elliptic_ed.cpp +++ b/libraries/libfc/src/crypto/elliptic_ed.cpp @@ -54,11 +54,12 @@ namespace fc { namespace crypto { namespace ed { FC_THROW_EXCEPTION(exception, "Failed to create ED25519 signature"); } - // 4) Pack into your signature_shim + // 4) Pack into signature_shim: 32-byte pubkey + 64-byte sig signature_shim out; - // zero‑pad entire buffer then copy memset(out._data.data(), 0, out.size); - memcpy(out._data.data(), sigbuf, crypto_sign_BYTES); + auto pub = get_public_key(); + memcpy(out._data.data(), pub._data.data(), crypto_sign_PUBLICKEYBYTES); + memcpy(out._data.data() + crypto_sign_PUBLICKEYBYTES, sigbuf, crypto_sign_BYTES); return out; } @@ -69,9 +70,9 @@ namespace fc { namespace crypto { namespace ed { // 1) Convert raw digest to ASCII hex, added due to Phantom wallet limitations which doesn't allow signing raw binary data. Guard rails so users don't unknowingly sign malicious transactions. const std::string hex = fc::to_hex(digest.data(), digest.data_size()); - // 2) Verify signature on hex payload + // 2) Verify signature on hex payload (sig starts at offset 32) return crypto_sign_verify_detached( - _data.data(), + _data.data() + crypto_sign_PUBLICKEYBYTES, reinterpret_cast(hex.data()), hex.size(), pub._data.data() @@ -95,18 +96,34 @@ namespace fc { namespace crypto { namespace ed { FC_THROW_EXCEPTION(exception, "Failed to create ED25519 signature"); } + // Pack: 32-byte pubkey + 64-byte sig signature_shim out; memset(out._data.data(), 0, out.size); - memcpy(out._data.data(), sigbuf, crypto_sign_BYTES); + auto pub = get_public_key(); + memcpy(out._data.data(), pub._data.data(), crypto_sign_PUBLICKEYBYTES); + memcpy(out._data.data() + crypto_sign_PUBLICKEYBYTES, sigbuf, crypto_sign_BYTES); return out; } + // Recover public key from embedded bytes, verifying signature first + public_key_shim signature_shim::recover(const sha256& digest) const { + // Extract the embedded public key from [0..31] + public_key_shim pub; + memcpy(pub._data.data(), _data.data(), crypto_sign_PUBLICKEYBYTES); + + // Verify the signature against the embedded key + FC_ASSERT( verify(digest, pub), "ED25519 signature verification failed during recovery" ); + + return pub; + } + // Verify raw bytes directly (for Solana transaction verification) bool signature_shim::verify_solana(const uint8_t* data, size_t len, const public_key_shim& pub) const { sodium_init_guard(); + // Sig starts at offset 32 return crypto_sign_verify_detached( - _data.data(), + _data.data() + crypto_sign_PUBLICKEYBYTES, data, len, pub._data.data() diff --git a/libraries/libfc/src/crypto/private_key.cpp b/libraries/libfc/src/crypto/private_key.cpp index 6cb11df50f..7bde852c35 100644 --- a/libraries/libfc/src/crypto/private_key.cpp +++ b/libraries/libfc/src/crypto/private_key.cpp @@ -18,6 +18,21 @@ namespace fc { namespace crypto { }, _storage)); } + private_key private_key::generate(key_type t) { + // Compile-time dispatch table indexed by key_type + using gen_fn = private_key(*)(); + static constexpr gen_fn generators[] = { + [] { return private_key(storage_type(ecc::private_key_shim::generate())); }, // k1 + [] { return private_key(storage_type(r1::private_key_shim::generate())); }, // r1 + [] { return private_key(storage_type(em::private_key_shim::generate())); }, // em + [] { return private_key(storage_type(ed::private_key_shim::generate())); }, // ed + [] { return private_key(storage_type(bls::private_key_shim::generate())); }, // bls + }; + auto idx = static_cast(t); + FC_ASSERT(idx < std::size(generators), "Key type does not support generation"); + return generators[idx](); + } + private_key private_key::from_string(const std::string& str, key_type type) { switch (type) { case key_type::k1: diff --git a/libraries/libfc/src/crypto/solana/solana_crypto_utils.cpp b/libraries/libfc/src/crypto/solana/solana_crypto_utils.cpp index 40ea6a349c..b21e61a512 100644 --- a/libraries/libfc/src/crypto/solana/solana_crypto_utils.cpp +++ b/libraries/libfc/src/crypto/solana/solana_crypto_utils.cpp @@ -59,8 +59,9 @@ solana_signature solana_signature::from_base58(const std::string& str) { solana_signature solana_signature::from_ed_signature(const fc::crypto::ed::signature_shim& sig) { solana_signature result; - static_assert(sizeof(sig._data) == SIZE, "ED25519 signature size mismatch"); - std::ranges::copy(sig._data, result.data.begin()); + // Copy the 64-byte signature from offset 32 (after the embedded pubkey) + static_assert(SIZE == crypto_sign_BYTES, "Solana signature size must be 64 bytes"); + std::memcpy(result.data.data(), sig._data.data() + crypto_sign_PUBLICKEYBYTES, SIZE); return result; } diff --git a/libraries/libfc/test/crypto/test_cypher_suites.cpp b/libraries/libfc/test/crypto/test_cypher_suites.cpp index 37d55ff4df..fb2c13c798 100644 --- a/libraries/libfc/test/crypto/test_cypher_suites.cpp +++ b/libraries/libfc/test/crypto/test_cypher_suites.cpp @@ -89,7 +89,7 @@ BOOST_AUTO_TEST_CASE(test_r1) try { BOOST_AUTO_TEST_CASE(test_k1_recovery) try { auto payload = "Test Cases"; auto digest = sha256::hash(payload, const_strlen(payload)); - auto key = private_key::generate(); + auto key = private_key::generate(); auto pub = key.get_public_key(); auto sig = key.sign(digest); @@ -108,7 +108,7 @@ BOOST_AUTO_TEST_CASE(test_k1_recovery) try { BOOST_AUTO_TEST_CASE(test_r1_recovery) try { auto payload = "Test Cases"; auto digest = sha256::hash(payload, const_strlen(payload)); - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::r1); auto pub = key.get_public_key(); auto sig = key.sign(digest); @@ -125,7 +125,7 @@ BOOST_AUTO_TEST_CASE(test_r1_recovery) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_k1_recyle) try { - auto key = private_key::generate(); + auto key = private_key::generate(); auto pub = key.get_public_key(); auto pub_str = pub.to_string({}); auto recycled_pub = public_key::from_string(pub_str); @@ -134,7 +134,7 @@ BOOST_AUTO_TEST_CASE(test_k1_recyle) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_r1_recyle) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::r1); auto pub = key.get_public_key(); auto pub_str = pub.to_string({}); auto recycled_pub = public_key::from_string(pub_str); @@ -143,7 +143,7 @@ BOOST_AUTO_TEST_CASE(test_r1_recyle) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_em) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto priv_str = key.to_string({}); auto pub_str = pub.to_string({}); @@ -168,7 +168,7 @@ BOOST_AUTO_TEST_CASE(test_em) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_em_alt) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto priv_str = key.to_string({}, true); auto pub_str = pub.to_string({}, true); @@ -195,7 +195,7 @@ BOOST_AUTO_TEST_CASE(test_em_alt) try { BOOST_AUTO_TEST_CASE(test_em_recovery_of_trx) try { auto payload = "Test Cases"; auto digest_raw = fc::sha256::hash(payload); // pretend payload is a transaction - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto sig = key.sign(digest_raw); std::string sig_str = sig.to_string({}); @@ -218,7 +218,7 @@ BOOST_AUTO_TEST_CASE(test_em_recovery_of_trx) try { BOOST_AUTO_TEST_CASE(test_em_recovery_of_eth) try { auto payload = "Test Cases"; auto digest_raw = ethereum::hash_message(ethereum::to_uint8_span(payload)); // pretend payload is an eth transaction (not EIP-191) - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto sig = signature(signature::storage_type(key.get().sign_keccak256(digest_raw))); std::string sig_str = sig.to_string({}); @@ -242,7 +242,7 @@ BOOST_AUTO_TEST_CASE(test_em_is_canonical) try { fc::sha256 msg = fc::sha256::hash(std::string("hello canonical world")); // Generate a private key - auto priv = fc::crypto::private_key::generate(); + auto priv = fc::crypto::private_key::generate(private_key::key_type::em); auto sig = priv.sign(msg); // Force S > n/2 to simulate a non-canonical signature @@ -280,13 +280,21 @@ BOOST_AUTO_TEST_CASE(test_ed_priv_str) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_ed_sig_str) try { - auto sig_str = "4cdd1oX7cfVALfr26tP52BZ6cSzrgnNGtYD7BFhm6FFeZV5sPTnRvg6NRn8yC6DbEikXcrNChBM5vVJnTgKhGhVu"; - auto sig = fc::crypto::signature::from_string(sig_str, signature::sig_type::ed); - BOOST_CHECK_EQUAL(sig_str, sig.to_string({})); + // Generate a real 96-byte ED25519 signature (64 sig + 32 embedded pubkey) + auto priv = fc::crypto::private_key::generate(private_key::key_type::ed); + auto digest = fc::sha256::hash(std::string("test_ed_sig_str")); + auto sig = priv.sign(digest); + auto sig_str = sig.to_string({}); + BOOST_TEST(!sig_str.empty()); + // Roundtrip: base58 -> signature -> base58 + auto sig_rt = fc::crypto::signature::from_string(sig_str, signature::sig_type::ed); + BOOST_CHECK_EQUAL(sig_str, sig_rt.to_string({})); + // Prefixed form BOOST_TEST(sig.to_string({}, true).starts_with("SIG_ED_")); BOOST_TEST(sig.to_string({}, true).ends_with(sig_str)); auto sig2 = fc::crypto::signature::from_string(sig.to_string({}, true)); BOOST_CHECK_EQUAL(sig.to_string({}), sig2.to_string({})); + // Variant roundtrip fc::variant test_sig_variant{sig}; signature test_sig2 = test_sig_variant.as(); BOOST_CHECK_EQUAL(sig.to_string({}), test_sig2.to_string({})); @@ -333,7 +341,7 @@ BOOST_AUTO_TEST_CASE(test_bls_sig_str) try { // --- sign_eth (shim-level): recovery round-trip with multiple messages --- BOOST_AUTO_TEST_CASE(test_sign_eth_recovery_roundtrip) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto& em_key = key.get(); @@ -352,7 +360,7 @@ BOOST_AUTO_TEST_CASE(test_sign_eth_recovery_roundtrip) try { // --- sign_eth (shim-level): signing the same message twice produces the same signature (deterministic RFC-6979 nonce) --- BOOST_AUTO_TEST_CASE(test_sign_eth_deterministic) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::em); auto& em_key = key.get(); auto digest = ethereum::hash_message(ethereum::to_uint8_span("determinism check")); @@ -364,7 +372,7 @@ BOOST_AUTO_TEST_CASE(test_sign_eth_deterministic) try { // --- sign_solana (shim-level) + verify_solana round-trip --- BOOST_AUTO_TEST_CASE(test_sign_solana_verify_roundtrip) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::ed); auto pub = key.get_public_key(); auto& ed_key = key.get(); auto& ed_pub = pub.get(); @@ -378,7 +386,7 @@ BOOST_AUTO_TEST_CASE(test_sign_solana_verify_roundtrip) try { // --- verify_solana rejects a tampered message --- BOOST_AUTO_TEST_CASE(test_verify_solana_rejects_tampered_message) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::ed); auto pub = key.get_public_key(); auto& ed_key = key.get(); auto& ed_pub = pub.get(); @@ -396,9 +404,9 @@ BOOST_AUTO_TEST_CASE(test_verify_solana_rejects_tampered_message) try { // --- verify_solana rejects wrong public key --- BOOST_AUTO_TEST_CASE(test_verify_solana_rejects_wrong_key) try { - auto key1 = fc::crypto::private_key::generate(); + auto key1 = fc::crypto::private_key::generate(private_key::key_type::ed); auto pub1 = key1.get_public_key(); - auto key2 = fc::crypto::private_key::generate(); + auto key2 = fc::crypto::private_key::generate(private_key::key_type::ed); auto pub2 = key2.get_public_key(); auto msg = ethereum::to_uint8_span("signed by key1"); @@ -410,7 +418,7 @@ BOOST_AUTO_TEST_CASE(test_verify_solana_rejects_wrong_key) try { // --- sign_solana (shim-level) on binary data (non-UTF8) --- BOOST_AUTO_TEST_CASE(test_sign_solana_binary_payload) try { - auto key = fc::crypto::private_key::generate(); + auto key = fc::crypto::private_key::generate(private_key::key_type::ed); auto pub = key.get_public_key(); auto& ed_key = key.get(); auto& ed_pub = pub.get(); @@ -425,10 +433,10 @@ BOOST_AUTO_TEST_CASE(test_sign_solana_binary_payload) try { // --- Solana verify rejects non-ED key types via FC_ASSERT --- BOOST_AUTO_TEST_CASE(test_verify_solana_rejects_non_ed_key) try { - auto em_key = fc::crypto::private_key::generate(); + auto em_key = fc::crypto::private_key::generate(private_key::key_type::em); auto em_pub = em_key.get_public_key(); - auto ed_priv = fc::crypto::private_key::generate(); + auto ed_priv = fc::crypto::private_key::generate(private_key::key_type::ed); auto& ed_key = ed_priv.get(); auto msg = ethereum::to_uint8_span("test"); auto ed_sig = ed_key.sign_raw(msg.data(), msg.size()); @@ -462,7 +470,7 @@ signature_provider_t make_provider(const private_key& key, chain_key_type_t key_ } // anonymous namespace BOOST_AUTO_TEST_CASE(test_eth_client_signer_sign_recover) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto provider = make_provider(key, chain_key_type_ethereum); @@ -476,7 +484,7 @@ BOOST_AUTO_TEST_CASE(test_eth_client_signer_sign_recover) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_wire_eth_signer_sign_recover) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::em); auto pub = key.get_public_key(); auto provider = make_provider(key, chain_key_type_ethereum); @@ -491,7 +499,7 @@ BOOST_AUTO_TEST_CASE(test_wire_eth_signer_sign_recover) try { // --- wire_eth_signer must produce the same signature as em::sign_sha256 (the shim-level Wire signing path) --- BOOST_AUTO_TEST_CASE(test_wire_eth_signer_matches_sign_sha256) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::em); auto provider = make_provider(key, chain_key_type_ethereum); auto& em_key = key.get(); @@ -511,7 +519,7 @@ BOOST_AUTO_TEST_CASE(test_wire_eth_signer_matches_sign_sha256) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_sol_client_signer_sign_verify) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::ed); auto pub = key.get_public_key(); auto provider = make_provider(key, chain_key_type_solana); @@ -528,7 +536,7 @@ BOOST_AUTO_TEST_CASE(test_sol_client_signer_sign_verify) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_wire_signer_k1) try { - auto key = private_key::generate(); + auto key = private_key::generate(); auto pub = key.get_public_key(); auto provider = make_provider(key, chain_key_type_wire); @@ -543,7 +551,7 @@ BOOST_AUTO_TEST_CASE(test_wire_signer_k1) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_wire_signer_ed) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::ed); auto pub = key.get_public_key(); auto provider = make_provider(key, chain_key_type_solana); @@ -559,7 +567,7 @@ BOOST_AUTO_TEST_CASE(test_wire_signer_ed) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_wire_signer_sol) try { - auto key = private_key::generate(); + auto key = private_key::generate(private_key::key_type::ed); auto pub = key.get_public_key(); auto provider = make_provider(key, chain_key_type_solana); @@ -584,7 +592,7 @@ BOOST_AUTO_TEST_CASE(test_wire_signer_sol) try { } FC_LOG_AND_RETHROW(); BOOST_AUTO_TEST_CASE(test_signer_rejects_wrong_key_type) try { - auto key = private_key::generate(); + auto key = private_key::generate(); auto provider = make_provider(key, chain_key_type_wire); // Trying to use a K1 provider with eth_client_signer should fail diff --git a/plugins/signature_provider_manager_plugin/src/signature_provider_manager_plugin.cpp b/plugins/signature_provider_manager_plugin/src/signature_provider_manager_plugin.cpp index a31a0fc2bb..4b27372868 100644 --- a/plugins/signature_provider_manager_plugin/src/signature_provider_manager_plugin.cpp +++ b/plugins/signature_provider_manager_plugin/src/signature_provider_manager_plugin.cpp @@ -259,11 +259,11 @@ class signature_provider_manager_plugin_impl { fc::crypto::private_key privkey; switch (key_type) { case fc::crypto::chain_key_type_wire: { - privkey = fc::crypto::private_key::generate(); + privkey = fc::crypto::private_key::generate(); break; } case fc::crypto::chain_key_type_wire_bls: { - privkey = fc::crypto::private_key::generate(); + privkey = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::bls); break; } default: { diff --git a/plugins/signature_provider_manager_plugin/test/test_create_provider_specs.cpp b/plugins/signature_provider_manager_plugin/test/test_create_provider_specs.cpp index 905b3a04dd..09d37464d4 100644 --- a/plugins/signature_provider_manager_plugin/test/test_create_provider_specs.cpp +++ b/plugins/signature_provider_manager_plugin/test/test_create_provider_specs.cpp @@ -259,15 +259,19 @@ BOOST_AUTO_TEST_CASE(create_provider_solana_fixture_pub_priv_sig_interoperable) BOOST_CHECK_EQUAL(ed_pub_key_base58, fixture.address); BOOST_CHECK_EQUAL(ed_pub_key_base58, fixture.public_key); - // Parse the fixture signature from base58 - ed::signature_shim fixture_sig = ed::signature_shim::from_base58_string(fixture.signature); + // Decode the raw 64-byte fixture signature from base58 + auto raw_sig_bytes = fc::from_base58(fixture.signature); + BOOST_REQUIRE_EQUAL(raw_sig_bytes.size(), crypto_sign_BYTES); - // The Python keygen signs the raw payload bytes (not a hash) - // But our C++ implementation signs a SHA256 hash, so we verify - // that the fixture signature verifies against the raw payload - // by using libsodium directly + // Build the full signature blob: [pubkey 32B][sig 64B] + ed::signature_shim fixture_sig; + memcpy(fixture_sig._data.data(), ed_pub_key._data.data(), crypto_sign_PUBLICKEYBYTES); + memcpy(fixture_sig._data.data() + crypto_sign_PUBLICKEYBYTES, raw_sig_bytes.data(), crypto_sign_BYTES); + + // Verify the fixture signature against the raw payload using libsodium directly + // (Python keygen signs raw bytes, our C++ signs SHA256 hash) int verify_result = crypto_sign_verify_detached( - fixture_sig._data.data(), + fixture_sig._data.data() + crypto_sign_PUBLICKEYBYTES, reinterpret_cast(fixture.payload.data()), fixture.payload.size(), ed_pub_key._data.data()); diff --git a/plugins/wallet_plugin/src/wallet.cpp b/plugins/wallet_plugin/src/wallet.cpp index 3415d3bf53..922d005f3a 100644 --- a/plugins/wallet_plugin/src/wallet.cpp +++ b/plugins/wallet_plugin/src/wallet.cpp @@ -267,9 +267,9 @@ class soft_wallet_impl { private_key_type priv_key; if (key_type == "K1") - priv_key = fc::crypto::private_key::generate(); + priv_key = fc::crypto::private_key::generate(); else if (key_type == "R1") - priv_key = fc::crypto::private_key::generate(); + priv_key = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::r1); else SYS_THROW(chain::unsupported_key_type_exception, "Key type \"{}\" not supported by software wallet", key_type); diff --git a/programs/clio/main.cpp b/programs/clio/main.cpp index c4cd2c06ab..3a83a80815 100644 --- a/programs/clio/main.cpp +++ b/programs/clio/main.cpp @@ -1899,7 +1899,7 @@ int main( int argc, char** argv ) { return; } - auto pk = r1 ? private_key_type::generate_r1() : private_key_type::generate(); + auto pk = r1 ? private_key_type::generate(crypto::private_key::key_type::r1) : private_key_type::generate(); auto privs = pk.to_string({}, k1); auto pubs = pk.get_public_key().to_string({}, k1); if (print_console) { diff --git a/unittests/auth_tests.cpp b/unittests/auth_tests.cpp index 4cd851fd38..d13ab9bc97 100644 --- a/unittests/auth_tests.cpp +++ b/unittests/auth_tests.cpp @@ -51,7 +51,7 @@ BOOST_AUTO_TEST_CASE( bls_key_not_allowed_for_trx ) { try { chain.create_accounts( {"alice"_n} ); chain.produce_block(); - private_key_type bls_active_priv_key = private_key_type::generate(); // bls sigs not allowed + private_key_type bls_active_priv_key = private_key_type::generate(private_key_type::key_type::bls); // bls sigs not allowed public_key_type bls_active_pub_key = bls_active_priv_key.get_public_key(); BOOST_REQUIRE_THROW( chain.set_authority(name("alice"), name("active"), authority(bls_active_pub_key), name("owner"), { permission_level{name("alice"), name("active")} }, { chain.get_private_key(name("alice"), "active") }), diff --git a/unittests/ed25519_tests.cpp b/unittests/ed25519_tests.cpp index bcaa88ebf6..cf74c348bd 100644 --- a/unittests/ed25519_tests.cpp +++ b/unittests/ed25519_tests.cpp @@ -171,13 +171,21 @@ BOOST_AUTO_TEST_CASE(shared_secret_symmetry) { } FC_LOG_AND_RETHROW() } -// Test 4: signature_shim.recover() must throw (unsupported for ED25519) -BOOST_AUTO_TEST_CASE(signature_recover_throws) { +// Test 4: signature_shim.recover() succeeds with valid embedded pubkey +BOOST_AUTO_TEST_CASE(signature_recover_works) { try { - signature_shim sig; - fc::sha256 dummy; - // Recover is unsupported—should always throw - BOOST_CHECK_THROW(sig.recover(dummy), fc::exception); + auto sk = private_key_shim::generate(); + auto expected_pk = sk.get_public_key(); + auto digest = fc::sha256::hash(std::string("recover test")); + + signature_shim sig = sk.sign_sha256(digest); + + // recover() should verify and return the embedded pubkey + auto recovered = sig.recover(digest); + BOOST_CHECK_MESSAGE( + recovered._data == expected_pk._data, + "recover() returned wrong public key" + ); } FC_LOG_AND_RETHROW() } @@ -200,38 +208,38 @@ BOOST_AUTO_TEST_CASE(pack_unpack_public_key) { } FC_LOG_AND_RETHROW() } -// Test 6: pack/unpack signature_shim preserves 64 bytes + zero-padding +// Test 6: pack/unpack signature_shim preserves all 96 bytes BOOST_AUTO_TEST_CASE(pack_unpack_signature) { try { - // 1) Prepare dummy signature_shim (64x 0x5A + pad=0) + // 1) Prepare dummy signature_shim (96 bytes) signature_shim orig; - std::fill_n(orig._data.data(), crypto_sign_BYTES, 0x5A); + std::fill_n(orig._data.data(), signature_shim::size, 0x5A); // 2) Pack: Use fc standard packing/unpacking auto blob = fc::raw::pack(orig); BOOST_CHECK_MESSAGE( - blob.size() == crypto_sign_BYTES, + blob.size() == signature_shim::size, "blob.size()=" << blob.size() - << ", expected=" << crypto_sign_BYTES + << ", expected=" << signature_shim::size ); - // 3) Unpack back → got + // 3) Unpack back -> got auto got = fc::raw::unpack(blob); - // 4) Verify the 64 data bytes match + // 4) Verify all 96 bytes match BOOST_CHECK_MESSAGE( - memcmp(orig._data.data(), got._data.data(), crypto_sign_BYTES) == 0, + memcmp(orig._data.data(), got._data.data(), signature_shim::size) == 0, "signature bytes mismatch after unpack" ); } FC_LOG_AND_RETHROW() } -// Test 7: padding persistence through multiple pack/unpack loops +// Test 7: persistence through multiple pack/unpack loops (96 bytes) BOOST_AUTO_TEST_CASE(signature_padding_persistence) { try { // Prepare dummy signature_shim signature_shim orig; - std::fill_n(orig._data.data(), crypto_sign_BYTES, 0xA5); + std::fill_n(orig._data.data(), signature_shim::size, 0xA5); // Pack/unpack twice auto b1 = fc::raw::pack(orig); @@ -239,12 +247,43 @@ BOOST_AUTO_TEST_CASE(signature_padding_persistence) { auto b2 = fc::raw::pack(u1); auto u2 = fc::raw::unpack(b2); - // Inner 64 bytes must remain unchanged + // All 96 bytes must remain unchanged BOOST_CHECK_MESSAGE( - memcmp(orig._data.data(), u2._data.data(), crypto_sign_BYTES) == 0, + memcmp(orig._data.data(), u2._data.data(), signature_shim::size) == 0, "signature bytes corrupted after pack/unpack loops" ); } FC_LOG_AND_RETHROW() } +// Test 8: sign embeds the correct pubkey in last 32 bytes +BOOST_AUTO_TEST_CASE(sign_embeds_pubkey) { + try { + auto sk = private_key_shim::generate(); + auto expected_pk = sk.get_public_key(); + auto digest = fc::sha256::hash(std::string("embed test")); + + signature_shim sig = sk.sign_sha256(digest); + + // First 32 bytes should match the public key + BOOST_CHECK_MESSAGE( + memcmp(sig._data.data(), + expected_pk._data.data(), + crypto_sign_PUBLICKEYBYTES) == 0, + "embedded pubkey doesn't match get_public_key()" + ); + + // Also check sign_raw embeds pubkey + const std::string msg = "raw embed test"; + signature_shim raw_sig = sk.sign_raw( + reinterpret_cast(msg.data()), msg.size()); + + BOOST_CHECK_MESSAGE( + memcmp(raw_sig._data.data(), + expected_pk._data.data(), + crypto_sign_PUBLICKEYBYTES) == 0, + "sign_raw: embedded pubkey doesn't match get_public_key()" + ); + } FC_LOG_AND_RETHROW() +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/signature_recovery_tests.cpp b/unittests/signature_recovery_tests.cpp index e05dff00e6..193ff15e7a 100644 --- a/unittests/signature_recovery_tests.cpp +++ b/unittests/signature_recovery_tests.cpp @@ -6,13 +6,16 @@ #include #include +#include #include #include +#include #include #include #include using namespace sysio::chain; +using namespace sysio::testing; using fc::crypto::private_key; using fc::crypto::public_key; @@ -23,11 +26,11 @@ struct sig_fixture { chain_id_type chain_id; }; -/// 1. Pure recoverable still works (order‐agnostic) +/// 1. Pure recoverable still works (order-agnostic) BOOST_FIXTURE_TEST_CASE(pure_recoverable_sigs, sig_fixture) { auto p1 = private_key::generate(); auto p2 = private_key::generate(); - auto trx = test::make_signed_trx({p1, p2}, chain_id, /*include_ed_ext=*/false); + auto trx = test::make_signed_trx({p1, p2}, chain_id); boost::container::flat_set keys; trx.get_signature_keys( @@ -38,15 +41,14 @@ BOOST_FIXTURE_TEST_CASE(pure_recoverable_sigs, sig_fixture) { ); BOOST_REQUIRE_EQUAL(keys.size(), 2u); - // membership, order‐agnostic BOOST_CHECK(keys.count(p1.get_public_key()) == 1); BOOST_CHECK(keys.count(p2.get_public_key()) == 1); } -/// 2. Duplicate recoverable → tx_duplicate_sig +/// 2. Duplicate recoverable -> tx_duplicate_sig BOOST_FIXTURE_TEST_CASE(duplicate_sig_rejected, sig_fixture) { auto priv = private_key::generate(); - auto trx = test::make_signed_trx({priv, priv}, chain_id, false); + auto trx = test::make_signed_trx({priv, priv}, chain_id); boost::container::flat_set keys; bool caught = false; @@ -65,14 +67,11 @@ BOOST_FIXTURE_TEST_CASE(duplicate_sig_rejected, sig_fixture) { } } -/// 3a. Invalid recovery byte → fc::exception -/// The recovery byte encodes 27 + recid (recid 0-3). Setting it outside -/// this range causes our code to reject before calling libsecp256k1. +/// 3a. Invalid recovery byte -> fc::exception BOOST_FIXTURE_TEST_CASE(invalid_recovery_byte_rejected, sig_fixture) { auto p1 = private_key::generate(); - auto trx = test::make_signed_trx({p1}, chain_id, false); + auto trx = test::make_signed_trx({p1}, chain_id); - // packed[0] = variant index, packed[1] = recovery byte (27 + recid) auto packed = fc::raw::pack(trx.signatures[0]); packed[1] = 0; // Invalid: must be 27-30 for k1 fc::crypto::signature bad; @@ -90,15 +89,12 @@ BOOST_FIXTURE_TEST_CASE(invalid_recovery_byte_rejected, sig_fixture) { ); } -/// 3b. Corrupted signature data → fc::exception from libsecp256k1 -/// Zeroing the R and S components makes secp256k1_ecdsa_recover fail. +/// 3b. Corrupted signature data -> fc::exception BOOST_FIXTURE_TEST_CASE(corrupted_sig_data_rejected, sig_fixture) { auto p1 = private_key::generate(); - auto trx = test::make_signed_trx({p1}, chain_id, false); + auto trx = test::make_signed_trx({p1}, chain_id); - // packed[0] = variant index, packed[1] = recovery byte, packed[2..65] = R || S auto packed = fc::raw::pack(trx.signatures[0]); - // Zero out R and S (64 bytes starting at offset 2) std::memset(packed.data() + 2, 0, 64); fc::crypto::signature bad; fc::datastream ds(packed.data(), packed.size()); @@ -115,10 +111,10 @@ BOOST_FIXTURE_TEST_CASE(corrupted_sig_data_rejected, sig_fixture) { ); } -/// 4. Timeout path → any fc::exception +/// 4. Timeout path -> any fc::exception BOOST_FIXTURE_TEST_CASE(signature_deadline_timeout, sig_fixture) { auto priv = private_key::generate(); - auto trx = test::make_signed_trx({priv}, chain_id, false); + auto trx = test::make_signed_trx({priv}, chain_id); auto past = fc::time_point::now() - fc::microseconds(1); boost::container::flat_set keys; @@ -128,7 +124,7 @@ BOOST_FIXTURE_TEST_CASE(signature_deadline_timeout, sig_fixture) { ); } -/// 5. Zero signatures → empty +/// 5. Zero signatures -> empty BOOST_FIXTURE_TEST_CASE(zero_sigs_zero_ext, sig_fixture) { signed_transaction trx; trx.set_reference_block(block_id_type()); @@ -142,7 +138,7 @@ BOOST_FIXTURE_TEST_CASE(zero_sigs_zero_ext, sig_fixture) { BOOST_CHECK(keys.empty()); } -/// 6. Max signatures edge → all accepted +/// 6. Max signatures edge -> all accepted BOOST_FIXTURE_TEST_CASE(max_sigs_and_ext, sig_fixture) { constexpr size_t MAX = 32; std::vector privs; @@ -150,7 +146,7 @@ BOOST_FIXTURE_TEST_CASE(max_sigs_and_ext, sig_fixture) { for (size_t i = 0; i < MAX; ++i) privs.push_back(private_key::generate()); - auto trx = test::make_signed_trx(privs, chain_id, false); + auto trx = test::make_signed_trx(privs, chain_id); boost::container::flat_set keys; trx.get_signature_keys(chain_id, @@ -160,93 +156,134 @@ BOOST_FIXTURE_TEST_CASE(max_sigs_and_ext, sig_fixture) { BOOST_CHECK_EQUAL(keys.size(), MAX); } -// --- ED extension mismatch cases (no ED signatures) --- +/// 7. ED25519 signature recovery works through get_signature_keys +BOOST_FIXTURE_TEST_CASE(ed_sig_recovery_works, sig_fixture) { + auto ed_priv = private_key::generate(private_key::key_type::ed); + auto ed_pub = ed_priv.get_public_key(); -/// 7. ED extension without ED signature → unsatisfied_authorization -BOOST_FIXTURE_TEST_CASE(ed_extension_without_sig_rejected, sig_fixture) { - auto p1 = private_key::generate(); - auto trx = test::make_signed_trx({p1}, chain_id, false); + signed_transaction trx; + trx.set_reference_block(block_id_type()); + trx.expiration = fc::time_point_sec(fc::time_point::now() + fc::seconds(3600)); - // wrap the raw shim in the proper public_key variant and append a single ED pubkey extension - fc::crypto::public_key pk(test::hardcoded_ed_pubkey); - trx.transaction_extensions.emplace_back( - test::ED_EXTENSION_ID, - fc::raw::pack(pk) - ); + auto digest = trx.sig_digest(chain_id); + trx.signatures.emplace_back(ed_priv.sign(digest)); boost::container::flat_set keys; - bool caught = false; - try { - trx.get_signature_keys(chain_id, - fc::time_point::maximum(), - keys, - false); - } catch (const fc::exception& e) { - if (std::strcmp(e.name(), "unsatisfied_authorization") == 0) { - caught = true; - } - } - if (!caught) { - BOOST_FAIL("Expected unsatisfied_authorization but none thrown"); - } + trx.get_signature_keys(chain_id, + fc::time_point::maximum(), + keys, + false); + + BOOST_REQUIRE_EQUAL(keys.size(), 1u); + BOOST_CHECK(keys.count(ed_pub) == 1); } -/// 8. Multiple ED extensions, no ED signatures → unsatisfied_authorization -BOOST_FIXTURE_TEST_CASE(multiple_ed_exts_no_sig_rejected, sig_fixture) { - auto p1 = private_key::generate(); - auto trx = test::make_signed_trx({p1}, chain_id, false); +/// 8. Mixed K1 and ED sigs on same transaction, both recovered +BOOST_FIXTURE_TEST_CASE(mixed_k1_and_ed_sigs, sig_fixture) { + auto k1_priv = private_key::generate(); + auto ed_priv = private_key::generate(private_key::key_type::ed); + auto k1_pub = k1_priv.get_public_key(); + auto ed_pub = ed_priv.get_public_key(); + + signed_transaction trx; + trx.set_reference_block(block_id_type()); + trx.expiration = fc::time_point_sec(fc::time_point::now() + fc::seconds(3600)); - fc::crypto::public_key pk(test::hardcoded_ed_pubkey); - // append it twice - trx.transaction_extensions.emplace_back(test::ED_EXTENSION_ID, - fc::raw::pack(pk)); - trx.transaction_extensions.emplace_back(test::ED_EXTENSION_ID, - fc::raw::pack(pk)); + auto digest = trx.sig_digest(chain_id); + trx.signatures.emplace_back(k1_priv.sign(digest)); + trx.signatures.emplace_back(ed_priv.sign(digest)); boost::container::flat_set keys; - bool caught = false; - try { - trx.get_signature_keys(chain_id, - fc::time_point::maximum(), - keys, - true); - } catch (const fc::exception& e) { - if (std::strcmp(e.name(), "unsatisfied_authorization") == 0) { - caught = true; - } - } - if (!caught) { - BOOST_FAIL("Expected unsatisfied_authorization but none thrown"); - } + trx.get_signature_keys(chain_id, + fc::time_point::maximum(), + keys, + false); + + BOOST_REQUIRE_EQUAL(keys.size(), 2u); + BOOST_CHECK(keys.count(k1_pub) == 1); + BOOST_CHECK(keys.count(ed_pub) == 1); } -/// 9. Duplicate ED extension → tx_duplicate_sig -BOOST_FIXTURE_TEST_CASE(duplicate_ed_extension_rejected, sig_fixture) { - auto p1 = private_key::generate(); - auto trx = test::make_signed_trx({p1}, chain_id, false); +/// 9. ED sig with corrupted embedded pubkey is rejected +BOOST_FIXTURE_TEST_CASE(ed_sig_corrupted_pubkey_rejected, sig_fixture) { + auto ed_priv = private_key::generate(private_key::key_type::ed); + + signed_transaction trx; + trx.set_reference_block(block_id_type()); + trx.expiration = fc::time_point_sec(fc::time_point::now() + fc::seconds(3600)); - fc::crypto::public_key pk(test::hardcoded_ed_pubkey); - // duplicate - trx.transaction_extensions.emplace_back(test::ED_EXTENSION_ID, - fc::raw::pack(pk)); - trx.transaction_extensions.emplace_back(test::ED_EXTENSION_ID, - fc::raw::pack(pk)); + auto digest = trx.sig_digest(chain_id); + auto sig = ed_priv.sign(digest); + + // Corrupt the embedded pubkey bytes (first 32 bytes of ED sig data) + auto packed = fc::raw::pack(sig); + // variant index (1 byte) + 96 bytes sig data; pubkey starts at offset 1 + std::memset(packed.data() + 1, 0xFF, 32); + fc::crypto::signature bad; + fc::datastream ds(packed.data(), packed.size()); + fc::raw::unpack(ds, bad); + trx.signatures.emplace_back(bad); boost::container::flat_set keys; + BOOST_CHECK_THROW( + trx.get_signature_keys(chain_id, + fc::time_point::maximum(), + keys, + false), + fc::exception + ); +} + +/// 10. Unknown transaction extensions are rejected by validate_and_extract_extensions +BOOST_FIXTURE_TEST_CASE(trx_extension_rejected, sig_fixture) { + signed_transaction trx; + trx.set_reference_block(block_id_type()); + trx.expiration = fc::time_point_sec(fc::time_point::now() + fc::seconds(3600)); + + // Add an extension with an unregistered ID + trx.transaction_extensions.emplace_back( + uint16_t(0x8000), + fc::raw::pack(std::string("bogus")) + ); + bool caught = false; try { - trx.get_signature_keys(chain_id, - fc::time_point::maximum(), - keys, - false); + trx.validate_and_extract_extensions(); } catch (const fc::exception& e) { - if (std::strcmp(e.name(), "tx_duplicate_sig") == 0) { + if (std::strcmp(e.name(), "invalid_transaction_extension") == 0) { caught = true; } } if (!caught) { - BOOST_FAIL("Expected tx_duplicate_sig but none thrown"); + BOOST_FAIL("Expected invalid_transaction_extension but none thrown"); } } +/// 11. Transaction with extensions rejected at consensus layer (init_for_input_trx) +BOOST_AUTO_TEST_CASE(trx_extension_rejected_on_push) { + validating_tester chain; + + signed_transaction trx; + trx.actions.emplace_back( + vector{{"sysio"_n, config::active_name}}, + "sysio"_n, "reqactivated"_n, + fc::raw::pack(fc::unsigned_int(0)) + ); + chain.set_transaction_headers(trx); + + // Add a bogus transaction extension + trx.transaction_extensions.emplace_back( + uint16_t(0x8000), + fc::raw::pack(std::string("bogus")) + ); + + trx.sign(chain.get_private_key("sysio"_n, "active"), chain.get_chain_id()); + + BOOST_CHECK_EXCEPTION( + chain.push_transaction(trx), + invalid_transaction_extension, + fc_exception_message_is("transaction extensions are not currently supported") + ); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/test_signature_utils.hpp b/unittests/test_signature_utils.hpp index 1603b64e22..e177accbad 100644 --- a/unittests/test_signature_utils.hpp +++ b/unittests/test_signature_utils.hpp @@ -3,42 +3,19 @@ #include #include -#include // for ed::public_key_shim #include #include #include #include -#include // for std::memcpy namespace test { -static constexpr uint16_t ED_EXTENSION_ID = 0x8000; - -/// RFC 8032 Test Vector #1 public key (32 bytes) -inline fc::crypto::ed::public_key_shim make_rfc8032_pubkey() { - std::array arr; - constexpr unsigned char bytes[crypto_sign_PUBLICKEYBYTES] = { - // pubkey from RFC8032 vector - 0xd7,0x5a,0x98,0x01, 0x82,0xb1,0x0a,0xb7, - 0xd5,0x4b,0xfe,0xd3, 0xc9,0x64,0x07,0x3a, - 0x0e,0xe1,0x72,0xf3, 0xfa,0xa6,0x23,0x25, - 0xaf,0x02,0x1a,0x68, 0xf7,0x07,0x51,0x1a - }; - std::memcpy(arr.data(), bytes, crypto_sign_PUBLICKEYBYTES); - return fc::crypto::ed::public_key_shim{arr}; -} - -/// ED pubkey for extension‐mismatch tests -static const fc::crypto::ed::public_key_shim hardcoded_ed_pubkey = make_rfc8032_pubkey(); - -/// Build a signed_transaction that signs with each recoverable key. -/// If include_ed_ext==true *and* any signature is non-recoverable, it -/// would append that matching pubkey as an extension—but here you -/// only call it with recoverable keys. +/// Build a signed_transaction signed with each key. +/// All signature types (K1, ED, etc.) now go through recover(), +/// so no extensions are needed. inline sysio::chain::signed_transaction make_signed_trx( const std::vector& privs, - const sysio::chain::chain_id_type& chain_id, - bool include_ed_ext = false + const sysio::chain::chain_id_type& chain_id ) { using namespace sysio::chain; @@ -50,18 +27,7 @@ inline sysio::chain::signed_transaction make_signed_trx( auto digest = trx.sig_digest(chain_id); for (auto& priv : privs) { - // this will always be a recoverable signature (e.g. K1) - auto sig = priv.sign(digest); - trx.signatures.emplace_back(sig); - - // only if a non‑recoverable path ever happens: - if (include_ed_ext && !sig.is_recoverable()) { - auto pub = priv.get_public_key(); - trx.transaction_extensions.emplace_back( - ED_EXTENSION_ID, - fc::raw::pack(pub) - ); - } + trx.signatures.emplace_back(priv.sign(digest)); } return trx;