Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Add eth_sign support #263

Merged
merged 1 commit into from May 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions ethsigner/core/build.gradle
Expand Up @@ -44,6 +44,7 @@ dependencies {

testImplementation 'io.vertx:vertx-codegen'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
Expand Down
Expand Up @@ -23,6 +23,7 @@
import tech.pegasys.ethsigner.core.requesthandler.VertxRequestTransmitter;
import tech.pegasys.ethsigner.core.requesthandler.VertxRequestTransmitterFactory;
import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthAccountsBodyProvider;
import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthSignBodyProvider;
import tech.pegasys.ethsigner.core.requesthandler.internalresponse.InternalResponseHandler;
import tech.pegasys.ethsigner.core.requesthandler.passthrough.PassThroughHandler;
import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.SendTransactionHandler;
Expand Down Expand Up @@ -151,6 +152,10 @@ private RequestMapper createRequestMapper(
responseFactory,
new EthAccountsBodyProvider(transactionSignerProvider::availableAddresses),
jsonDecoder));
requestMapper.addHandler(
"eth_sign",
new InternalResponseHandler(
responseFactory, new EthSignBodyProvider(transactionSignerProvider), jsonDecoder));

return requestMapper;
}
Expand Down
@@ -0,0 +1,92 @@
/*
* Copyright 2020 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.
*/
package tech.pegasys.ethsigner.core.requesthandler.internalresponse;

import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.INTERNAL_ERROR;
import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.INVALID_PARAMS;
import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT;

import tech.pegasys.ethsigner.core.jsonrpc.JsonRpcRequest;
import tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcSuccessResponse;
import tech.pegasys.ethsigner.core.requesthandler.BodyProvider;
import tech.pegasys.ethsigner.core.requesthandler.JsonRpcBody;
import tech.pegasys.ethsigner.core.signing.Signature;
import tech.pegasys.ethsigner.core.signing.TransactionSigner;
import tech.pegasys.ethsigner.core.signing.TransactionSignerProvider;
import tech.pegasys.ethsigner.core.util.ByteUtils;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;

import io.vertx.core.json.Json;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.web3j.utils.Numeric;

