|
15 | 15 | package org.hyperledger.besu.evm.precompile; |
16 | 16 |
|
17 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 18 | +import static org.junit.jupiter.api.Assertions.assertTrue; |
18 | 19 | import static org.mockito.Mockito.mock; |
19 | 20 |
|
| 21 | +import org.hyperledger.besu.crypto.SECP256R1; |
| 22 | +import org.hyperledger.besu.crypto.SignatureAlgorithm; |
20 | 23 | import org.hyperledger.besu.evm.frame.MessageFrame; |
21 | 24 | import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; |
22 | 25 |
|
|
27 | 30 |
|
28 | 31 | import com.fasterxml.jackson.databind.ObjectMapper; |
29 | 32 | import org.apache.tuweni.bytes.Bytes; |
| 33 | +import org.apache.tuweni.bytes.Bytes32; |
30 | 34 | import org.junit.jupiter.api.BeforeAll; |
31 | 35 | import org.junit.jupiter.api.Test; |
32 | 36 | import org.junit.jupiter.params.ParameterizedTest; |
33 | 37 | import org.junit.jupiter.params.provider.MethodSource; |
34 | 38 |
|
35 | 39 | class P256VerifyPrecompiledContractTest { |
36 | 40 |
|
| 41 | + private static final SignatureAlgorithm secp256R1 = new SECP256R1(); |
37 | 42 | private static final P256VerifyPrecompiledContract contract = |
38 | | - new P256VerifyPrecompiledContract(new SpuriousDragonGasCalculator()); |
| 43 | + new P256VerifyPrecompiledContract(new SpuriousDragonGasCalculator(), secp256R1); |
39 | 44 |
|
40 | 45 | private static final MessageFrame messageFrame = mock(MessageFrame.class); |
41 | 46 |
|
@@ -87,4 +92,110 @@ void testP256Verify(final TestCase testCase) { |
87 | 92 | void sanityCheck() { |
88 | 93 | assertEquals(6_900L, contract.gasRequirement(Bytes.wrap(new byte[128]))); |
89 | 94 | } |
| 95 | + |
| 96 | + @Test |
| 97 | + void testInvalidCurvePointReturnsInvalid() { |
| 98 | + // Test using a realistic attack scenario: |
| 99 | + // Create r,s values that would verify against an invalid public key point |
| 100 | + // The point is in the field but NOT on the secp256r1 curve |
| 101 | + |
| 102 | + String messageHash = "44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56"; |
| 103 | + |
| 104 | + // Point in field but NOT on curve: these coordinates are valid field elements |
| 105 | + // but do not satisfy the secp256r1 curve equation y² = x³ - 3x + b |
| 106 | + String invalidPubKeyX = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; |
| 107 | + String invalidPubKeyY = "b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777"; |
| 108 | + |
| 109 | + // Generate r,s values that could theoretically verify with this invalid point |
| 110 | + // r component from a valid signature |
| 111 | + String r = "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"; |
| 112 | + // s component that would correspond to this scenario |
| 113 | + String s = "30dae23890abb63e378e003d7f1d5006ab23cc7b3b65b3d0c7b45c7e1e2e08b9"; |
| 114 | + |
| 115 | + String input = messageHash + r + s + invalidPubKeyX + invalidPubKeyY; |
| 116 | + Bytes inputBytes = Bytes.fromHexString(input); |
| 117 | + |
| 118 | + // call default implementation: |
| 119 | + secp256R1.disableNative(); |
| 120 | + var defaultResult = contract.computeDefault(inputBytes); |
| 121 | + // call default native implementation: |
| 122 | + secp256R1.maybeEnableNative(); |
| 123 | + var defaultMaybeNativeResult = contract.computeDefault(inputBytes); |
| 124 | + |
| 125 | + // verify boringssl precompile-specific implementation |
| 126 | + // this should be configured/run statically, adding the call here just for clarity |
| 127 | + P256VerifyPrecompiledContract.maybeEnableNativeBoringSSL(); |
| 128 | + var maybeNativeResult = contract.computePrecompile(inputBytes, messageFrame); |
| 129 | + |
| 130 | + // Should return empty bytes (INVALID) because curve validation catches |
| 131 | + // the invalid point before signature verification attempts |
| 132 | + assertTrue( |
| 133 | + defaultResult.cachedResult().isSuccessful(), |
| 134 | + "Invalid curve point should succeed with empty result"); |
| 135 | + assertEquals( |
| 136 | + Bytes.EMPTY, |
| 137 | + defaultResult.cachedResult().output(), |
| 138 | + "Invalid curve point should return empty result"); |
| 139 | + assertTrue( |
| 140 | + defaultMaybeNativeResult.cachedResult().isSuccessful(), |
| 141 | + "Invalid curve point should succeed with empty result"); |
| 142 | + assertEquals( |
| 143 | + Bytes.EMPTY, |
| 144 | + defaultMaybeNativeResult.cachedResult().output(), |
| 145 | + "Invalid curve point should return empty result"); |
| 146 | + assertTrue( |
| 147 | + maybeNativeResult.isSuccessful(), |
| 148 | + "Invalid curve point should succeed with empty result for native implementation"); |
| 149 | + assertEquals( |
| 150 | + Bytes.EMPTY, |
| 151 | + maybeNativeResult.output(), |
| 152 | + "Invalid curve point should return empty result for native implementation"); |
| 153 | + } |
| 154 | + |
| 155 | + @Test |
| 156 | + void testModularComparisonWhenRPrimeExceedsN() { |
| 157 | + |
| 158 | + // parameters borrowed from execution-spec-tests for R' exceeding N: |
| 159 | + // https://github.com/ethereum/execution-spec-tests/blob/61f8ac90841770d9fc407f1498ec0e5229b27508/tests/osaka/eip7951_p256verify_precompiles/test_p256verify.py#L303-L315 |
| 160 | + String messageHash = "BB5A52F42F9C9261ED4361F59422A1E30036E7C32B270C8807A419FECA605023"; |
| 161 | + String r = "000000000000000000000000000000004319055358E8617B0C46353D039CDAAB"; |
| 162 | + String s = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254E"; |
| 163 | + String pubKeyX = "0AD99500288D466940031D72A9F5445A4D43784640855BF0A69874D2DE5FE103"; |
| 164 | + String pubKeyY = "C5011E6EF2C42DCD50D5D3D29F99AE6EBA2C80C9244F4C5422F0979FF0C3BA5E"; |
| 165 | + String input = messageHash + r + s + pubKeyX + pubKeyY; |
| 166 | + Bytes inputBytes = Bytes.fromHexString(input); |
| 167 | + |
| 168 | + // call the signaturebased impl explicitly, with native disabled: |
| 169 | + secp256R1.disableNative(); |
| 170 | + var defaultResult = contract.computeDefault(inputBytes); |
| 171 | + // call the signaturebased impl explicitly, with openssl native (maybe)enabled: |
| 172 | + secp256R1.maybeEnableNative(); |
| 173 | + var defaultMaybeNativeResult = contract.computeDefault(inputBytes); |
| 174 | + |
| 175 | + // maybeNative implementation: |
| 176 | + secp256R1.maybeEnableNative(); |
| 177 | + var maybeNativeResult = contract.computePrecompile(inputBytes, messageFrame); |
| 178 | + |
| 179 | + // This signature should be VALID if modular comparison is implemented correctly |
| 180 | + // The verification should compute R' with x-coordinate that may exceed n, |
| 181 | + // but R'.x ≡ r (mod n) should still hold true |
| 182 | + assertTrue( |
| 183 | + defaultResult.cachedResult().isSuccessful(), |
| 184 | + "Valid signature with R'.x > n should verify correctly via modular comparison"); |
| 185 | + assertEquals( |
| 186 | + Bytes32.leftPad(Bytes.of(1)), |
| 187 | + defaultResult.cachedResult().output(), |
| 188 | + "Valid signature with R'.x > n should verify correctly via modular comparison"); |
| 189 | + assertTrue( |
| 190 | + defaultMaybeNativeResult.cachedResult().isSuccessful(), |
| 191 | + "Valid signature with R'.x > n should verify correctly via modular comparison for native SignatureAlgorithm"); |
| 192 | + assertEquals( |
| 193 | + Bytes32.leftPad(Bytes.of(1)), |
| 194 | + defaultMaybeNativeResult.cachedResult().output(), |
| 195 | + "Valid signature with R'.x > n should verify correctly via modular comparison for native SignatureAlgorithm"); |
| 196 | + assertEquals( |
| 197 | + Bytes32.leftPad(Bytes.of(1)), |
| 198 | + maybeNativeResult.output(), |
| 199 | + "Valid signature with R'.x > n should verify correctly via modular comparison with Native implementation"); |
| 200 | + } |
90 | 201 | } |
0 commit comments