Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions libraries/sysiolib/core/sysio/crypto.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "serialize.hpp"

#include <array>
#include <optional>

namespace sysio {

Expand Down Expand Up @@ -446,13 +447,43 @@ namespace sysio {
/**
* Calculates the public key used for a given signature on a given digest.
*
* Aborts the transaction if the signature is malformed or cannot be
* recovered. Use @ref try_recover_key instead when the signature bytes
* are untrusted and the caller must not abort on bad input.
*
* @ingroup crypto
* @param digest - Digest of the message that was signed
* @param sig - Signature
* @return sysio::public_key - Recovered public key
*/
sysio::public_key recover_key( const sysio::checksum256& digest, const sysio::signature& sig );

/**
* Recovers the public key for a signature over a digest without aborting
* the transaction on a malformed or unrecoverable signature.
*
* Wraps the same host `recover_key` intrinsic as @ref recover_key, but
* treats the host's contract-observable failure code (bad / empty /
* truncated signature, unactivated or unknown signature variant, or
* recovery-math failure) as a recoverable result. Intended for callers
* that accept untrusted, user-submitted signatures and must not halt the
* transaction on bad input (for example consensus inline-action
* handlers).
*
* @ingroup crypto
* @param digest - Digest of the message that was signed
* @param sig - Signature
* @return the recovered public key, or `std::nullopt` if the signature
* could not be recovered
*
* @note The non-throwing guarantee requires a host whose `recover_key`
* intrinsic returns a negative code on failure rather than
* aborting. The host's subjective WebAuthn variable-size guard is
* deliberately not maskable: it rejects the transaction before the
* contract observes anything.
*/
std::optional<sysio::public_key> try_recover_key( const sysio::checksum256& digest, const sysio::signature& sig );

/**
* Tests a given public key with the recovered public key from digest and signature.
*
Expand Down
112 changes: 84 additions & 28 deletions libraries/sysiolib/crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
*/
#include "core/sysio/crypto.hpp"
#include "core/sysio/datastream.hpp"
#include "core/sysio/check.hpp"

#include <cstring>
#include <optional>

extern "C" {
struct __attribute__((aligned (16))) capi_checksum160 { uint8_t hash[20]; };
Expand Down Expand Up @@ -90,35 +92,82 @@ namespace sysio {
return {hash.hash};
}

sysio::public_key recover_key( const sysio::checksum256& digest, const sysio::signature& sig ) {
auto digest_data = digest.extract_as_byte_array();

auto sig_data = sysio::pack(sig);

char optimistic_pubkey_data[256];
size_t pubkey_size = ::recover_key( reinterpret_cast<const capi_checksum256*>(digest_data.data()),
sig_data.data(), sig_data.size(),
optimistic_pubkey_data, sizeof(optimistic_pubkey_data) );

sysio::public_key pubkey;
if ( pubkey_size <= sizeof(optimistic_pubkey_data) ) {
sysio::datastream<const char*> pubkey_ds( optimistic_pubkey_data, pubkey_size );
pubkey_ds >> pubkey;
} else {
constexpr static size_t max_stack_buffer_size = 512;
void* pubkey_data = (max_stack_buffer_size < pubkey_size) ? malloc(pubkey_size) : alloca(pubkey_size);

::recover_key( reinterpret_cast<const capi_checksum256*>(digest_data.data()),
sig_data.data(), sig_data.size(),
reinterpret_cast<char*>(pubkey_data), pubkey_size );
sysio::datastream<const char*> pubkey_ds( reinterpret_cast<const char*>(pubkey_data), pubkey_size );
pubkey_ds >> pubkey;

if( max_stack_buffer_size < pubkey_size ) {
free(pubkey_data);
namespace {
/// Shared raw signature-recovery path. Returns the recovered public key,
/// or std::nullopt when the host signals a contract-observable failure
/// via a negative return code (bad / empty / truncated signature,
/// unactivated or unknown signature variant, or recovery-math failure).
/// Handles the host's query-then-fill contract for public keys larger
/// than the optimistic stack buffer. One implementation shared by
/// recover_key (which aborts on failure) and try_recover_key (which
/// surfaces it to the caller).
std::optional<sysio::public_key>
recover_key_impl( const sysio::checksum256& digest, const sysio::signature& sig ) {
auto digest_data = digest.extract_as_byte_array();

auto sig_data = sysio::pack(sig);

// capi_checksum256 is declared alignas(16); digest_data is byte-array
// storage (alignment 1). reinterpret_cast'ing its data to
// capi_checksum256* is a misaligned-pointer access -- UB that can
// trap or be miscompiled on strict / native targets (this path is
// also exercised natively by the unit tests). Copy into a properly
// aligned local and pass that instead.
capi_checksum256 capi_digest{};
std::memcpy( capi_digest.hash, digest_data.data(), sizeof(capi_digest.hash) );

char optimistic_pubkey_data[256];
// `::recover_key` returns int: the required public-key size on
// success, or a negative code on a contract-observable failure.
// Capturing it as signed first is essential -- assigning the -1
// sentinel straight into a size_t yields SIZE_MAX and walks the
// large-buffer path with a bogus length.
int rc = ::recover_key( &capi_digest,
sig_data.data(), sig_data.size(),
optimistic_pubkey_data, sizeof(optimistic_pubkey_data) );
if ( rc < 0 )
return std::nullopt;

size_t pubkey_size = static_cast<size_t>(rc);
sysio::public_key pubkey;
if ( pubkey_size <= sizeof(optimistic_pubkey_data) ) {
sysio::datastream<const char*> pubkey_ds( optimistic_pubkey_data, pubkey_size );
pubkey_ds >> pubkey;
} else {
constexpr static size_t max_stack_buffer_size = 512;
void* pubkey_data = (max_stack_buffer_size < pubkey_size) ? malloc(pubkey_size) : alloca(pubkey_size);
// malloc can fail; alloca cannot return null. A failed allocation
// is an environment failure (not a bad-signature condition), so it
// aborts rather than masquerading as an unrecoverable signature.
sysio::check( pubkey_data != nullptr, "recover_key: public-key buffer allocation failed" );

int refill_rc = ::recover_key( &capi_digest,
sig_data.data(), sig_data.size(),
reinterpret_cast<char*>(pubkey_data), pubkey_size );
// The first call already succeeded, so the refill must report the
// same size; anything else is an inconsistent host rather than a
// contract-observable failure.
sysio::check( refill_rc >= 0 && static_cast<size_t>(refill_rc) == pubkey_size,
"recover_key: inconsistent public-key size on refill" );
sysio::datastream<const char*> pubkey_ds( reinterpret_cast<const char*>(pubkey_data), pubkey_size );
pubkey_ds >> pubkey;

if( max_stack_buffer_size < pubkey_size ) {
free(pubkey_data);
}
}
return pubkey;
}
return pubkey;
} // namespace

sysio::public_key recover_key( const sysio::checksum256& digest, const sysio::signature& sig ) {
auto recovered = recover_key_impl( digest, sig );
sysio::check( recovered.has_value(), "unable to recover public key from signature" );
return *recovered;
}

std::optional<sysio::public_key> try_recover_key( const sysio::checksum256& digest, const sysio::signature& sig ) {
return recover_key_impl( digest, sig );
}

void assert_recover_key( const sysio::checksum256& digest, const sysio::signature& sig, const sysio::public_key& pubkey ) {
Expand All @@ -127,7 +176,14 @@ namespace sysio {
auto sig_data = sysio::pack(sig);
auto pubkey_data = sysio::pack(pubkey);

::assert_recover_key( reinterpret_cast<const capi_checksum256*>(digest_data.data()),
// capi_checksum256 is alignas(16); digest_data is byte-array storage
// (alignment 1). Copy into a properly aligned local rather than
// reinterpret_cast'ing through a misaligned pointer (UB; can trap on
// strict / native targets).
capi_checksum256 capi_digest{};
std::memcpy( capi_digest.hash, digest_data.data(), sizeof(capi_digest.hash) );

::assert_recover_key( &capi_digest,
sig_data.data(), sig_data.size(),
pubkey_data.data(), pubkey_data.size() );
}
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/crypto_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#include <sysio/tester.hpp>
#include <sysio/crypto.hpp>

#include <cstring>
#include <optional>

using sysio::public_key;
using sysio::signature;
using namespace sysio::native;
Expand Down Expand Up @@ -36,6 +39,54 @@ SYSIO_TEST_BEGIN(signature_type_test)
CHECK_EQUAL( (signature(std::in_place_index<0>, std::array<char, 65>{}) != signature(std::in_place_index<0>, std::array<char, 65>{})), false )
SYSIO_TEST_END

// Exercises the recover_key / try_recover_key wrappers end to end against a
// stubbed host intrinsic (set_intrinsic): wrapper -> ::recover_key -> the
// native intrinsic table -> datastream unpack. Pins the host return-code
// contract the wrappers depend on: a non-negative result is the recovered
// public key's packed size; a negative result is a contract-observable
// failure. recover_key aborts on failure; try_recover_key surfaces it as
// std::nullopt without trapping.
SYSIO_TEST_BEGIN(try_recover_key_test)
// Deterministic K1 public key (variant index 0) round-tripped through the
// stub so the recovered key is checkable without a real secp256k1 vector.
std::array<char, 33> raw{};
for ( size_t i = 0; i < raw.size(); ++i ) raw[i] = static_cast<char>(i + 1);
const public_key expected( std::in_place_index<0>, raw );
const std::vector<char> packed = sysio::pack( expected );

const sysio::checksum256 digest; // host stubbed; contents irrelevant
const signature sig( std::in_place_index<0>, std::array<char, 65>{} );

// --- success: host returns the packed key and its size ---
intrinsics::set_intrinsic<intrinsics::recover_key>(
[&]( const capi_checksum256*, const char*, size_t, char* pub, size_t publen ) -> int {
if ( pub != nullptr && publen >= packed.size() )
std::memcpy( pub, packed.data(), packed.size() );
return static_cast<int>( packed.size() );
} );
{
auto recovered = sysio::try_recover_key( digest, sig );
CHECK_EQUAL( recovered.has_value(), true )
// CHECK_EQUAL is non-fatal; guard the deref so a regression reports a
// clean failure instead of dereferencing an empty optional (UB).
if ( recovered )
CHECK_EQUAL( (*recovered == expected), true )
CHECK_EQUAL( (sysio::recover_key( digest, sig ) == expected), true )
}

// --- failure: host signals a contract-observable failure (rc < 0) ---
intrinsics::set_intrinsic<intrinsics::recover_key>(
[]( const capi_checksum256*, const char*, size_t, char*, size_t ) -> int {
return -1;
} );
{
auto recovered = sysio::try_recover_key( digest, sig );
CHECK_EQUAL( recovered.has_value(), false ) // clean nullopt, no trap
CHECK_ASSERT( "unable to recover public key from signature",
([&](){ sysio::recover_key( digest, sig ); }) )
}
SYSIO_TEST_END

int main(int argc, char* argv[]) {
bool verbose = false;
if( argc >= 2 && std::strcmp( argv[1], "-v" ) == 0 ) {
Expand All @@ -45,5 +96,6 @@ int main(int argc, char* argv[]) {

SYSIO_TEST(public_key_type_test)
SYSIO_TEST(signature_type_test)
SYSIO_TEST(try_recover_key_test)
return has_failed();
}