diff --git a/cpp/HybridNativeUtils.cpp b/cpp/HybridNativeUtils.cpp index 20f0de1..3976b68 100644 --- a/cpp/HybridNativeUtils.cpp +++ b/cpp/HybridNativeUtils.cpp @@ -207,4 +207,105 @@ double HybridNativeUtils::multiply(double a, double b) { return a * b; } +static void sha256Hash(const uint8_t* data, size_t dataLen, uint8_t* output) { + auto hasher = Botan::HashFunction::create("SHA-256"); + if (!hasher) { + throw std::runtime_error("Failed to create SHA-256 hasher"); + } + hasher->update(data, dataLen); + hasher->final(output); +} + +bool HybridNativeUtils::ecdsaVerify( + const std::shared_ptr& signature, + const std::shared_ptr& message, + const std::shared_ptr& pubKey, + bool prehash, + bool lowS, + const std::string& format) { + initializeContext(); + + const uint8_t* sigBytes = static_cast(signature->data()); + size_t sigLen = signature->size(); + const uint8_t* msgBytes = static_cast(message->data()); + size_t msgLen = message->size(); + const uint8_t* pubKeyBytes = static_cast(pubKey->data()); + size_t pubKeyLen = pubKey->size(); + + // Parse the signature based on format + secp256k1_ecdsa_signature sig; + + if (format == "compact") { + if (sigLen != 64) { + return false; + } + if (!secp256k1_ecdsa_signature_parse_compact(g_ctx, &sig, sigBytes)) { + return false; + } + } else if (format == "recovered") { + // Recovered format is 65 bytes: recovery byte + 64 byte compact signature + if (sigLen != 65) { + return false; + } + // Skip the first byte (recovery byte) and parse the remaining 64 bytes as compact + if (!secp256k1_ecdsa_signature_parse_compact(g_ctx, &sig, sigBytes + 1)) { + return false; + } + } else if (format == "der") { + // Defensive checks for DER format to prevent crashes on malformed input + // Valid secp256k1 DER signatures are typically 70-72 bytes, max ~73 bytes + // Minimum is 8 bytes (2 for SEQUENCE header + 2x3 for minimal integers) + // Strict DER validation prevents parsing ambiguities and improves security + if (sigLen < 8 || sigLen > 73) { + return false; + } + // Must start with SEQUENCE tag (0x30) + if (sigBytes[0] != 0x30) { + return false; + } + // The length byte should indicate remaining length + // For short form (which all valid ECDSA sigs use), it should be sigLen - 2 + if (sigBytes[1] != sigLen - 2) { + return false; + } + if (!secp256k1_ecdsa_signature_parse_der(g_ctx, &sig, sigBytes, sigLen)) { + return false; + } + } else { + throw std::runtime_error("Invalid signature format. Must be 'compact', 'recovered', or 'der'"); + } + + // Check if signature has high S and handle accordingly + // secp256k1_ecdsa_signature_normalize returns 1 if the signature had high S (was normalized) + // We always normalize the signature for verification (both (r,s) and (r,n-s) are mathematically valid) + // but only reject high-S when lowS=true + bool wasHighS = secp256k1_ecdsa_signature_normalize(g_ctx, &sig, &sig) == 1; + + // Reject high-S signatures if lowS enforcement is requested + if (lowS && wasHighS) { + return false; + } + + // Compute message hash if prehash is true + uint8_t msgHash[32]; + if (prehash) { + sha256Hash(msgBytes, msgLen, msgHash); + } else { + // Message should already be a 32-byte hash + if (msgLen != 32) { + return false; + } + memcpy(msgHash, msgBytes, 32); + } + + // Parse the public key + secp256k1_pubkey parsedPubKey; + if (!secp256k1_ec_pubkey_parse(g_ctx, &parsedPubKey, pubKeyBytes, pubKeyLen)) { + return false; + } + + // Verify the signature + return secp256k1_ecdsa_verify(g_ctx, &sig, msgHash, &parsedPubKey) == 1; +} + } // namespace margelo::nitro::metamask_nativeutils \ No newline at end of file diff --git a/cpp/HybridNativeUtils.hpp b/cpp/HybridNativeUtils.hpp index 9d87658..ecfe332 100644 --- a/cpp/HybridNativeUtils.hpp +++ b/cpp/HybridNativeUtils.hpp @@ -17,6 +17,7 @@ class HybridNativeUtils : public HybridNativeUtilsSpec { std::shared_ptr keccak256FromBytes(const std::shared_ptr& data) override; std::shared_ptr pubToAddress(const std::shared_ptr& pubKey, bool sanitize = false) override; std::shared_ptr hmacSha512(const std::shared_ptr& key, const std::shared_ptr& data) override; + bool ecdsaVerify(const std::shared_ptr& signature, const std::shared_ptr& message, const std::shared_ptr& pubKey, bool prehash, bool lowS, const std::string& format) override; }; } // namespace margelo::nitro::metamask_nativeutils diff --git a/example/src/App.tsx b/example/src/App.tsx index 24109b9..5c4486f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -52,6 +52,10 @@ import { runAllEd25519Benchmarks, type BenchmarkResult as Ed25519BenchmarkResult, } from './benchmarks/ed25519Benchmark'; +import { + runAllEcdsaVerifyBenchmarks, + type BenchmarkResult as EcdsaVerifyBenchmarkResult, +} from './benchmarks/ecdsaVerifyBenchmark'; import { testEd25519BasicFunctionality, testEd25519PublicKeyFormat, @@ -66,6 +70,7 @@ import { verifyMultipleEd25519Vectors, type Ed25519VerificationResult, } from './tests/ed25519NobleCompatibilityTests'; +import { runAllEcdsaVerifyTests } from './tests/ecdsaVerifyTests'; // Define test suite configuration interface TestSuite { @@ -91,6 +96,7 @@ export default function App() { ed25519: TestResult[]; ed25519Noble: TestResult[]; ed25519Verification: Ed25519VerificationResult[]; + ecdsaVerify: TestResult[]; }>({ basic: [], noble: [], @@ -103,6 +109,7 @@ export default function App() { ed25519: [], ed25519Noble: [], ed25519Verification: [], + ecdsaVerify: [], }); const [benchmarkResults, setBenchmarkResults] = useState<{ @@ -111,12 +118,14 @@ export default function App() { pubToAddressSuite: PubToAddressBenchmarkResult[] | null; keccak256Suite: Keccak256BenchmarkResult[] | null; ed25519Suite: Ed25519BenchmarkResult[] | null; + ecdsaVerifySuite: EcdsaVerifyBenchmarkResult[] | null; }>({ suite: null, hmacSuite: null, pubToAddressSuite: null, keccak256Suite: null, ed25519Suite: null, + ecdsaVerifySuite: null, }); const [isRunning, setIsRunning] = useState(false); @@ -201,6 +210,11 @@ export default function App() { key: 'ed25519Verification', runner: () => verifyMultipleEd25519Vectors(), }, + { + name: 'ECDSA Verify - secp256k1 signature verification', + key: 'ecdsaVerify', + runner: () => runAllEcdsaVerifyTests(), + }, ]; const clearAllResults = () => { @@ -217,6 +231,7 @@ export default function App() { ed25519: [], ed25519Noble: [], ed25519Verification: [], + ecdsaVerify: [], }); setBenchmarkResults({ suite: null, @@ -224,6 +239,7 @@ export default function App() { pubToAddressSuite: null, keccak256Suite: null, ed25519Suite: null, + ecdsaVerifySuite: null, }); }; @@ -340,6 +356,7 @@ export default function App() { ...testResults.ed25519.map((r) => ({ success: r.success })), ...testResults.ed25519Noble.map((r) => ({ success: r.success })), ...testResults.ed25519Verification.map((r) => ({ success: r.matches })), + ...testResults.ecdsaVerify.map((r) => ({ success: r.success })), ]; const totalTests = allResults.length; @@ -371,6 +388,8 @@ export default function App() { passed = results.filter((r: TestResult) => r.success).length; } else if (key === 'keccak256') { passed = results.filter((r: TestResult) => r.success).length; + } else if (key === 'ecdsaVerify') { + passed = results.filter((r: TestResult) => r.success).length; } else { passed = results.filter((r: TestResult) => r.success).length; } @@ -416,6 +435,17 @@ export default function App() { disabled={isRunning} /> + +