-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Design Documents SECP256R1 Support
The ECDSA (Elliptic Curve Digital Signature Algorithm) is a cryptographically secure digital signature scheme, based on elliptic-curve cryptography (ECC). ECDSA relies on the math of the cyclic groups of elliptic curves over finite fields and on the difficulty of the ECDLP problem (elliptic-curve discrete logarithm problem). The ECDSA sign/verify algorithm relies on EC point multiplication and works as described below. ECDSA keys and signatures are shorter than in RSA for the same security level. A 256-bit ECDSA signature has the same security strength as a 3072-bit RSA signature. ECDSA uses cryptographic elliptic curves (EC) over finite fields in the classical Weierstrass form.
These curves are described by their EC domain parameters, specified by various cryptographic standards such as SECG: SEC 2. Elliptic curves, used in cryptography, define:
- Generator point G, used for scalar multiplication on the curve (multiply integer by EC point).
- Order n of the subgroup of EC points, generated by G, which defines the length of the private keys (e.g. 256 bits).
For example, the 256-bit elliptic curve secp256r1 has:
- Order n = 115792089210356248762697446949407573529996955224135760342422259061068512044369 (prime number)
- Generator point G {x = 48439561293906451759052585252797914202762949526041747995844080717082404635286, y = 36134250956749795798585127919587881956611106672985015071877198253568414405109}
The ECDSA key-pair consists of:
- private key (integer):
privKey - public key (EC point):
pubKey = privKey * G
The private key is generated as a random integer in the range [0...n-1]. The public key pubKey is a point on the elliptic curve, calculated by the EC point multiplication: pubKey = privKey * G (the private key, multiplied by the generator point G).
The public key EC point {x, y} can be compressed to just one of the coordinates + 1 bit (parity).
For the secp256r1 curve, the private key is a 256-bit integer (32 bytes) and the compressed public key is a 257-bit integer (~ 33 bytes).
The ECDSA signing algorithm (RFC 6979) takes as input a message msg + a private key privKey and produces as output a signature, which consists of a pair of integers {r, s}. The ECDSA signing algorithm is based on the ElGamal signature scheme and works as follows (with minor simplifications):
- Calculate the message hash, using a cryptographic hash function like SHA-256: h = hash(msg).
- Generate securely a random number k in the range [1..n-1]. In the case of deterministic-ECDSA, the value k is HMAC-derived from h + privKey (see RFC 6979).
- Calculate the random point R = k * G and take its x-coordinate: r = R.x.
- Calculate the signature proof: s = k−1 ∗ (h + r ∗ privKey)(mod n).
The modular inverse k−1(mod n) is an integer, such that k ∗ k−1 ≡ 1(mod n).
- Return the signature {r, s}.
The calculated signature {r, s} is a pair of integers, each in the range [1...n-1]. It encodes the random point R = k * G, along with a proof s, confirming that the signer knows the message h and the private key privKey. The proof s is verifiable using the corresponding pubKey. ECDSA signatures are 2 times longer than the signer's private key for the curve used during the signing process. For example, for 256-bit elliptic curves (like secp256r1) the ECDSA signature is 512 bits (64 bytes) and for 521-bit curves (like secp521r1) the signature is 1042 bits.
The algorithm to verify an ECDSA signature takes as input the signed message msg + the signature {r, s} produced from the signing algorithm + the public key pubKey, corresponding to the signer's private key. The output is a boolean value: valid or invalid signature. The ECDSA signature verify algorithm works as follows (with minor simplifications):
- Calculate the message hash, with the same cryptographic hash function used during the signing: h = hash(msg).
- Calculate the modular inverse of the signature proof: s1 = s−1(mod n).
- Recover the random point used during the signing: R' = (h * s1) * G + (r * s1) * pubKey.
- Take from R' its x-coordinate: r' = R'.x.
- Calculate the signature validation result by comparing whether r' == r.
The general idea of the signature verification is to recover the point R' using the public key and check whether it is the same point R, generated randomly during the signing process.
The ECDSA signature {r, s} has the following simple explanation:
- The signing encodes a random point R (represented by its x-coordinate only) through elliptic-curve transformations using the private key
privKeyand the message hash h into a number s, which is the proof that the message signer knows the private keyprivKey. - The signature {r, s} cannot reveal the private key due to the difficulty of the ECDLP problem.
- The signature verification decodes the proof number s from the signature back to its original point R, using the public key
pubKeyand the message hash h and compares the x-coordinate of the recovered R with the r value from the signature.
It is important to know that the ECDSA signature scheme allows the public key to be recovered from the signed message together with the signature. The recovery process is based on some mathematical computations (described in the SECG: SEC 1 standard) and returns 0, 1 or 2 possible EC points that are valid public keys, corresponding to the signature. To avoid this ambiguity, some ECDSA implementations add one additional bit v to the signature during the signing process and it takes the form {r, s, v}. From this extended ECDSA signature {r, s, v} + the signed message, the signer's public key can be restored with confidence.
The public key recovery from the ECDSA signature is very useful in bandwidth-constrained or storage-constrained environments (such as blockchain systems), when transmission or storage of the public keys cannot be afforded. For example, the Ethereum blockchain uses extended signatures {r, s, v} for the signed transactions on the chain to save storage and bandwidth.
Public key recovery is possible for signatures based on the ElGamal signature scheme (such as DSA and ECDSA).
For any message hash there are two valid signatures. It is possible to calculate the second valid signature with the private key, just by knowing the first signature, which is public. One can take any transaction, flip the s value from s to n - s, flip the v value (27 -> 28, 28 -> 27), and the resulting signature would still be valid.
Therefore, allowing transactions with any s value with 0 < s < n opens a transaction malleability concern. This is not a serious security flaw, especially since Ethereum uses addresses and not transaction hashes as the input to an ether value transfer or other transaction, but all Ethereum nodes, including Besu, will only allow signatures where s <= n/2. This makes sure that only one of the two possible valid signatures will be accepted. As any ECDSA library that is not focused on blockchains (e.g. libcrypto from OpenSSL) will not take this constraint into consideration, any created signature has to be normalized after creation to fit the above-mentioned criteria.
The elliptic curve signature is made available in Besu via the interface org.hyperledger.besu.crypto.SignatureAlgorithm. It is injected in various places across the different modules. There are two classes that implement the interface:
org.hyperledger.besu.crypto.SECP256K1org.hyperledger.besu.crypto.SECP256R1
As the names of the classes suggest, they implement the SECP256K1 or SECP256R1 signature algorithms. SECP256K1 is the default signature algorithm, as it is used in Ethereum Mainnet and all public testnets. SECP256R1 has been added as an alternative for private networks, as it is NIST compliant while SECP256K1 is not.
The only difference between SECP256K1 and SECP256R1 is the curve parameters of their respective elliptic curves. Therefore, all calculations are exactly the same, only the input parameters are different. Because of this, the class org.hyperledger.besu.crypto.AbstractSECP256 contains all the logic of the ECDSA calculations and the classes SECP256K1 and SECP256R1 extend it.
For performance reasons, an implementation of the SECP256R1 signature algorithm has been created in C, as an addition to the Java implementation. The repository can be found at Consensys/besu-native-ec.
The library uses libcrypto from OpenSSL, which contains all necessary functionality for the ECDSA signature algorithm. The functions for signing and verifying the signature are directly provided by libcrypto. The public key recovery is implemented using the elliptic curve operations provided by libcrypto.
For debugging the library, gdb can be used. A basic tutorial for gdb can be found here: gdb tutorial.
It needs an executable in order to work. The tests can be used for this purpose, as they are executables. They are in the directory build and have the file extension out.
To debug, for example, the tests for signing it would be started with:
gdb build/test_ec_sign.outIn C, memory management is the responsibility of the developer. Therefore, it can happen easily that memory leaks are introduced. The pipeline in the repository tests for it, to avoid that any memory leaks are introduced in the code.
To test for them locally, valgrind can be used. As with gdb, it needs an executable as well, so the tests can be used here too.
For example, to test the tests for signing for memory leaks, the following command can be used:
valgrind --leak-check=full build/test_ec_sign.outIf a memory leak is detected, an error similar to the following is displayed:
==586133==
==586133== 26,780 (2,280 direct, 24,500 indirect) bytes in 15 blocks are definitely lost in loss record 64 of 64
==586133== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==586133== by 0x4A086AD: CRYPTO_zalloc (in .../build/libs/libbesu_native_ec_crypto.so)
==586133== by 0x49EE8EB: EVP_PKEY_new (in .../build/libs/libbesu_native_ec_crypto.so)
==586133== by 0x49F3A3D: EVP_PKEY_fromdata (in .../build/libs/libbesu_native_ec_crypto.so)
==586133== by 0x10F567: create_key (ec_key.c:89)
==586133== by 0x10F73D: create_key_pair (ec_key.c:40)
==586133== by 0x10B483: sign (ec_sign.c:65)
==586133== by 0x10B6B9: p256_sign (ec_sign.c:36)
==586133== by 0x10AB7B: p256_sign_should_create_valid_signatures (test_ec_sign.c:44)
==586133== by 0x10ACF0: p256_sign_should_create_valid_signatures_from_sha224_hashes (test_ec_sign.c:76)
==586133== by 0x10F105: UnityDefaultTestRun (in .../build/test_ec_sign.out)
==586133== by 0x10A94B: main (test_ec_sign.c:118)
==586133==
==586133== LEAK SUMMARY:
==586133== definitely lost: 9,120 bytes in 60 blocks
==586133== indirectly lost: 95,600 bytes in 2,352 blocks
==586133== possibly lost: 0 bytes in 0 blocks
==586133== still reachable: 0 bytes in 0 blocks
==586133== suppressed: 0 bytes in 0 blocks
==586133==
The output has to be read starting at the bottom:
The LEAK SUMMARY lines tell us that a memory leak has occurred and how many bytes have not been freed.
The main line is the starting point of the stack trace. Following it down, the entry for ec_key.c line 89 tells us where memory was allocated by calling functions from the library libbesu_native_ec_crypto.so. Because this has caused an error, it means that this allocated memory was never freed.
Now that we know where the memory leak occurs, we need to free the memory at the appropriate place. In almost all cases this will be at the end of the function which has the memory allocated, or if the allocated memory is the return value, at the end of the calling function.
The native library is integrated via another repository: besu-eth/besu-native.
This repository uses JNA to create the bridge between the native library and Java. The corresponding code can be found here: besu-eth/besu-native/secp256r1.
The code calls the corresponding functions of the native library and converts the results to basic Java data types. Further, it needs to convert the received parameters from the representation in Java to one that the native library expects.
Especially important are the functions convertToNonNegativeRepresentation and convertToNativeRepresentation. They are responsible for converting the signatures into the correct representation. The native library expects a big-endian encoded byte array which is always positive, while in Java byte arrays can be positive or negative. The leftmost bit of a byte array needs to be tested in order to verify if a conversion is needed or not.
For development, it can be necessary to test a new version of a besu-native library. This can be done locally. The following steps are necessary:
-
Create a new jar file by executing in besu-native:
cd secp256r1 ../gradlew buildThis creates a new jar file in
secp256r1/build/libs. -
Create a directory for the jar file in besu:
mkdir libs
-
Copy the jar file from step 1 from besu-native to the newly created directory:
cp $BESU_NATIVE_DIR/secp256r1/build/libs/besu-native-secp256r1-$VERSION.jar $BESU_DIR/libs
-
Add the newly created directory as a Gradle repository in besu. Modify
$BESU_DIR/build.gradle:repositories { // … flatDir { dirs 'libs' } } -
Add the dependency to the crypto module. Modify
$BESU_DIR/crypto/build.gradle:dependencies { // … api 'org.hyperledger.besu:secp256r1:$VERSION_OF_JAR_FILE' }
In past occasions, the gradle task checkLicenses failed when a local jar file was added like this. To avoid the error, the task has to be excluded:
# for example
./gradlew build -x checkLicensesThe native library currently only supports SECP256R1, but it is prepared to be extended to allow other ECDSA signature algorithms. To extend it, the following steps have to be taken:
- The 3 new functions to sign, verify and recover the public key have to be defined in
/src/besu_native_ec.h. They can be directly copied from the existing ones for SECP256R1 (P256) and only have to be renamed. - The public key recovery function, which was just defined, needs to be implemented in
src/ec_key_recovery.c. The existing functionp256_key_recoverycan be copied. It calls the functionkey_recovery. The new function needs to set the correct parameters forcurve_nidandcurve_byte_length. The correct value forcurve_nidcan be found in the fileopenssl/obj_mac.h. - The signing function, which was just defined, needs to be implemented in
src/ec_sign.c. The existing functionp256_signcan be copied. It calls the functionsign. The new function needs to set the correct parameters forprivate_key_len,public_key_len,group_nameandcurve_nid. The parameter forcurve_nidis the same as in the public key recovery function. The correct value forgroup_namecan be found inopenssl/obj_mac.h. - The verification function, which was just defined, needs to be implemented in
src/ec_verify.c. The existing functionp256_verifycan be copied. It calls the functionverify. The new function needs to set the correct parameters forpublic_key_len,group_nameandcurve_nid. The parametersgroup_nameandcurve_nidare the same as in the signing function.
All Ethereum libraries support per default only SECP256K1, as this is the signature algorithm used in the public Ethereum networks. Support for SECP256R1 does not exist and has to be added.
In Web3J, support can be added by using a custom transaction manager. The transaction manager is responsible for decoding, signing and sending a transaction. Therefore, replacing the signing functions to use SECP256R1 is sufficient to add basic support.
A working version has already been created here: besu-secp256r1-demo Web3jNist example.
To use the custom transaction manager, it has to be provided as a parameter for the functions deploy and load.
In Truffle, transactions are handled by so-called providers. The standard provider for Truffle is called hdwallet-provider. Its implementation can be found here: trufflesuite/truffle hdwallet-provider.
To add functionality for SECP256R1, the hdwallet-provider could be used as a basis and the signing functions would need to be changed to use SECP256R1.
This newly created provider needs to be set in the network configuration in truffle-config.js.
besu-eth/wiki and edits made here are overwritten on the next publish. To change a page, open a pull request against the source repo instead. See Home for how.
Contributing
Development & Testing
Developing & Conventions
Project Process
Governance
Incident Reports
- 2024-01-06 Mainnet Halting Event
- 2022-11-11 Fork on Sepolia
- 2022-05-30 Phishing PRs
- 2021-08-04 Value Transfer Public Transactions Rejected
- 2021-04-23 ATs Failures
Performance & Stability
- Improvements Since the Merge
- Memory Usage Investigations (23.7.3-RC)
- Reduce Memory Usage
- Testing Taskforce Brainstorming
- Q4 2022 Stability and Improvements
- Permissioned Chain Testing
Design Documents
- Design Documents
- Modular Besu
- Refactor EVM into a Standalone Library
- Bonsai Tries Design Overview
- Bonsai Archive Feature
- Bonsai Archive State Proofs
- Switchable Consensus Parameters
- SECP256R1 Support
- RPC Endpoint Service
- Feature Proposal Template
- Feature Flags
- CI/CD Tooling and Process
Programs & Mentorship