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

feat: Commit Boost API - Request Signature #1045

Merged
merged 1 commit into from
Nov 28, 2024
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
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
Loading