public class EthSignBodyProvider implements BodyProvider {

private static final Logger LOG = LogManager.getLogger();

private final TransactionSignerProvider transactionSignerProvider;

public EthSignBodyProvider(final TransactionSignerProvider transactionSignerProvider) {
this.transactionSignerProvider = transactionSignerProvider;
}

@Override
public JsonRpcBody getBody(final JsonRpcRequest request) {
try {
@SuppressWarnings("unchecked")
final List<String> params = (List<String>) request.getParams();
if (params == null || params.size() != 2) {
LOG.info(
"eth_sign should have a list of 2 parameters, but has {}",
params == null ? "null" : params.size());
return new JsonRpcBody(INVALID_PARAMS);
}
final String address = params.get(0);
final Optional<TransactionSigner> transactionSigner =
transactionSignerProvider.getSigner(address);
if (transactionSigner.isEmpty()) {
LOG.info("Address ({}) does not match any available account", address);
return new JsonRpcBody(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT);
}
final TransactionSigner signer = transactionSigner.get();
final String originalMessage = params.get(1);
final String message =
(char) 25 + "Ethereum Signed Message:\n" + originalMessage.length() + originalMessage;
final Signature signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));

final Bytes outputSignature =
Bytes.concatenate(
Bytes32.leftPad(Bytes.wrap(ByteUtils.bigIntegerToBytes(signature.getR()))),
Bytes32.leftPad(Bytes.wrap(ByteUtils.bigIntegerToBytes(signature.getS()))),
Bytes.wrap(ByteUtils.bigIntegerToBytes(signature.getV())));
final JsonRpcSuccessResponse response =
new JsonRpcSuccessResponse(
request.getId(), Numeric.toHexString(outputSignature.toArray()));
return new JsonRpcBody(Json.encodeToBuffer(response));
} catch (final ClassCastException e) {
LOG.info(
"eth_sign should have a list of 2 parameters, but received an object: {}",
request.getParams());
return new JsonRpcBody(INVALID_PARAMS);
} catch (final Exception e) {
LOG.info("Unexpected error", e);
return new JsonRpcBody(INTERNAL_ERROR);
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2020 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.
*/
package tech.pegasys.ethsigner.core.util;

import java.math.BigInteger;

public class ByteUtils {

/**
* Omitting sign indication byte. <br>
* <br>
* Instead of {@link org.bouncycastle.util.BigIntegers#asUnsignedByteArray(BigInteger)} <br>
* we use this custom method to avoid an empty array in case of BigInteger.ZERO
*
* @param value - any big integer number. A <code>null</code>-value will return <code>null</code>
* @return A byte array without a leading zero byte if present in the signed encoding.
* BigInteger.ZERO will return an array with length 1 and byte-value 0.
*/
public static byte[] bigIntegerToBytes(final BigInteger value) {
if (value == null) {
return null;
}

byte[] data = value.toByteArray();

if (data.length != 1 && data[0] == 0) {
byte[] tmp = new byte[data.length - 1];
System.arraycopy(data, 1, tmp, 0, tmp.length);
data = tmp;
}
return data;
}
}
@@ -0,0 +1,171 @@
/*
* Copyright 2020 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.
*/
package tech.pegasys.ethsigner.core.jsonrpcproxy;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import tech.pegasys.ethsigner.core.jsonrpc.JsonRpcRequest;
import tech.pegasys.ethsigner.core.jsonrpc.JsonRpcRequestId;
import tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError;
import tech.pegasys.ethsigner.core.requesthandler.JsonRpcBody;
import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthSignBodyProvider;
import tech.pegasys.ethsigner.core.signing.Signature;
import tech.pegasys.ethsigner.core.signing.TransactionSigner;
import tech.pegasys.ethsigner.core.signing.TransactionSignerProvider;

import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import io.vertx.core.json.JsonObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.NullSource;
import org.web3j.crypto.ECDSASignature;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;

public class EthSignBodyProviderTest {

@ParameterizedTest
@ArgumentsSource(InvalidParamsProvider.class)
@NullSource
public void ifParamIsInvalidErrorIsReturned(final Object params) {
final TransactionSignerProvider mockSignerProvider = mock(TransactionSignerProvider.class);
final EthSignBodyProvider bodyProvider = new EthSignBodyProvider(mockSignerProvider);

final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_sign");
request.setId(new JsonRpcRequestId(1));
request.setParams(params);
final JsonRpcBody body = bodyProvider.getBody(request);

assertThat(body.hasError()).isTrue();
assertThat(body.error().getCode()).isEqualTo(JsonRpcError.INVALID_PARAMS.getCode());
}

@Test
public void ifAddressIsNotUnlockedErrorIsReturned() {
final TransactionSignerProvider mockSignerProvider = mock(TransactionSignerProvider.class);
final EthSignBodyProvider bodyProvider = new EthSignBodyProvider(mockSignerProvider);

final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_sign");
request.setId(new JsonRpcRequestId(1));
request.setParams(List.of("address", "message"));
final JsonRpcBody body = bodyProvider.getBody(request);

assertThat(body.hasError()).isTrue();
assertThat(body.error().getCode())
.isEqualTo(JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT.getCode());
}

@Test
public void signatureHasTheExpectedFormat() {
final TransactionSigner mockTransactionSigner = mock(TransactionSigner.class);
final BigInteger v = BigInteger.ONE;
final BigInteger r = BigInteger.TWO;
final BigInteger s = BigInteger.TEN;
doReturn(new Signature(v, r, s)).when(mockTransactionSigner).sign(any(byte[].class));
final TransactionSignerProvider mockSignerProvider = mock(TransactionSignerProvider.class);
doReturn(Optional.of(mockTransactionSigner)).when(mockSignerProvider).getSigner(anyString());
final EthSignBodyProvider bodyProvider = new EthSignBodyProvider(mockSignerProvider);

final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_sign");
final int id = 1;
request.setId(new JsonRpcRequestId(id));
request.setParams(List.of("address", "message"));

final JsonRpcBody body = bodyProvider.getBody(request);
final JsonObject jsonObj = new JsonObject(body.body());

assertThat(body.hasError()).isFalse();
assertThat(jsonObj.getString("jsonrpc")).isEqualTo("2.0");
assertThat(jsonObj.getInteger("id")).isEqualTo(id);
final String hexSignature = jsonObj.getString("result");
assertThat(hexSignature).hasSize(132);

final byte[] signature = Numeric.hexStringToByteArray(hexSignature);

assertThat(new BigInteger(1, signature, 0, 32)).isEqualTo(r);
assertThat(new BigInteger(1, signature, 32, 32)).isEqualTo(s);
assertThat(new BigInteger(1, signature, 64, 1)).isEqualTo(v);
}

@Test
public void returnsExpectedSignature() {
final ECKeyPair keyPair =
ECKeyPair.create(
Numeric.hexStringToByteArray(
"0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"));
final TransactionSigner mockTransactionSigner = mock(TransactionSigner.class);
doAnswer(
answer -> {
byte[] data = answer.getArgument(0, byte[].class);
final Sign.SignatureData signature = Sign.signMessage(data, keyPair);
return new Signature(
new BigInteger(signature.getV()),
new BigInteger(1, signature.getR()),
new BigInteger(1, signature.getS()));
})
.when(mockTransactionSigner)
.sign(any(byte[].class));

final TransactionSignerProvider mockSignerProvider = mock(TransactionSignerProvider.class);
doReturn(Optional.of(mockTransactionSigner)).when(mockSignerProvider).getSigner(anyString());
final EthSignBodyProvider bodyProvider = new EthSignBodyProvider(mockSignerProvider);

final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_sign");
final int id = 1;
request.setId(new JsonRpcRequestId(id));
request.setParams(
List.of(
"address",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tubulum"
+ " fuisse, qua illum, cuius is condemnatus est rogatione, P. Eaedem res maneant alio modo."));

final JsonRpcBody body = bodyProvider.getBody(request);
assertThat(body.hasError()).isFalse();
final JsonObject jsonObj = new JsonObject(body.body());
final String hexSignature = jsonObj.getString("result");
final byte[] signature = Numeric.hexStringToByteArray(hexSignature);

final ECDSASignature expectedSignature =
keyPair.sign(
Numeric.hexStringToByteArray(
"0xe63325d74baa84af003dfb6a974f41672be881b56aa2c12c093f8259321bd460"));
assertThat(new BigInteger(1, signature, 0, 32)).isEqualTo(expectedSignature.r);
assertThat(new BigInteger(1, signature, 32, 32)).isEqualTo(expectedSignature.s);
}

private static class InvalidParamsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(final ExtensionContext context) {
return Stream.of(
Arguments.of(Collections.emptyList()),
Arguments.of(Collections.singleton(2)),
Arguments.of(List.of(1, 2, 3)),
Arguments.of(new Object()));
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2020 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.
*/
package tech.pegasys.ethsigner.core.util;

import static org.assertj.core.api.Assertions.assertThat;

import java.math.BigInteger;

import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test;

class ByteUtilsTest {

@Test
public void omitsSignIndicationByteProperly() {
final BigInteger a = new BigInteger(1, Hex.decode("ff12345678"));
final byte[] a5 = ByteUtils.bigIntegerToBytes(a);
assertThat(a5.length).isEqualTo(5);
assertThat(a5).containsExactly(Hex.decode("ff12345678"));

final BigInteger b = new BigInteger(1, Hex.decode("0f12345678"));
final byte[] b5 = ByteUtils.bigIntegerToBytes(b);
assertThat(b5.length).isEqualTo(5);
assertThat(b5).containsExactly(Hex.decode("0f12345678"));
}

@Test
public void ifParameterIsNullReturnsNull() {
final byte[] a = ByteUtils.bigIntegerToBytes(null);
assertThat(a).isNull();
}

@Test
public void ifBigIntegerZeroReturnsZeroValueArray() {
final byte[] a = ByteUtils.bigIntegerToBytes(BigInteger.ZERO);
assertThat(a).containsExactly(0);
}
}