diff --git a/Cargo.lock b/Cargo.lock index 0c7e4ec10c..3291b2eef0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2416,6 +2416,7 @@ dependencies = [ "sbor", "scrypto", "scrypto-test", + "secp256k1", "serde", "serde_json", "trybuild", diff --git a/radix-common/src/crypto/secp256k1/public_key.rs b/radix-common/src/crypto/secp256k1/public_key.rs index 309a0e2435..6a1018d08b 100644 --- a/radix-common/src/crypto/secp256k1/public_key.rs +++ b/radix-common/src/crypto/secp256k1/public_key.rs @@ -2,7 +2,19 @@ use crate::internal_prelude::*; #[cfg(feature = "fuzzing")] use arbitrary::Arbitrary; -/// Represents an ECDSA Secp256k1 public key. +/// Represents an uncompressed ECDSA Secp256k1 public key. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Sbor)] +#[sbor(transparent)] +pub struct Secp256k1UncompressedPublicKey( + #[cfg_attr(feature = "serde", serde(with = "hex::serde"))] pub [u8; Self::LENGTH], +); + +impl Secp256k1UncompressedPublicKey { + pub const LENGTH: usize = 65; +} + +/// Represents a compressed ECDSA Secp256k1 public key, which is the default format used in the Radix stack. #[cfg_attr(feature = "fuzzing", derive(Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive( diff --git a/radix-common/src/crypto/signature_validator.rs b/radix-common/src/crypto/signature_validator.rs index 74c299b320..eeb2a7cf43 100644 --- a/radix-common/src/crypto/signature_validator.rs +++ b/radix-common/src/crypto/signature_validator.rs @@ -22,6 +22,28 @@ pub fn verify_and_recover_secp256k1( None } +#[cfg(feature = "secp256k1_sign_and_validate")] +pub fn verify_and_recover_secp256k1_uncompressed( + signed_hash: &Hash, + signature: &Secp256k1Signature, +) -> Option { + let recovery_id = signature.0[0]; + let signature_data = &signature.0[1..]; + if let Ok(id) = ::secp256k1::ecdsa::RecoveryId::from_i32(recovery_id.into()) { + if let Ok(sig) = ::secp256k1::ecdsa::RecoverableSignature::from_compact(signature_data, id) + { + let msg = ::secp256k1::Message::from_digest_slice(&signed_hash.0) + .expect("Hash is always a valid message"); + + // The recover method also verifies the signature as part of the recovery process + if let Ok(pk) = SECP256K1_CTX.recover_ecdsa(&msg, &sig) { + return Some(Secp256k1UncompressedPublicKey(pk.serialize_uncompressed())); + } + } + } + None +} + #[cfg(feature = "secp256k1_sign_and_validate")] pub fn verify_secp256k1( signed_hash: &Hash, diff --git a/radix-engine-tests/Cargo.toml b/radix-engine-tests/Cargo.toml index f8420e26e9..3cc9616eaa 100644 --- a/radix-engine-tests/Cargo.toml +++ b/radix-engine-tests/Cargo.toml @@ -41,6 +41,7 @@ paste = { workspace = true } hex = { workspace = true } trybuild = { workspace = true } extend = { workspace = true } +secp256k1 = { workspace = true } [build-dependencies] walkdir = { workspace = true, optional = true } diff --git a/radix-engine-tests/assets/blueprints/crypto_scrypto_v2/src/lib.rs b/radix-engine-tests/assets/blueprints/crypto_scrypto_v2/src/lib.rs index 415b60ce35..f801863ada 100644 --- a/radix-engine-tests/assets/blueprints/crypto_scrypto_v2/src/lib.rs +++ b/radix-engine-tests/assets/blueprints/crypto_scrypto_v2/src/lib.rs @@ -68,5 +68,12 @@ mod component_module { ) -> Secp256k1PublicKey { CryptoUtils::secp256k1_ecdsa_verify_and_key_recover(&hash, &signature) } + + pub fn secp256k1_ecdsa_verify_and_key_recover_uncompressed( + hash: Hash, + signature: Secp256k1Signature, + ) -> Secp256k1UncompressedPublicKey { + CryptoUtils::secp256k1_ecdsa_verify_and_key_recover_uncompressed(&hash, &signature) + } } } diff --git a/radix-engine-tests/tests/system/crypto_utils.rs b/radix-engine-tests/tests/system/crypto_utils.rs index c6b419bc07..05e286804a 100644 --- a/radix-engine-tests/tests/system/crypto_utils.rs +++ b/radix-engine-tests/tests/system/crypto_utils.rs @@ -576,6 +576,7 @@ fn crypto_scrypto_secp256k1_ecdsa_verify_and_key_recover( package_address: PackageAddress, hash: Hash, signature: Secp256k1Signature, + compressed: bool, ) -> TransactionReceipt { runner.execute_manifest( ManifestBuilder::new() @@ -583,7 +584,11 @@ fn crypto_scrypto_secp256k1_ecdsa_verify_and_key_recover( .call_function( package_address, "CryptoScrypto", - "secp256k1_ecdsa_verify_and_key_recover", + if compressed { + "secp256k1_ecdsa_verify_and_key_recover" + } else { + "secp256k1_ecdsa_verify_and_key_recover_uncompressed" + }, manifest_args!(hash, signature), ) .build(), @@ -688,16 +693,31 @@ fn test_crypto_scrypto_key_recover_secp256k1_ecdsa() { let hash1_signature = Secp256k1Signature::from_str(hash1_signature).unwrap(); // Act - let pk_recovered: Secp256k1PublicKey = + let pk_recovered1: Secp256k1PublicKey = get_output!(crypto_scrypto_secp256k1_ecdsa_verify_and_key_recover( &mut ledger, package_address, hash1, hash1_signature, + true + )); + let pk_recovered2: [u8; 65] = + get_output!(crypto_scrypto_secp256k1_ecdsa_verify_and_key_recover( + &mut ledger, + package_address, + hash1, + hash1_signature, + false )); // Assert - assert_eq!(pk, pk_recovered); + assert_eq!(pk, pk_recovered1); + assert_eq!( + secp256k1::PublicKey::from_slice(pk.as_ref()) + .unwrap() + .serialize_uncompressed(), + pk_recovered2 + ); // Test for key recovery error let invalid_signature = "01cd8dcd5bb841430dd0a6f45565a1b8bdb4a204eb868832cd006f963a89a662813ab844a542fcdbfda4086a83fbbde516214113051b9c8e42a206c98d564d7122"; @@ -708,6 +728,7 @@ fn test_crypto_scrypto_key_recover_secp256k1_ecdsa() { package_address, hash1, invalid_signature, + true )); // Assert diff --git a/radix-engine/src/vm/wasm/constants.rs b/radix-engine/src/vm/wasm/constants.rs index 0e99bc3eed..13b5d2b5bd 100644 --- a/radix-engine/src/vm/wasm/constants.rs +++ b/radix-engine/src/vm/wasm/constants.rs @@ -94,6 +94,8 @@ pub const CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_FUNCTION_NAME: &str = "crypto_utils_secp256k1_ecdsa_verify"; pub const CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_FUNCTION_NAME: &str = "crypto_utils_secp256k1_ecdsa_verify_and_key_recover"; +pub const CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_UNCOMPRESSED_FUNCTION_NAME: &str = + "crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed"; //================= // WASM Shim diff --git a/radix-engine/src/vm/wasm/prepare.rs b/radix-engine/src/vm/wasm/prepare.rs index a6af9e31d2..ad7c88f1e4 100644 --- a/radix-engine/src/vm/wasm/prepare.rs +++ b/radix-engine/src/vm/wasm/prepare.rs @@ -965,6 +965,32 @@ impl WasmModule { )); } } + CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_UNCOMPRESSED_FUNCTION_NAME => + { + if version < ScryptoVmVersion::crypto_utils_v2() { + return Err(PrepareError::InvalidImport( + InvalidImport::ProtocolVersionMismatch { + name: entry.name.to_string(), + current_version: version.into(), + expected_version: ScryptoVmVersion::crypto_utils_v2().into(), + }, + )); + } + + if let TypeRef::Func(type_index) = entry.ty { + if Self::function_type_matches( + &self.module, + type_index, + vec![ValType::I32, ValType::I32, ValType::I32, ValType::I32], + vec![ValType::I64], + ) { + continue; + } + return Err(PrepareError::InvalidImport( + InvalidImport::InvalidFunctionType(entry.name.to_string()), + )); + } + } // Crypto Utils v2 end _ => {} }; @@ -1553,6 +1579,7 @@ mod tests { CRYPTO_UTILS_ED25519_VERIFY_FUNCTION_NAME, CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_FUNCTION_NAME, CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_FUNCTION_NAME, + CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_UNCOMPRESSED_FUNCTION_NAME, ], ), ] { diff --git a/radix-engine/src/vm/wasm/traits.rs b/radix-engine/src/vm/wasm/traits.rs index f6ce7bc64c..d5958ee85c 100644 --- a/radix-engine/src/vm/wasm/traits.rs +++ b/radix-engine/src/vm/wasm/traits.rs @@ -255,6 +255,12 @@ pub trait WasmRuntime { message: Vec, signature: Vec, ) -> Result>; + + fn crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed( + &mut self, + message: Vec, + signature: Vec, + ) -> Result>; } /// Represents an instantiated, invocable Scrypto module. diff --git a/radix-engine/src/vm/wasm/wasmi.rs b/radix-engine/src/vm/wasm/wasmi.rs index 80071a0614..ab7a42b94e 100644 --- a/radix-engine/src/vm/wasm/wasmi.rs +++ b/radix-engine/src/vm/wasm/wasmi.rs @@ -910,6 +910,29 @@ fn secp256k1_ecdsa_verify_and_key_recover( .map(|buffer| buffer.0) } +fn secp256k1_ecdsa_verify_and_key_recover_uncompressed( + mut caller: Caller<'_, HostState>, + message_ptr: u32, + message_len: u32, + signature_ptr: u32, + signature_len: u32, +) -> Result> { + let runtime = grab_runtime!(caller); + let memory = grab_memory!(caller); + + let message = read_memory(caller.as_context_mut(), memory, message_ptr, message_len)?; + let signature = read_memory( + caller.as_context_mut(), + memory, + signature_ptr, + signature_len, + )?; + + runtime + .crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed(message, signature) + .map(|buffer| buffer.0) +} + #[cfg(feature = "radix_engine_tests")] fn test_host_read_memory( mut caller: Caller<'_, HostState>, @@ -1623,6 +1646,24 @@ impl WasmiModule { .map_err(|e| e.into()) }, ); + let host_secp2561k1_ecdsa_verify_and_key_recover_uncompressed = Func::wrap( + store.as_context_mut(), + |caller: Caller<'_, HostState>, + message_ptr: u32, + message_len: u32, + signature_ptr: u32, + signature_len: u32| + -> Result { + secp256k1_ecdsa_verify_and_key_recover_uncompressed( + caller, + message_ptr, + message_len, + signature_ptr, + signature_len, + ) + .map_err(|e| e.into()) + }, + ); let mut linker = >::new(); @@ -1826,6 +1867,11 @@ impl WasmiModule { CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_FUNCTION_NAME, host_secp2561k1_ecdsa_verify_and_key_recover ); + linker_define!( + linker, + CRYPTO_UTILS_SECP256K1_ECDSA_VERIFY_AND_KEY_RECOVER_UNCOMPRESSED_FUNCTION_NAME, + host_secp2561k1_ecdsa_verify_and_key_recover_uncompressed + ); #[cfg(feature = "radix_engine_tests")] { diff --git a/radix-engine/src/vm/wasm_runtime/no_op_runtime.rs b/radix-engine/src/vm/wasm_runtime/no_op_runtime.rs index a30b41e017..5cf8482bfc 100644 --- a/radix-engine/src/vm/wasm_runtime/no_op_runtime.rs +++ b/radix-engine/src/vm/wasm_runtime/no_op_runtime.rs @@ -378,4 +378,12 @@ impl<'a> WasmRuntime for NoOpWasmRuntime<'a> { ) -> Result> { Err(InvokeError::SelfError(WasmRuntimeError::NotImplemented)) } + + fn crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed( + &mut self, + message: Vec, + signature: Vec, + ) -> Result> { + Err(InvokeError::SelfError(WasmRuntimeError::NotImplemented)) + } } diff --git a/radix-engine/src/vm/wasm_runtime/scrypto_runtime.rs b/radix-engine/src/vm/wasm_runtime/scrypto_runtime.rs index ee61989c15..d90294bc55 100644 --- a/radix-engine/src/vm/wasm_runtime/scrypto_runtime.rs +++ b/radix-engine/src/vm/wasm_runtime/scrypto_runtime.rs @@ -794,4 +794,25 @@ impl<'y, Y: SystemApi> WasmRuntime for ScryptoRuntime<'y, Y> { self.allocate_buffer(key.to_vec()) } + + /// This method is only available to packages uploaded after "Cuttlefish" + /// protocol update due to checks in [`ScryptoV1WasmValidator::validate`]. + #[trace_resources] + fn crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed( + &mut self, + message: Vec, + signature: Vec, + ) -> Result> { + let hash = Hash::try_from(message.as_slice()).map_err(WasmRuntimeError::InvalidHash)?; + let signature = Secp256k1Signature::try_from(signature.as_ref()) + .map_err(WasmRuntimeError::InvalidSecp256k1Signature)?; + + self.api + .consume_cost_units(ClientCostingEntry::Secp256k1EcdsaKeyRecover)?; + + let key = verify_and_recover_secp256k1_uncompressed(&hash, &signature) + .ok_or(WasmRuntimeError::Secp256k1KeyRecoveryError)?; + + self.allocate_buffer(key.0.to_vec()) + } } diff --git a/scrypto/src/crypto_utils/crypto_utils.rs b/scrypto/src/crypto_utils/crypto_utils.rs index e1a774c0e8..06c9464428 100644 --- a/scrypto/src/crypto_utils/crypto_utils.rs +++ b/scrypto/src/crypto_utils/crypto_utils.rs @@ -1,6 +1,9 @@ use crate::engine::wasm_api::{copy_buffer, crypto_utils}; use radix_common::{ - crypto::{Ed25519PublicKey, Ed25519Signature, Secp256k1PublicKey, Secp256k1Signature}, + crypto::{ + Ed25519PublicKey, Ed25519Signature, Secp256k1PublicKey, Secp256k1Signature, + Secp256k1UncompressedPublicKey, + }, prelude::{scrypto_decode, scrypto_encode, Bls12381G1PublicKey, Bls12381G2Signature, Hash}, }; use sbor::prelude::Vec; @@ -156,4 +159,19 @@ impl CryptoUtils { }); Secp256k1PublicKey(key.try_into().unwrap()) } + + pub fn secp256k1_ecdsa_verify_and_key_recover_uncompressed( + message_hash: impl AsRef, + signature: impl AsRef, + ) -> Secp256k1UncompressedPublicKey { + let key = copy_buffer(unsafe { + crypto_utils::crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed( + message_hash.as_ref().0.as_ptr(), + message_hash.as_ref().0.len(), + signature.as_ref().0.as_ptr(), + signature.as_ref().0.len(), + ) + }); + Secp256k1UncompressedPublicKey(key.try_into().unwrap()) + } } diff --git a/scrypto/src/engine/wasm_api.rs b/scrypto/src/engine/wasm_api.rs index 4fde4a6fff..746740c037 100644 --- a/scrypto/src/engine/wasm_api.rs +++ b/scrypto/src/engine/wasm_api.rs @@ -346,6 +346,12 @@ pub mod crypto_utils { message_len: usize, signature_ptr: *const u8, signature_len: usize) -> Buffer; + + pub fn crypto_utils_secp256k1_ecdsa_verify_and_key_recover_uncompressed( + message_ptr: *const u8, + message_len: usize, + signature_ptr: *const u8, + signature_len: usize) -> Buffer; } }