Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eth_signTypedData #893

Merged
merged 18 commits into from
Sep 6, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =
gfukushima marked this conversation as resolved.
Show resolved Hide resolved
"""
{
"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");
jframe marked this conversation as resolved.
Show resolved Hide resolved

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