Skip to content

Commit

Permalink
Add eth_signTypedData (#893)
Browse files Browse the repository at this point in the history
* add new rpc method

* update web3j to 4.10.2

* Add unit and integration test

* Refactor tests

* Address review

* Add the comment to mention how we got the json and validation source

* Add changelog entry
  • Loading branch information
gfukushima authored Sep 6, 2023
1 parent b2ae89d commit 01b08f4
Show file tree
Hide file tree
Showing 7 changed files with 401 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Bulk load Ethereum v3 wallet files in eth1 mode.
- Eth2 Signing request body now supports both `signingRoot` and the `signing_root` property
- Add network configuration for Holesky testnet
- Add `eth_signTypedData` RPC method under the eth1 subcommand. [#893](https://github.com/Consensys/web3signer/pull/893)

### Bugs fixed
- Upcheck was using application/json accept headers instead text/plain accept headers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2023 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.web3signer.core.jsonrpcproxy;

import static java.util.Collections.singletonList;
import static org.web3j.crypto.Keys.getAddress;
import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT;

import tech.pegasys.web3signer.core.jsonrpcproxy.support.EthSignTypedData;
import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcErrorResponse;
import tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcSuccessResponse;

import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Map;

import io.netty.handler.codec.http.HttpHeaderValues;
import io.vertx.core.json.Json;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.junit.jupiter.api.Test;
import org.web3j.crypto.Keys;
import org.web3j.protocol.core.Request;

public class EthSignTypedDataIntegrationTest extends IntegrationTestBase {

// Json taken and validated using https://metamask.github.io/test-dapp/#signTypedDataV4
private static final String eip712Json =
"""
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
]
},
"domain": {
"name": "My Dapp",
"version": "1.0",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"primaryType": "Person",
"message": {
"name": "John Doe",
"wallet": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
}
}
""";

@Test
void ethSignTypedDataSignsDataWhenAnUnlockedAccountIsPassed() {
final Request<?, EthSignTypedData> requestBody =
new Request<>(
"eth_signTypedData",
Arrays.asList(unlockedAccount, eip712Json),
null,
EthSignTypedData.class);

final Iterable<Map.Entry<String, String>> expectedHeaders =
singletonList(ImmutablePair.of("Content", HttpHeaderValues.APPLICATION_JSON.toString()));

final JsonRpcSuccessResponse responseBody =
new JsonRpcSuccessResponse(
requestBody.getId(),
"0x11cb46f70ad43da86e15ca7c6bb28356859a5f4ba430b44dbf1e65726d467be6072be9d1e5b40bd5b7abe8888eb91a69f0e6d56a8a094718ed8080baf02d61c31c");

sendPostRequestAndVerifyResponse(
request.web3Signer(Json.encode(requestBody)),
response.web3Signer(expectedHeaders, Json.encode(responseBody)));
}

@Test
void ethSignTypedDataDoNotSignMessageWhenSignerAccountIsNotUnlocked()
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
final String A_RANDOM_ADDRESS = getAddress(Keys.createEcKeyPair().getPublicKey());

final Request<?, EthSignTypedData> requestBody =
new Request<>(
"eth_signTypedData",
Arrays.asList(A_RANDOM_ADDRESS, eip712Json),
null,
EthSignTypedData.class);

final Iterable<Map.Entry<String, String>> expectedHeaders =
singletonList(ImmutablePair.of("Content", HttpHeaderValues.APPLICATION_JSON.toString()));

final JsonRpcErrorResponse responseBody =
new JsonRpcErrorResponse(requestBody.getId(), SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT);

sendPostRequestAndVerifyResponse(
request.web3Signer(Json.encode(requestBody)),
response.web3Signer(expectedHeaders, Json.encode(responseBody)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2023 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.web3signer.core.jsonrpcproxy.support;

import org.web3j.protocol.core.Response;

public class EthSignTypedData extends Response<String> {
public String getSignature() {
return getResult();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.RequestMapper;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.EthSignResultProvider;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.EthSignTransactionResultProvider;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.EthSignTypedDataResultProvider;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.InternalResponseHandler;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.SendTransactionHandler;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory;
Expand Down Expand Up @@ -306,6 +307,10 @@ private RequestMapper createRequestMapper(
requestMapper.addHandler(
"eth_sign",
new InternalResponseHandler<>(responseFactory, new EthSignResultProvider(secpSigner)));
requestMapper.addHandler(
"eth_signTypedData",
new InternalResponseHandler<>(
responseFactory, new EthSignTypedDataResultProvider(secpSigner)));
requestMapper.addHandler(
"eth_signTransaction",
new InternalResponseHandler<>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2023 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.web3signer.core.service.jsonrpc.handlers.internalresponse;

import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.INVALID_PARAMS;
import static tech.pegasys.web3signer.core.service.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT;
import static tech.pegasys.web3signer.signing.util.IdentifierUtils.normaliseIdentifier;

import tech.pegasys.web3signer.core.service.http.handlers.signing.SignerForIdentifier;
import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest;
import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.ResultProvider;
import tech.pegasys.web3signer.signing.SecpArtifactSignature;

import java.io.IOException;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes;
import org.web3j.crypto.StructuredDataEncoder;

public class EthSignTypedDataResultProvider implements ResultProvider<String> {

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

private final SignerForIdentifier<SecpArtifactSignature> transactionSignerProvider;

public EthSignTypedDataResultProvider(
final SignerForIdentifier<SecpArtifactSignature> transactionSignerProvider) {
this.transactionSignerProvider = transactionSignerProvider;
}

@Override
public String createResponseResult(final JsonRpcRequest request) {
final List<String> params = getParams(request);
if (params == null || params.size() != 2) {
LOG.debug(
"eth_signTypedData should have a list of 2 parameters, but has {}",
params == null ? "null" : params.size());
throw new JsonRpcException(INVALID_PARAMS);
}

final String eth1Address = params.get(0);
final String jsonData = params.get(1);

final StructuredDataEncoder dataEncoder;
try {
dataEncoder = new StructuredDataEncoder(jsonData);
} catch (IOException e) {
throw new RuntimeException("Exception thrown while enconding the json provided");
}
final Bytes structuredData = Bytes.of(dataEncoder.getStructuredData());
return transactionSignerProvider
.sign(normaliseIdentifier(eth1Address), structuredData)
.orElseThrow(
() -> {
LOG.debug("Address ({}) does not match any available account", eth1Address);
return new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT);
});
}

private List<String> getParams(final JsonRpcRequest request) {
try {
@SuppressWarnings("unchecked")
final List<String> params = (List<String>) request.getParams();
return params;
} catch (final ClassCastException e) {
LOG.debug(
"eth_signTypedData should have a list of 2 parameters, but received an object: {}",
request.getParams());
throw new JsonRpcException(INVALID_PARAMS);
}
}
}
Loading

0 comments on commit 01b08f4

Please sign in to comment.