diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java index eb25ca3a6a2..b14571f0af4 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.cli.config.NetworkName; import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.crypto.KeyPairUtil; +import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.core.Address; @@ -35,6 +36,7 @@ import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationProvider; import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.TransactionWithSignatureAlgorithm; import org.hyperledger.besu.tests.acceptance.dsl.transaction.admin.AdminRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.BftRequestFactory; import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.ConsensusType; @@ -691,6 +693,13 @@ public T execute(final Transaction transaction) { return transaction.execute(nodeRequests()); } + @Override + public T execute( + final TransactionWithSignatureAlgorithm transaction, + final SignatureAlgorithm signatureAlgorithm) { + return transaction.execute(nodeRequests(), signatureAlgorithm); + } + @Override public void verify(final Condition expected) { expected.verify(this); diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/Node.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/Node.java index f50511112ba..1f7b8e38006 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/Node.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/Node.java @@ -14,12 +14,17 @@ */ package org.hyperledger.besu.tests.acceptance.dsl.node; +import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.TransactionWithSignatureAlgorithm; public interface Node { T execute(Transaction transaction); + T execute( + TransactionWithSignatureAlgorithm transaction, SignatureAlgorithm signatureAlgorithm); + void verify(final Condition expected); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java index fdca8442ca8..e887df10204 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java @@ -17,6 +17,9 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; +import org.hyperledger.besu.config.GenesisConfigFile; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.crypto.SignatureAlgorithmType; import org.hyperledger.besu.enclave.EnclaveFactory; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi; @@ -38,6 +41,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Paths; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -50,6 +54,8 @@ public class BesuNodeFactory { private final NodeConfigurationFactory node = new NodeConfigurationFactory(); public BesuNode create(final BesuNodeConfiguration config) throws IOException { + instantiateSignatureAlgorithmFactory(config); + return new BesuNode( config.getName(), config.getDataPath(), @@ -458,4 +464,32 @@ public BesuNode createNodeWithStaticNodes(final String name, final List st public BesuNode runCommand(final String command) throws IOException { return create(new BesuNodeConfigurationBuilder().name("run " + command).run(command).build()); } + + private void instantiateSignatureAlgorithmFactory(final BesuNodeConfiguration config) { + if (SignatureAlgorithmFactory.isInstanceSet()) { + return; + } + + Optional ecCurve = getEcCurveFromGenesisFile(config); + + if (ecCurve.isEmpty()) { + SignatureAlgorithmFactory.setDefaultInstance(); + return; + } + + SignatureAlgorithmFactory.setInstance(SignatureAlgorithmType.create(ecCurve.get())); + } + + private Optional getEcCurveFromGenesisFile(final BesuNodeConfiguration config) { + Optional genesisConfig = + config.getGenesisConfigProvider().create(Collections.emptyList()); + + if (genesisConfig.isEmpty()) { + return Optional.empty(); + } + + GenesisConfigFile genesisConfigFile = GenesisConfigFile.fromConfig(genesisConfig.get()); + + return genesisConfigFile.getConfigOptions().getEcCurve(); + } } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/SignUtil.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/SignUtil.java new file mode 100644 index 00000000000..86fcba1785e --- /dev/null +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/SignUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.tests.acceptance.dsl.transaction; + +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes32; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.Sign; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.rlp.RlpEncoder; +import org.web3j.rlp.RlpList; +import org.web3j.rlp.RlpType; +import org.web3j.utils.Numeric; + +public class SignUtil { + + private SignUtil() {} + + public static String signTransaction( + final RawTransaction transaction, + final Account sender, + final SignatureAlgorithm signatureAlgorithm) { + byte[] encodedTransaction = TransactionEncoder.encode(transaction); + + Credentials credentials = sender.web3jCredentialsOrThrow(); + SECPPrivateKey privateKey = + signatureAlgorithm.createPrivateKey(credentials.getEcKeyPair().getPrivateKey()); + + byte[] transactionHash = org.web3j.crypto.Hash.sha3(encodedTransaction); + + SECPSignature secpSignature = + signatureAlgorithm.sign( + Bytes32.wrap(transactionHash), signatureAlgorithm.createKeyPair(privateKey)); + + Sign.SignatureData signature = + new Sign.SignatureData( + // In Ethereum transaction 27 is added to recId (v) + // See https://ethereum.github.io/yellowpaper/paper.pdf + // Appendix F. Signing Transactions (281) + (byte) (secpSignature.getRecId() + 27), + secpSignature.getR().toByteArray(), + secpSignature.getS().toByteArray()); + List values = TransactionEncoder.asRlpValues(transaction, signature); + RlpList rlpList = new RlpList(values); + final byte[] encodedSignedTransaction = RlpEncoder.encode(rlpList); + + return Numeric.toHexString(encodedSignedTransaction); + } +} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/TransactionWithSignatureAlgorithm.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/TransactionWithSignatureAlgorithm.java new file mode 100644 index 00000000000..81b4bb7c1a3 --- /dev/null +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/TransactionWithSignatureAlgorithm.java @@ -0,0 +1,23 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.tests.acceptance.dsl.transaction; + +import org.hyperledger.besu.crypto.SignatureAlgorithm; + +@FunctionalInterface +public interface TransactionWithSignatureAlgorithm { + T execute(final NodeRequests node, final SignatureAlgorithm signatureAlgorithm); +} diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/account/TransferTransaction.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/account/TransferTransaction.java index 61d3a00ae67..866d7809658 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/account/TransferTransaction.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/account/TransferTransaction.java @@ -14,13 +14,14 @@ */ package org.hyperledger.besu.tests.acceptance.dsl.transaction.account; -import static org.web3j.utils.Numeric.toHexString; - +import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.ethereum.core.Hash; import org.hyperledger.besu.tests.acceptance.dsl.account.Account; import org.hyperledger.besu.tests.acceptance.dsl.blockchain.Amount; import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.SignUtil; import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.TransactionWithSignatureAlgorithm; import java.io.IOException; import java.math.BigDecimal; @@ -31,8 +32,10 @@ import org.web3j.crypto.TransactionEncoder; import org.web3j.utils.Convert; import org.web3j.utils.Convert.Unit; +import org.web3j.utils.Numeric; -public class TransferTransaction implements Transaction { +public class TransferTransaction + implements Transaction, TransactionWithSignatureAlgorithm { /** Price for each for each GAS units in this transaction (wei). */ private static final BigInteger MINIMUM_GAS_PRICE = BigInteger.valueOf(1000); @@ -64,12 +67,14 @@ public TransferTransaction( @Override public Hash execute(final NodeRequests node) { final String signedTransactionData = signedTransactionData(); - try { - return Hash.fromHexString( - node.eth().ethSendRawTransaction(signedTransactionData).send().getTransactionHash()); - } catch (final IOException e) { - throw new RuntimeException(e); - } + return sendRawTransaction(node, signedTransactionData); + } + + @Override + public Hash execute(final NodeRequests node, final SignatureAlgorithm signatureAlgorithm) { + final String signedTransactionData = + signedTransactionDataWithSignatureAlgorithm(signatureAlgorithm); + return sendRawTransaction(node, signedTransactionData); } public Amount executionCost() { @@ -77,20 +82,26 @@ public Amount executionCost() { } public String signedTransactionData() { - final Optional nonce = getNonce(); - - final RawTransaction transaction = - RawTransaction.createEtherTransaction( - nonce.orElse(nonce.orElseGet(sender::getNextNonce)), - gasPrice, - INTRINSIC_GAS, - recipient.getAddress(), - Convert.toWei(transferAmount, transferUnit).toBigIntegerExact()); + final RawTransaction transaction = createRawTransaction(); - return toHexString( + return Numeric.toHexString( TransactionEncoder.signMessage(transaction, sender.web3jCredentialsOrThrow())); } + private String signedTransactionDataWithSignatureAlgorithm( + final SignatureAlgorithm signatureAlgorithm) { + return SignUtil.signTransaction(createRawTransaction(), sender, signatureAlgorithm); + } + + private Hash sendRawTransaction(final NodeRequests node, final String signedTransactionData) { + try { + return Hash.fromHexString( + node.eth().ethSendRawTransaction(signedTransactionData).send().getTransactionHash()); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + private Optional getNonce() { return nonce == null ? Optional.empty() : Optional.of(nonce); } @@ -108,4 +119,15 @@ private BigInteger convertGasPriceToWei(final Amount unconverted) { return price; } + + private RawTransaction createRawTransaction() { + final Optional nonce = getNonce(); + + return RawTransaction.createEtherTransaction( + nonce.orElse(nonce.orElseGet(sender::getNextNonce)), + gasPrice, + INTRINSIC_GAS, + recipient.getAddress(), + Convert.toWei(transferAmount, transferUnit).toBigIntegerExact()); + } } diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java new file mode 100644 index 00000000000..697dfb03aac --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java @@ -0,0 +1,57 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.tests.acceptance.crypto; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.crypto.SECP256R1; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.node.Node; + +import org.junit.Before; +import org.junit.Test; + +public class SECP256R1AcceptanceTest extends AcceptanceTestBase { + private Node minerNode; + private Node fullNode; + + protected static final String GENESIS_FILE = "/crypto/secp256r1.json"; + + @Before + public void setUp() throws Exception { + minerNode = besu.createCustomGenesisNode("node1", GENESIS_FILE, true, true); + fullNode = besu.createCustomGenesisNode("node2", GENESIS_FILE, false); + cluster.start(minerNode, fullNode); + } + + @Test + public void shouldConnectToOtherPeer() { + minerNode.verify(net.awaitPeerCount(1)); + fullNode.verify(net.awaitPeerCount(1)); + } + + @Test + public void transactionShouldBeSuccessful() { + final Account recipient = accounts.createAccount("recipient"); + + final Hash transactionHash = + minerNode.execute(accountTransactions.createTransfer(recipient, 5), new SECP256R1()); + assertThat(transactionHash).isNotNull(); + cluster.verify(recipient.balanceEquals(5)); + } +} diff --git a/acceptance-tests/tests/src/test/resources/crypto/secp256r1.json b/acceptance-tests/tests/src/test/resources/crypto/secp256r1.json new file mode 100644 index 00000000000..630e394c357 --- /dev/null +++ b/acceptance-tests/tests/src/test/resources/crypto/secp256r1.json @@ -0,0 +1,23 @@ +{ + "config": { + "chainId": 1981, + "ecCurve": "secp256r1", + "constantinoplefixblock": 0, + "ethash": { + "fixeddifficulty": 1000 + } + }, + "nonce": "0x0", + "timestamp": "0x58ee40ba", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "alloc": { + "91240f5b6994c7ed80f9f94b1aa847880ad3b150": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "info": "This genesis file uses SECP256R1 as elliptic curve. The address is only valid for this curve and invalid with the default SECP256K1 curve.", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + } + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 5b3f82dc513..ec24a60dc63 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -1180,6 +1180,7 @@ public void run() { try { configureLogging(true); + instantiateSignatureAlgorithmFactory(); configureNativeLibs(); logger.info("Starting Besu version: {}", BesuInfo.nodeName(identityString)); // Need to create vertx after cmdline has been parsed, such that metricsSystem is configurable @@ -1579,7 +1580,6 @@ private BesuCommand configure() throws Exception { metricsConfiguration = metricsConfiguration(); logger.info("Security Module: {}", securityModuleName); - instantiateSignatureAlgorithmFactory(); return this; } diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 09a6f13c94a..5eed57783e1 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -4329,7 +4329,7 @@ public void invalidEcCurveFailsWithErrorMessage() throws IOException { assertThat(commandErrorOutput.toString()) .contains( "Invalid genesis file configuration. " - + "Elliptic curve (ecCurve) abcd is not in the list of valid elliptic curves [secp256k1]"); + + "Elliptic curve (ecCurve) abcd is not in the list of valid elliptic curves [secp256k1, secp256r1]"); } @Test diff --git a/crypto/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java b/crypto/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java new file mode 100644 index 00000000000..76ecdddffe4 --- /dev/null +++ b/crypto/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java @@ -0,0 +1,370 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.crypto; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.UnaryOperator; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECPoint; + +public abstract class AbstractSECP256 implements SignatureAlgorithm { + protected static final int PRIVATE_KEY_BYTE_LENGTH = 32; + protected static final int PUBLIC_KEY_BYTE_LENGTH = 64; + protected static final int SIGNATURE_BYTE_LENGTH = 65; + + public static final String PROVIDER = "BC"; + + protected final ECDomainParameters curve; + protected final BigInteger halfCurveOrder; + + protected final KeyPairGenerator keyPairGenerator; + protected final BigInteger curveOrder; + + final BigInteger prime; + + protected AbstractSECP256(final String curveName, final BigInteger prime) { + this.prime = prime; + Security.addProvider(new BouncyCastleProvider()); + + final X9ECParameters params = SECNamedCurves.getByName(curveName); + curve = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + curveOrder = curve.getN(); + halfCurveOrder = curveOrder.shiftRight(1); + try { + keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER); + } catch (final Exception e) { + throw new RuntimeException(e); + } + final ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(curveName); + try { + keyPairGenerator.initialize(ecGenParameterSpec, SecureRandomProvider.createSecureRandom()); + } catch (final InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + @Override + public SECPSignature normaliseSignature( + final BigInteger nativeR, + final BigInteger nativeS, + final SECPPublicKey publicKey, + final Bytes32 dataHash) { + + BigInteger s = nativeS; + // Automatically adjust the S component to be less than or equal to half the curve + // order, if necessary. This is required because for every signature (r,s) the signature + // (r, -s (mod N)) is a valid signature of the same message. However, we dislike the + // ability to modify the bits of a Bitcoin transaction after it's been signed, as that + // violates various assumed invariants. Thus in future only one of those forms will be + // considered legal and the other will be banned. + if (s.compareTo(halfCurveOrder) > 0) { + // The order of the curve is the number of valid points that exist on that curve. + // If S is in the upper half of the number of valid points, then bring it back to + // the lower half. Otherwise, imagine that + // N = 10 + // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions. + // 10 - 8 == 2, giving us always the latter solution, which is canonical. + s = curve.getN().subtract(s); + } + + // Now we have to work backwards to figure out the recId needed to recover the signature. + int recId = -1; + final BigInteger publicKeyBI = publicKey.getEncodedBytes().toUnsignedBigInteger(); + for (int i = 0; i < 4; i++) { + final BigInteger k = recoverFromSignature(i, nativeR, s, dataHash); + if (k != null && k.equals(publicKeyBI)) { + recId = i; + break; + } + } + if (recId == -1) { + throw new RuntimeException( + "Could not construct a recoverable key. This should never happen."); + } + + return new SECPSignature(nativeR, s, (byte) recId); + } + + /** + * Calculates an ECDH key agreement between the private and the public key. + * + * @param privKey The private key. + * @param theirPubKey The public key. + * @return The agreed secret. + */ + @Override + public Bytes32 calculateECDHKeyAgreement( + final SECPPrivateKey privKey, final SECPPublicKey theirPubKey) { + checkArgument(privKey != null, "missing private key"); + checkArgument(theirPubKey != null, "missing remote public key"); + + final ECPrivateKeyParameters privKeyP = new ECPrivateKeyParameters(privKey.getD(), curve); + final ECPublicKeyParameters pubKeyP = + new ECPublicKeyParameters(theirPubKey.asEcPoint(curve), curve); + + final ECDHBasicAgreement agreement = new ECDHBasicAgreement(); + agreement.init(privKeyP); + final BigInteger agreed = agreement.calculateAgreement(pubKeyP); + + return UInt256.valueOf(agreed).toBytes(); + } + + @Override + public SECPPrivateKey createPrivateKey(final BigInteger key) { + return SECPPrivateKey.create(key, ALGORITHM); + } + + @Override + public SECPPrivateKey createPrivateKey(final Bytes32 key) { + return SECPPrivateKey.create(key, ALGORITHM); + } + + @Override + public SECPPublicKey createPublicKey(final SECPPrivateKey privateKey) { + return SECPPublicKey.create(privateKey, curve, ALGORITHM); + } + + @Override + public SECPPublicKey createPublicKey(final BigInteger key) { + return SECPPublicKey.create(key, ALGORITHM); + } + + @Override + public SECPPublicKey createPublicKey(final Bytes encoded) { + return SECPPublicKey.create(encoded, ALGORITHM); + } + + @Override + public ECPoint publicKeyAsEcPoint(final SECPPublicKey publicKey) { + return publicKey.asEcPoint(curve); + } + + @Override + public KeyPair createKeyPair(final SECPPrivateKey privateKey) { + return KeyPair.create(privateKey, curve, ALGORITHM); + } + + @Override + public KeyPair generateKeyPair() { + return KeyPair.generate(keyPairGenerator, ALGORITHM); + } + + @Override + public SECPSignature createSignature(final BigInteger r, final BigInteger s, final byte recId) { + return SECPSignature.create(r, s, recId, curveOrder); + } + + @Override + public SECPSignature decodeSignature(final Bytes bytes) { + return SECPSignature.decode(bytes, curveOrder); + } + + @Override + public BigInteger getHalfCurveOrder() { + return halfCurveOrder; + } + + @Override + public String getProvider() { + return PROVIDER; + } + + // Decompress a compressed public key (x co-ord and low-bit of y-coord). + protected ECPoint decompressKey(final BigInteger xBN, final boolean yBit) { + final X9IntegerConverter x9 = new X9IntegerConverter(); + final byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(curve.getCurve())); + compEnc[0] = (byte) (yBit ? 0x03 : 0x02); + // TODO: Find a better way to handle an invalid point compression here. + // Currently ECCurve#decodePoint throws an IllegalArgumentException. + return curve.getCurve().decodePoint(compEnc); + } + + /** + * Given the components of a signature and a selector value, recover and return the public key + * that generated the signature according to the algorithm in SEC1v2 section 4.1.6. + * + *

If this method returns null it means recovery was not possible and recId should be iterated. + * + *

Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, + * and if the output is null OR a key that is not the one you expect, you try again with the next + * recId. + * + * @param recId Which possible key to recover. + * @param r The R component of the signature. + * @param s The S component of the signature. + * @param dataHash Hash of the data that was signed. + * @return An ECKey containing only the public part, or null if recovery wasn't possible. + */ + protected BigInteger recoverFromSignature( + final int recId, final BigInteger r, final BigInteger s, final Bytes32 dataHash) { + assert (recId >= 0); + assert (r.signum() >= 0); + assert (s.signum() >= 0); + assert (dataHash != null); + + // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) + // 1.1 Let x = r + jn + final BigInteger n = curve.getN(); // Curve order. + final BigInteger i = BigInteger.valueOf((long) recId / 2); + final BigInteger x = r.add(i.multiply(n)); + // 1.2. Convert the integer x to an octet string X of length mlen using the conversion + // routine specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. + // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R + // using the conversion routine specified in Section 2.3.4. If this conversion + // routine outputs "invalid", then do another iteration of Step 1. + // + // More concisely, what these points mean is to use X as a compressed public key. + if (x.compareTo(prime) >= 0) { + // Cannot have point co-ordinates larger than this as everything takes place modulo Q. + return null; + } + // Compressed keys require you to know an extra bit of data about the y-coord as there are + // two possibilities. So it's encoded in the recId. + final ECPoint R = decompressKey(x, (recId & 1) == 1); + // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers + // responsibility). + if (!R.multiply(n).isInfinity()) { + return null; + } + // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. + final BigInteger e = dataHash.toUnsignedBigInteger(); + // 1.6. For k from 1 to 2 do the following. (loop is outside this function via + // iterating recId) + // 1.6.1. Compute a candidate public key as: + // Q = mi(r) * (sR - eG) + // + // Where mi(x) is the modular multiplicative inverse. We transform this into the following: + // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) + // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). + // In the above equation ** is point multiplication and + is point addition (the EC group + // operator). + // + // We can find the additive inverse by subtracting e from zero then taking the mod. For + // example the additive inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and + // -3 mod 11 = 8. + final BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n); + final BigInteger rInv = r.modInverse(n); + final BigInteger srInv = rInv.multiply(s).mod(n); + final BigInteger eInvrInv = rInv.multiply(eInv).mod(n); + final ECPoint q = ECAlgorithms.sumOfTwoMultiplies(curve.getG(), eInvrInv, R, srInv); + + if (q.isInfinity()) { + return null; + } + + final byte[] qBytes = q.getEncoded(false); + // We remove the prefix + return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length)); + } + + @Override + public SECPSignature sign(final Bytes32 dataHash, final KeyPair keyPair) { + final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + + final ECPrivateKeyParameters privKey = + new ECPrivateKeyParameters( + keyPair.getPrivateKey().getEncodedBytes().toUnsignedBigInteger(), curve); + signer.init(true, privKey); + + final BigInteger[] components = signer.generateSignature(dataHash.toArrayUnsafe()); + + return normaliseSignature(components[0], components[1], keyPair.getPublicKey(), dataHash); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + *

When using native ECDSA verification, data must be 32 bytes, and no element may be larger + * than 520 bytes. + * + * @param data Hash of the data to verify. + * @param signature ASN.1 encoded signature. + * @param pub The public key bytes to use. + * @return True if the verification is successful. + */ + @Override + public boolean verify(final Bytes data, final SECPSignature signature, final SECPPublicKey pub) { + final ECDSASigner signer = new ECDSASigner(); + final Bytes toDecode = Bytes.wrap(Bytes.of((byte) 4), pub.getEncodedBytes()); + final ECPublicKeyParameters params = + new ECPublicKeyParameters(curve.getCurve().decodePoint(toDecode.toArrayUnsafe()), curve); + signer.init(false, params); + try { + return signer.verifySignature(data.toArrayUnsafe(), signature.getR(), signature.getS()); + } catch (final NullPointerException e) { + // Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those + // signatures + // are inherently invalid/attack sigs so we just fail them here rather than crash the thread. + return false; + } + } + + /** + * Verifies the given ECDSA signature using the public key bytes against the message bytes, + * previously passed through a preprocessor function, which is normally a hashing function. + * + * @param data The data to verify. + * @param signature ASN.1 encoded signature. + * @param pub The public key bytes to use. + * @param preprocessor The function to apply to the data before verifying the signature, normally + * a hashing function. + * @return True if the verification is successful. + */ + @Override + public boolean verify( + final Bytes data, + final SECPSignature signature, + final SECPPublicKey pub, + final UnaryOperator preprocessor) { + checkArgument(preprocessor != null, "preprocessor must not be null"); + return verify(preprocessor.apply(data), signature, pub); + } + + @Override + public Optional recoverPublicKeyFromSignature( + final Bytes32 dataHash, final SECPSignature signature) { + final BigInteger publicKeyBI = + recoverFromSignature(signature.getRecId(), signature.getR(), signature.getS(), dataHash); + return Optional.of(SECPPublicKey.create(publicKeyBI, ALGORITHM)); + } + + @Override + public Bytes compressPublicKey(final SECPPublicKey uncompressedPublicKey) { + return Bytes.wrap(publicKeyAsEcPoint(uncompressedPublicKey).getEncoded(true)); + } +} diff --git a/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256K1.java b/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256K1.java index a6050e1a490..3b7d772be59 100644 --- a/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256K1.java +++ b/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256K1.java @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.crypto; -import static com.google.common.base.Preconditions.checkArgument; import static org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1.SECP256K1_EC_UNCOMPRESSED; import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1; @@ -22,15 +21,8 @@ import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1.secp256k1_ecdsa_signature; import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1.secp256k1_pubkey; -import java.math.BigInteger; import java.nio.ByteBuffer; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPairGenerator; -import java.security.Security; -import java.security.spec.ECGenParameterSpec; -import java.util.Arrays; import java.util.Optional; -import java.util.function.UnaryOperator; import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.LongByReference; @@ -38,20 +30,6 @@ import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; -import org.apache.tuweni.units.bigints.UInt256; -import org.bouncycastle.asn1.sec.SECNamedCurves; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.asn1.x9.X9IntegerConverter; -import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.params.ECDomainParameters; -import org.bouncycastle.crypto.params.ECPrivateKeyParameters; -import org.bouncycastle.crypto.params.ECPublicKeyParameters; -import org.bouncycastle.crypto.signers.ECDSASigner; -import org.bouncycastle.crypto.signers.HMacDSAKCalculator; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.math.ec.ECAlgorithms; -import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; /* @@ -62,39 +40,16 @@ * Adapted from the web3j (Apache 2 License) implementations: * https://github.com/web3j/web3j/crypto/src/main/java/org/web3j/crypto/*.java */ -public class SECP256K1 implements SignatureAlgorithm { +public class SECP256K1 extends AbstractSECP256 { private static final Logger LOG = LogManager.getLogger(); private boolean useNative = true; public static final String CURVE_NAME = "secp256k1"; - public static final String PROVIDER = "BC"; - - private final ECDomainParameters curve; - private final BigInteger halfCurveOrder; - - private final KeyPairGenerator keyPairGenerator; - private final BigInteger curveOrder; public SECP256K1() { - Security.addProvider(new BouncyCastleProvider()); - - final X9ECParameters params = SECNamedCurves.getByName(CURVE_NAME); - curve = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); - curveOrder = curve.getN(); - halfCurveOrder = curveOrder.shiftRight(1); - try { - keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER); - } catch (final Exception e) { - throw new RuntimeException(e); - } - final ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(CURVE_NAME); - try { - keyPairGenerator.initialize(ecGenParameterSpec, SecureRandomProvider.createSecureRandom()); - } catch (final InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); - } + super(CURVE_NAME, SecP256K1Curve.q); } @Override @@ -108,7 +63,7 @@ public SECPSignature sign(final Bytes32 dataHash, final KeyPair keyPair) { if (useNative) { return signNative(dataHash, keyPair); } else { - return signDefault(dataHash, keyPair); + return super.sign(dataHash, keyPair); } } @@ -128,239 +83,8 @@ public boolean verify(final Bytes data, final SECPSignature signature, final SEC if (useNative) { return verifyNative(data, signature, pub); } else { - return verifyDefault(data, signature, pub); - } - } - - /** Decompress a compressed public key (x co-ord and low-bit of y-coord). */ - private ECPoint decompressKey(final BigInteger xBN, final boolean yBit) { - final X9IntegerConverter x9 = new X9IntegerConverter(); - final byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(curve.getCurve())); - compEnc[0] = (byte) (yBit ? 0x03 : 0x02); - // TODO: Find a better way to handle an invalid point compression here. - // Currently ECCurve#decodePoint throws an IllegalArgumentException. - return curve.getCurve().decodePoint(compEnc); - } - - /** - * Given the components of a signature and a selector value, recover and return the public key - * that generated the signature according to the algorithm in SEC1v2 section 4.1.6. - * - *

If this method returns null it means recovery was not possible and recId should be iterated. - * - *

Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, - * and if the output is null OR a key that is not the one you expect, you try again with the next - * recId. - * - * @param recId Which possible key to recover. - * @param r The R component of the signature. - * @param s The S component of the signature. - * @param dataHash Hash of the data that was signed. - * @return An ECKey containing only the public part, or null if recovery wasn't possible. - */ - private BigInteger recoverFromSignature( - final int recId, final BigInteger r, final BigInteger s, final Bytes32 dataHash) { - assert (recId >= 0); - assert (r.signum() >= 0); - assert (s.signum() >= 0); - assert (dataHash != null); - - // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) - // 1.1 Let x = r + jn - final BigInteger n = curve.getN(); // Curve order. - final BigInteger i = BigInteger.valueOf((long) recId / 2); - final BigInteger x = r.add(i.multiply(n)); - // 1.2. Convert the integer x to an octet string X of length mlen using the conversion - // routine specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. - // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R - // using the conversion routine specified in Section 2.3.4. If this conversion - // routine outputs "invalid", then do another iteration of Step 1. - // - // More concisely, what these points mean is to use X as a compressed public key. - final BigInteger prime = SecP256K1Curve.q; - if (x.compareTo(prime) >= 0) { - // Cannot have point co-ordinates larger than this as everything takes place modulo Q. - return null; - } - // Compressed keys require you to know an extra bit of data about the y-coord as there are - // two possibilities. So it's encoded in the recId. - final ECPoint R = decompressKey(x, (recId & 1) == 1); - // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers - // responsibility). - if (!R.multiply(n).isInfinity()) { - return null; - } - // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. - final BigInteger e = dataHash.toUnsignedBigInteger(); - // 1.6. For k from 1 to 2 do the following. (loop is outside this function via - // iterating recId) - // 1.6.1. Compute a candidate public key as: - // Q = mi(r) * (sR - eG) - // - // Where mi(x) is the modular multiplicative inverse. We transform this into the following: - // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) - // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). - // In the above equation ** is point multiplication and + is point addition (the EC group - // operator). - // - // We can find the additive inverse by subtracting e from zero then taking the mod. For - // example the additive inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and - // -3 mod 11 = 8. - final BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n); - final BigInteger rInv = r.modInverse(n); - final BigInteger srInv = rInv.multiply(s).mod(n); - final BigInteger eInvrInv = rInv.multiply(eInv).mod(n); - final ECPoint q = ECAlgorithms.sumOfTwoMultiplies(curve.getG(), eInvrInv, R, srInv); - - if (q.isInfinity()) { - return null; + return super.verify(data, signature, pub); } - - final byte[] qBytes = q.getEncoded(false); - // We remove the prefix - return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length)); - } - - private SECPSignature signDefault(final Bytes32 dataHash, final KeyPair keyPair) { - final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); - - final ECPrivateKeyParameters privKey = - new ECPrivateKeyParameters( - keyPair.getPrivateKey().getEncodedBytes().toUnsignedBigInteger(), curve); - signer.init(true, privKey); - - final BigInteger[] components = signer.generateSignature(dataHash.toArrayUnsafe()); - - return normaliseSignature(components[0], components[1], keyPair.getPublicKey(), dataHash); - } - - @Override - public SECPSignature normaliseSignature( - final BigInteger nativeR, - final BigInteger nativeS, - final SECPPublicKey publicKey, - final Bytes32 dataHash) { - - BigInteger s = nativeS; - // Automatically adjust the S component to be less than or equal to half the curve - // order, if necessary. This is required because for every signature (r,s) the signature - // (r, -s (mod N)) is a valid signature of the same message. However, we dislike the - // ability to modify the bits of a Bitcoin transaction after it's been signed, as that - // violates various assumed invariants. Thus in future only one of those forms will be - // considered legal and the other will be banned. - if (s.compareTo(halfCurveOrder) > 0) { - // The order of the curve is the number of valid points that exist on that curve. - // If S is in the upper half of the number of valid points, then bring it back to - // the lower half. Otherwise, imagine that - // N = 10 - // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions. - // 10 - 8 == 2, giving us always the latter solution, which is canonical. - s = curve.getN().subtract(s); - } - - // Now we have to work backwards to figure out the recId needed to recover the signature. - int recId = -1; - final BigInteger publicKeyBI = publicKey.getEncodedBytes().toUnsignedBigInteger(); - for (int i = 0; i < 4; i++) { - final BigInteger k = recoverFromSignature(i, nativeR, s, dataHash); - if (k != null && k.equals(publicKeyBI)) { - recId = i; - break; - } - } - if (recId == -1) { - throw new RuntimeException( - "Could not construct a recoverable key. This should never happen."); - } - - return new SECPSignature(nativeR, s, (byte) recId); - } - - private boolean verifyDefault( - final Bytes data, final SECPSignature signature, final SECPPublicKey pub) { - final ECDSASigner signer = new ECDSASigner(); - final Bytes toDecode = Bytes.wrap(Bytes.of((byte) 4), pub.getEncodedBytes()); - final ECPublicKeyParameters params = - new ECPublicKeyParameters(curve.getCurve().decodePoint(toDecode.toArrayUnsafe()), curve); - signer.init(false, params); - try { - return signer.verifySignature(data.toArrayUnsafe(), signature.getR(), signature.getS()); - } catch (final NullPointerException e) { - // Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those - // signatures - // are inherently invalid/attack sigs so we just fail them here rather than crash the thread. - return false; - } - } - - /** - * Verifies the given ECDSA signature using the public key bytes against the message bytes, - * previously passed through a preprocessor function, which is normally a hashing function. - * - * @param data The data to verify. - * @param signature ASN.1 encoded signature. - * @param pub The public key bytes to use. - * @param preprocessor The function to apply to the data before verifying the signature, normally - * a hashing function. - * @return True if the verification is successful. - */ - @Override - public boolean verify( - final Bytes data, - final SECPSignature signature, - final SECPPublicKey pub, - final UnaryOperator preprocessor) { - checkArgument(preprocessor != null, "preprocessor must not be null"); - return verify(preprocessor.apply(data), signature, pub); - } - - /** - * Calculates an ECDH key agreement between the private and the public key. - * - * @param privKey The private key. - * @param theirPubKey The public key. - * @return The agreed secret. - */ - @Override - public Bytes32 calculateECDHKeyAgreement( - final SECPPrivateKey privKey, final SECPPublicKey theirPubKey) { - checkArgument(privKey != null, "missing private key"); - checkArgument(theirPubKey != null, "missing remote public key"); - - final ECPrivateKeyParameters privKeyP = new ECPrivateKeyParameters(privKey.getD(), curve); - final ECPublicKeyParameters pubKeyP = - new ECPublicKeyParameters(theirPubKey.asEcPoint(curve), curve); - - final ECDHBasicAgreement agreement = new ECDHBasicAgreement(); - agreement.init(privKeyP); - final BigInteger agreed = agreement.calculateAgreement(pubKeyP); - - return UInt256.valueOf(agreed).toBytes(); - } - - @Override - public SECPPrivateKey createPrivateKey(final BigInteger key) { - return SECPPrivateKey.create(key, ALGORITHM); - } - - @Override - public SECPPrivateKey createPrivateKey(final Bytes32 key) { - return SECPPrivateKey.create(key, ALGORITHM); - } - - @Override - public SECPPublicKey createPublicKey(final SECPPrivateKey privateKey) { - return SECPPublicKey.create(privateKey, curve, ALGORITHM); - } - - @Override - public SECPPublicKey createPublicKey(final BigInteger key) { - return SECPPublicKey.create(key, ALGORITHM); - } - - @Override - public SECPPublicKey createPublicKey(final Bytes encoded) { - return SECPPublicKey.create(encoded, ALGORITHM); } @Override @@ -369,47 +93,10 @@ public Optional recoverPublicKeyFromSignature( if (useNative) { return recoverFromSignatureNative(dataHash, signature); } else { - final BigInteger publicKeyBI = - recoverFromSignature(signature.getRecId(), signature.getR(), signature.getS(), dataHash); - return Optional.of(SECPPublicKey.create(publicKeyBI, ALGORITHM)); + return super.recoverPublicKeyFromSignature(dataHash, signature); } } - @Override - public ECPoint publicKeyAsEcPoint(final SECPPublicKey publicKey) { - return publicKey.asEcPoint(curve); - } - - @Override - public KeyPair createKeyPair(final SECPPrivateKey privateKey) { - return KeyPair.create(privateKey, curve, ALGORITHM); - } - - @Override - public KeyPair generateKeyPair() { - return KeyPair.generate(keyPairGenerator, ALGORITHM); - } - - @Override - public SECPSignature createSignature(final BigInteger r, final BigInteger s, final byte recId) { - return SECPSignature.create(r, s, recId, curveOrder); - } - - @Override - public SECPSignature decodeSignature(final Bytes bytes) { - return SECPSignature.decode(bytes, curveOrder); - } - - @Override - public BigInteger getHalfCurveOrder() { - return halfCurveOrder; - } - - @Override - public String getProvider() { - return PROVIDER; - } - @Override public String getCurveName() { return CURVE_NAME; diff --git a/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256R1.java b/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256R1.java new file mode 100644 index 00000000000..3f67cf61a0b --- /dev/null +++ b/crypto/src/main/java/org/hyperledger/besu/crypto/SECP256R1.java @@ -0,0 +1,39 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.crypto; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.math.ec.custom.sec.SecP256R1Curve; + +public class SECP256R1 extends AbstractSECP256 { + + private static final Logger LOG = LogManager.getLogger(); + public static final String CURVE_NAME = "secp256r1"; + + public SECP256R1() { + super(CURVE_NAME, SecP256R1Curve.q); + } + + @Override + public void enableNative() { + LOG.warn("Native secp256r1 requested but not available"); + } + + @Override + public String getCurveName() { + return CURVE_NAME; + } +} diff --git a/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java b/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java index 32663c6dcc3..c4a8d18eff0 100644 --- a/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java +++ b/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java @@ -74,4 +74,6 @@ Optional recoverPublicKeyFromSignature( SECPSignature createSignature(final BigInteger r, final BigInteger s, final byte recId); SECPSignature decodeSignature(final Bytes bytes); + + Bytes compressPublicKey(final SECPPublicKey uncompressedKey); } diff --git a/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmFactory.java b/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmFactory.java index 0b3e844f084..97197df7604 100644 --- a/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmFactory.java +++ b/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmFactory.java @@ -15,8 +15,13 @@ package org.hyperledger.besu.crypto; import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class SignatureAlgorithmFactory { + + private static final Logger LOG = LogManager.getLogger(); + private static SignatureAlgorithm instance = null; private SignatureAlgorithmFactory() {} @@ -33,6 +38,14 @@ public static void setInstance(final SignatureAlgorithmType signatureAlgorithmTy } instance = signatureAlgorithmType.getInstance(); + + if (!SignatureAlgorithmType.isDefault(instance)) { + LOG.info( + new StringBuilder("The signature algorithm uses the elliptic curve ") + .append(instance.getCurveName()) + .append(". The usage of alternative elliptic curves is still experimental.") + .toString()); + } } /** diff --git a/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmType.java b/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmType.java index 32120749fd0..9dc43abad72 100644 --- a/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmType.java +++ b/crypto/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithmType.java @@ -18,13 +18,16 @@ import java.util.Map; import java.util.function.Supplier; +import com.google.common.collect.ImmutableMap; + public class SignatureAlgorithmType { - private static final Map> SUPPORTED_ALGORITHMS = - Map.of("secp256k1", SECP256K1::new); + private static final String DEFAULT_EC_CURVE_NAME = "secp256k1"; + private static final ImmutableMap> SUPPORTED_ALGORITHMS = + ImmutableMap.of(DEFAULT_EC_CURVE_NAME, SECP256K1::new, "secp256r1", SECP256R1::new); public static final Supplier DEFAULT_SIGNATURE_ALGORITHM_TYPE = - SUPPORTED_ALGORITHMS.get("secp256k1"); + SUPPORTED_ALGORITHMS.get(DEFAULT_EC_CURVE_NAME); private final Supplier instantiator; @@ -59,6 +62,10 @@ public static boolean isValidType(final String ecCurve) { return SUPPORTED_ALGORITHMS.containsKey(ecCurve); } + public static boolean isDefault(final SignatureAlgorithm signatureAlgorithm) { + return signatureAlgorithm.getCurveName().equals(DEFAULT_EC_CURVE_NAME); + } + private static String getEcCurvesListAsString() { Iterator>> it = SUPPORTED_ALGORITHMS.entrySet().iterator(); diff --git a/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java b/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java index 67da84085da..87690321ac3 100644 --- a/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java +++ b/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256K1Test.java @@ -44,7 +44,6 @@ public static void setTestSuiteStartTime() { LocalDateTime.now(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); suiteName(SECP256K1Test.class); - ; } @Before diff --git a/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java b/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java new file mode 100644 index 00000000000..85f54de29ff --- /dev/null +++ b/crypto/src/test/java/org/hyperledger/besu/crypto/SECP256R1Test.java @@ -0,0 +1,126 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.crypto.Hash.keccak256; + +import java.io.File; +import java.math.BigInteger; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class SECP256R1Test { + + protected SECP256R1 secp256R1; + + protected static String suiteStartTime = null; + protected static String suiteName = null; + + @BeforeClass + public static void setTestSuiteStartTime() { + suiteStartTime = + LocalDateTime.now(ZoneId.systemDefault()) + .format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); + suiteName(SECP256R1Test.class); + } + + @Before + public void setUp() { + secp256R1 = new SECP256R1(); + } + + public static void suiteName(final Class clazz) { + suiteName = clazz.getSimpleName() + "-" + suiteStartTime; + } + + public static String suiteName() { + return suiteName; + } + + @Test + public void recoverPublicKeyFromSignature() { + final SECPPrivateKey privateKey = + secp256R1.createPrivateKey( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + final KeyPair keyPair = secp256R1.createKeyPair(privateKey); + + final Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + final Bytes32 dataHash = keccak256(data); + final SECPSignature signature = secp256R1.sign(dataHash, keyPair); + + final SECPPublicKey recoveredPublicKey = + secp256R1.recoverPublicKeyFromSignature(dataHash, signature).get(); + assertThat(recoveredPublicKey.toString()).isEqualTo(keyPair.getPublicKey().toString()); + } + + @Test + public void signatureGeneration() { + final SECPPrivateKey privateKey = + secp256R1.createPrivateKey( + new BigInteger("909753034398cf9371b88871c0a8b3051f1bb55d4f28d3d7261abe7d32adcdde", 16)); + final KeyPair keyPair = secp256R1.createKeyPair(privateKey); + + final Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + final Bytes32 dataHash = keccak256(data); + final SECPSignature expectedSignature = + secp256R1.createSignature( + new BigInteger("6ae3ac096d1b69ab1e18a721689cc40f2710ab25c35a4f465b8384c470e7079b", 16), + new BigInteger("28a39d61a8812005312b552e022afd6fa3db323754f48033c87f4acf6e9960e6", 16), + (byte) 1); + + final SECPSignature actualSignature = secp256R1.sign(dataHash, keyPair); + assertThat(actualSignature).isEqualTo(expectedSignature); + } + + @Test + public void signatureVerification() { + final SECPPrivateKey privateKey = + secp256R1.createPrivateKey( + new BigInteger("a7e8b16ad7ffa26fce80be2b0e00008018aadf1b16dea4ecc913b8c1c4f18531", 16)); + final KeyPair keyPair = secp256R1.createKeyPair(privateKey); + + final Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + final Bytes32 dataHash = keccak256(data); + + final SECPSignature signature = secp256R1.sign(dataHash, keyPair); + assertThat(secp256R1.verify(data, signature, keyPair.getPublicKey(), Hash::keccak256)).isTrue(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidFileThrowsInvalidKeyPairException() throws Exception { + final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "not valid".getBytes(UTF_8)); + KeyPairUtil.load(tempFile); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidMultiLineFileThrowsInvalidIdException() throws Exception { + final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "not\n\nvalid".getBytes(UTF_8)); + KeyPairUtil.load(tempFile); + } +} diff --git a/crypto/src/test/java/org/hyperledger/besu/crypto/SignatureAlgorithmTypeTest.java b/crypto/src/test/java/org/hyperledger/besu/crypto/SignatureAlgorithmTypeTest.java index 089c354d07f..75ca3369bf1 100644 --- a/crypto/src/test/java/org/hyperledger/besu/crypto/SignatureAlgorithmTypeTest.java +++ b/crypto/src/test/java/org/hyperledger/besu/crypto/SignatureAlgorithmTypeTest.java @@ -33,6 +33,6 @@ public void shouldThrowExceptionWhenInvalidParameterIsGiven() { assertThatThrownBy(() -> SignatureAlgorithmType.create("abcd")) .hasMessage( "Invalid genesis file configuration. Elliptic curve (ecCurve) abcd is not in the list" - + " of valid elliptic curves [secp256k1]"); + + " of valid elliptic curves [secp256k1, secp256r1]"); } } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/PeerDiscoveryAgent.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/PeerDiscoveryAgent.java index c3ea951aa69..4c86d6fbe6d 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/PeerDiscoveryAgent.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/PeerDiscoveryAgent.java @@ -21,6 +21,8 @@ import org.hyperledger.besu.crypto.Hash; import org.hyperledger.besu.crypto.NodeKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; import org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration; import org.hyperledger.besu.ethereum.p2p.discovery.internal.Packet; import org.hyperledger.besu.ethereum.p2p.discovery.internal.PeerDiscoveryController; @@ -53,6 +55,7 @@ import java.util.stream.Stream; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; import com.google.common.net.InetAddresses; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -62,7 +65,6 @@ import org.ethereum.beacon.discovery.schema.IdentitySchema; import org.ethereum.beacon.discovery.schema.NodeRecord; import org.ethereum.beacon.discovery.schema.NodeRecordFactory; -import org.ethereum.beacon.discovery.util.Functions; /** * The peer discovery agent is the network component that sends and receives peer discovery messages @@ -71,6 +73,8 @@ public abstract class PeerDiscoveryAgent { private static final Logger LOG = LogManager.getLogger(); private static final String SEQ_NO_STORE_KEY = "local-enr-seqno"; + private static final com.google.common.base.Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); // The devp2p specification says only accept packets up to 1280, but some // clients ignore that, so we add in a little extra padding. @@ -212,7 +216,12 @@ public void updateNodeRecord() { nodeRecordFactory.createFromValues( sequenceNumber, new EnrField(EnrField.ID, IdentitySchema.V4), - new EnrField(EnrField.PKEY_SECP256K1, Functions.compressPublicKey(id)), + new EnrField( + SIGNATURE_ALGORITHM.get().getCurveName(), + SIGNATURE_ALGORITHM + .get() + .compressPublicKey( + SIGNATURE_ALGORITHM.get().createPublicKey(id))), new EnrField(EnrField.IP_V4, addressBytes), new EnrField(EnrField.TCP, listeningPort), new EnrField(EnrField.UDP, discoveryPort),