Skip to content
Open
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
101 changes: 101 additions & 0 deletions cpp/HybridNativeUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArrayBuffer>& signature,
const std::shared_ptr<ArrayBuffer>& message,
const std::shared_ptr<ArrayBuffer>& pubKey,
bool prehash,
bool lowS,
const std::string& format) {
initializeContext();

const uint8_t* sigBytes = static_cast<const uint8_t*>(signature->data());
size_t sigLen = signature->size();
const uint8_t* msgBytes = static_cast<const uint8_t*>(message->data());
size_t msgLen = message->size();
const uint8_t* pubKeyBytes = static_cast<const uint8_t*>(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
1 change: 1 addition & 0 deletions cpp/HybridNativeUtils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class HybridNativeUtils : public HybridNativeUtilsSpec {
std::shared_ptr<ArrayBuffer> keccak256FromBytes(const std::shared_ptr<ArrayBuffer>& data) override;
std::shared_ptr<ArrayBuffer> pubToAddress(const std::shared_ptr<ArrayBuffer>& pubKey, bool sanitize = false) override;
std::shared_ptr<ArrayBuffer> hmacSha512(const std::shared_ptr<ArrayBuffer>& key, const std::shared_ptr<ArrayBuffer>& data) override;
bool ecdsaVerify(const std::shared_ptr<ArrayBuffer>& signature, const std::shared_ptr<ArrayBuffer>& message, const std::shared_ptr<ArrayBuffer>& pubKey, bool prehash, bool lowS, const std::string& format) override;
};

} // namespace margelo::nitro::metamask_nativeutils
119 changes: 119 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -66,6 +70,7 @@ import {
verifyMultipleEd25519Vectors,
type Ed25519VerificationResult,
} from './tests/ed25519NobleCompatibilityTests';
import { runAllEcdsaVerifyTests } from './tests/ecdsaVerifyTests';

// Define test suite configuration
interface TestSuite {
Expand All @@ -91,6 +96,7 @@ export default function App() {
ed25519: TestResult[];
ed25519Noble: TestResult[];
ed25519Verification: Ed25519VerificationResult[];
ecdsaVerify: TestResult[];
}>({
basic: [],
noble: [],
Expand All @@ -103,6 +109,7 @@ export default function App() {
ed25519: [],
ed25519Noble: [],
ed25519Verification: [],
ecdsaVerify: [],
});

const [benchmarkResults, setBenchmarkResults] = useState<{
Expand All @@ -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);
Expand Down Expand Up @@ -201,6 +210,11 @@ export default function App() {
key: 'ed25519Verification',
runner: () => verifyMultipleEd25519Vectors(),
},
{
name: 'ECDSA Verify - secp256k1 signature verification',
key: 'ecdsaVerify',
runner: () => runAllEcdsaVerifyTests(),
},
];

const clearAllResults = () => {
Expand All @@ -217,13 +231,15 @@ export default function App() {
ed25519: [],
ed25519Noble: [],
ed25519Verification: [],
ecdsaVerify: [],
});
setBenchmarkResults({
suite: null,
hmacSuite: null,
pubToAddressSuite: null,
keccak256Suite: null,
ed25519Suite: null,
ecdsaVerifySuite: null,
});
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -416,6 +435,17 @@ export default function App() {
disabled={isRunning}
/>
</View>
<View style={styles.buttonContainer}>
<Button
title={
isRunning ? '⏳ Running...' : '✅ ECDSA Verify Benchmark \n'
}
onPress={() =>
runBenchmark('ecdsaVerifySuite', runAllEcdsaVerifyBenchmarks)
}
disabled={isRunning}
/>
</View>
</View>
<View style={styles.buttonRow}>
<View style={styles.buttonContainer}>
Expand Down Expand Up @@ -1053,6 +1083,95 @@ export default function App() {
})}
</View>
)}

{benchmarkResults.ecdsaVerifySuite && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>
✅ ECDSA Verify Benchmark Suite
</Text>
<View style={styles.benchmarkSummary}>
<Text style={styles.benchmarkSummaryTitle}>
📊 Performance Overview
</Text>
<Text style={styles.benchmarkSummaryText}>
{benchmarkResults.ecdsaVerifySuite.length} verification
scenarios tested
</Text>
<Text style={styles.benchmarkSummaryText}>
Average Speedup:{' '}
{(
benchmarkResults.ecdsaVerifySuite.reduce(
(sum: number, r: any) => sum + r.comparison.speedupFactor,
0,
) / benchmarkResults.ecdsaVerifySuite.length
).toFixed(2)}
x
</Text>
<Text style={styles.benchmarkSummaryText}>
All Faster:{' '}
{benchmarkResults.ecdsaVerifySuite.every(
(r: any) => r.comparison.nativeIsFaster,
)
? '✅ Yes'
: '❌ No'}
</Text>
</View>
<Text style={styles.sectionSubtitle}>Individual Test Results:</Text>
{benchmarkResults.ecdsaVerifySuite.map(
(result: any, index: number) => {
const getTestIcon = (testName: string) => {
if (testName.includes('Pre-hashed')) return '🔐';
if (testName.includes('Prehash')) return '📝';
if (testName.includes('Compressed')) return '🗜️';
return '✅';
};

return (
<View key={index} style={styles.benchmarkResult}>
<Text style={styles.benchmarkTitle}>
{getTestIcon(result.testName)} {result.testName}
</Text>
<View style={styles.benchmarkMetrics}>
<Text style={styles.benchmarkDetails}>
🚀 Native: {result.native.averageTime.toFixed(3)}ms avg
• {result.native.iops.toFixed(0)} ops/sec
</Text>
<Text style={styles.benchmarkDetails}>
📜 Noble: {result.javascript.averageTime.toFixed(3)}ms
avg • {result.javascript.iops.toFixed(0)} ops/sec
</Text>
<Text
style={[
styles.benchmarkComparison,
result.comparison.nativeIsFaster
? styles.success
: styles.failure,
]}
>
⚡ {result.comparison.speedupFactor.toFixed(2)}x{' '}
{result.comparison.nativeIsFaster ? 'faster' : 'slower'}{' '}
• {result.comparison.performanceGain.toFixed(1)}%
improvement
</Text>
<Text style={styles.benchmarkRange}>
📈 Range: {result.native.minTime.toFixed(3)}ms -{' '}
{result.native.maxTime.toFixed(3)}ms (Native) |{' '}
{result.javascript.minTime.toFixed(3)}ms -{' '}
{result.javascript.maxTime.toFixed(3)}ms (Noble)
</Text>
<Text style={styles.benchmarkStats}>
📊 Std Dev: ±
{result.native.standardDeviation.toFixed(3)}ms (Native)
• ±{result.javascript.standardDeviation.toFixed(3)}ms
(Noble)
</Text>
</View>
</View>
);
},
)}
</View>
)}
</View>
</ScrollView>
);
Expand Down
Loading