diff --git a/besu/src/main/java/org/web3j/tx/PrivateTransactionManager.java b/besu/src/main/java/org/web3j/tx/PrivateTransactionManager.java index 72338000e..2b376725e 100644 --- a/besu/src/main/java/org/web3j/tx/PrivateTransactionManager.java +++ b/besu/src/main/java/org/web3j/tx/PrivateTransactionManager.java @@ -24,8 +24,9 @@ import org.web3j.protocol.core.methods.response.EthCall; import org.web3j.protocol.core.methods.response.EthGetCode; import org.web3j.protocol.core.methods.response.EthSendTransaction; -import org.web3j.protocol.eea.crypto.PrivateTransactionEncoder; import org.web3j.protocol.eea.crypto.RawPrivateTransaction; +import org.web3j.service.TxSignService; +import org.web3j.service.TxSignServiceImpl; import org.web3j.tx.response.TransactionReceiptProcessor; import org.web3j.utils.Base64String; import org.web3j.utils.Numeric; @@ -36,7 +37,7 @@ public class PrivateTransactionManager extends TransactionManager { private final Besu besu; - private final Credentials credentials; + private final TxSignService txSignService; private final long chainId; private final Base64String privateFrom; @@ -56,12 +57,12 @@ public PrivateTransactionManager( final Restriction restriction) { super(transactionReceiptProcessor, credentials.getAddress()); this.besu = besu; - this.credentials = credentials; this.chainId = chainId; this.privateFrom = privateFrom; this.privateFor = null; this.privacyGroupId = privacyGroupId; this.restriction = restriction; + this.txSignService = new TxSignServiceImpl(credentials); } public PrivateTransactionManager( @@ -74,12 +75,12 @@ public PrivateTransactionManager( final Restriction restriction) { super(transactionReceiptProcessor, credentials.getAddress()); this.besu = besu; - this.credentials = credentials; this.chainId = chainId; this.privateFrom = privateFrom; this.privateFor = privateFor; this.privacyGroupId = PrivacyGroupUtils.generateLegacyGroup(privateFrom, privateFor); this.restriction = restriction; + this.txSignService = new TxSignServiceImpl(credentials); } @Override @@ -93,7 +94,7 @@ public EthSendTransaction sendTransaction( throws IOException { final BigInteger nonce = - besu.privGetTransactionCount(credentials.getAddress(), privacyGroupId) + besu.privGetTransactionCount(txSignService.getAddress(), privacyGroupId) .send() .getTransactionCount(); @@ -138,7 +139,7 @@ public EthSendTransaction sendEIP1559Transaction( boolean constructor) throws IOException { final BigInteger nonce = - besu.privGetTransactionCount(credentials.getAddress(), privacyGroupId) + besu.privGetTransactionCount(txSignService.getAddress(), privacyGroupId) .send() .getTransactionCount(); @@ -200,14 +201,7 @@ public EthGetCode getCode( public String sign(final RawPrivateTransaction rawTransaction) { - final byte[] signedMessage; - - if (chainId > ChainIdLong.NONE) { - signedMessage = - PrivateTransactionEncoder.signMessage(rawTransaction, chainId, credentials); - } else { - signedMessage = PrivateTransactionEncoder.signMessage(rawTransaction, credentials); - } + final byte[] signedMessage = txSignService.sign(rawTransaction, chainId); return Numeric.toHexString(signedMessage); } diff --git a/core/src/main/java/org/web3j/dto/HSMHTTPRequestDTO.java b/core/src/main/java/org/web3j/dto/HSMHTTPRequestDTO.java new file mode 100644 index 000000000..03fb96a18 --- /dev/null +++ b/core/src/main/java/org/web3j/dto/HSMHTTPRequestDTO.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.dto; + +public class HSMHTTPRequestDTO { + private String message; + + public HSMHTTPRequestDTO(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/core/src/main/java/org/web3j/service/HSMHTTPRequestProcessor.java b/core/src/main/java/org/web3j/service/HSMHTTPRequestProcessor.java new file mode 100644 index 000000000..fd0eb9011 --- /dev/null +++ b/core/src/main/java/org/web3j/service/HSMHTTPRequestProcessor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.service; + +import java.io.IOException; +import java.io.InputStream; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.web3j.crypto.CryptoUtils; +import org.web3j.crypto.ECDSASignature; +import org.web3j.crypto.HSMHTTPPass; +import org.web3j.crypto.Sign; +import org.web3j.protocol.exceptions.ClientConnectionException; +import org.web3j.utils.Numeric; + +/** + * Request processor to a HSM through the HTTP + * + * @param Object with required parameters to perform request to a HSM + */ +public abstract class HSMHTTPRequestProcessor + implements HSMRequestProcessor { + + private static final Logger log = LoggerFactory.getLogger(HSMHTTPRequestProcessor.class); + + public static final MediaType JSON = MediaType.parse("application/json"); + + private final OkHttpClient client; + + public HSMHTTPRequestProcessor(OkHttpClient okHttpClient) { + this.client = okHttpClient; + } + + @Override + public Sign.SignatureData callHSM(byte[] dataToSign, HSMHTTPPass pass) { + Request request = createRequest(dataToSign, pass); + + try (okhttp3.Response response = client.newCall(request).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + if (responseBody != null) { + String signHex = readResponse(responseBody.byteStream()); + byte[] signBytes = Numeric.hexStringToByteArray(signHex); + ECDSASignature signature = CryptoUtils.fromDerFormat(signBytes); + + return Sign.createSignatureData(signature, pass.getPublicKey(), dataToSign); + } else { + return null; + } + } else { + int code = response.code(); + String text = responseBody == null ? "N/A" : responseBody.string(); + throw new ClientConnectionException( + "Invalid response received: " + code + "; " + text); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + + return null; + } + + protected abstract Request createRequest(byte[] dataToSign, HSMHTTPPass pass); + + protected abstract String readResponse(InputStream responseData); +} diff --git a/core/src/main/java/org/web3j/service/HSMRequestProcessor.java b/core/src/main/java/org/web3j/service/HSMRequestProcessor.java new file mode 100644 index 000000000..c5ac43072 --- /dev/null +++ b/core/src/main/java/org/web3j/service/HSMRequestProcessor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.service; + +import org.web3j.crypto.HSMPass; +import org.web3j.crypto.Sign; + +/** + * Request processor to a HSM (hardware security module). + * + * @param Object with required parameters to perform request to a HSM. + */ +public interface HSMRequestProcessor { + + /** + * Call a HSM (hardware security module) + * + * @param dataToSign message hash to sign. + * @param pass Object with required parameters to perform request to a HSM. + * @return SignatureData v | r | s + */ + Sign.SignatureData callHSM(byte[] dataToSign, T pass); +} diff --git a/core/src/main/java/org/web3j/service/TxHSMSignService.java b/core/src/main/java/org/web3j/service/TxHSMSignService.java new file mode 100644 index 000000000..39781ff3c --- /dev/null +++ b/core/src/main/java/org/web3j/service/TxHSMSignService.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.service; + +import org.web3j.crypto.HSMPass; +import org.web3j.crypto.Hash; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.Sign; +import org.web3j.crypto.transaction.type.TransactionType; +import org.web3j.tx.ChainId; + +import static org.web3j.crypto.TransactionEncoder.createEip155SignatureData; +import static org.web3j.crypto.TransactionEncoder.encode; + +/** Service to sign transaction with HSM (hardware security module). */ +public class TxHSMSignService implements TxSignService { + + private final T hsmPass; + private final HSMRequestProcessor hsmRequestProcessor; + + public TxHSMSignService(HSMRequestProcessor hsmRequestProcessor, T hsmPass) { + this.hsmPass = hsmPass; + this.hsmRequestProcessor = hsmRequestProcessor; + } + + @Override + public byte[] sign(RawTransaction rawTransaction, long chainId) { + byte[] finalBytes; + byte[] encodedTransaction; + Sign.SignatureData signatureData; + boolean isNewTx = + chainId > ChainId.NONE && rawTransaction.getType().equals(TransactionType.LEGACY); + + if (isNewTx) { + encodedTransaction = encode(rawTransaction, chainId); + } else { + encodedTransaction = encode(rawTransaction); + } + + byte[] messageHash = Hash.sha3(encodedTransaction); + + signatureData = hsmRequestProcessor.callHSM(messageHash, hsmPass); + + if (isNewTx) { + signatureData = createEip155SignatureData(signatureData, chainId); + } + + finalBytes = encode(rawTransaction, signatureData); + + return finalBytes; + } + + @Override + public String getAddress() { + return hsmPass.getAddress(); + } +} diff --git a/core/src/main/java/org/web3j/service/TxSignService.java b/core/src/main/java/org/web3j/service/TxSignService.java new file mode 100644 index 000000000..8a7eda34b --- /dev/null +++ b/core/src/main/java/org/web3j/service/TxSignService.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.service; + +import org.web3j.crypto.RawTransaction; + +/** Service to sign transaction. */ +public interface TxSignService { + + /** + * Sign raw transaction. + * + * @param rawTransaction Raw transaction + * @param chainId Ethereum chain id, -1 is NONE + * @return Transaction signature + */ + byte[] sign(RawTransaction rawTransaction, long chainId); + + /** + * Get key address of the current wallet + * + * @return Wallet address + */ + String getAddress(); +} diff --git a/core/src/main/java/org/web3j/service/TxSignServiceImpl.java b/core/src/main/java/org/web3j/service/TxSignServiceImpl.java new file mode 100644 index 000000000..6c1a0b52f --- /dev/null +++ b/core/src/main/java/org/web3j/service/TxSignServiceImpl.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.service; + +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.tx.ChainId; + +/** Service to base sign transaction. */ +public class TxSignServiceImpl implements TxSignService { + + private final Credentials credentials; + + public TxSignServiceImpl(Credentials credentials) { + this.credentials = credentials; + } + + @Override + public byte[] sign(RawTransaction rawTransaction, long chainId) { + final byte[] signedMessage; + + if (chainId > ChainId.NONE) { + signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials); + } else { + signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials); + } + return signedMessage; + } + + @Override + public String getAddress() { + return credentials.getAddress(); + } +} diff --git a/core/src/main/java/org/web3j/tx/FastRawTransactionManager.java b/core/src/main/java/org/web3j/tx/FastRawTransactionManager.java index 5721206ae..3ccd2b876 100644 --- a/core/src/main/java/org/web3j/tx/FastRawTransactionManager.java +++ b/core/src/main/java/org/web3j/tx/FastRawTransactionManager.java @@ -17,6 +17,7 @@ import org.web3j.crypto.Credentials; import org.web3j.protocol.Web3j; +import org.web3j.service.TxSignService; import org.web3j.tx.response.TransactionReceiptProcessor; /** @@ -31,6 +32,12 @@ public FastRawTransactionManager(Web3j web3j, Credentials credentials, long chai super(web3j, credentials, chainId); } + public FastRawTransactionManager( + Web3j web3j, TxSignService txSignService, long chainId, BigInteger nonce) { + super(web3j, txSignService, chainId); + this.nonce = nonce; + } + public FastRawTransactionManager(Web3j web3j, Credentials credentials) { super(web3j, credentials); } diff --git a/core/src/main/java/org/web3j/tx/RawTransactionManager.java b/core/src/main/java/org/web3j/tx/RawTransactionManager.java index 4cb5d6341..34395e1f2 100644 --- a/core/src/main/java/org/web3j/tx/RawTransactionManager.java +++ b/core/src/main/java/org/web3j/tx/RawTransactionManager.java @@ -18,7 +18,6 @@ import org.web3j.crypto.Credentials; import org.web3j.crypto.Hash; import org.web3j.crypto.RawTransaction; -import org.web3j.crypto.TransactionEncoder; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameter; import org.web3j.protocol.core.DefaultBlockParameterName; @@ -27,6 +26,8 @@ import org.web3j.protocol.core.methods.response.EthGetCode; import org.web3j.protocol.core.methods.response.EthGetTransactionCount; import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.service.TxSignService; +import org.web3j.service.TxSignServiceImpl; import org.web3j.tx.exceptions.TxHashMismatchException; import org.web3j.tx.response.TransactionReceiptProcessor; import org.web3j.utils.Numeric; @@ -43,7 +44,7 @@ public class RawTransactionManager extends TransactionManager { private final Web3j web3j; - final Credentials credentials; + private final TxSignService txSignService; private final long chainId; @@ -51,11 +52,16 @@ public class RawTransactionManager extends TransactionManager { public RawTransactionManager(Web3j web3j, Credentials credentials, long chainId) { super(web3j, credentials.getAddress()); - this.web3j = web3j; - this.credentials = credentials; + this.chainId = chainId; + this.txSignService = new TxSignServiceImpl(credentials); + } + public RawTransactionManager(Web3j web3j, TxSignService txSignService, long chainId) { + super(web3j, txSignService.getAddress()); + this.web3j = web3j; this.chainId = chainId; + this.txSignService = txSignService; } public RawTransactionManager( @@ -66,9 +72,8 @@ public RawTransactionManager( super(transactionReceiptProcessor, credentials.getAddress()); this.web3j = web3j; - this.credentials = credentials; - this.chainId = chainId; + this.txSignService = new TxSignServiceImpl(credentials); } public RawTransactionManager( @@ -76,9 +81,8 @@ public RawTransactionManager( super(web3j, attempts, sleepDuration, credentials.getAddress()); this.web3j = web3j; - this.credentials = credentials; - this.chainId = chainId; + this.txSignService = new TxSignServiceImpl(credentials); } public RawTransactionManager(Web3j web3j, Credentials credentials) { @@ -93,7 +97,7 @@ public RawTransactionManager( protected BigInteger getNonce() throws IOException { EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount( - credentials.getAddress(), DefaultBlockParameterName.PENDING) + this.getFromAddress(), DefaultBlockParameterName.PENDING) .send(); return ethGetTransactionCount.getTransactionCount(); @@ -179,13 +183,7 @@ public EthGetCode getCode( */ public String sign(RawTransaction rawTransaction) { - byte[] signedMessage; - - if (chainId > ChainId.NONE) { - signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials); - } else { - signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials); - } + byte[] signedMessage = txSignService.sign(rawTransaction, chainId); return Numeric.toHexString(signedMessage); } diff --git a/core/src/test/java/org/web3j/tx/HSMHTTPRequestProcessorTestImpl.java b/core/src/test/java/org/web3j/tx/HSMHTTPRequestProcessorTestImpl.java new file mode 100644 index 000000000..58fc81ea7 --- /dev/null +++ b/core/src/test/java/org/web3j/tx/HSMHTTPRequestProcessorTestImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.tx; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.web3j.crypto.HSMHTTPPass; +import org.web3j.dto.HSMHTTPRequestDTO; +import org.web3j.service.HSMHTTPRequestProcessor; +import org.web3j.utils.Numeric; + +public class HSMHTTPRequestProcessorTestImpl + extends HSMHTTPRequestProcessor { + private static final Logger log = + LoggerFactory.getLogger(HSMHTTPRequestProcessorTestImpl.class); + + public HSMHTTPRequestProcessorTestImpl(OkHttpClient okHttpClient) { + super(okHttpClient); + } + + protected Request createRequest(byte[] dataToSign, HSMHTTPPass pass) { + HSMHTTPRequestDTO requestDto = + new HSMHTTPRequestDTO(Numeric.toHexStringNoPrefix(dataToSign)); + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + + String json; + try { + json = ow.writeValueAsString(requestDto); + } catch (JsonProcessingException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } + + return new Request.Builder() + .url(pass.getUrl()) + .post(RequestBody.create(json, JSON)) + .build(); + } + + protected String readResponse(InputStream responseData) { + return new BufferedReader(new InputStreamReader(responseData)) + .lines() + .collect(Collectors.joining("\n")); + } +} diff --git a/core/src/test/java/org/web3j/tx/ManagedTransactionTester.java b/core/src/test/java/org/web3j/tx/ManagedTransactionTester.java index 41c02614c..13ececc76 100644 --- a/core/src/test/java/org/web3j/tx/ManagedTransactionTester.java +++ b/core/src/test/java/org/web3j/tx/ManagedTransactionTester.java @@ -13,10 +13,12 @@ package org.web3j.tx; import java.io.IOException; +import java.math.BigInteger; import org.junit.jupiter.api.BeforeEach; import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; import org.web3j.crypto.SampleKeys; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; @@ -34,7 +36,7 @@ public abstract class ManagedTransactionTester { - static final String ADDRESS = "0x3d6cb163f7c72d20b0fcd6baae5889329d138a4a"; + public static final String ADDRESS = "0x3d6cb163f7c72d20b0fcd6baae5889329d138a4a"; static final String TRANSACTION_HASH = "0xHASH"; protected Web3j web3j; protected TxHashVerifier txHashVerifier; @@ -60,12 +62,24 @@ public TransactionManager getVerifiedTransactionManager(Credentials credentials) return transactionManager; } - void prepareTransaction(TransactionReceipt transactionReceipt) throws IOException { + public void prepareTransaction(TransactionReceipt transactionReceipt) throws IOException { prepareNonceRequest(); prepareTransactionRequest(); prepareTransactionReceipt(transactionReceipt); } + public RawTransaction createRawTx() { + BigInteger nonce = BigInteger.ZERO; + BigInteger gasPrice = BigInteger.ONE; + BigInteger gasLimit = BigInteger.TEN; + String to = "0x0add5355"; + BigInteger value = BigInteger.valueOf(Long.MAX_VALUE); + RawTransaction rawTransaction = + RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, value); + + return rawTransaction; + } + @SuppressWarnings("unchecked") void prepareNonceRequest() throws IOException { EthGetTransactionCount ethGetTransactionCount = new EthGetTransactionCount(); diff --git a/core/src/test/java/org/web3j/tx/RawTransactionManagerTest.java b/core/src/test/java/org/web3j/tx/RawTransactionManagerTest.java index f1198642b..a1fe89f9a 100644 --- a/core/src/test/java/org/web3j/tx/RawTransactionManagerTest.java +++ b/core/src/test/java/org/web3j/tx/RawTransactionManagerTest.java @@ -15,17 +15,38 @@ import java.io.IOException; import java.math.BigDecimal; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.junit.jupiter.api.Test; +import org.web3j.crypto.HSMHTTPPass; import org.web3j.crypto.SampleKeys; import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.service.HSMHTTPRequestProcessor; +import org.web3j.service.TxHSMSignService; +import org.web3j.service.TxSignService; import org.web3j.tx.exceptions.TxHashMismatchException; import org.web3j.utils.Convert; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class RawTransactionManagerTest extends ManagedTransactionTester { + // Get From org.web3j.crypto.TransactionDecoderTest.testDecodingSigned + private static final String TX_SIGN_FORMAT_DER_HEX = + "3044022046360b50498ddf5566551ce1ce69c46c565f1f478bb0ee680caf31fbc08ab72702201b2f1432de16d110407d544f519fc91b84c8e16d3b6ec899592d486a94974cd0"; + private static final String TX_SIGN_RESULT_HEX = + "0xf85580010a840add5355887fffffffffffffff801ca046360b50498ddf5566551ce1ce69c46c565f1f478bb0ee680caf31fbc08ab727a01b2f1432de16d110407d544f519fc91b84c8e16d3b6ec899592d486a94974cd0"; + @Test public void testTxHashMismatch() throws IOException { TransactionReceipt transactionReceipt = prepareTransfer(); @@ -38,4 +59,42 @@ public void testTxHashMismatch() throws IOException { TxHashMismatchException.class, () -> transfer.sendFunds(ADDRESS, BigDecimal.ONE, Convert.Unit.ETHER).send()); } + + @Test + public void testSignRawTxWithHSM() throws IOException { + TransactionReceipt transactionReceipt = prepareTransfer(); + prepareTransaction(transactionReceipt); + + OkHttpClient okHttpClient = mock(OkHttpClient.class); + Call call = mock(Call.class); + Response hmsResponse = + new Response.Builder() + .code(200) + .request(new Request.Builder().url("http://test-call.com").build()) + .protocol(Protocol.HTTP_1_1) + .message("response message") + .body( + ResponseBody.create( + TX_SIGN_FORMAT_DER_HEX, MediaType.parse("text/plain"))) + .build(); + + when(call.execute()).thenReturn(hmsResponse); + when(okHttpClient.newCall(any())).thenReturn(call); + + HSMHTTPRequestProcessor hsmRequestProcessor = + new HSMHTTPRequestProcessorTestImpl<>(okHttpClient); + HSMHTTPPass hsmhttpPass = + new HSMHTTPPass( + SampleKeys.CREDENTIALS.getAddress(), + SampleKeys.CREDENTIALS.getEcKeyPair().getPublicKey(), + "http://mock_request_url.com"); + + TxSignService txHSMSignService = new TxHSMSignService<>(hsmRequestProcessor, hsmhttpPass); + RawTransactionManager transactionManager = + new RawTransactionManager(web3j, txHSMSignService, ChainId.NONE); + + String sign = transactionManager.sign(createRawTx()); + + assertEquals(TX_SIGN_RESULT_HEX, sign); + } } diff --git a/crypto/src/main/java/org/web3j/crypto/CryptoUtils.java b/crypto/src/main/java/org/web3j/crypto/CryptoUtils.java new file mode 100644 index 000000000..194a7e8a1 --- /dev/null +++ b/crypto/src/main/java/org/web3j/crypto/CryptoUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.crypto; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.DERSequenceGenerator; +import org.bouncycastle.asn1.DLSequence; + +public class CryptoUtils { + + public static byte[] toDerFormat(ECDSASignature signature) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + DERSequenceGenerator seq = new DERSequenceGenerator(baos); + seq.addObject(new ASN1Integer(signature.r)); + seq.addObject(new ASN1Integer(signature.s)); + seq.close(); + return baos.toByteArray(); + } catch (IOException ex) { + return new byte[0]; + } + } + + public static ECDSASignature fromDerFormat(byte[] bytes) { + try (ASN1InputStream decoder = new ASN1InputStream(bytes)) { + DLSequence seq = (DLSequence) decoder.readObject(); + if (seq == null) { + throw new RuntimeException("Reached past end of ASN.1 stream."); + } + ASN1Integer r, s; + try { + r = (ASN1Integer) seq.getObjectAt(0); + s = (ASN1Integer) seq.getObjectAt(1); + } catch (ClassCastException e) { + throw new IllegalArgumentException(e); + } + // OpenSSL deviates from the DER spec by interpreting these values + // as unsigned, though they should not be + // Thus, we always use the positive versions. See: + // http://r6.ca/blog/20111119T211504Z.html + return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/crypto/src/main/java/org/web3j/crypto/HSMHTTPPass.java b/crypto/src/main/java/org/web3j/crypto/HSMHTTPPass.java new file mode 100644 index 000000000..fe426d1de --- /dev/null +++ b/crypto/src/main/java/org/web3j/crypto/HSMHTTPPass.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.crypto; + +import java.math.BigInteger; + +/** Object with required parameters to perform request to a HSM through the HTTP */ +public class HSMHTTPPass extends HSMPass { + + /** HSM URL */ + private String url; + + public HSMHTTPPass(String address, BigInteger publicKey, String url) { + super(address, publicKey); + this.url = url; + } + + public String getUrl() { + return url; + } +} diff --git a/crypto/src/main/java/org/web3j/crypto/HSMPass.java b/crypto/src/main/java/org/web3j/crypto/HSMPass.java new file mode 100644 index 000000000..deff06825 --- /dev/null +++ b/crypto/src/main/java/org/web3j/crypto/HSMPass.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.crypto; + +import java.math.BigInteger; + +/** Object with required parameters to perform request to a HSM. */ +public class HSMPass { + + /** Key address */ + private final String address; + + /** PublicKey */ + private BigInteger publicKey; + + public HSMPass(String address, BigInteger publicKey) { + this.address = address; + this.publicKey = publicKey; + } + + public String getAddress() { + return address; + } + + public BigInteger getPublicKey() { + return publicKey; + } +} diff --git a/crypto/src/main/java/org/web3j/crypto/Sign.java b/crypto/src/main/java/org/web3j/crypto/Sign.java index 5ab11a33d..bf1bad152 100644 --- a/crypto/src/main/java/org/web3j/crypto/Sign.java +++ b/crypto/src/main/java/org/web3j/crypto/Sign.java @@ -84,6 +84,12 @@ public static SignatureData signMessage(byte[] message, ECKeyPair keyPair, boole } ECDSASignature sig = keyPair.sign(messageHash); + + return createSignatureData(sig, publicKey, messageHash); + } + + public static Sign.SignatureData createSignatureData( + ECDSASignature sig, BigInteger publicKey, byte[] messageHash) { // Now we have to work backwards to figure out the recId needed to recover the signature. int recId = -1; for (int i = 0; i < 4; i++) { @@ -105,7 +111,7 @@ public static SignatureData signMessage(byte[] message, ECKeyPair keyPair, boole byte[] r = Numeric.toBytesPadded(sig.r, 32); byte[] s = Numeric.toBytesPadded(sig.s, 32); - return new SignatureData(v, r, s); + return new Sign.SignatureData(v, r, s); } /** diff --git a/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java b/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java index 0a8315666..97810c462 100644 --- a/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java +++ b/crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java @@ -91,7 +91,7 @@ public static byte[] encode(RawTransaction rawTransaction, byte chainId) { return encode(rawTransaction, (long) chainId); } - private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) { + public static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) { List values = asRlpValues(rawTransaction, signatureData); RlpList rlpList = new RlpList(values); byte[] encoded = RlpEncoder.encode(rlpList); diff --git a/crypto/src/test/java/org/web3j/crypto/CryptoUtilsTest.java b/crypto/src/test/java/org/web3j/crypto/CryptoUtilsTest.java new file mode 100644 index 000000000..fa55d53cf --- /dev/null +++ b/crypto/src/test/java/org/web3j/crypto/CryptoUtilsTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Web3 Labs Ltd. + * + * 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. + */ +package org.web3j.crypto; + +import java.math.BigInteger; + +import org.junit.jupiter.api.Test; + +import org.web3j.utils.Numeric; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CryptoUtilsTest { + + // Get From org.web3j.crypto.TransactionDecoderTest.testDecodingSigned + private static final String TX_SIGN_FORMAT_DER_HEX = + "3044022046360b50498ddf5566551ce1ce69c46c565f1f478bb0ee680caf31fbc08ab72702201b2f1432de16d110407d544f519fc91b84c8e16d3b6ec899592d486a94974cd0"; + private ECDSASignature ecdsaSignatureExample = + new ECDSASignature( + new BigInteger( + "31757387226078388218879983949976195107190435398991401751066049942728919922471"), + new BigInteger( + "12295628130105760695929025310777619861262324839583119058006543455570535861456")); + + @Test + void toDerFormat() { + byte[] signDER = CryptoUtils.toDerFormat(ecdsaSignatureExample); + + assertArrayEquals(Numeric.hexStringToByteArray(TX_SIGN_FORMAT_DER_HEX), signDER); + } + + @Test + void fromDerFormat() { + ECDSASignature ecdsaSignature = + CryptoUtils.fromDerFormat(Numeric.hexStringToByteArray(TX_SIGN_FORMAT_DER_HEX)); + + assertEquals(ecdsaSignatureExample.r, ecdsaSignature.r); + assertEquals(ecdsaSignatureExample.s, ecdsaSignature.s); + } +}