diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fab48afe..7669117f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ ### Features Added - 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) -- Commit boost API - Get Public Keys. [#1031](https://github.com/Consensys/web3signer/pull/1031) +- Commit Boost API - Get Public Keys. [#1031](https://github.com/Consensys/web3signer/pull/1031) +- Commit Boost API - Generate Proxy Keys ### Bugs fixed - Override protobuf-java to 3.25.5 which is a transitive dependency from google-cloud-secretmanager. It fixes CVE-2024-7254. diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/ValidBLSSignatureMatcher.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/ValidBLSSignatureMatcher.java new file mode 100644 index 000000000..4a9b7df77 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/ValidBLSSignatureMatcher.java @@ -0,0 +1,44 @@ +/* + * 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.dsl.utils; + +import tech.pegasys.teku.bls.BLS; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSignature; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hamcrest.TypeSafeMatcher; + +/** A Hamcrest matcher that verifies a BLS scheme signature is valid. */ +public class ValidBLSSignatureMatcher extends TypeSafeMatcher { + private final BLSPublicKey blsPublicKey; + private final Bytes32 signingRoot; + + public ValidBLSSignatureMatcher(final String blsPubHex, final Bytes32 signingRoot) { + this.blsPublicKey = BLSPublicKey.fromHexString(blsPubHex); + this.signingRoot = signingRoot; + } + + @Override + protected boolean matchesSafely(final String signature) { + final BLSSignature blsSignature = + BLSSignature.fromBytesCompressed(Bytes.fromHexString(signature.replace("\"", ""))); + return BLS.verify(blsPublicKey, signingRoot, blsSignature); + } + + @Override + public void describeTo(final org.hamcrest.Description description) { + description.appendText("a valid BLS signature"); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/ValidK256SignatureMatcher.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/ValidK256SignatureMatcher.java new file mode 100644 index 000000000..bd8c20650 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/ValidK256SignatureMatcher.java @@ -0,0 +1,44 @@ +/* + * 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.dsl.utils; + +import tech.pegasys.web3signer.K256TestUtil; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.math.ec.ECPoint; +import org.hamcrest.TypeSafeMatcher; + +/** A Hamcrest matcher that verifies a K256 scheme signature (R+S format) is valid. */ +public class ValidK256SignatureMatcher extends TypeSafeMatcher { + + private final ECPoint ecPoint; + private final Bytes32 signingRoot; + + public ValidK256SignatureMatcher(final String ecPubKeyHex, final Bytes32 signingRoot) { + this.ecPoint = EthPublicKeyUtils.bytesToBCECPoint(Bytes.fromHexString(ecPubKeyHex)); + this.signingRoot = signingRoot; + } + + @Override + protected boolean matchesSafely(final String signature) { + final byte[] sig = Bytes.fromHexString(signature.replace("\"", "")).toArray(); + return K256TestUtil.verifySignature(ecPoint, signingRoot.toArray(), sig); + } + + @Override + public void describeTo(final org.hamcrest.Description description) { + description.appendText("a valid K256 signature"); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/AcceptanceTestBase.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/AcceptanceTestBase.java index d28165845..3d4cc2f27 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/AcceptanceTestBase.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/AcceptanceTestBase.java @@ -18,12 +18,13 @@ import tech.pegasys.web3signer.dsl.signer.SignerConfiguration; import tech.pegasys.web3signer.signing.KeyType; +import java.security.SecureRandom; import java.util.Set; import org.junit.jupiter.api.AfterEach; public class AcceptanceTestBase { - + protected static final SecureRandom SECURE_RANDOM = new SecureRandom(); protected Signer signer; public static final Long DEFAULT_CHAIN_ID = 1337L; diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java index 5f34348d8..30bffec81 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java @@ -27,7 +27,6 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,8 +44,7 @@ // See https://commit-boost.github.io/commit-boost-client/api/ for Commit Boost spec public class CommitBoostAcceptanceTest extends AcceptanceTestBase { - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - private static final String KEYSTORE_PASSWORD = "password"; + static final String KEYSTORE_PASSWORD = "password"; private List consensusBlsKeys = randomBLSKeyPairs(2); private Map> proxyBLSKeysMap = new HashMap<>(); @@ -73,7 +71,7 @@ void setup() throws Exception { } // commit boost proxy keys password file - final Path commitBoostPasswordFile = createCommitBoostPasswordFile(); + final Path commitBoostPasswordFile = createCommitBoostPasswordFile(commitBoostPasswordDir); // start web3signer with keystores and commit boost parameters final KeystoresParameters keystoresParameters = @@ -139,7 +137,7 @@ private List getProxyBLSPubKeys(final String consensusKeyHex) { .toList(); } - private Path createCommitBoostPasswordFile() { + static Path createCommitBoostPasswordFile(final Path commitBoostPasswordDir) { try { return Files.writeString( commitBoostPasswordDir.resolve("cb_password.txt"), KEYSTORE_PASSWORD); @@ -208,7 +206,7 @@ private List createProxyBLSKeys(final BLSKeyPair consensusKeyPair) { * @param count number of key pairs to generate * @return list of ECKeyPairs */ - private static List randomECKeyPairs(final int count) { + static List randomECKeyPairs(final int count) { return Stream.generate( () -> { try { @@ -227,7 +225,7 @@ private static List randomECKeyPairs(final int count) { * @param count number of key pairs to generate * @return list of BLSKeyPairs */ - private static List randomBLSKeyPairs(final int count) { + static List randomBLSKeyPairs(final int count) { return Stream.generate(() -> BLSKeyPair.random(SECURE_RANDOM)).limit(count).toList(); } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostGenerateProxyKeyAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostGenerateProxyKeyAcceptanceTest.java new file mode 100644 index 000000000..23ded2c67 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostGenerateProxyKeyAcceptanceTest.java @@ -0,0 +1,128 @@ +/* + * 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 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 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.ProxyDelegation; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyKeySignatureScheme; +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.signing.config.KeystoresParameters; +import tech.pegasys.web3signer.tests.AcceptanceTestBase; + +import java.nio.file.Path; +import java.util.List; + +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.Test; +import org.junit.jupiter.api.io.TempDir; + +public class CommitBoostGenerateProxyKeyAcceptanceTest extends AcceptanceTestBase { + private static final SigningRootGenerator SIGNING_ROOT_GENERATOR = + new SigningRootGenerator(getSpec(Eth2Network.MAINNET)); + private final List consensusBlsKeys = randomBLSKeyPairs(1); + + @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); + } + + // 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 commitBoostParameters = + Pair.of(commitBoostKeystoresPath, commitBoostPasswordFile); + + final SignerConfigurationBuilder configBuilder = + new SignerConfigurationBuilder() + .withMode("eth2") + .withNetwork("mainnet") + .withKeystoresParameters(keystoresParameters) + .withCommitBoostParameters(commitBoostParameters); + + startSigner(configBuilder.build()); + } + + @Test + void generateCommitBoostProxyKeys() { + for (ProxyKeySignatureScheme scheme : ProxyKeySignatureScheme.values()) { + final Response response = + signer.callCommitBoostGenerateProxyKey( + consensusBlsKeys.get(0).getPublicKey().toHexString(), scheme.name()); + + // verify we got new proxy public key and a valid bls signature + response + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("message.delegator", equalTo(consensusBlsKeys.get(0).getPublicKey().toHexString())) + .body( + "signature", + resp -> { + final String messageProxy = resp.path("message.proxy"); + final String delegator = resp.path("message.delegator"); + final Bytes32 hashTreeRoot = + new ProxyDelegation(delegator, messageProxy) + .toMerkleizable(scheme) + .hashTreeRoot(); + final Bytes32 signingRoot = SIGNING_ROOT_GENERATOR.computeSigningRoot(hashTreeRoot); + return new ValidBLSSignatureMatcher(delegator, signingRoot); + }); + } + + // verify we can get the public keys containing the generated proxy keys + final Response pubKeyResponse = signer.callCommitBoostGetPubKeys(); + + pubKeyResponse + .then() + .log() + .body() + .statusCode(200) + .contentType(ContentType.JSON) + .body("keys[0].consensus", equalTo(consensusBlsKeys.get(0).getPublicKey().toHexString())) + .body("keys[0].proxy_bls", hasSize(1)) + .body("keys[0].proxy_ecdsa", hasSize(1)); + } + + private static Spec getSpec(Eth2Network eth2Network) { + final Eth2NetworkConfiguration.Builder builder = Eth2NetworkConfiguration.builder(); + return builder.applyNetworkDefaults(eth2Network).build().getSpec(); + } +} diff --git a/core/build.gradle b/core/build.gradle index edba2f10f..48cf053ba 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -50,6 +50,7 @@ dependencies { runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl' testImplementation (testFixtures(project(":signing"))) + testImplementation 'tech.pegasys.teku.internal:networks' testImplementation 'io.vertx:vertx-junit5' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java b/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java index a856351b2..3137d66f3 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java @@ -25,6 +25,7 @@ import tech.pegasys.web3signer.core.config.BaseConfig; import tech.pegasys.web3signer.core.routes.PublicKeysListRoute; 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.Eth2SignExtensionRoute; import tech.pegasys.web3signer.core.routes.eth2.Eth2SignRoute; @@ -143,6 +144,7 @@ public void populateRouter(final Context context) { } if (commitBoostApiParameters.isEnabled()) { new CommitBoostPublicKeysRoute(context).register(); + new CommitBoostGenerateProxyKeyRoute(context, commitBoostApiParameters, eth2Spec).register(); } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostGenerateProxyKeyRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostGenerateProxyKeyRoute.java new file mode 100644 index 000000000..423382dcd --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostGenerateProxyKeyRoute.java @@ -0,0 +1,89 @@ +/* + * 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 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.CommitBoostGenerateProxyKeyHandler; +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; +import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; +import tech.pegasys.web3signer.signing.config.KeystoresParameters; + +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; + +public class CommitBoostGenerateProxyKeyRoute implements Web3SignerRoute { + private static final String PATH = "/signer/v1/generate_proxy_key"; + private final Context context; + private final ArtifactSignerProvider artifactSignerProvider; + private final KeystoresParameters commitBoostParameters; + private final Spec eth2Spec; + + public CommitBoostGenerateProxyKeyRoute( + final Context context, final KeystoresParameters commitBoostParameters, final Spec eth2Spec) { + this.context = context; + this.commitBoostParameters = commitBoostParameters; + this.eth2Spec = eth2Spec; + + // there should be only one DefaultArtifactSignerProvider in eth2 mode + artifactSignerProvider = + context.getArtifactSignerProviders().stream() + .filter(p -> p instanceof DefaultArtifactSignerProvider) + .findFirst() + .orElseThrow(); + } + + @Override + public void register() { + context + .getRouter() + .route(HttpMethod.POST, PATH) + .blockingHandler( + new CommitBoostGenerateProxyKeyHandler( + artifactSignerProvider, commitBoostParameters, eth2Spec), + false) + .failureHandler(context.getErrorHandler()) + .failureHandler( + ctx -> { + final int statusCode = ctx.statusCode(); + if (statusCode == 400) { + ctx.response() + .setStatusCode(statusCode) + .end( + new JsonObject() + .put("code", statusCode) + .put("message", "Bad Request") + .encode()); + } else if (statusCode == 404) { + ctx.response() + .setStatusCode(statusCode) + .end( + new JsonObject() + .put("code", statusCode) + .put("message", "Identifier not found.") + .encode()); + } else if (statusCode == 500) { + ctx.response() + .setStatusCode(statusCode) + .end( + new JsonObject() + .put("code", statusCode) + .put("message", "Internal Server Error") + .encode()); + } else { + ctx.next(); // go to global failure handler + } + }); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostGenerateProxyKeyHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostGenerateProxyKeyHandler.java new file mode 100644 index 000000000..afd036942 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostGenerateProxyKeyHandler.java @@ -0,0 +1,113 @@ +/* + * 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.service.http.handlers.commitboost; + +import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; +import static tech.pegasys.web3signer.core.service.http.handlers.ContentTypes.JSON_UTF_8; +import static tech.pegasys.web3signer.signing.util.IdentifierUtils.normaliseIdentifier; + +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.web3signer.core.service.http.SigningObjectMapperFactory; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.CommitBoostSignRequestType; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.GenerateProxyKeyBody; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyDelegation; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.SignedProxyDelegation; +import tech.pegasys.web3signer.signing.ArtifactSigner; +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; +import tech.pegasys.web3signer.signing.config.KeystoresParameters; + +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.apache.tuweni.bytes.Bytes32; + +public class CommitBoostGenerateProxyKeyHandler implements Handler { + private static final ObjectMapper JSON_MAPPER = SigningObjectMapperFactory.createObjectMapper(); + private static final int NOT_FOUND = 404; + private static final int BAD_REQUEST = 400; + private static final int INTERNAL_ERROR = 500; + + private final CommitBoostSignerProvider commitBoostSignerProvider; + private final ProxyKeysGenerator proxyKeyGenerator; + private final SigningRootGenerator signingRootGenerator; + + public CommitBoostGenerateProxyKeyHandler( + final ArtifactSignerProvider artifactSignerProvider, + final KeystoresParameters commitBoostParameters, + final Spec eth2Spec) { + commitBoostSignerProvider = new CommitBoostSignerProvider(artifactSignerProvider); + proxyKeyGenerator = new ProxyKeysGenerator(commitBoostParameters); + signingRootGenerator = new SigningRootGenerator(eth2Spec); + } + + @Override + public void handle(final RoutingContext context) { + final String body = context.body().asString(); + + // read and validate incoming json body + final GenerateProxyKeyBody proxyKeyBody; + try { + proxyKeyBody = JSON_MAPPER.readValue(body, GenerateProxyKeyBody.class); + } catch (final JsonProcessingException | IllegalArgumentException e) { + context.fail(BAD_REQUEST); + return; + } + + // Check for identifier, if not exist, fail with 404 + final String consensusPubKey = normaliseIdentifier(proxyKeyBody.blsPublicKey()); + final boolean signerAvailable = + commitBoostSignerProvider.isSignerAvailable( + consensusPubKey, CommitBoostSignRequestType.CONSENSUS); + if (!signerAvailable) { + context.fail(NOT_FOUND); + return; + } + + try { + // Generate actual proxy key and encrypted keystore based on signature scheme + final ArtifactSigner proxyArtifactSigner = + switch (proxyKeyBody.scheme()) { + case BLS -> proxyKeyGenerator.generateBLSProxyKey(consensusPubKey); + case ECDSA -> proxyKeyGenerator.generateECProxyKey(consensusPubKey); + }; + + // Add generated proxy ArtifactSigner to ArtifactSignerProvider + commitBoostSignerProvider.addProxySigner(proxyArtifactSigner, consensusPubKey); + + final ProxyDelegation proxyDelegation = + new ProxyDelegation(consensusPubKey, proxyArtifactSigner.getIdentifier()); + final Bytes32 signingRoot = + signingRootGenerator.computeSigningRoot( + proxyDelegation.toMerkleizable(proxyKeyBody.scheme()).hashTreeRoot()); + final Optional optionalSig = + commitBoostSignerProvider.sign( + consensusPubKey, CommitBoostSignRequestType.CONSENSUS, signingRoot); + if (optionalSig.isEmpty()) { + context.fail(NOT_FOUND); + return; + } + + final SignedProxyDelegation signedProxyDelegation = + new SignedProxyDelegation(proxyDelegation, optionalSig.get()); + + // Encode and send response + final String jsonEncoded = JSON_MAPPER.writeValueAsString(signedProxyDelegation); + context.response().putHeader(CONTENT_TYPE, JSON_UTF_8).end(jsonEncoded); + } catch (final Exception e) { + context.fail(INTERNAL_ERROR, e); + } + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostSignerProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostSignerProvider.java new file mode 100644 index 000000000..76033bdd0 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostSignerProvider.java @@ -0,0 +1,81 @@ +/* + * 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.service.http.handlers.commitboost; + +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.CommitBoostSignRequestType; +import tech.pegasys.web3signer.signing.ArtifactSigner; +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; + +/** + * This class wraps the {@link ArtifactSignerProvider} and provides a way to check if a signer is + * available, consensus or proxy,and to sign a message. + */ +public class CommitBoostSignerProvider { + private final ArtifactSignerProvider artifactSignerProvider; + + /** + * Constructor for the CommitBoostSignerProvider + * + * @param artifactSignerProvider The {@link ArtifactSignerProvider} to use for signing + */ + public CommitBoostSignerProvider(final ArtifactSignerProvider artifactSignerProvider) { + this.artifactSignerProvider = artifactSignerProvider; + } + + /** + * Check if a signer is available for the given identifier and type + * + * @param identifier The identifier to check + * @param type The type of signer to check + * @return true if a signer is available, false otherwise + */ + public boolean isSignerAvailable(final String identifier, final CommitBoostSignRequestType type) { + return switch (type) { + case CONSENSUS -> artifactSignerProvider.availableIdentifiers().contains(identifier); + case PROXY_BLS, PROXY_ECDSA -> artifactSignerProvider.getProxySigner(identifier).isPresent(); + }; + } + + /** + * Sign a message with the given identifier and type + * + * @param identifier The identifier to sign with + * @param type The type of signer to use + * @param signingRoot The root to sign + * @return An optional string of the signature in hex format. Empty if no signer available for + * given identifier + */ + public Optional sign( + final String identifier, final CommitBoostSignRequestType type, final Bytes32 signingRoot) { + final Optional optionalArtifactSigner = + type == CommitBoostSignRequestType.CONSENSUS + ? artifactSignerProvider.getSigner(identifier) + : artifactSignerProvider.getProxySigner(identifier); + + return optionalArtifactSigner.map(signer -> signer.sign(signingRoot).asHex()); + } + + /** + * Add a proxy signer to the consensus signer + * + * @param proxySigner The proxy signer to add + * @param consensusPubKey The consensus public key to associate with the proxy signer + */ + public void addProxySigner(final ArtifactSigner proxySigner, final String consensusPubKey) { + artifactSignerProvider.addProxySigner(proxySigner, consensusPubKey); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeysGenerator.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeysGenerator.java new file mode 100644 index 000000000..8d91d84f3 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeysGenerator.java @@ -0,0 +1,154 @@ +/* + * 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.service.http.handlers.commitboost; + +import static tech.pegasys.teku.bls.keystore.model.Pbkdf2PseudoRandomFunction.HMAC_SHA256; + +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.teku.bls.keystore.KeyStore; +import tech.pegasys.teku.bls.keystore.KeyStoreLoader; +import tech.pegasys.teku.bls.keystore.model.Cipher; +import tech.pegasys.teku.bls.keystore.model.CipherFunction; +import tech.pegasys.teku.bls.keystore.model.KdfParam; +import tech.pegasys.teku.bls.keystore.model.KeyStoreData; +import tech.pegasys.teku.bls.keystore.model.Pbkdf2Param; +import tech.pegasys.web3signer.core.service.http.SigningObjectMapperFactory; +import tech.pegasys.web3signer.signing.ArtifactSigner; +import tech.pegasys.web3signer.signing.BlsArtifactSigner; +import tech.pegasys.web3signer.signing.K256ArtifactSigner; +import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.config.KeystoresParameters; +import tech.pegasys.web3signer.signing.config.metadata.SignerOrigin; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.signing.util.SecureRandomProvider; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes48; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Wallet; +import org.web3j.crypto.WalletFile; +import org.web3j.crypto.exception.CipherException; + +/** Generate BLS and SECP256K1 proxy keys for Commit Boost API */ +public class ProxyKeysGenerator { + private static final Logger LOG = LogManager.getLogger(); + private static final ObjectMapper JSON_MAPPER = SigningObjectMapperFactory.createObjectMapper(); + private final KeystoresParameters commitBoostParameters; + + public ProxyKeysGenerator(final KeystoresParameters commitBoostParameters) { + this.commitBoostParameters = commitBoostParameters; + } + + /** + * Generate a random K256 proxy key and encrypted keystore for the given consensus public key + * + * @param consensusPubKey the public key of the consensus signer for which the proxy key is being + * generated + * @return an instance of K256ArtifactSigner representing the generated proxy key + */ + public ArtifactSigner generateECProxyKey(final String consensusPubKey) { + final ECKeyPair ecKeyPair = ECKeyPair.create(EthPublicKeyUtils.generateK256KeyPair()); + final Path ecWalletFile = createECWalletFile(ecKeyPair, consensusPubKey); + LOG.debug( + "Created proxy EC wallet file {} for consensus key: {}", ecWalletFile, consensusPubKey); + return new K256ArtifactSigner(ecKeyPair); + } + + /** + * Generate a random BLS proxy key and encrypted keystore for the given consensus public key + * + * @param consensusPubKey the public key of the consensus signer for which the proxy key is being + * generated + * @return as instance of BlsArtifactSigner representing the generated proxy key + */ + public ArtifactSigner generateBLSProxyKey(final String consensusPubKey) { + final BLSKeyPair blsKeyPair = BLSKeyPair.random(SecureRandomProvider.getSecureRandom()); + final Path blsKeystoreFile = createBLSKeystoreFile(blsKeyPair, consensusPubKey); + LOG.debug( + "Created proxy BLS keystore file {} for consensus key: {}", + blsKeystoreFile, + consensusPubKey); + return new BlsArtifactSigner(blsKeyPair, SignerOrigin.FILE_KEYSTORE); + } + + private Path createBLSKeystoreFile(final BLSKeyPair keyPair, final String consensusPubKey) { + final Bytes salt = Bytes.random(32, SecureRandomProvider.getSecureRandom()); + final Bytes iv = Bytes.random(16, SecureRandomProvider.getSecureRandom()); + final int counter = 65536; // 2^16 + final KdfParam kdfParam = new Pbkdf2Param(32, counter, HMAC_SHA256, salt); + final Cipher cipher = new Cipher(CipherFunction.AES_128_CTR, iv); + final Bytes48 publicKey = keyPair.getPublicKey().toBytesCompressed(); + final String password = readFile(commitBoostParameters.getKeystoresPasswordFile()); + final KeyStoreData keyStoreData = + KeyStore.encrypt( + keyPair.getSecretKey().toBytes(), publicKey, password, "", kdfParam, cipher); + try { + final Path keystoreDir = + createSubDirectories( + commitBoostParameters.getKeystoresPath(), consensusPubKey, KeyType.BLS); + final Path keystoreFile = keystoreDir.resolve(publicKey + ".json"); + KeyStoreLoader.saveToFile(keystoreFile, keyStoreData); + return keystoreFile; + } catch (final IOException e) { + throw new UncheckedIOException("Unable to create keystore file", e); + } + } + + private Path createECWalletFile(final ECKeyPair ecKeyPair, final String consensusPubKey) { + final String password = readFile(commitBoostParameters.getKeystoresPasswordFile()); + final Path keystoreDir = + createSubDirectories( + commitBoostParameters.getKeystoresPath(), consensusPubKey, KeyType.SECP256K1); + + final String compressedPubHex = + EthPublicKeyUtils.toHexStringCompressed( + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey())); + + final Path keystoreFile = keystoreDir.resolve(compressedPubHex + ".json"); + try { + final WalletFile walletFile = Wallet.createStandard(password, ecKeyPair); + JSON_MAPPER.writeValue(keystoreFile.toFile(), walletFile); + return keystoreFile; + } catch (final CipherException | IOException e) { + throw new RuntimeException(e); + } + } + + private static String readFile(final Path file) { + try { + return Files.readString(file, StandardCharsets.UTF_8); + } catch (final IOException e) { + throw new UncheckedIOException("Unable to read file", e); + } + } + + private static Path createSubDirectories( + final Path parentDirectory, final String directoryName, final KeyType keyType) { + final Path subDirectory = parentDirectory.resolve(directoryName).resolve(keyType.name()); + try { + Files.createDirectories(subDirectory); + } catch (final IOException e) { + throw new UncheckedIOException("Unable to create directory: " + subDirectory, e); + } + return subDirectory; + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGenerator.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGenerator.java new file mode 100644 index 000000000..26eb3fea1 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGenerator.java @@ -0,0 +1,60 @@ +/* + * 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.service.http.handlers.commitboost; + +import tech.pegasys.teku.infrastructure.bytes.Bytes4; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.state.ForkData; +import tech.pegasys.teku.spec.datastructures.state.SigningData; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * Generates the signing root for a given object root using the commit boost domain. + * + *

