Skip to content

Commit

Permalink
feat: Commit Boost API - Request Signature (#1045)
Browse files Browse the repository at this point in the history
-- Routes and Handlers
-- Acceptance Test
  • Loading branch information
usmansaleem authored Nov 28, 2024
1 parent c389a3f commit f8e60be
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 16 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
- Java 21 for build and runtime. [#995](https://github.com/Consensys/web3signer/pull/995)
- Electra fork support. [#1020](https://github.com/Consensys/web3signer/pull/1020) and [#1023](https://github.com/Consensys/web3signer/pull/1023)
- Teku and Besu libraries updated to 24.10.3 and 24.10.0 respectively.
- Commit Boost API - Get Public Keys [#1031][cb_pr1], Generate Proxy Keys [#1043][cb_pr2].
- Commit Boost API - Get Public Keys [#1031][cb_pr1], Generate Proxy Keys [#1043][cb_pr2] and Request Signature [#1045][cb_pr3].

[cb_pr1]: https://github.com/Consensys/web3signer/pull/1031
[cb_pr2]: https://github.com/Consensys/web3signer/pull/1043
[cb_pr3]: https://github.com/Consensys/web3signer/pull/1045

### Bugs fixed
- Override protobuf-java to 3.25.5 which is a transitive dependency from google-cloud-secretmanager. It fixes CVE-2024-7254.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostAcceptanceTest.KEYSTORE_PASSWORD;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostAcceptanceTest.createCommitBoostPasswordFile;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostAcceptanceTest.randomBLSKeyPairs;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.KEYSTORE_PASSWORD;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.createCommitBoostPasswordFile;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.randomBLSKeyPairs;

import tech.pegasys.teku.bls.BLSKeyPair;
import tech.pegasys.teku.networks.Eth2NetworkConfiguration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@
import org.web3j.crypto.WalletUtils;

// See https://commit-boost.github.io/commit-boost-client/api/ for Commit Boost spec
public class CommitBoostAcceptanceTest extends AcceptanceTestBase {
public class CommitBoostGetPubKeysAcceptanceTest extends AcceptanceTestBase {
static final String KEYSTORE_PASSWORD = "password";

private List<BLSKeyPair> consensusBlsKeys = randomBLSKeyPairs(2);
private Map<String, List<BLSKeyPair>> proxyBLSKeysMap = new HashMap<>();
private Map<String, List<ECKeyPair>> proxySECPKeysMap = new HashMap<>();
private final List<BLSKeyPair> consensusBlsKeys = randomBLSKeyPairs(2);
private final Map<String, List<BLSKeyPair>> proxyBLSKeysMap = new HashMap<>();
private final Map<String, List<ECKeyPair>> proxySECPKeysMap = new HashMap<>();

@TempDir private Path keystoreDir;
@TempDir private Path passwordDir;
// commit boost directories
Expand All @@ -62,11 +63,13 @@ void setup() throws Exception {
KeystoreUtil.createKeystore(blsKeyPair, keystoreDir, passwordDir, KEYSTORE_PASSWORD);

// create 2 proxy bls
final List<BLSKeyPair> proxyBLSKeys = createProxyBLSKeys(blsKeyPair);
final List<BLSKeyPair> proxyBLSKeys =
createProxyBLSKeys(blsKeyPair, 2, commitBoostKeystoresPath);
proxyBLSKeysMap.put(blsKeyPair.getPublicKey().toHexString(), proxyBLSKeys);

// create 2 proxy secp keys
final List<ECKeyPair> proxyECKeyPairs = createProxyECKeys(blsKeyPair);
final List<ECKeyPair> proxyECKeyPairs =
createProxyECKeys(blsKeyPair, 2, commitBoostKeystoresPath);
proxySECPKeysMap.put(blsKeyPair.getPublicKey().toHexString(), proxyECKeyPairs);
}

Expand Down Expand Up @@ -147,12 +150,15 @@ static Path createCommitBoostPasswordFile(final Path commitBoostPasswordDir) {
}

/**
* Generate 2 random proxy EC key pairs and their encrypted keystores
* Generate random proxy EC key pairs and their encrypted keystores for given consensus BLS key
*
* @param consensusKeyPair consensus BLS key pair whose public key will be used as directory name
* @param count number of proxy key pairs to generate
* @param commitBoostKeystoresPath path to store the generated keystores
* @return list of ECKeyPairs
*/
private List<ECKeyPair> createProxyECKeys(final BLSKeyPair consensusKeyPair) {
static List<ECKeyPair> createProxyECKeys(
final BLSKeyPair consensusKeyPair, final int count, final Path commitBoostKeystoresPath) {
final Path proxySecpKeyStoreDir =
commitBoostKeystoresPath
.resolve(consensusKeyPair.getPublicKey().toHexString())
Expand All @@ -163,7 +169,7 @@ private List<ECKeyPair> createProxyECKeys(final BLSKeyPair consensusKeyPair) {
throw new UncheckedIOException(e);
}
// create 2 random proxy secp keys and their keystores
final List<ECKeyPair> proxyECKeyPairs = randomECKeyPairs(2);
final List<ECKeyPair> proxyECKeyPairs = randomECKeyPairs(count);
proxyECKeyPairs.forEach(
proxyECKey -> {
try {
Expand All @@ -177,12 +183,15 @@ private List<ECKeyPair> createProxyECKeys(final BLSKeyPair consensusKeyPair) {
}

/**
* Generate 2 random proxy BLS key pairs and their encrypted keystores
* Generate random proxy BLS key pairs and their encrypted keystores for given BLS consensus key
*
* @param consensusKeyPair consensus BLS key pair whose public key will be used as directory name
* @param count number of proxy key pairs to generate
* @param commitBoostKeystoresPath path to store the generated keystores
* @return list of BLSKeyPairs
*/
private List<BLSKeyPair> createProxyBLSKeys(final BLSKeyPair consensusKeyPair) {
static List<BLSKeyPair> createProxyBLSKeys(
final BLSKeyPair consensusKeyPair, final int count, final Path commitBoostKeystoresPath) {
final Path proxyBlsKeyStoreDir =
commitBoostKeystoresPath
.resolve(consensusKeyPair.getPublicKey().toHexString())
Expand All @@ -193,7 +202,7 @@ private List<BLSKeyPair> createProxyBLSKeys(final BLSKeyPair consensusKeyPair) {
throw new UncheckedIOException(e);
}
// create 2 proxy bls keys and their keystores
List<BLSKeyPair> blsKeyPairs = randomBLSKeyPairs(2);
List<BLSKeyPair> blsKeyPairs = randomBLSKeyPairs(count);
blsKeyPairs.forEach(
blsKeyPair ->
KeystoreUtil.createKeystoreFile(blsKeyPair, proxyBlsKeyStoreDir, KEYSTORE_PASSWORD));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2024 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.tests.commitboost;

import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.KEYSTORE_PASSWORD;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.createCommitBoostPasswordFile;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.createProxyBLSKeys;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.createProxyECKeys;
import static tech.pegasys.web3signer.tests.commitboost.CommitBoostGetPubKeysAcceptanceTest.randomBLSKeyPairs;

import tech.pegasys.teku.bls.BLSKeyPair;
import tech.pegasys.teku.networks.Eth2NetworkConfiguration;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.networks.Eth2Network;
import tech.pegasys.web3signer.KeystoreUtil;
import tech.pegasys.web3signer.core.service.http.handlers.commitboost.SigningRootGenerator;
import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.CommitBoostSignRequestType;
import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder;
import tech.pegasys.web3signer.dsl.utils.DefaultKeystoresParameters;
import tech.pegasys.web3signer.dsl.utils.ValidBLSSignatureMatcher;
import tech.pegasys.web3signer.dsl.utils.ValidK256SignatureMatcher;
import tech.pegasys.web3signer.signing.config.KeystoresParameters;
import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils;
import tech.pegasys.web3signer.tests.AcceptanceTestBase;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.web3j.crypto.ECKeyPair;

public class CommitBoostSigningRequestAcceptanceTest extends AcceptanceTestBase {
private static final SigningRootGenerator SIGNING_ROOT_GENERATOR =
new SigningRootGenerator(getMainnetSpec());
private final List<BLSKeyPair> consensusBlsKeys = randomBLSKeyPairs(1);
private final Map<String, List<BLSKeyPair>> proxyBLSKeysMap = new HashMap<>();
private final Map<String, List<ECKeyPair>> proxySECPKeysMap = new HashMap<>();

@TempDir private Path keystoreDir;
@TempDir private Path passwordDir;
// commit boost directories
@TempDir private Path commitBoostKeystoresPath;
@TempDir private Path commitBoostPasswordDir;

@BeforeEach
void setup() throws Exception {
for (final BLSKeyPair blsKeyPair : consensusBlsKeys) {
// create consensus bls keystore
KeystoreUtil.createKeystore(blsKeyPair, keystoreDir, passwordDir, KEYSTORE_PASSWORD);

// create 1 proxy bls
final List<BLSKeyPair> proxyBLSKeys =
createProxyBLSKeys(blsKeyPair, 1, commitBoostKeystoresPath);
proxyBLSKeysMap.put(blsKeyPair.getPublicKey().toHexString(), proxyBLSKeys);

// create 1 proxy secp keys
final List<ECKeyPair> proxyECKeyPairs =
createProxyECKeys(blsKeyPair, 1, commitBoostKeystoresPath);
proxySECPKeysMap.put(blsKeyPair.getPublicKey().toHexString(), proxyECKeyPairs);
}

// commit boost proxy keys password file
final Path commitBoostPasswordFile = createCommitBoostPasswordFile(commitBoostPasswordDir);

// start web3signer with keystores and commit boost parameters
final KeystoresParameters keystoresParameters =
new DefaultKeystoresParameters(keystoreDir, passwordDir, null);
final Pair<Path, Path> commitBoostParameters =
Pair.of(commitBoostKeystoresPath, commitBoostPasswordFile);

final SignerConfigurationBuilder configBuilder =
new SignerConfigurationBuilder()
.withMode("eth2")
.withNetwork("mainnet")
.withKeystoresParameters(keystoresParameters)
.withCommitBoostParameters(commitBoostParameters);

startSigner(configBuilder.build());
}

@ParameterizedTest
@EnumSource(CommitBoostSignRequestType.class)
void requestCommitBoostSignature(final CommitBoostSignRequestType signRequestType) {
final String consensusPubKey =
consensusBlsKeys.stream().findFirst().orElseThrow().getPublicKey().toHexString();
final String pubKey =
switch (signRequestType) {
case CONSENSUS -> consensusPubKey;
case PROXY_BLS ->
proxyBLSKeysMap.get(consensusPubKey).stream()
.findFirst()
.orElseThrow()
.getPublicKey()
.toHexString();
case PROXY_ECDSA ->
EthPublicKeyUtils.toHexStringCompressed(
EthPublicKeyUtils.web3JPublicKeyToECPublicKey(
proxySECPKeysMap.get(consensusPubKey).stream()
.findFirst()
.orElseThrow()
.getPublicKey()));
};

// object root is data to sign
final Bytes32 objectRoot = Bytes32.random(new Random(0));
// signature is calculated on signing root
final Bytes32 signingRoot = SIGNING_ROOT_GENERATOR.computeSigningRoot(objectRoot);

final Response response =
signer.callCommitBoostRequestForSignature(signRequestType.name(), pubKey, objectRoot);

response
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body(
signRequestType == CommitBoostSignRequestType.PROXY_ECDSA
? new ValidK256SignatureMatcher(pubKey, signingRoot)
: new ValidBLSSignatureMatcher(pubKey, signingRoot));
}

private static Spec getMainnetSpec() {
final Eth2NetworkConfiguration.Builder builder = Eth2NetworkConfiguration.builder();
return builder.applyNetworkDefaults(Eth2Network.MAINNET).build().getSpec();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import tech.pegasys.web3signer.core.routes.ReloadRoute;
import tech.pegasys.web3signer.core.routes.eth2.CommitBoostGenerateProxyKeyRoute;
import tech.pegasys.web3signer.core.routes.eth2.CommitBoostPublicKeysRoute;
import tech.pegasys.web3signer.core.routes.eth2.CommitBoostRequestSignatureRoute;
import tech.pegasys.web3signer.core.routes.eth2.Eth2SignExtensionRoute;
import tech.pegasys.web3signer.core.routes.eth2.Eth2SignRoute;
import tech.pegasys.web3signer.core.routes.eth2.HighWatermarkRoute;
Expand Down Expand Up @@ -145,6 +146,7 @@ public void populateRouter(final Context context) {
if (commitBoostApiParameters.isEnabled()) {
new CommitBoostPublicKeysRoute(context).register();
new CommitBoostGenerateProxyKeyRoute(context, commitBoostApiParameters, eth2Spec).register();
new CommitBoostRequestSignatureRoute(context, eth2Spec).register();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2024 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.routes.eth2;

import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;

import tech.pegasys.teku.spec.Spec;
import tech.pegasys.web3signer.core.Context;
import tech.pegasys.web3signer.core.routes.Web3SignerRoute;
import tech.pegasys.web3signer.core.service.http.handlers.commitboost.CommitBoostRequestSignatureHandler;
import tech.pegasys.web3signer.signing.ArtifactSignerProvider;
import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider;

import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonObject;

public class CommitBoostRequestSignatureRoute implements Web3SignerRoute {
private static final String PATH = "/signer/v1/request_signature";
private final Context context;
private final Spec eth2Spec;
private final ArtifactSignerProvider artifactSignerProvider;

public CommitBoostRequestSignatureRoute(final Context context, final Spec eth2Spec) {
this.context = context;
this.eth2Spec = eth2Spec;

// there should be only one DefaultArtifactSignerProvider in eth2 mode
artifactSignerProvider =
context.getArtifactSignerProviders().stream()
.filter(p -> p instanceof DefaultArtifactSignerProvider)
.findFirst()
.orElseThrow(
() ->
new IllegalStateException(
"No DefaultArtifactSignerProvider found in Context for eth2 mode"));
}

@Override
public void register() {
context
.getRouter()
.route(HttpMethod.POST, PATH)
.blockingHandler(
new CommitBoostRequestSignatureHandler(artifactSignerProvider, eth2Spec), false)
.failureHandler(context.getErrorHandler())
.failureHandler(
ctx -> {
final int statusCode = ctx.statusCode();
if (statusCode == HTTP_BAD_REQUEST) {
ctx.response()
.setStatusCode(statusCode)
.end(
new JsonObject()
.put("code", statusCode)
.put("message", "Bad Request")
.encode());
} else if (statusCode == HTTP_NOT_FOUND) {
ctx.response()
.setStatusCode(statusCode)
.end(
new JsonObject()
.put("code", statusCode)
.put("message", "Unknown pubkey")
.encode());
} else if (statusCode == HTTP_INTERNAL_ERROR) {
ctx.response()
.setStatusCode(statusCode)
.end(
new JsonObject()
.put("code", statusCode)
.put("message", "Internal Error")
.encode());
} else {
ctx.next(); // go to global failure handler
}
});
}
}
Loading

0 comments on commit f8e60be

Please sign in to comment.