From f27d7eab687477e906f31be5bae91772f634f1c6 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 26 Nov 2024 18:25:56 +1000 Subject: [PATCH] Commit Boost Acceptance Test for List Pub Keys (#1040) * Add commit boost acceptance test for list pub keys API. Also fix the public key identifiers used by commit boost API by introducing K256ArtifactSigner. * Refactor SignerForIdentifier and removed signAndGetArtifactSignature. Derive SignatureData from existing SecpArtifactSignature builder method --- .../pegasys/web3signer/dsl/signer/Signer.java | 29 +++ .../dsl/signer/SignerConfiguration.java | 10 +- .../signer/SignerConfigurationBuilder.java | 11 +- .../runner/CmdLineParamsConfigFileImpl.java | 23 ++ .../runner/CmdLineParamsDefaultImpl.java | 16 ++ .../CommitBoostAcceptanceTest.java | 233 ++++++++++++++++++ .../eth2/CommitBoostPublicKeysRoute.java | 11 +- .../CommitBoostPublicKeysHandler.java | 33 +-- .../commitboost/json/PublicKeyMappings.java | 10 +- .../handlers/signing/SignerForIdentifier.java | 15 -- .../signing/TransactionSerializer.java | 8 +- .../EthSignTransactionResultProviderTest.java | 8 +- .../signing/ArtifactSignerProvider.java | 9 +- .../signing/K256ArtifactSigner.java | 144 +++++++++++ .../SecpV3KeystoresBulkLoader.java | 46 +++- .../config/DefaultArtifactSignerProvider.java | 87 ++++--- .../SecpArtifactSignerProviderAdapter.java | 7 - .../signing/K256ArtifactSignerTest.java | 53 ++++ .../DefaultArtifactSignerProviderTest.java | 17 +- .../tech/pegasys/web3signer/K256TestUtil.java | 52 ++++ 20 files changed, 705 insertions(+), 117 deletions(-) create mode 100644 acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/K256ArtifactSigner.java create mode 100644 signing/src/test/java/tech/pegasys/web3signer/signing/K256ArtifactSignerTest.java create mode 100644 signing/src/testFixtures/java/tech/pegasys/web3signer/K256TestUtil.java diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java index 14acb4ac0..df146e997 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java @@ -47,6 +47,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.Ethereum; import org.web3j.protocol.core.JsonRpc2_0Web3j; @@ -187,6 +188,34 @@ public Response callApiPublicKeys(final KeyType keyType) { return given().baseUri(getUrl()).get(publicKeysPath(keyType)); } + public Response callCommitBoostGetPubKeys() { + return given().baseUri(getUrl()).get("/signer/v1/get_pubkeys"); + } + + public Response callCommitBoostGenerateProxyKey(final String pubkey, final String scheme) { + return given() + .baseUri(getUrl()) + .contentType(ContentType.JSON) + .body(new JsonObject().put("pubkey", pubkey).put("scheme", scheme).toString()) + .post("/signer/v1/generate_proxy_key"); + } + + public Response callCommitBoostRequestForSignature( + final String signRequestType, final String pubkey, final Bytes32 objectRoot) { + return given() + .baseUri(getUrl()) + .contentType(ContentType.JSON) + .log() + .all() + .body( + new JsonObject() + .put("type", signRequestType) + .put("pubkey", pubkey) + .put("object_root", objectRoot.toHexString()) + .toString()) + .post("/signer/v1/request_signature"); + } + public List listPublicKeys(final KeyType keyType) { return callApiPublicKeys(keyType).as(new TypeRef<>() {}); } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java index c3034d955..dc172c53e 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfiguration.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.Level; public class SignerConfiguration { @@ -81,6 +82,7 @@ public class SignerConfiguration { private final Optional v3KeystoresBulkloadParameters; private final boolean signingExtEnabled; + private Optional> commitBoostParameters; public SignerConfiguration( final String hostname, @@ -128,7 +130,8 @@ public SignerConfiguration( final Optional downstreamTlsOptions, final ChainIdProvider chainIdProvider, final Optional v3KeystoresBulkloadParameters, - final boolean signingExtEnabled) { + final boolean signingExtEnabled, + final Optional> commitBoostParameters) { this.hostname = hostname; this.logLevel = logLevel; this.httpRpcPort = httpRpcPort; @@ -175,6 +178,7 @@ public SignerConfiguration( this.chainIdProvider = chainIdProvider; this.v3KeystoresBulkloadParameters = v3KeystoresBulkloadParameters; this.signingExtEnabled = signingExtEnabled; + this.commitBoostParameters = commitBoostParameters; } public String hostname() { @@ -368,4 +372,8 @@ public Optional getV3KeystoresBulkloadParameters() { public boolean isSigningExtEnabled() { return signingExtEnabled; } + + public Optional> getCommitBoostParameters() { + return commitBoostParameters; + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java index d396d5303..c7160ffc9 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/SignerConfigurationBuilder.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.Level; public class SignerConfigurationBuilder { @@ -85,6 +86,7 @@ public class SignerConfigurationBuilder { private KeystoresParameters v3KeystoresBulkloadParameters; private boolean signingExtEnabled; + private Pair commitBoostParameters; public SignerConfigurationBuilder withLogLevel(final Level logLevel) { this.logLevel = logLevel; @@ -331,6 +333,12 @@ public SignerConfigurationBuilder withSigningExtEnabled(final boolean signingExt return this; } + public SignerConfigurationBuilder withCommitBoostParameters( + final Pair commitBoostParameters) { + this.commitBoostParameters = commitBoostParameters; + return this; + } + public SignerConfiguration build() { if (mode == null) { throw new IllegalArgumentException("Mode cannot be null"); @@ -381,6 +389,7 @@ public SignerConfiguration build() { Optional.ofNullable(downstreamTlsOptions), chainIdProvider, Optional.ofNullable(v3KeystoresBulkloadParameters), - signingExtEnabled); + signingExtEnabled, + Optional.ofNullable(commitBoostParameters)); } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsConfigFileImpl.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsConfigFileImpl.java index c909bb37b..e9fcb2bd3 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsConfigFileImpl.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsConfigFileImpl.java @@ -54,6 +54,7 @@ import java.util.function.Consumer; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; public class CmdLineParamsConfigFileImpl implements CmdLineParamsBuilder { private final SignerConfiguration signerConfig; @@ -163,6 +164,12 @@ public List createCmdLineParams() { String.format(YAML_BOOLEAN_FMT, "eth2.Xsigning-ext-enabled", Boolean.TRUE)); } + signerConfig + .getCommitBoostParameters() + .ifPresent( + commitBoostParameters -> + appendCommitBoostParameters(commitBoostParameters, yamlConfig)); + final CommandArgs subCommandArgs = createSubCommandArgs(); params.addAll(subCommandArgs.params); yamlConfig.append(subCommandArgs.yamlConfig); @@ -204,6 +211,22 @@ public List createCmdLineParams() { return params; } + private static void appendCommitBoostParameters( + final Pair commitBoostParameters, final StringBuilder yamlConfig) { + yamlConfig.append( + String.format(YAML_BOOLEAN_FMT, "eth2.commit-boost-api-enabled", Boolean.TRUE)); + yamlConfig.append( + String.format( + YAML_STRING_FMT, + "eth2.proxy-keystores-path", + commitBoostParameters.getLeft().toAbsolutePath())); + yamlConfig.append( + String.format( + YAML_STRING_FMT, + "eth2.proxy-keystores-password-file", + commitBoostParameters.getRight().toAbsolutePath())); + } + private Consumer setV3KeystoresBulkloadParameters( final StringBuilder yamlConfig) { return keystoresParameters -> { diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java index d5725f30d..4875a2932 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/runner/CmdLineParamsDefaultImpl.java @@ -53,6 +53,7 @@ import java.util.function.Consumer; import com.google.common.collect.Lists; +import org.apache.commons.lang3.tuple.Pair; public class CmdLineParamsDefaultImpl implements CmdLineParamsBuilder { private final SignerConfiguration signerConfig; @@ -138,6 +139,12 @@ public List createCmdLineParams() { if (signerConfig.isSigningExtEnabled()) { params.add("--Xsigning-ext-enabled=true"); } + + signerConfig + .getCommitBoostParameters() + .ifPresent( + commitBoostParameters -> params.addAll(commitBoostOptions(commitBoostParameters))); + } else if (signerConfig.getMode().equals("eth1")) { params.add("--downstream-http-port"); params.add(Integer.toString(signerConfig.getDownstreamHttpPort())); @@ -160,6 +167,15 @@ public List createCmdLineParams() { return params; } + private static List commitBoostOptions(final Pair commitBoostParameters) { + return List.of( + "--commit-boost-api-enabled=true", + "--proxy-keystores-path", + commitBoostParameters.getLeft().toAbsolutePath().toString(), + "--proxy-keystores-password-file", + commitBoostParameters.getRight().toAbsolutePath().toString()); + } + private static Consumer setV3KeystoresBulkloadParameters( final List params) { return keystoresParameters -> { 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 new file mode 100644 index 000000000..5f34348d8 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java @@ -0,0 +1,233 @@ +/* + * 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.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; + +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.web3signer.KeystoreUtil; +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.dsl.utils.DefaultKeystoresParameters; +import tech.pegasys.web3signer.signing.config.KeystoresParameters; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.tests.AcceptanceTestBase; + +import java.io.IOException; +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; +import java.util.stream.Stream; + +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; +import org.web3j.crypto.WalletUtils; + +// 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"; + + private List consensusBlsKeys = randomBLSKeyPairs(2); + private Map> proxyBLSKeysMap = new HashMap<>(); + private Map> 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 2 proxy bls + final List proxyBLSKeys = createProxyBLSKeys(blsKeyPair); + proxyBLSKeysMap.put(blsKeyPair.getPublicKey().toHexString(), proxyBLSKeys); + + // create 2 proxy secp keys + final List proxyECKeyPairs = createProxyECKeys(blsKeyPair); + proxySECPKeysMap.put(blsKeyPair.getPublicKey().toHexString(), proxyECKeyPairs); + } + + // commit boost proxy keys password file + final Path commitBoostPasswordFile = createCommitBoostPasswordFile(); + + // 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 listCommitBoostPublicKeys() { + final Response response = signer.callCommitBoostGetPubKeys(); + response + .then() + .log() + .body() + .statusCode(200) + .contentType(ContentType.JSON) + .body("keys", hasSize(2)); + + // extract consensus public keys from response + final List> responseKeys = response.jsonPath().getList("keys"); + for (final Map responseKeyMap : responseKeys) { + final String consensusKeyHex = (String) responseKeyMap.get("consensus"); + // verify if consensus public key is present in the map + assertThat(proxyBLSKeysMap.keySet()).contains(consensusKeyHex); + + // verify if proxy BLS keys are present in the response + final List responseProxyBlsKeys = (List) responseKeyMap.get("proxy_bls"); + final List expectedProxyBLSKeys = getProxyBLSPubKeys(consensusKeyHex); + assertThat(responseProxyBlsKeys) + .containsExactlyInAnyOrder(expectedProxyBLSKeys.toArray(String[]::new)); + + // verify if proxy SECP keys are present in the response + final List responseProxySECPKeys = (List) responseKeyMap.get("proxy_ecdsa"); + final List expectedProxySECPKeys = getProxyECPubKeys(consensusKeyHex); + assertThat(responseProxySECPKeys) + .containsExactlyInAnyOrder(expectedProxySECPKeys.toArray(String[]::new)); + } + } + + private List getProxyECPubKeys(final String consensusKeyHex) { + // return compressed secp256k1 public keys in hex format + return proxySECPKeysMap.get(consensusKeyHex).stream() + .map( + ecKeyPair -> + EthPublicKeyUtils.toHexStringCompressed( + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey()))) + .toList(); + } + + private List getProxyBLSPubKeys(final String consensusKeyHex) { + return proxyBLSKeysMap.get(consensusKeyHex).stream() + .map(blsKeyPair -> blsKeyPair.getPublicKey().toHexString()) + .toList(); + } + + private Path createCommitBoostPasswordFile() { + try { + return Files.writeString( + commitBoostPasswordDir.resolve("cb_password.txt"), KEYSTORE_PASSWORD); + } catch (IOException e) { + throw new IllegalStateException("Unable to write password file"); + } + } + + /** + * Generate 2 random proxy EC key pairs and their encrypted keystores + * + * @param consensusKeyPair consensus BLS key pair whose public key will be used as directory name + * @return list of ECKeyPairs + */ + private List createProxyECKeys(final BLSKeyPair consensusKeyPair) { + final Path proxySecpKeyStoreDir = + commitBoostKeystoresPath + .resolve(consensusKeyPair.getPublicKey().toHexString()) + .resolve("SECP256K1"); + try { + Files.createDirectories(proxySecpKeyStoreDir); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + // create 2 random proxy secp keys and their keystores + final List proxyECKeyPairs = randomECKeyPairs(2); + proxyECKeyPairs.forEach( + proxyECKey -> { + try { + WalletUtils.generateWalletFile( + KEYSTORE_PASSWORD, proxyECKey, proxySecpKeyStoreDir.toFile(), false); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + }); + return proxyECKeyPairs; + } + + /** + * Generate 2 random proxy BLS key pairs and their encrypted keystores + * + * @param consensusKeyPair consensus BLS key pair whose public key will be used as directory name + * @return list of BLSKeyPairs + */ + private List createProxyBLSKeys(final BLSKeyPair consensusKeyPair) { + final Path proxyBlsKeyStoreDir = + commitBoostKeystoresPath + .resolve(consensusKeyPair.getPublicKey().toHexString()) + .resolve("BLS"); + try { + Files.createDirectories(proxyBlsKeyStoreDir); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + // create 2 proxy bls keys and their keystores + List blsKeyPairs = randomBLSKeyPairs(2); + blsKeyPairs.forEach( + blsKeyPair -> + KeystoreUtil.createKeystoreFile(blsKeyPair, proxyBlsKeyStoreDir, KEYSTORE_PASSWORD)); + return blsKeyPairs; + } + + /** + * Generate random SECP256K1 KeyPairs using Web3J library + * + * @param count number of key pairs to generate + * @return list of ECKeyPairs + */ + private static List randomECKeyPairs(final int count) { + return Stream.generate( + () -> { + try { + return Keys.createEcKeyPair(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .limit(count) + .toList(); + } + + /** + * Generate random BLS KeyPairs using Teku library + * + * @param count number of key pairs to generate + * @return list of BLSKeyPairs + */ + private static List randomBLSKeyPairs(final int count) { + return Stream.generate(() -> BLSKeyPair.random(SECURE_RANDOM)).limit(count).toList(); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostPublicKeysRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostPublicKeysRoute.java index 261290049..abdc3cb47 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostPublicKeysRoute.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostPublicKeysRoute.java @@ -15,6 +15,8 @@ import tech.pegasys.web3signer.core.Context; import tech.pegasys.web3signer.core.routes.Web3SignerRoute; import tech.pegasys.web3signer.core.service.http.handlers.commitboost.CommitBoostPublicKeysHandler; +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; @@ -23,9 +25,16 @@ public class CommitBoostPublicKeysRoute implements Web3SignerRoute { private static final String PATH = "/signer/v1/get_pubkeys"; private final Context context; + private final ArtifactSignerProvider artifactSignerProvider; public CommitBoostPublicKeysRoute(final Context context) { this.context = context; + // there should be only one DefaultArtifactSignerProvider in eth2 mode + artifactSignerProvider = + context.getArtifactSignerProviders().stream() + .filter(p -> p instanceof DefaultArtifactSignerProvider) + .findFirst() + .orElseThrow(); } @Override @@ -36,7 +45,7 @@ public void register() { .produces(JSON_HEADER) .handler( new BlockingHandlerDecorator( - new CommitBoostPublicKeysHandler(context.getArtifactSignerProviders()), false)) + new CommitBoostPublicKeysHandler(artifactSignerProvider), false)) .failureHandler(context.getErrorHandler()) .failureHandler( ctx -> { diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostPublicKeysHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostPublicKeysHandler.java index 12a268195..318af30f0 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostPublicKeysHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostPublicKeysHandler.java @@ -20,10 +20,9 @@ import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.PublicKeysResponse; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; import tech.pegasys.web3signer.signing.KeyType; -import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; -import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonProcessingException; @@ -35,22 +34,15 @@ public class CommitBoostPublicKeysHandler implements Handler { private static final Logger LOG = LogManager.getLogger(); - private final List artifactSignerProviders; + private final ArtifactSignerProvider artifactSignerProvider; private final ObjectMapper objectMapper = SigningObjectMapperFactory.createObjectMapper(); - public CommitBoostPublicKeysHandler(final List artifactSignerProviders) { - this.artifactSignerProviders = artifactSignerProviders; + public CommitBoostPublicKeysHandler(final ArtifactSignerProvider artifactSignerProvider) { + this.artifactSignerProvider = artifactSignerProvider; } @Override public void handle(final RoutingContext context) { - // obtain DefaultArtifactSignerProvider as that is the only one we are dealing in eth2 mode. - final ArtifactSignerProvider artifactSignerProvider = - artifactSignerProviders.stream() - .filter(provider -> provider instanceof DefaultArtifactSignerProvider) - .findFirst() - .orElseThrow(); - final PublicKeysResponse publicKeysResponse = toPublicKeysResponse(artifactSignerProvider); try { final String jsonEncoded = objectMapper.writeValueAsString(publicKeysResponse); @@ -65,17 +57,18 @@ public void handle(final RoutingContext context) { private PublicKeysResponse toPublicKeysResponse(final ArtifactSignerProvider provider) { return new PublicKeysResponse( provider.availableIdentifiers().stream() - .map(identifier -> toPublicKeyMappings(provider, identifier)) + .map(consensusPubKey -> toPublicKeyMappings(provider, consensusPubKey)) .collect(Collectors.toList())); } private static PublicKeyMappings toPublicKeyMappings( - final ArtifactSignerProvider provider, final String identifier) { - final Map> proxyIdentifiers = provider.getProxyIdentifiers(identifier); - final List proxyBlsPublicKeys = - proxyIdentifiers.computeIfAbsent(KeyType.BLS, k -> List.of()); - final List proxyEcdsaPublicKeys = - proxyIdentifiers.computeIfAbsent(KeyType.SECP256K1, k -> List.of()); - return new PublicKeyMappings(identifier, proxyBlsPublicKeys, proxyEcdsaPublicKeys); + final ArtifactSignerProvider provider, final String consensusPubKey) { + final Map> proxyIdentifiers = + provider.getProxyIdentifiers(consensusPubKey); + final Set proxyBlsPublicKeys = + proxyIdentifiers.computeIfAbsent(KeyType.BLS, k -> Set.of()); + final Set proxyEcdsaPublicKeys = + proxyIdentifiers.computeIfAbsent(KeyType.SECP256K1, k -> Set.of()); + return new PublicKeyMappings(consensusPubKey, proxyBlsPublicKeys, proxyEcdsaPublicKeys); } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/PublicKeyMappings.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/PublicKeyMappings.java index 08ca0690b..630adc955 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/PublicKeyMappings.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/PublicKeyMappings.java @@ -12,7 +12,7 @@ */ package tech.pegasys.web3signer.core.service.http.handlers.commitboost.json; -import java.util.List; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonProperty; @@ -20,10 +20,10 @@ * Represents the public key mappings for get_pubkeys API * * @param consensus BLS Public Key in hex string format - * @param proxyBlsPublicKeys List of Proxy BLS Public Key in hex string format - * @param proxyEcdsaPublicKeys List of Proxy ECDSA (SECP256K1) Public Key in hex string format + * @param proxyBlsPublicKeys Set of Proxy BLS Public Key in hex string format + * @param proxyEcdsaPublicKeys Set of Proxy ECDSA (SECP256K1) Public Key in hex string format */ public record PublicKeyMappings( @JsonProperty(value = "consensus") String consensus, - @JsonProperty(value = "proxy_bls") List proxyBlsPublicKeys, - @JsonProperty(value = "proxy_ecdsa") List proxyEcdsaPublicKeys) {} + @JsonProperty(value = "proxy_bls") Set proxyBlsPublicKeys, + @JsonProperty(value = "proxy_ecdsa") Set proxyEcdsaPublicKeys) {} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SignerForIdentifier.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SignerForIdentifier.java index 69e22f85f..443ac1688 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SignerForIdentifier.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SignerForIdentifier.java @@ -12,7 +12,6 @@ */ package tech.pegasys.web3signer.core.service.http.handlers.signing; -import tech.pegasys.web3signer.signing.ArtifactSignature; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; import java.util.Optional; @@ -43,20 +42,6 @@ public Optional sign(final String identifier, final Bytes data) { return signerProvider.getSigner(identifier).map(signer -> signer.sign(data).asHex()); } - /** - * Sign data for given identifier and return ArtifactSignature. Useful for SECP signing usages. - * - * @param The specific type of ArtifactSignature - * @param identifier The identifier for which to sign data. - * @param data Bytes which is signed - * @return Optional ArtifactSignature of type T. Empty if no signer available for given identifier - */ - @SuppressWarnings("unchecked") - public > Optional signAndGetArtifactSignature( - final String identifier, final Bytes data) { - return signerProvider.getSigner(identifier).map(signer -> (T) signer.sign(data)); - } - /** * Checks whether a signer for the passed identifier is present * diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java index 4535635ce..cd820ef6d 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/signing/TransactionSerializer.java @@ -77,13 +77,13 @@ private static byte[] prependEip1559TransactionType(byte[] bytesToSign) { } private SignatureData sign(final String eth1Address, final byte[] bytesToSign) { - final SecpArtifactSignature artifactSignature = + final String hexSignature = secpSigner - .signAndGetArtifactSignature( - normaliseIdentifier(eth1Address), Bytes.of(bytesToSign)) + .sign(normaliseIdentifier(eth1Address), Bytes.of(bytesToSign)) .orElseThrow(() -> new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); - final Signature signature = artifactSignature.getSignatureData(); + final Signature signature = + SecpArtifactSignature.fromBytes(Bytes.fromHexString(hexSignature)).getSignatureData(); return new SignatureData( signature.getV().toByteArray(), diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java index 6562768fa..0eacc2be0 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java @@ -115,9 +115,9 @@ public void signatureHasTheExpectedFormat() { final BigInteger v = BigInteger.ONE; final BigInteger r = BigInteger.TWO; final BigInteger s = BigInteger.TEN; - doReturn(Optional.of(new SecpArtifactSignature(new Signature(v, r, s)))) + doReturn(Optional.of(new SecpArtifactSignature(new Signature(v, r, s)).asHex())) .when(mockSignerForIdentifier) - .signAndGetArtifactSignature(any(String.class), any(Bytes.class)); + .sign(any(String.class), any(Bytes.class)); when(mockSignerForIdentifier.isSignerAvailable(any(String.class))).thenReturn(true); final EthSignTransactionResultProvider resultProvider = new EthSignTransactionResultProvider(chainId, mockSignerForIdentifier, jsonDecoder); @@ -177,10 +177,10 @@ private String executeEthSignTransaction(final JsonObject params) { doAnswer( answer -> { Bytes data = answer.getArgument(1, Bytes.class); - return signDataForKey(data, cs.getEcKeyPair()); + return signDataForKey(data, cs.getEcKeyPair()).map(SecpArtifactSignature::asHex); }) .when(mockSignerForIdentifier) - .signAndGetArtifactSignature(any(String.class), any(Bytes.class)); + .sign(any(String.class), any(Bytes.class)); when(mockSignerForIdentifier.isSignerAvailable(any(String.class))).thenReturn(true); 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 985100271..050bddcd7 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignerProvider.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignerProvider.java @@ -13,7 +13,6 @@ package tech.pegasys.web3signer.signing; import java.io.Closeable; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -44,12 +43,14 @@ public interface ArtifactSignerProvider extends Closeable { Set availableIdentifiers(); /** - * Get the proxy identifiers for the given identifier. Used for commit boost API. + * Get the proxy public keys for the given consensus public key. Used for commit boost API. * - * @param identifier the identifier of the signer + * @param consensusPubKey the identifier of the consensus signer * @return Map of Key Type (BLS, SECP256K1) and corresponding proxy identifiers */ - Map> getProxyIdentifiers(final String identifier); + default Map> getProxyIdentifiers(final String consensusPubKey) { + throw new UnsupportedOperationException("Proxy signers are not supported by this provider"); + } /** * Add a new signer to the signer provider. diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/K256ArtifactSigner.java b/signing/src/main/java/tech/pegasys/web3signer/signing/K256ArtifactSigner.java new file mode 100644 index 000000000..37bb0d3c2 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/K256ArtifactSigner.java @@ -0,0 +1,144 @@ +/* + * 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; + +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.signing.util.IdentifierUtils; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.web3j.crypto.ECDSASignature; +import org.web3j.crypto.ECKeyPair; + +/** + * An artifact signer for SECP256K1 keys used specifically for Commit Boost API ECDSA proxy keys. It + * uses compressed public key as identifier and signs the message with just sha256 digest. The + * signature complies with RFC-6979. + */ +public class K256ArtifactSigner implements ArtifactSigner { + private final ECKeyPair ecKeyPair; + public static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); + public static final ECDomainParameters CURVE = + new ECDomainParameters( + CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH()); + + public K256ArtifactSigner(final ECKeyPair web3JECKeypair) { + this.ecKeyPair = web3JECKeypair; + } + + @Override + public String getIdentifier() { + final String hexString = + EthPublicKeyUtils.toHexStringCompressed( + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey())); + return IdentifierUtils.normaliseIdentifier(hexString); + } + + @Override + public K256ArtifactSignature sign(final Bytes message) { + try { + // Use BouncyCastle's ECDSASigner with HMacDSAKCalculator for deterministic ECDSA + final ECPrivateKeyParameters privKey = + new ECPrivateKeyParameters(ecKeyPair.getPrivateKey(), CURVE); + final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + signer.init(true, privKey); + + // apply sha256 digest to the message before sending it to signing + final BigInteger[] components = signer.generateSignature(calculateSHA256(message.toArray())); + + // create a canonicalised signature using Web3J ECDSASignature class + final ECDSASignature signature = + new ECDSASignature(components[0], components[1]).toCanonicalised(); + + return new K256ArtifactSignature(signature); + } catch (final Exception e) { + throw new RuntimeException("Error signing message", e); + } + } + + @Override + public KeyType getKeyType() { + return KeyType.SECP256K1; + } + + public static byte[] calculateSHA256(byte[] message) { + try { + // Create a MessageDigest instance for SHA-256 + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + // Update the MessageDigest with the message bytes + return digest.digest(message); + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not found", e); + } + } + + /** An artifact signature for SECP256K1 keys used specifically for Commit Boost API ECDSA proxy */ + public static class K256ArtifactSignature implements ArtifactSignature { + final Bytes signature; + + public K256ArtifactSignature(final ECDSASignature signature) { + // convert to compact signature format + final MutableBytes concatenated = MutableBytes.create(64); + UInt256.valueOf(signature.r).copyTo(concatenated, 0); + UInt256.valueOf(signature.s).copyTo(concatenated, 32); + this.signature = concatenated; + } + + @Override + public KeyType getType() { + return KeyType.SECP256K1; + } + + @Override + public String asHex() { + return signature.toHexString(); + } + + @Override + public Bytes getSignatureData() { + return signature; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + K256ArtifactSignature that = (K256ArtifactSignature) o; + return Objects.equals(signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hashCode(signature); + } + + @Override + public String toString() { + return signature.toHexString(); + } + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/SecpV3KeystoresBulkLoader.java b/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/SecpV3KeystoresBulkLoader.java index d9e998c55..fb8c7767a 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/SecpV3KeystoresBulkLoader.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/SecpV3KeystoresBulkLoader.java @@ -15,6 +15,7 @@ import tech.pegasys.web3signer.keystorage.common.MappedResults; import tech.pegasys.web3signer.signing.ArtifactSigner; import tech.pegasys.web3signer.signing.EthSecpArtifactSigner; +import tech.pegasys.web3signer.signing.K256ArtifactSigner; import tech.pegasys.web3signer.signing.secp256k1.filebased.CredentialSigner; import tech.pegasys.web3signer.signing.secp256k1.util.JsonFilesUtil; @@ -34,8 +35,38 @@ public class SecpV3KeystoresBulkLoader { private static final Logger LOG = LogManager.getLogger(); + /** + * Bulk-load Ethereum compatible SECP Artifact Signers from encrypted v3 keystores. + * + * @param keystoresPath Path to the directory containing the v3 keystores + * @param pwrdFileOrDirPath Path to the password file or directory containing the passwords for + * the v3 keystores + * @return MappedResults containing the loaded ArtifactSigners + */ public static MappedResults loadV3KeystoresUsingPasswordFileOrDir( final Path keystoresPath, final Path pwrdFileOrDirPath) { + return loadV3KeystoresUsingPasswordFileOrDir(keystoresPath, pwrdFileOrDirPath, true); + } + + /** + * Bulk-load generic SECP Artifact Signers from encrypted v3 keystores that can be used by Commit + * Boost API. It uses compressed public key as identifier and apply SHA256 digest on the message + * before signing. + * + * @param keystoresPath Path to the directory containing the v3 keystores + * @param pwrdFileOrDirPath Path to the password file or directory containing the passwords for + * the v3 keystores + * @return MappedResults containing the loaded ArtifactSigners + */ + public static MappedResults loadECDSAProxyKeystores( + final Path keystoresPath, final Path pwrdFileOrDirPath) { + return loadV3KeystoresUsingPasswordFileOrDir(keystoresPath, pwrdFileOrDirPath, false); + } + + private static MappedResults loadV3KeystoresUsingPasswordFileOrDir( + final Path keystoresPath, + final Path pwrdFileOrDirPath, + final boolean ethereumSECPCompatible) { if (!Files.exists(pwrdFileOrDirPath)) { LOG.error("Password file or directory doesn't exist."); return MappedResults.errorResult(); @@ -67,12 +98,16 @@ public static MappedResults loadV3KeystoresUsingPasswordFileOrDi } return keystoresFiles.parallelStream() - .map(keystoreFile -> createSecpArtifactSigner(keystoreFile, passwordReader)) + .map( + keystoreFile -> + createSecpArtifactSigner(keystoreFile, passwordReader, ethereumSECPCompatible)) .reduce(MappedResults.newSetInstance(), MappedResults::merge); } private static MappedResults createSecpArtifactSigner( - final Path v3KeystorePath, final PasswordReader passwordReader) { + final Path v3KeystorePath, + final PasswordReader passwordReader, + final boolean ethereumSECPCompatible) { try { final String fileNameWithoutExt = FilenameUtils.removeExtension(v3KeystorePath.getFileName().toString()); @@ -81,8 +116,11 @@ private static MappedResults createSecpArtifactSigner( final Credentials credentials = WalletUtils.loadCredentials(password, v3KeystorePath.toFile()); - final EthSecpArtifactSigner artifactSigner = - new EthSecpArtifactSigner(new CredentialSigner(credentials)); + final ArtifactSigner artifactSigner = + ethereumSECPCompatible + ? new EthSecpArtifactSigner(new CredentialSigner(credentials)) + : new K256ArtifactSigner(credentials.getEcKeyPair()); + return MappedResults.newInstance(Set.of(artifactSigner), 0); } catch (final IOException | CipherException | RuntimeException e) { LOG.error("Error loading v3 keystore {}", v3KeystorePath, e); 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 82b087ddf..41b961219 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 @@ -24,16 +24,16 @@ import java.io.File; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -46,7 +46,7 @@ public class DefaultArtifactSignerProvider implements ArtifactSignerProvider { private static final Logger LOG = LogManager.getLogger(); private final Supplier> artifactSignerCollectionSupplier; private final Map signers = new HashMap<>(); - private final Map> proxySigners = new HashMap<>(); + private final Map> proxySigners = new HashMap<>(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final Optional commitBoostKeystoresParameters; @@ -83,17 +83,20 @@ public Future load() { signers .keySet() .forEach( - signerIdentifier -> { + consensusPubKey -> { LOG.trace( - "Loading proxy signers for signer '{}' ...", signerIdentifier); - final Path identifierPath = - keystoreParameter.getKeystoresPath().resolve(signerIdentifier); - if (canReadFromDirectory(identifierPath)) { - loadBlsProxySigners( - keystoreParameter, signerIdentifier, identifierPath); - loadSecpProxySigners( - keystoreParameter, signerIdentifier, identifierPath); - } + "Loading proxy signers for signer '{}' ...", consensusPubKey); + loadProxySigners( + keystoreParameter, + consensusPubKey, + SECP256K1.name(), + SecpV3KeystoresBulkLoader::loadECDSAProxyKeystores); + + loadProxySigners( + keystoreParameter, + consensusPubKey, + BLS.name(), + BlsKeystoreBulkLoader::loadKeystoresUsingPasswordFile); })); LOG.info("Total signers (keys) currently loaded in memory: {}", signers.size()); @@ -117,14 +120,14 @@ public Set availableIdentifiers() { } @Override - public Map> getProxyIdentifiers(final String identifier) { - final List artifactSigners = - proxySigners.computeIfAbsent(identifier, k -> List.of()); + public Map> getProxyIdentifiers(final String consensusPubKey) { + final Set artifactSigners = + proxySigners.computeIfAbsent(consensusPubKey, k -> Set.of()); return artifactSigners.stream() .collect( Collectors.groupingBy( ArtifactSigner::getKeyType, - Collectors.mapping(ArtifactSigner::getIdentifier, Collectors.toList()))); + Collectors.mapping(ArtifactSigner::getIdentifier, Collectors.toSet()))); } @Override @@ -142,6 +145,7 @@ public Future removeSigner(final String identifier) { return executorService.submit( () -> { signers.remove(identifier); + proxySigners.remove(identifier); LOG.info("Removed signer with identifier '{}'", identifier); return null; }); @@ -157,34 +161,29 @@ private static boolean canReadFromDirectory(final Path path) { return file.canRead() && file.isDirectory(); } - private void loadSecpProxySigners( + /** + * Load proxy signers for given consensus public key and add it to internal proxy signers map. + * + * @param keystoreParameter location of proxy keystores and password file + * @param consensusPubKey Consensus public key + * @param keyType BLS or SECP256K1 + * @param loaderFunction Bulkloading method reference + */ + private void loadProxySigners( final KeystoresParameters keystoreParameter, - final String identifier, - final Path identifierPath) { - final Path proxySecpDir = identifierPath.resolve(SECP256K1.name()); - if (canReadFromDirectory(proxySecpDir)) { - // load secp proxy signers - final MappedResults secpSignersResults = - SecpV3KeystoresBulkLoader.loadV3KeystoresUsingPasswordFileOrDir( - proxySecpDir, keystoreParameter.getKeystoresPasswordFile()); - final Collection secpSigners = secpSignersResults.getValues(); - proxySigners.computeIfAbsent(identifier, k -> new ArrayList<>()).addAll(secpSigners); - } - } - - private void loadBlsProxySigners( - final KeystoresParameters keystoreParameter, - final String identifier, - final Path identifierPath) { - final Path proxyBlsDir = identifierPath.resolve(BLS.name()); - - if (canReadFromDirectory(proxyBlsDir)) { - // load bls proxy signers - final MappedResults blsSignersResult = - BlsKeystoreBulkLoader.loadKeystoresUsingPasswordFile( - proxyBlsDir, keystoreParameter.getKeystoresPasswordFile()); - final Collection blsSigners = blsSignersResult.getValues(); - proxySigners.computeIfAbsent(identifier, k -> new ArrayList<>()).addAll(blsSigners); + final String consensusPubKey, + final String keyType, + final BiFunction> loaderFunction) { + + // Calculate identifierPath from keystoreParameter + final Path identifierPath = keystoreParameter.getKeystoresPath().resolve(consensusPubKey); + final Path proxyDir = identifierPath.resolve(keyType); + + if (canReadFromDirectory(proxyDir)) { + final MappedResults signersResult = + loaderFunction.apply(proxyDir, keystoreParameter.getKeystoresPasswordFile()); + final Collection signers = signersResult.getValues(); + proxySigners.computeIfAbsent(consensusPubKey, k -> new HashSet<>()).addAll(signers); } } } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/SecpArtifactSignerProviderAdapter.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/SecpArtifactSignerProviderAdapter.java index 36578bcdf..9155694ba 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/SecpArtifactSignerProviderAdapter.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/SecpArtifactSignerProviderAdapter.java @@ -17,10 +17,8 @@ import tech.pegasys.web3signer.signing.ArtifactSigner; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.KeyType; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -68,11 +66,6 @@ public Set availableIdentifiers() { return Set.copyOf(signers.keySet()); } - @Override - public Map> getProxyIdentifiers(final String identifier) { - throw new NotImplementedException(); - } - @Override public Future addSigner(final ArtifactSigner signer) { throw new NotImplementedException(); diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/K256ArtifactSignerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/K256ArtifactSignerTest.java new file mode 100644 index 000000000..6abdf9898 --- /dev/null +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/K256ArtifactSignerTest.java @@ -0,0 +1,53 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.K256TestUtil; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Sign; +import org.web3j.utils.Numeric; + +class K256ArtifactSignerTest { + private static final String PRIVATE_KEY_HEX = + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; + private static final Bytes OBJECT_ROOT = + Bytes.fromHexString("419a4f6b748659b3ac4fc3534f3767fffe78127d210af0b2e1c1c8e7b345cf64"); + private static final ECKeyPair EC_KEY_PAIR = ECKeyPair.create(Numeric.toBigInt(PRIVATE_KEY_HEX)); + + @Test + void signCreatesVerifiableSignature() { + // generate using K256ArtifactSigner + final K256ArtifactSigner k256ArtifactSigner = new K256ArtifactSigner(EC_KEY_PAIR); + final ArtifactSignature artifactSignature = k256ArtifactSigner.sign(OBJECT_ROOT); + final byte[] signature = Bytes.fromHexString(artifactSignature.asHex()).toArray(); + + // Verify the signature against public key + assertThat( + K256TestUtil.verifySignature( + Sign.publicPointFromPrivate(EC_KEY_PAIR.getPrivateKey()), + OBJECT_ROOT.toArray(), + signature)) + .isTrue(); + + // copied from Rust K-256 and Python ecdsa module + final Bytes expectedSignature = + Bytes.fromHexString( + "8C32902BE980399CA59FCC222CCF0A5FE355A159122DEA58789A3938E29D89797FC6C9C0ECCCD29705915729F5326BB7D245F8E54D3A793A06DE3C92ABA85057"); + assertThat(Bytes.fromHexString(artifactSignature.asHex())).isEqualTo(expectedSignature); + } +} 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 1c72573e1..dd1dd7c82 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 @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.IntStream; import org.junit.jupiter.api.AfterEach; @@ -135,21 +136,22 @@ void proxySignersAreLoadedCorrectly() throws IOException { assertThatCode(() -> signerProvider.load().get()).doesNotThrowAnyException(); // assert that the proxy keys are loaded correctly - final Map> key1ProxyPublicKeys = + final Map> key1ProxyPublicKeys = signerProvider.getProxyIdentifiers(PUBLIC_KEY1); assertThat(key1ProxyPublicKeys.get(KeyType.BLS)) .containsExactlyInAnyOrder(getPublicKeysArray(key1ProxyKeyPairs)); + // proxy public keys are compressed assertThat(key1ProxyPublicKeys.get(KeyType.SECP256K1)) - .containsExactlyInAnyOrder(getSecpPublicKeysArray(key1SecpKeyPairs)); + .containsExactlyInAnyOrder(getCompressedSECPPublicKeysArray(key1SecpKeyPairs)); - final Map> key2ProxyPublicKeys = + final Map> key2ProxyPublicKeys = signerProvider.getProxyIdentifiers(PUBLIC_KEY2); assertThat(key2ProxyPublicKeys.get(KeyType.BLS)) .containsExactlyInAnyOrder(getPublicKeysArray(key2ProxyKeyPairs)); assertThat(key2ProxyPublicKeys.get(KeyType.SECP256K1)) - .containsExactlyInAnyOrder(getSecpPublicKeysArray(key2SecpKeyPairs)); + .containsExactlyInAnyOrder(getCompressedSECPPublicKeysArray(key2SecpKeyPairs)); } @Test @@ -172,7 +174,7 @@ void emptyProxySignersAreLoadedSuccessfully() { assertThatCode(() -> signerProvider.load().get()).doesNotThrowAnyException(); for (String identifier : List.of(PUBLIC_KEY1, PUBLIC_KEY2)) { - final Map> keyProxyPublicKeys = + final Map> keyProxyPublicKeys = signerProvider.getProxyIdentifiers(identifier); assertThat(keyProxyPublicKeys).isEmpty(); } @@ -220,11 +222,12 @@ private static String[] getPublicKeysArray(final List blsKeyPairs) { .toArray(String[]::new); } - private static String[] getSecpPublicKeysArray(final List ecKeyPairs) { + private static String[] getCompressedSECPPublicKeysArray(final List ecKeyPairs) { + // compressed public keys return ecKeyPairs.stream() .map( keyPair -> - EthPublicKeyUtils.toHexString( + EthPublicKeyUtils.toHexStringCompressed( EthPublicKeyUtils.web3JPublicKeyToECPublicKey(keyPair.getPublicKey()))) .toList() .toArray(String[]::new); diff --git a/signing/src/testFixtures/java/tech/pegasys/web3signer/K256TestUtil.java b/signing/src/testFixtures/java/tech/pegasys/web3signer/K256TestUtil.java new file mode 100644 index 000000000..86139a24e --- /dev/null +++ b/signing/src/testFixtures/java/tech/pegasys/web3signer/K256TestUtil.java @@ -0,0 +1,52 @@ +/* + * 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; + +import static tech.pegasys.web3signer.signing.K256ArtifactSigner.CURVE; +import static tech.pegasys.web3signer.signing.K256ArtifactSigner.calculateSHA256; + +import java.math.BigInteger; +import java.util.Arrays; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.math.ec.ECPoint; + +public class K256TestUtil { + public static boolean verifySignature( + final ECPoint pubECPoint, final byte[] message, final byte[] compactSignature) { + try { + if (compactSignature.length != 64) { + throw new IllegalStateException("Expecting 64 bytes signature in R+S format"); + } + // we are assuming that we got 64 bytes signature in R+S format + byte[] rBytes = Arrays.copyOfRange(compactSignature, 0, 32); + byte[] sBytes = Arrays.copyOfRange(compactSignature, 32, 64); + + final BigInteger r = new BigInteger(1, rBytes); + final BigInteger s = new BigInteger(1, sBytes); + + final ECPublicKeyParameters ecPublicKeyParameters = + new ECPublicKeyParameters(pubECPoint, CURVE); + + final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + signer.init(false, ecPublicKeyParameters); + // apply sha-256 before verification + return signer.verifySignature(calculateSHA256(message), r, s); + } catch (Exception e) { + throw new RuntimeException("Error verifying signature", e); + } + } +}