The commit boost domain is computed using the genesis validators root and the genesis fork + * version. + */ +public class SigningRootGenerator { + private static final Bytes4 COMMIT_BOOST_DOMAIN = Bytes4.fromHexString("0x6d6d6f43"); + private static final Bytes32 GENESIS_VALIDATORS_ROOT = Bytes32.ZERO; + + private final Bytes32 domain; + + public SigningRootGenerator(final Spec eth2Spec) { + final Bytes4 genesisForkVersion = eth2Spec.getGenesisSpec().getConfig().getGenesisForkVersion(); + final Bytes32 forkHashTreeRoot = + new ForkData(genesisForkVersion, GENESIS_VALIDATORS_ROOT).hashTreeRoot(); + domain = + Bytes32.wrap( + Bytes.concatenate( + COMMIT_BOOST_DOMAIN.getWrappedBytes(), forkHashTreeRoot.slice(0, 28))); + } + + /** + * Computes the signing root for a given object root using commit boost domain. + * + * @param objectRoot the object root to compute the signing root for + * @return the signing data object root + */ + public Bytes32 computeSigningRoot(final Bytes32 objectRoot) { + return new SigningData(objectRoot, domain).hashTreeRoot(); + } + + @VisibleForTesting + Bytes32 getDomain() { + return domain; + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/BLSProxyDelegation.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/BLSProxyDelegation.java new file mode 100644 index 000000000..b0c65562f --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/BLSProxyDelegation.java @@ -0,0 +1,37 @@ +/* + * 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.service.http.handlers.commitboost.datastructure; + +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.ssz.containers.Container2; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.spec.datastructures.type.SszPublicKey; + +public class BLSProxyDelegation extends Container2 { + + public BLSProxyDelegation( + final BLSProxyDelegationSchema schema, + final BLSPublicKey delegator, + final BLSPublicKey proxy) { + super(schema, new SszPublicKey(delegator), new SszPublicKey(proxy)); + } + + BLSProxyDelegation(final BLSProxyDelegationSchema type, final TreeNode backingNode) { + super(type, backingNode); + } + + @Override + public BLSProxyDelegationSchema getSchema() { + return (BLSProxyDelegationSchema) super.getSchema(); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/BLSProxyDelegationSchema.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/BLSProxyDelegationSchema.java new file mode 100644 index 000000000..2a32777db --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/BLSProxyDelegationSchema.java @@ -0,0 +1,41 @@ +/* + * 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.service.http.handlers.commitboost.datastructure; + +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema2; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.spec.datastructures.type.SszPublicKey; +import tech.pegasys.teku.spec.datastructures.type.SszPublicKeySchema; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyDelegation; + +public class BLSProxyDelegationSchema + extends ContainerSchema2 { + public BLSProxyDelegationSchema() { + super( + "BLSProxyDelegationSchema", + namedSchema("delegator", SszPublicKeySchema.INSTANCE), + namedSchema("proxy", SszPublicKeySchema.INSTANCE)); + } + + public BLSProxyDelegation create(final ProxyDelegation proxyDelegation) { + final BLSPublicKey delegator = BLSPublicKey.fromHexString(proxyDelegation.blsPublicKey()); + final BLSPublicKey proxy = BLSPublicKey.fromHexString(proxyDelegation.proxyPublicKey()); + return new BLSProxyDelegation(this, delegator, proxy); + } + + @Override + public BLSProxyDelegation createFromBackingNode(TreeNode treeNode) { + return new BLSProxyDelegation(this, treeNode); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SECPProxyDelegation.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SECPProxyDelegation.java new file mode 100644 index 000000000..199326c27 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SECPProxyDelegation.java @@ -0,0 +1,40 @@ +/* + * 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.service.http.handlers.commitboost.datastructure; + +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.ssz.containers.Container2; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.spec.datastructures.type.SszPublicKey; + +import java.security.interfaces.ECPublicKey; + +public class SECPProxyDelegation + extends Container2 { + + public SECPProxyDelegation( + final SECPProxyDelegationSchema schema, + final BLSPublicKey delegator, + final ECPublicKey proxy) { + super(schema, new SszPublicKey(delegator), new SszSECPPublicKey(proxy)); + } + + SECPProxyDelegation(final SECPProxyDelegationSchema schema, final TreeNode backingNode) { + super(schema, backingNode); + } + + @Override + public SECPProxyDelegationSchema getSchema() { + return (SECPProxyDelegationSchema) super.getSchema(); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SECPProxyDelegationSchema.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SECPProxyDelegationSchema.java new file mode 100644 index 000000000..4a7713213 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SECPProxyDelegationSchema.java @@ -0,0 +1,47 @@ +/* + * 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.service.http.handlers.commitboost.datastructure; + +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema2; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.spec.datastructures.type.SszPublicKey; +import tech.pegasys.teku.spec.datastructures.type.SszPublicKeySchema; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyDelegation; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; + +import java.security.interfaces.ECPublicKey; + +import org.apache.tuweni.bytes.Bytes; + +public class SECPProxyDelegationSchema + extends ContainerSchema2 { + public SECPProxyDelegationSchema() { + super( + "SECPProxyDelegationSchema", + namedSchema("delegator", SszPublicKeySchema.INSTANCE), + namedSchema("proxy", SszSECPPublicKeySchema.INSTANCE)); + } + + public SECPProxyDelegation create(final ProxyDelegation proxyDelegation) { + final BLSPublicKey delegator = BLSPublicKey.fromHexString(proxyDelegation.blsPublicKey()); + final ECPublicKey proxy = + EthPublicKeyUtils.bytesToECPublicKey(Bytes.fromHexString(proxyDelegation.proxyPublicKey())); + return new SECPProxyDelegation(this, delegator, proxy); + } + + @Override + public SECPProxyDelegation createFromBackingNode(final TreeNode treeNode) { + return new SECPProxyDelegation(this, treeNode); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKey.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKey.java new file mode 100644 index 000000000..e3bca25b4 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKey.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 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.http.handlers.commitboost.datastructure; + +import tech.pegasys.teku.infrastructure.ssz.collections.impl.SszByteVectorImpl; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; + +import java.security.interfaces.ECPublicKey; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +public class SszSECPPublicKey extends SszByteVectorImpl { + + private final Supplier publicKey; + + public SszSECPPublicKey(final ECPublicKey publicKey) { + super( + SszSECPPublicKeySchema.INSTANCE, + Bytes.fromHexString(EthPublicKeyUtils.toHexStringCompressed(publicKey))); + this.publicKey = () -> publicKey; + } + + SszSECPPublicKey(final TreeNode backingNode) { + super(SszSECPPublicKeySchema.INSTANCE, backingNode); + this.publicKey = Suppliers.memoize(() -> EthPublicKeyUtils.bytesToECPublicKey(getBytes())); + } + + public ECPublicKey getECPublicKey() { + return publicKey.get(); + } + + @Override + public SszSECPPublicKeySchema getSchema() { + return (SszSECPPublicKeySchema) super.getSchema(); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKeySchema.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKeySchema.java new file mode 100644 index 000000000..2aa30f34c --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKeySchema.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 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.http.handlers.commitboost.datastructure; + +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.ssz.schema.SszPrimitiveSchemas; +import tech.pegasys.teku.infrastructure.ssz.schema.collections.impl.SszByteVectorSchemaImpl; +import tech.pegasys.teku.infrastructure.ssz.schema.json.SszPrimitiveTypeDefinitions; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; + +public class SszSECPPublicKeySchema extends SszByteVectorSchemaImpl { + private static final int SECP_COMPRESSED_PUBLIC_KEY_SIZE = 33; + + public static final SszSECPPublicKeySchema INSTANCE = new SszSECPPublicKeySchema(); + + private SszSECPPublicKeySchema() { + super(SszPrimitiveSchemas.BYTE_SCHEMA, SECP_COMPRESSED_PUBLIC_KEY_SIZE); + } + + @Override + protected DeserializableTypeDefinition createTypeDefinition() { + return SszPrimitiveTypeDefinitions.sszSerializedType(this, "Bytes33 hexadecimal"); + } + + @Override + public SszSECPPublicKey createFromBackingNode(final TreeNode node) { + return new SszSECPPublicKey(node); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/CommitBoostSignRequestType.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/CommitBoostSignRequestType.java new file mode 100644 index 000000000..75083843a --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/CommitBoostSignRequestType.java @@ -0,0 +1,22 @@ +/* + * 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.service.http.handlers.commitboost.json; + +/** + * Enum to represent the different types of requests that can be made to the CommitBoost service. + */ +public enum CommitBoostSignRequestType { + CONSENSUS, + PROXY_BLS, + PROXY_ECDSA +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/GenerateProxyKeyBody.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/GenerateProxyKeyBody.java new file mode 100644 index 000000000..db4c30470 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/GenerateProxyKeyBody.java @@ -0,0 +1,19 @@ +/* + * 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.service.http.handlers.commitboost.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GenerateProxyKeyBody( + @JsonProperty(value = "pubkey", required = true) String blsPublicKey, + @JsonProperty(value = "scheme", required = true) ProxyKeySignatureScheme scheme) {} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyDelegation.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyDelegation.java new file mode 100644 index 000000000..17deb1c60 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyDelegation.java @@ -0,0 +1,30 @@ +/* + * 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.service.http.handlers.commitboost.json; + +import tech.pegasys.teku.infrastructure.ssz.Merkleizable; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.datastructure.BLSProxyDelegationSchema; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.datastructure.SECPProxyDelegationSchema; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ProxyDelegation( + @JsonProperty(value = "delegator", required = true) String blsPublicKey, + @JsonProperty(value = "proxy", required = true) String proxyPublicKey) { + + public Merkleizable toMerkleizable(final ProxyKeySignatureScheme scheme) { + return scheme == ProxyKeySignatureScheme.BLS + ? new BLSProxyDelegationSchema().create(this) + : new SECPProxyDelegationSchema().create(this); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyKeySignatureScheme.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyKeySignatureScheme.java new file mode 100644 index 000000000..3f33902d9 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyKeySignatureScheme.java @@ -0,0 +1,18 @@ +/* + * 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.service.http.handlers.commitboost.json; + +public enum ProxyKeySignatureScheme { + BLS, + ECDSA +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/SignedProxyDelegation.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/SignedProxyDelegation.java new file mode 100644 index 000000000..478d41efc --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/SignedProxyDelegation.java @@ -0,0 +1,19 @@ +/* + * 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.service.http.handlers.commitboost.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record SignedProxyDelegation( + @JsonProperty(value = "message") ProxyDelegation proxyDelegation, + @JsonProperty(value = "signature") String signature) {} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeysGeneratorTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeysGeneratorTest.java new file mode 100644 index 000000000..565ac0079 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeysGeneratorTest.java @@ -0,0 +1,63 @@ +/* + * 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.service.http.handlers.commitboost; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.signing.ArtifactSigner; +import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.config.KeystoresParameters; +import tech.pegasys.web3signer.signing.config.TestCommitBoostParameters; + +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ProxyKeysGeneratorTest { + @TempDir private Path commitBoostKeystoresPath; + + @TempDir private Path commitBoostPasswordDir; + + private ProxyKeysGenerator proxyKeysGenerator; + + @BeforeEach + void init() { + final KeystoresParameters commitBoostParameters = + new TestCommitBoostParameters(commitBoostKeystoresPath, commitBoostPasswordDir); + proxyKeysGenerator = new ProxyKeysGenerator(commitBoostParameters); + } + + @Test + void generateBLSProxyKey() { + final ArtifactSigner artifactSigner = proxyKeysGenerator.generateBLSProxyKey("consensuspubkey"); + assertThat( + commitBoostKeystoresPath + .resolve("consensuspubkey") + .resolve(KeyType.BLS.name()) + .resolve(artifactSigner.getIdentifier() + ".json")) + .exists(); + } + + @Test + void generateECProxyKey() { + final ArtifactSigner artifactSigner = proxyKeysGenerator.generateECProxyKey("consensuspubkey"); + assertThat( + commitBoostKeystoresPath + .resolve("consensuspubkey") + .resolve(KeyType.SECP256K1.name()) + .resolve(artifactSigner.getIdentifier() + ".json")) + .exists(); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGeneratorTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGeneratorTest.java new file mode 100644 index 000000000..e8334f5c2 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGeneratorTest.java @@ -0,0 +1,190 @@ +/* + * 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.service.http.handlers.commitboost; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSecretKey; +import tech.pegasys.teku.networks.Eth2NetworkConfiguration; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyDelegation; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyKeySignatureScheme; +import tech.pegasys.web3signer.signing.BlsArtifactSignature; +import tech.pegasys.web3signer.signing.BlsArtifactSigner; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; + +import java.security.interfaces.ECPublicKey; +import java.util.HashMap; +import java.util.Map; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.web3j.crypto.ECKeyPair; +import org.web3j.utils.Numeric; + +class SigningRootGeneratorTest { + private static final Bytes32 GVR = Bytes32.ZERO; + private static final Map DOMAIN_MAP = new HashMap<>(); + private static final Map BLS_PROXY_ROOT_MAP = new HashMap<>(); + private static final Map SECP_PROXY_ROOT_MAP = new HashMap<>(); + + private static final Map BLS_PROXY_MESSAGE_SIGNATURE_MAP = new HashMap<>(); + private static final Map SECP_PROXY_MESSAGE_SIGNATURE_MAP = new HashMap<>(); + + private static final String BLS_DELEGATOR_PRIVATE_KEY = + "3ee2224386c82ffea477e2adf28a2929f5c349165a4196158c7f3a2ecca40f35"; + private static final String BLS_PROXY_PRIVATE_KEY = + "32ae313afff2daa2ef7005a7f834bdf291855608fe82c24d30be6ac2017093a8"; + private static final String SECP_PROXY_PRIVATE_KEY = + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; + + private static final BLSKeyPair DELEGATOR_KEY_PAIR = + new BLSKeyPair(BLSSecretKey.fromBytes(Bytes32.fromHexString(BLS_DELEGATOR_PRIVATE_KEY))); + private static final BLSPublicKey DELEGATOR_PUB_KEY = DELEGATOR_KEY_PAIR.getPublicKey(); + private static final BLSPublicKey BLS_PROXY_PUB_KEY = + new BLSKeyPair(BLSSecretKey.fromBytes(Bytes32.fromHexString(BLS_PROXY_PRIVATE_KEY))) + .getPublicKey(); + private static final ECKeyPair SECP_PROXY_KEY_PAIR = + ECKeyPair.create(Numeric.toBigInt(Bytes.fromHexString(SECP_PROXY_PRIVATE_KEY).toArray())); + private static final ECPublicKey SECP_PROXY_EC_PUB_KEY = + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(SECP_PROXY_KEY_PAIR.getPublicKey()); + private static final Bytes SECP_PROXY_PUB_KEY_ENC = + Bytes.fromHexString(EthPublicKeyUtils.toHexStringCompressed(SECP_PROXY_EC_PUB_KEY)); + + @BeforeAll + static void initExpectedSigningRoots() { + // precalculated values from Commit Boost client implementation + DOMAIN_MAP.put( + Eth2Network.MAINNET, + Bytes32.fromHexString( + "0x6d6d6f43f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a9")); + DOMAIN_MAP.put( + Eth2Network.HOLESKY, + Bytes32.fromHexString( + "0x6d6d6f435b83a23759c560b2d0c64576e1dcfc34ea94c4988f3e0d9f77f05387")); + DOMAIN_MAP.put( + Eth2Network.SEPOLIA, + Bytes32.fromHexString( + "0x6d6d6f43d3010778cd08ee514b08fe67b6c503b510987a4ce43f42306d97c67c")); + + BLS_PROXY_ROOT_MAP.put( + Eth2Network.MAINNET, + Bytes32.fromHexString( + "0x36700803956402c24e232e5da8d7dda12796ba96e49177f37daab87dd852f0cd")); + BLS_PROXY_ROOT_MAP.put( + Eth2Network.HOLESKY, + Bytes32.fromHexString( + "0xdb1b20106a8955ddb47eb2c8c2fe602af8801e61f682f068fc968c65644e45b6")); + BLS_PROXY_ROOT_MAP.put( + Eth2Network.SEPOLIA, + Bytes32.fromHexString( + "0x99615a149344fc1beffc2085ae98b676bff384b92b45dd28bc1f62127c41505e")); + + SECP_PROXY_ROOT_MAP.put( + Eth2Network.MAINNET, + Bytes32.fromHexString( + "0x419a4f6b748659b3ac4fc3534f3767fffe78127d210af0b2e1c1c8e7b345cf64")); + SECP_PROXY_ROOT_MAP.put( + Eth2Network.HOLESKY, + Bytes32.fromHexString( + "0xcc0cd2144f8b1c775eda156524e0a26ab794fdf39121ec902e51a4aff477fb74")); + SECP_PROXY_ROOT_MAP.put( + Eth2Network.SEPOLIA, + Bytes32.fromHexString( + "0xcc773c9f0ca058178f5b65b9c5fe9857c39e667f64f4b09a4e75731ac56fee41")); + + // precalculated Proxy Message Signature values from Commit Boost client implementation + BLS_PROXY_MESSAGE_SIGNATURE_MAP.put( + Eth2Network.MAINNET, + "0x99c739103b950777727a2e4da588de9017adbcae2ccb50ec1887ba0148a70055e55765441974e0a63f635267600b7bc00b2f616a8427f7d27ec7c1b6fd8520049294a9bbc07bea46aaeff59a254bf793fe57e0e67c41b457816839ff3da13f2e"); + BLS_PROXY_MESSAGE_SIGNATURE_MAP.put( + Eth2Network.HOLESKY, + "0xa13f26d6b77b17e385ce3411e7c0fba4b2bed81da5e14a50b419d3a2c2ea3c54a5b70c7c93b99bffa99781041b39335609c096639b7f742a0246204697ba67497ace0853a7f9d982356d78fdb99e98134302444596faec857570d9e5d578999c"); + BLS_PROXY_MESSAGE_SIGNATURE_MAP.put( + Eth2Network.SEPOLIA, + "0x8fd1736684a3eee3a4deea5785d41ddc7a96faf1dd4ff9778fb58db465a206571fff0ca9e55cd3654a28cfc0b0e065411633deb9cb8b9263f7189b73c013a61d6518a5aa2b40066a230a5cb1705bd9a80894badc7bfc65e3e2dd459e9fa9d7fc"); + + SECP_PROXY_MESSAGE_SIGNATURE_MAP.put( + Eth2Network.MAINNET, + "0x8cd715641bb61bca8ba50a5f7e5faf06da1aedb074d59b9fce0ab69e8840501975f5c0008de6625b7d343b5bd362e3220ef5be03c1b32842cddcd5073c3d25e22e9746144b8ff2361391af1b681520c111b5ea69f11097991cccb43b9b6fb0e9"); + SECP_PROXY_MESSAGE_SIGNATURE_MAP.put( + Eth2Network.HOLESKY, + "0x8f3c546da13ad082e2818b75b4662e4f28d38e193a5c4e24231233df454f26f15c8ab1fd4cc4772321ab7a4ac16acaf300a78c5fc1ac96c4009413ad7f9c6c5cd99cb9d7c92120177d828bd8f6e77b9ffb93c37f6f6b3cb264969fa4fea179d5"); + SECP_PROXY_MESSAGE_SIGNATURE_MAP.put( + Eth2Network.SEPOLIA, + "0x90000272c0a751852d28b953c9d30df31fd9eeb846fb3b575c8fdeee0325ee5dcc6f91bdf3d5d0f0814b707d088ab3af047977464cbe3b9eded66202c2ae70fbe478860cbcf4fc31d10a81aac7c682a6e422686a7cfa7cab272903f9cabf73bb"); + } + + @ParameterizedTest + @EnumSource(names = {"MAINNET", "HOLESKY", "SEPOLIA"}) + void validComputedDomain(final Eth2Network network) { + final Spec spec = getSpec(network); + final SigningRootGenerator signingRootGenerator = new SigningRootGenerator(spec); + assertThat(signingRootGenerator.getDomain()).isEqualTo(DOMAIN_MAP.get(network)); + } + + @ParameterizedTest + @EnumSource(names = {"MAINNET", "HOLESKY", "SEPOLIA"}) + void computeSigningRootForBLSProxyKey(final Eth2Network network) { + final Spec spec = getSpec(network); + final SigningRootGenerator signingRootGenerator = new SigningRootGenerator(spec); + + final ProxyDelegation proxyDelegation = + new ProxyDelegation(DELEGATOR_PUB_KEY.toHexString(), BLS_PROXY_PUB_KEY.toHexString()); + final Bytes signingRoot = + signingRootGenerator.computeSigningRoot( + proxyDelegation.toMerkleizable(ProxyKeySignatureScheme.BLS).hashTreeRoot()); + + assertThat(signingRoot).isEqualTo(BLS_PROXY_ROOT_MAP.get(network)); + + // verify BLS Signature matching Commit Boost client implementation as well + final BlsArtifactSigner artifactSigner = new BlsArtifactSigner(DELEGATOR_KEY_PAIR, null); + final String signature = artifactSigner.sign(signingRoot).asHex(); + + assertThat(signature).isEqualTo(BLS_PROXY_MESSAGE_SIGNATURE_MAP.get(network)); + } + + @ParameterizedTest + @EnumSource(names = {"MAINNET", "HOLESKY", "SEPOLIA"}) + void computeSigningRootforSECPProxyKey(final Eth2Network network) { + final Spec spec = getSpec(network); + final SigningRootGenerator signingRootGenerator = new SigningRootGenerator(spec); + + final ProxyDelegation proxyDelegation = + new ProxyDelegation(DELEGATOR_PUB_KEY.toHexString(), SECP_PROXY_PUB_KEY_ENC.toHexString()); + + final Bytes signingRoot = + signingRootGenerator.computeSigningRoot( + proxyDelegation.toMerkleizable(ProxyKeySignatureScheme.ECDSA).hashTreeRoot()); + + assertThat(signingRoot).isEqualTo(SECP_PROXY_ROOT_MAP.get(network)); + + // verify BLS Signature matching Commit Boost client implementation as well + final BlsArtifactSigner artifactSigner = new BlsArtifactSigner(DELEGATOR_KEY_PAIR, null); + BlsArtifactSignature blsArtifactSignature = artifactSigner.sign(signingRoot); + String signature = blsArtifactSignature.asHex(); + + assertThat(signature).isEqualTo(SECP_PROXY_MESSAGE_SIGNATURE_MAP.get(network)); + } + + private static Spec getSpec(final Eth2Network network) { + final Eth2NetworkConfiguration.Builder builder = Eth2NetworkConfiguration.builder(); + return builder.applyNetworkDefaults(network).build().getSpec(); + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignerProvider.java b/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignerProvider.java index 050bddcd7..7a1b11a6e 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignerProvider.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignerProvider.java @@ -35,6 +35,16 @@ public interface ArtifactSignerProvider extends Closeable { */ Optional getSigner(final String identifier); + /** + * Get the proxy signer for the given proxy public key. + * + * @param proxyPubKey the public key of the proxy signer + * @return the signer or empty if no signer is found + */ + default Optional getProxySigner(final String proxyPubKey) { + throw new UnsupportedOperationException("Proxy signers are not supported by this provider"); + } + /** * Get the available identifiers for the loaded signers. * @@ -68,6 +78,18 @@ default Map> getProxyIdentifiers(final String consensusPubK */ Future removeSigner(final String identifier); + /** + * Add a proxy signer to the signer provider. + * + * @param proxySigner Instance of ArtifactSigner that represents the proxy signer + * @param consensusPubKey Public Key of the consensus signer for which proxy signer is being added + * @return a future that completes when the proxy signer is added + */ + default Future addProxySigner( + final ArtifactSigner proxySigner, final String consensusPubKey) { + throw new UnsupportedOperationException("Proxy signers are not supported by this provider"); + } + /** Close the executor service and release any resources. */ @Override void close(); diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProvider.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProvider.java index 41b961219..bfa2893b5 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProvider.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProvider.java @@ -114,6 +114,14 @@ public Optional getSigner(final String identifier) { return result; } + @Override + public Optional getProxySigner(final String proxyPubKey) { + return proxySigners.values().stream() + .flatMap(Set::stream) + .filter(signer -> signer.getIdentifier().equals(proxyPubKey)) + .findFirst(); + } + @Override public Set availableIdentifiers() { return Set.copyOf(signers.keySet()); @@ -151,6 +159,20 @@ public Future removeSigner(final String identifier) { }); } + @Override + public Future addProxySigner( + final ArtifactSigner proxySigner, final String consensusPubKey) { + return executorService.submit( + () -> { + proxySigners.computeIfAbsent(consensusPubKey, k -> new HashSet<>()).add(proxySigner); + LOG.info( + "Loaded new proxy signer {} for consensus public key '{}'", + proxySigner.getIdentifier(), + consensusPubKey); + return null; + }); + } + @Override public void close() { executorService.shutdownNow(); diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java index 8a1f18c4e..6f53ae666 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java @@ -12,6 +12,8 @@ */ package tech.pegasys.web3signer.signing.secp256k1; +import tech.pegasys.web3signer.signing.util.SecureRandomProvider; + import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.KeyFactory; @@ -19,7 +21,6 @@ import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; import java.security.spec.ECGenParameterSpec; import java.security.spec.ECPrivateKeySpec; @@ -43,7 +44,6 @@ */ public class EthPublicKeyUtils { private static final BouncyCastleProvider BC_PROVIDER = new BouncyCastleProvider(); - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); private static final ECDomainParameters SECP256K1_DOMAIN; private static final ECParameterSpec BC_SECP256K1_SPEC; private static final java.security.spec.ECParameterSpec JAVA_SECP256K1_SPEC; @@ -80,7 +80,7 @@ public static KeyPair generateK256KeyPair() { try { final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALGORITHM, BC_PROVIDER); - keyPairGenerator.initialize(EC_KEYGEN_PARAM, SECURE_RANDOM); + keyPairGenerator.initialize(EC_KEYGEN_PARAM, SecureRandomProvider.getSecureRandom()); return keyPairGenerator.generateKeyPair(); } catch (final GeneralSecurityException e) { throw new RuntimeException(e); diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/util/SecureRandomProvider.java b/signing/src/main/java/tech/pegasys/web3signer/signing/util/SecureRandomProvider.java new file mode 100644 index 000000000..dcc30ef96 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/util/SecureRandomProvider.java @@ -0,0 +1,29 @@ +/* + * 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.signing.util; + +import java.security.SecureRandom; + +/** This class provides a secure random number generator. */ +public class SecureRandomProvider { + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /** + * Get the secure random number generator. + * + * @return the secure random number generator + */ + public static SecureRandom getSecureRandom() { + return SECURE_RANDOM; + } +} diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java index dd1dd7c82..962db1102 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java @@ -26,7 +26,6 @@ import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyPair; @@ -232,41 +231,4 @@ private static String[] getCompressedSECPPublicKeysArray(final List e .toList() .toArray(String[]::new); } - - private static class TestCommitBoostParameters implements KeystoresParameters { - private final Path keystorePath; - private final Path passwordFile; - - public TestCommitBoostParameters(final Path keystorePath, final Path passwordDir) { - this.keystorePath = keystorePath; - // create password file in passwordDir - this.passwordFile = passwordDir.resolve("password.txt"); - // write text to password file - try { - Files.writeString(passwordFile, "password"); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public Path getKeystoresPath() { - return keystorePath; - } - - @Override - public Path getKeystoresPasswordsPath() { - return null; - } - - @Override - public Path getKeystoresPasswordFile() { - return passwordFile; - } - - @Override - public boolean isEnabled() { - return true; - } - } } diff --git a/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/TestCommitBoostParameters.java b/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/TestCommitBoostParameters.java new file mode 100644 index 000000000..9fe54975d --- /dev/null +++ b/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/TestCommitBoostParameters.java @@ -0,0 +1,56 @@ +/* + * 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.signing.config; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Test fixture for CommitBoostParameters */ +public class TestCommitBoostParameters implements KeystoresParameters { + private final Path keystorePath; + private final Path passwordFile; + + public TestCommitBoostParameters(final Path keystorePath, final Path passwordDir) { + this.keystorePath = keystorePath; + // create password file in passwordDir + this.passwordFile = passwordDir.resolve("password.txt"); + // write text to password file + try { + Files.writeString(passwordFile, "password"); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public Path getKeystoresPath() { + return keystorePath; + } + + @Override + public Path getKeystoresPasswordsPath() { + return null; + } + + @Override + public Path getKeystoresPasswordFile() { + return passwordFile; + } + + @Override + public boolean isEnabled() { + return true; + } +}