From 67ff1433256d0926c1368dad041148f7bf55ee90 Mon Sep 17 00:00:00 2001 From: Jonathan Glanz Date: Sat, 16 May 2026 19:51:51 -0400 Subject: [PATCH] crypto: add recover_key_nothrow intrinsic + CDT bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CDT contracts compile with -fno-exceptions, so the throwing recover_key intrinsic halts the WASM on attacker-controlled signature bytes — fatal inside any handler that runs in the evalcons inline-action chain (e.g. sysio.uwrit::try_select_winner), because a halt stalls consensus. This adds a recover_key_nothrow variant that returns std::nullopt on any failure path (malformed bytes, unactivated sig type, recovery math failure, subjective-size limit) instead of throwing. Wired through: - imports/cdt.imports.in — declare the intrinsic name for the WASM importer. - libraries/sysiolib/capi/sysio/crypto.h — C-API entry with the WASM import attribute + full Doxygen. - libraries/sysiolib/crypto.cpp — C++ wrapper returning std::optional, mirroring the existing recover_key buffer-resize loop but binding the no-throw intrinsic. - libraries/sysiolib/core/sysio/crypto.hpp — header declaration + include. - libraries/native/intrinsics.cpp + intrinsics_def.hpp — native-shim entry so test contracts running on the native runtime resolve the symbol. The host-side wrapping (try/catch around recover_key) lives in wire-sysio. Used at the depot by sysio.uwrit::verify_uic_signature on the underwriter race path. Co-Authored-By: Claude Opus 4.7 (1M context) --- imports/cdt.imports.in | 1 + libraries/native/intrinsics.cpp | 3 ++ .../native/native/sysio/intrinsics_def.hpp | 1 + libraries/sysiolib/capi/sysio/crypto.h | 26 +++++++++++ libraries/sysiolib/core/sysio/crypto.hpp | 19 ++++++++ libraries/sysiolib/crypto.cpp | 44 +++++++++++++++++++ 6 files changed, 94 insertions(+) diff --git a/imports/cdt.imports.in b/imports/cdt.imports.in index 3a3584e9b..c910abfd4 100644 --- a/imports/cdt.imports.in +++ b/imports/cdt.imports.in @@ -124,6 +124,7 @@ publication_time read_action_data read_transaction recover_key +recover_key_nothrow remove_security_group_participants require_auth require_auth2 diff --git a/libraries/native/intrinsics.cpp b/libraries/native/intrinsics.cpp index 9bcdaf312..779f0dc58 100644 --- a/libraries/native/intrinsics.cpp +++ b/libraries/native/intrinsics.cpp @@ -58,6 +58,9 @@ extern "C" { int recover_key( const capi_checksum256* digest, const char* sig, size_t siglen, char* pub, size_t publen ) { return intrinsics::get().call(digest, sig, siglen, pub, publen); } + int recover_key_nothrow( const capi_checksum256* digest, const char* sig, size_t siglen, char* pub, size_t publen ) { + return intrinsics::get().call(digest, sig, siglen, pub, publen); + } void assert_sha256( const char* data, uint32_t length, const capi_checksum256* hash ) { return intrinsics::get().call(data, length, hash); } diff --git a/libraries/native/native/sysio/intrinsics_def.hpp b/libraries/native/native/sysio/intrinsics_def.hpp index 560d2ed70..0d7dc327b 100644 --- a/libraries/native/native/sysio/intrinsics_def.hpp +++ b/libraries/native/native/sysio/intrinsics_def.hpp @@ -59,6 +59,7 @@ intrinsic_macro(preactivate_feature) \ intrinsic_macro(get_active_producers) \ intrinsic_macro(assert_recover_key) \ intrinsic_macro(recover_key) \ +intrinsic_macro(recover_key_nothrow) \ intrinsic_macro(assert_sha256) \ intrinsic_macro(assert_sha1) \ intrinsic_macro(assert_sha512) \ diff --git a/libraries/sysiolib/capi/sysio/crypto.h b/libraries/sysiolib/capi/sysio/crypto.h index beab370d1..1195fceea 100644 --- a/libraries/sysiolib/capi/sysio/crypto.h +++ b/libraries/sysiolib/capi/sysio/crypto.h @@ -204,6 +204,32 @@ void ripemd160( const char* data, uint32_t length, struct capi_checksum160* hash __attribute__((sysio_wasm_import)) int recover_key( const struct capi_checksum256* digest, const char* sig, size_t siglen, char* pub, size_t publen ); +/** + * Non-throwing variant of `recover_key`. Catches every exception the + * host crypto path can raise (malformed signature bytes, unactivated + * signature type, recovery math failure, subjective-size limit, etc.) + * and returns -1 instead of halting the contract. On success returns + * the same byte count as `recover_key`. + * + * CDT contracts compile with `-fno-exceptions`, so a throwing + * intrinsic call from inside a contract halts dispatch — there is no + * try/catch around it. Inbound-attestation handlers + * (`feedback_opp_handlers_never_throw.md`) MUST NOT halt; they need + * this nothrow variant when verifying attacker-controlled signature + * bytes (e.g. the underwriter race resolver in `sysio.uwrit`). + * + * @param digest - Pre-hashed message to recover against + * @param sig - Packed signature bytes (sysio::signature wire format) + * @param siglen - Signature byte count + * @param pub - Destination buffer for the recovered packed public key + * @param publen - Capacity of `pub` in bytes + * + * @return Bytes written to `pub` on success; -1 on any host-side error + * (no exception escapes the call). + */ +__attribute__((sysio_wasm_import)) +int recover_key_nothrow( const struct capi_checksum256* digest, const char* sig, size_t siglen, char* pub, size_t publen ); + /** * Tests a given public key with the generated key from digest and the signature. * diff --git a/libraries/sysiolib/core/sysio/crypto.hpp b/libraries/sysiolib/core/sysio/crypto.hpp index 42e73bb4d..ccb3dd704 100644 --- a/libraries/sysiolib/core/sysio/crypto.hpp +++ b/libraries/sysiolib/core/sysio/crypto.hpp @@ -9,6 +9,7 @@ #include "serialize.hpp" #include +#include namespace sysio { @@ -453,6 +454,24 @@ namespace sysio { */ sysio::public_key recover_key( const sysio::checksum256& digest, const sysio::signature& sig ); + /** + * Non-throwing variant of `recover_key`. Returns the recovered key on + * success, `std::nullopt` if the host caught any exception (malformed + * signature bytes, unactivated signature type, recovery math failure, + * subjective-size limit, etc.). Use this from CDT contracts that MUST + * NOT halt on attacker-controlled signature bytes — see + * `feedback_opp_handlers_never_throw.md`. CDT compiles with + * `-fno-exceptions` so the throwing variant cannot be wrapped in + * contract-side `try/catch`; the host-side wrapper catches instead. + * + * @ingroup crypto + * @param digest - Digest of the message that was signed + * @param sig - Signature + * @return Recovered public key, or `std::nullopt` on any failure. + */ + std::optional recover_key_nothrow( const sysio::checksum256& digest, + const sysio::signature& sig ); + /** * Tests a given public key with the recovered public key from digest and signature. * diff --git a/libraries/sysiolib/crypto.cpp b/libraries/sysiolib/crypto.cpp index fbffd46b4..3a082cc58 100644 --- a/libraries/sysiolib/crypto.cpp +++ b/libraries/sysiolib/crypto.cpp @@ -39,6 +39,10 @@ extern "C" { int recover_key( const capi_checksum256* digest, const char* sig, size_t siglen, char* pub, size_t publen ); + __attribute__((sysio_wasm_import)) + int recover_key_nothrow( const capi_checksum256* digest, const char* sig, + size_t siglen, char* pub, size_t publen ); + __attribute__((sysio_wasm_import)) void assert_recover_key( const capi_checksum256* digest, const char* sig, size_t siglen, const char* pub, size_t publen ); @@ -121,6 +125,46 @@ namespace sysio { return pubkey; } + std::optional recover_key_nothrow( 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]; + int rc = ::recover_key_nothrow( + reinterpret_cast(digest_data.data()), + sig_data.data(), sig_data.size(), + optimistic_pubkey_data, sizeof(optimistic_pubkey_data) ); + if ( rc < 0 ) return std::nullopt; + + const size_t pubkey_size = static_cast(rc); + sysio::public_key pubkey; + if ( pubkey_size <= sizeof(optimistic_pubkey_data) ) { + sysio::datastream 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); + + int rc2 = ::recover_key_nothrow( + reinterpret_cast(digest_data.data()), + sig_data.data(), sig_data.size(), + reinterpret_cast(pubkey_data), pubkey_size ); + if ( rc2 < 0 ) { + if ( max_stack_buffer_size < pubkey_size ) free(pubkey_data); + return std::nullopt; + } + sysio::datastream pubkey_ds( + reinterpret_cast(pubkey_data), pubkey_size ); + pubkey_ds >> pubkey; + + if ( max_stack_buffer_size < pubkey_size ) free(pubkey_data); + } + return pubkey; + } + void assert_recover_key( const sysio::checksum256& digest, const sysio::signature& sig, const sysio::public_key& pubkey ) { auto digest_data = digest.extract_as_byte_array();