diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fab48afe..52474c935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 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 - Generate Proxy Keys. [#1033](https://github.com/Consensys/web3signer/pull/1033) ### 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/signer/Signer.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java index 14acb4ac0..a2fe80906 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 @@ -19,6 +19,7 @@ import static tech.pegasys.web3signer.signing.KeyType.BLS; import tech.pegasys.web3signer.core.service.http.SigningObjectMapperFactory; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.SignRequestType; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.Eth2SigningRequestBody; import tech.pegasys.web3signer.dsl.Accounts; import tech.pegasys.web3signer.dsl.Eth; @@ -47,6 +48,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 +189,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 callCommitBoostReqeustForSignature( + final SignRequestType type, final String pubkey, final Bytes32 objectRoot) { + return given() + .baseUri(getUrl()) + .contentType(ContentType.JSON) + .log() + .all() + .body( + new JsonObject() + .put("type", type.name().toLowerCase()) + .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..22b3f8535 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 @@ -18,6 +18,7 @@ import tech.pegasys.web3signer.dsl.tls.TlsCertificateDefinition; import tech.pegasys.web3signer.signing.config.AwsVaultParameters; import tech.pegasys.web3signer.signing.config.AzureKeyVaultParameters; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; import tech.pegasys.web3signer.signing.config.GcpSecretManagerParameters; import tech.pegasys.web3signer.signing.config.KeystoresParameters; @@ -81,6 +82,7 @@ public class SignerConfiguration { private final Optional v3KeystoresBulkloadParameters; private final boolean signingExtEnabled; + private final CommitBoostParameters 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 CommitBoostParameters 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 Optional.ofNullable(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..d9950b37d 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 @@ -22,6 +22,7 @@ import tech.pegasys.web3signer.dsl.tls.TlsCertificateDefinition; import tech.pegasys.web3signer.signing.config.AwsVaultParameters; import tech.pegasys.web3signer.signing.config.AzureKeyVaultParameters; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; import tech.pegasys.web3signer.signing.config.GcpSecretManagerParameters; import tech.pegasys.web3signer.signing.config.KeystoresParameters; @@ -85,6 +86,7 @@ public class SignerConfigurationBuilder { private KeystoresParameters v3KeystoresBulkloadParameters; private boolean signingExtEnabled; + private CommitBoostParameters 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 CommitBoostParameters 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, + 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..0d3addbc5 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 @@ -41,6 +41,7 @@ import tech.pegasys.web3signer.dsl.utils.DatabaseUtil; import tech.pegasys.web3signer.signing.config.AwsVaultParameters; import tech.pegasys.web3signer.signing.config.AzureKeyVaultParameters; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; import tech.pegasys.web3signer.signing.config.GcpSecretManagerParameters; import tech.pegasys.web3signer.signing.config.KeystoresParameters; @@ -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,23 @@ public List createCmdLineParams() { return params; } + private static void appendCommitBoostParameters( + final CommitBoostParameters commitBoostParameters, final StringBuilder yamlConfig) { + yamlConfig.append( + String.format( + YAML_BOOLEAN_FMT, "eth2.commit-boost-api-enabled", commitBoostParameters.isEnabled())); + yamlConfig.append( + String.format( + YAML_STRING_FMT, + "eth2.proxy-keystores-path", + commitBoostParameters.getProxyKeystoresPath().toAbsolutePath())); + yamlConfig.append( + String.format( + YAML_STRING_FMT, + "eth2.proxy-keystores-password-file", + commitBoostParameters.getProxyKeystoresPasswordFile().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..ffd1d9def 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 @@ -41,6 +41,7 @@ import tech.pegasys.web3signer.dsl.utils.DatabaseUtil; import tech.pegasys.web3signer.signing.config.AwsVaultParameters; import tech.pegasys.web3signer.signing.config.AzureKeyVaultParameters; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; import tech.pegasys.web3signer.signing.config.GcpSecretManagerParameters; import tech.pegasys.web3signer.signing.config.KeystoresParameters; @@ -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(CommitBoostParameters commitBoostParameters) { + return List.of( + "--commit-boost-api-enabled=" + commitBoostParameters.isEnabled(), + "--proxy-keystores-path", + commitBoostParameters.getProxyKeystoresPath().toAbsolutePath().toString(), + "--proxy-keystores-password-file", + commitBoostParameters.getProxyKeystoresPasswordFile().toAbsolutePath().toString()); + } + private static Consumer setV3KeystoresBulkloadParameters( final List params) { return keystoresParameters -> { diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/CommitBoostATParameters.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/CommitBoostATParameters.java new file mode 100644 index 000000000..ea12f9a05 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/CommitBoostATParameters.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.dsl.utils; + +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; + +import java.nio.file.Path; +import java.util.Objects; + +public class CommitBoostATParameters implements CommitBoostParameters { + private final boolean enabled; + private final Path proxyKeystoresPath; + private final Path proxyKeystoresPasswordFile; + + public CommitBoostATParameters( + final boolean enabled, final Path proxyKeystoresPath, final Path proxyKeystoresPasswordFile) { + this.enabled = enabled; + this.proxyKeystoresPath = proxyKeystoresPath; + this.proxyKeystoresPasswordFile = proxyKeystoresPasswordFile; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public Path getProxyKeystoresPath() { + return proxyKeystoresPath; + } + + @Override + public Path getProxyKeystoresPasswordFile() { + return proxyKeystoresPasswordFile; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CommitBoostATParameters that)) return false; + return enabled == that.enabled + && Objects.equals(proxyKeystoresPath, that.proxyKeystoresPath) + && Objects.equals(proxyKeystoresPasswordFile, that.proxyKeystoresPasswordFile); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, proxyKeystoresPath, proxyKeystoresPasswordFile); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java index 95b9df5a1..c029bead2 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java @@ -13,7 +13,7 @@ package tech.pegasys.web3signer.dsl.utils; import static java.util.Collections.emptyList; -import static tech.pegasys.web3signer.core.util.DepositSigningRootUtil.computeDomain; +import static tech.pegasys.web3signer.core.util.Web3SignerSigningRootUtil.computeDomain; import tech.pegasys.teku.api.schema.AggregateAndProof; import tech.pegasys.teku.api.schema.Attestation; @@ -48,7 +48,7 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.RandaoReveal; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.SyncCommitteeMessage; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.ValidatorRegistration; -import tech.pegasys.web3signer.core.util.DepositSigningRootUtil; +import tech.pegasys.web3signer.core.util.Web3SignerSigningRootUtil; import java.util.Random; import java.util.concurrent.ExecutionException; @@ -216,7 +216,7 @@ private static Eth2SigningRequestBody createDepositRequest() { genesisForkVersion); final Bytes32 depositDomain = computeDomain(Domain.DEPOSIT, genesisForkVersion, Bytes32.ZERO); final Bytes signingRoot = - DepositSigningRootUtil.computeSigningRoot( + Web3SignerSigningRootUtil.computeSigningRoot( depositMessage.asInternalDepositMessage(), depositDomain); return Eth2SigningRequestBodyBuilder.anEth2SigningRequestBody() .withType(ArtifactType.DEPOSIT) diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java index 1ef6afba1..b9b4b2a9d 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java @@ -60,7 +60,8 @@ static void initV3Keystores() throws IOException, GeneralSecurityException, Ciph publicKeys = new ArrayList<>(); for (int i = 0; i < 4; i++) { final ECKeyPair ecKeyPair = Keys.createEcKeyPair(); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(ecKeyPair.getPublicKey()); + final ECPublicKey ecPublicKey = + EthPublicKeyUtils.bigIntegerToECPublicKey(ecKeyPair.getPublicKey()); final String publicKeyHex = IdentifierUtils.normaliseIdentifier(EthPublicKeyUtils.toHexString(ecPublicKey)); publicKeys.add(publicKeyHex); 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..7e6b12911 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostAcceptanceTest.java @@ -0,0 +1,312 @@ +/* + * 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.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +import tech.pegasys.teku.bls.BLS; +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.networks.Eth2NetworkConfiguration; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.web3signer.BLSTestUtil; +import tech.pegasys.web3signer.K256TestUtil; +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.core.service.http.handlers.commitboost.json.SignRequestType; +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.dsl.utils.CommitBoostATParameters; +import tech.pegasys.web3signer.dsl.utils.DefaultKeystoresParameters; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; +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.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.math.ec.ECPoint; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.web3j.crypto.ECKeyPair; +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 { + public static final SigningRootGenerator SIGNING_ROOT_GENERATOR = + new SigningRootGenerator(getSpec(), Bytes32.ZERO); + private static final BLSKeyPair KEY_PAIR_1 = BLSTestUtil.randomKeyPair(0); + private static final List PROXYY_BLS_KEYS = randomBLSKeyPairs(); + private static final List PROXY_SECP_KEYS = randomECKeyPairs(); + + private static final String KEYSTORE_PASSWORD = "password"; + @TempDir private Path keystoreDir; + @TempDir private Path passwordDir; + // commit boost directories + @TempDir private Path commitBoostKeystoresPath; + @TempDir private Path commitBoostPasswordDir; + + private static List randomECKeyPairs() { + + try { + return List.of(Keys.createEcKeyPair(), Keys.createEcKeyPair()); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + private static List randomBLSKeyPairs() { + return List.of(BLSTestUtil.randomKeyPair(1), BLSTestUtil.randomKeyPair(2)); + } + + @BeforeEach + void setup() throws Exception { + // create main bls key + KeystoreUtil.createKeystore(KEY_PAIR_1, keystoreDir, passwordDir, KEYSTORE_PASSWORD); + + // commit boost proxy keys password file + final Path commitBoostPasswordFile = createCommitBoostPasswordFile(); + + createProxyBLSKeys(); + + createProxyECKeys(); + + // start web3signer with keystores and commit boost parameters + final KeystoresParameters keystoresParameters = + new DefaultKeystoresParameters(keystoreDir, passwordDir, null); + final CommitBoostParameters commitBoostParameters = + new CommitBoostATParameters(true, commitBoostKeystoresPath, commitBoostPasswordFile); + + final SignerConfigurationBuilder configBuilder = + new SignerConfigurationBuilder() + .withMode("eth2") + .withNetwork("mainnet") + .withKeystoresParameters(keystoresParameters) + .withCommitBoostParameters(commitBoostParameters); + + startSigner(configBuilder.build()); + } + + @Test + void listCommitBoostPublicKeys() { + final List proxyBlsPubKeys = getBlsProxyPubKeys(); + final List proxyECPubKeys = getProxyECPubKeys(); + + final Response response = signer.callCommitBoostGetPubKeys(); + // the response should have 1 keys entry. + response + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("keys", hasSize(1)) + .body("keys[0].consensus", equalTo(KEY_PAIR_1.getPublicKey().toHexString())) + .body("keys[0].proxy_bls", containsInAnyOrder(proxyBlsPubKeys.toArray())) + .body("keys[0].proxy_ecdsa", containsInAnyOrder(proxyECPubKeys.toArray())); + } + + @ParameterizedTest + @EnumSource(ProxyKeySignatureScheme.class) + void generateCommitBoostProxyKeys(final ProxyKeySignatureScheme scheme) { + final Response response = + signer.callCommitBoostGenerateProxyKey( + KEY_PAIR_1.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(KEY_PAIR_1.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); + }); + + // the number of (scheme) proxy public keys should've increased + final Response pubKeyResponse = signer.callCommitBoostGetPubKeys(); + final List updatedProxyKeys = + new ArrayList<>( + scheme == ProxyKeySignatureScheme.BLS ? getBlsProxyPubKeys() : getProxyECPubKeys()); + updatedProxyKeys.add(response.path("message.proxy")); + assertThat(updatedProxyKeys).hasSize(3); + + final String jsonMatch = "keys[0].proxy_" + scheme.name().toLowerCase(); + pubKeyResponse + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body(jsonMatch, containsInAnyOrder(updatedProxyKeys.toArray())); + } + + @ParameterizedTest + @EnumSource(SignRequestType.class) + void requestCommitBoostSignature(final SignRequestType signRequestType) { + final String pubKey = + switch (signRequestType) { + case CONSENSUS -> KEY_PAIR_1.getPublicKey().toHexString(); + case PROXY_BLS -> + PROXYY_BLS_KEYS.stream().findFirst().orElseThrow().getPublicKey().toHexString(); + case PROXY_ECDSA -> + EthPublicKeyUtils.getEncoded( + EthPublicKeyUtils.bigIntegerToECPublicKey( + PROXY_SECP_KEYS.stream().findFirst().orElseThrow().getPublicKey()), + true) + .toHexString(); + }; + + // object root is data to sign + final Bytes32 objectRoot = Bytes32.random(new Random(0)); + // signature is calculated on signing root + final Bytes32 signingRoot = SIGNING_ROOT_GENERATOR.computeSigningRoot(objectRoot); + + final Response response = + signer.callCommitBoostReqeustForSignature(signRequestType, pubKey, objectRoot); + + response + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body( + signRequestType == SignRequestType.PROXY_ECDSA + ? new ValidK256K1SignatureMatcher(pubKey, signingRoot) + : new ValidBLSSignatureMatcher(pubKey, signingRoot)); + } + + private static Spec getSpec() { + final Eth2NetworkConfiguration.Builder builder = Eth2NetworkConfiguration.builder(); + builder.applyNetworkDefaults(Eth2Network.MAINNET); + Eth2NetworkConfiguration eth2NetworkConfiguration = builder.build(); + return eth2NetworkConfiguration.getSpec(); + } + + private static List getProxyECPubKeys() { + return PROXY_SECP_KEYS.stream() + .map( + ecKeyPair -> + EthPublicKeyUtils.getEncoded( + EthPublicKeyUtils.bigIntegerToECPublicKey(ecKeyPair.getPublicKey()), true) + .toHexString()) + .toList(); + } + + private static List getBlsProxyPubKeys() { + return PROXYY_BLS_KEYS.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"); + } + } + + private void createProxyECKeys() throws IOException { + final Path proxySecpKeyStoreDir = + commitBoostKeystoresPath + .resolve(KEY_PAIR_1.getPublicKey().toHexString()) + .resolve("SECP256K1"); + Files.createDirectories(proxySecpKeyStoreDir); + PROXY_SECP_KEYS.forEach( + proxyECKey -> { + try { + WalletUtils.generateWalletFile( + KEYSTORE_PASSWORD, proxyECKey, proxySecpKeyStoreDir.toFile(), false); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + } + + private void createProxyBLSKeys() throws IOException { + final Path proxyBlsKeyStoreDir = + commitBoostKeystoresPath.resolve(KEY_PAIR_1.getPublicKey().toHexString()).resolve("BLS"); + Files.createDirectories(proxyBlsKeyStoreDir); + PROXYY_BLS_KEYS.forEach( + proxyBlsKey -> + KeystoreUtil.createKeystoreFile(proxyBlsKey, proxyBlsKeyStoreDir, KEYSTORE_PASSWORD)); + } + + static class ValidBLSSignatureMatcher extends TypeSafeMatcher { + private final BLSPublicKey blsPublicKey; + private final Bytes32 signingRoot; + + public ValidBLSSignatureMatcher(final String blsPublicKey, final Bytes32 signingRoot) { + this.blsPublicKey = BLSPublicKey.fromHexString(blsPublicKey); + 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"); + } + } + + static class ValidK256K1SignatureMatcher extends TypeSafeMatcher { + private final ECPoint ecPoint; + private final Bytes32 signingRoot; + + public ValidK256K1SignatureMatcher(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/publickeys/YubiHsmKeysAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java index 297ad1b13..37dec504d 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java @@ -121,7 +121,7 @@ private void createConfigurationFiles(final Set opaqueDataIds, final Ke private String getPublicKey(final String key) { return normaliseIdentifier( EthPublicKeyUtils.toHexString( - EthPublicKeyUtils.createPublicKey( + EthPublicKeyUtils.bigIntegerToECPublicKey( Credentials.create(key).getEcKeyPair().getPublicKey()))); } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java index a4d5769c8..9660d885c 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java @@ -188,13 +188,14 @@ private void signAndVerifySignature(final String publicKeyHex) { void verifySignature(final Bytes signature, final String publicKeyHex) { final ECPublicKey expectedPublicKey = - EthPublicKeyUtils.createPublicKey(Bytes.fromHexString(publicKeyHex)); + EthPublicKeyUtils.bytesToECPublicKey(Bytes.fromHexString(publicKeyHex)); final byte[] r = signature.slice(0, 32).toArray(); final byte[] s = signature.slice(32, 32).toArray(); final byte[] v = signature.slice(64).toArray(); final BigInteger messagePublicKey = recoverPublicKey(new SignatureData(v, r, s)); - assertThat(EthPublicKeyUtils.createPublicKey(messagePublicKey)).isEqualTo(expectedPublicKey); + assertThat(EthPublicKeyUtils.bigIntegerToECPublicKey(messagePublicKey)) + .isEqualTo(expectedPublicKey); } private BigInteger recoverPublicKey(final SignatureData signature) { diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliNetworkOverrides.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliNetworkOverrides.java new file mode 100644 index 000000000..f1e49851b --- /dev/null +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliNetworkOverrides.java @@ -0,0 +1,111 @@ +/* + * 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.commandline; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.networks.Eth2NetworkConfiguration; +import tech.pegasys.web3signer.commandline.subcommands.Eth2SubCommand; + +import org.apache.commons.lang3.StringUtils; +import picocli.CommandLine; + +/** Mixin class to hold network overrides for the PicoCLI parser. */ +public class PicoCliNetworkOverrides { + @CommandLine.Option( + names = {"--Xnetwork-altair-fork-epoch"}, + hidden = true, + paramLabel = "", + description = "Override the Altair fork activation epoch.", + arity = "1", + converter = Eth2SubCommand.UInt64Converter.class) + private UInt64 altairForkEpoch; + + @CommandLine.Option( + names = {"--Xnetwork-bellatrix-fork-epoch"}, + hidden = true, + paramLabel = "", + description = "Override the Bellatrix fork activation epoch.", + arity = "1", + converter = Eth2SubCommand.UInt64Converter.class) + private UInt64 bellatrixForkEpoch; + + @CommandLine.Option( + names = {"--Xnetwork-capella-fork-epoch"}, + hidden = true, + paramLabel = "", + description = "Override the Capella fork activation epoch.", + arity = "1", + converter = Eth2SubCommand.UInt64Converter.class) + private UInt64 capellaForkEpoch; + + @CommandLine.Option( + names = {"--Xnetwork-deneb-fork-epoch"}, + hidden = true, + paramLabel = "", + description = "Override the Deneb fork activation epoch.", + arity = "1", + converter = Eth2SubCommand.UInt64Converter.class) + private UInt64 denebForkEpoch; + + @CommandLine.Option( + names = {"--Xnetwork-electra-fork-epoch"}, + hidden = true, + paramLabel = "", + description = "Override the Electra fork activation epoch.", + arity = "1", + converter = Eth2SubCommand.UInt64Converter.class) + private UInt64 electraForkEpoch; + + @CommandLine.Option( + names = {"--Xtrusted-setup"}, + hidden = true, + paramLabel = "", + description = + "The trusted setup which is needed for KZG commitments. Only required when creating a custom network. This value should be a file or URL pointing to a trusted setup.", + arity = "1") + private String trustedSetup = null; // Depends on network configuration + + @CommandLine.Option( + names = {"--Xgenesis-state"}, + paramLabel = "", + hidden = true, + description = + "Override the genesis state. This value should be a file or URL pointing to an SSZ-encoded finalized checkpoint " + + "state.", + arity = "1") + private String genesisState; + + public void applyOverrides(final Eth2NetworkConfiguration.Builder builder) { + if (altairForkEpoch != null) { + builder.altairForkEpoch(altairForkEpoch); + } + if (bellatrixForkEpoch != null) { + builder.bellatrixForkEpoch(bellatrixForkEpoch); + } + if (capellaForkEpoch != null) { + builder.capellaForkEpoch(capellaForkEpoch); + } + if (denebForkEpoch != null) { + builder.denebForkEpoch(denebForkEpoch); + } + if (electraForkEpoch != null) { + builder.electraForkEpoch(electraForkEpoch); + } + if (trustedSetup != null) { + builder.trustedSetup(trustedSetup); + } + if (StringUtils.isNotBlank(genesisState)) { + builder.customGenesisState(genesisState); + } + } +} diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/config/PicoCommitBoostApiParameters.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/config/PicoCommitBoostApiParameters.java index cf2c4293d..b78703658 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/config/PicoCommitBoostApiParameters.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/config/PicoCommitBoostApiParameters.java @@ -14,17 +14,26 @@ import static tech.pegasys.web3signer.commandline.DefaultCommandValues.PATH_FORMAT_HELP; -import tech.pegasys.web3signer.signing.config.KeystoresParameters; +import tech.pegasys.teku.networks.Eth2NetworkConfiguration; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.datastructures.util.ChainDataLoader; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; import java.nio.file.Path; +import org.apache.tuweni.bytes.Bytes32; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; import picocli.CommandLine.ParameterException; import picocli.CommandLine.Spec; -public class PicoCommitBoostApiParameters implements KeystoresParameters { +public class PicoCommitBoostApiParameters implements CommitBoostParameters { + // commit boost client defaults gvr to ZERO for its domain calculations. Set this to `false` to + // use the actual network's gvr value + private static final boolean USE_ZERO_GENESIS_VALIDATORS_ROOT = true; + private Bytes32 genesisValidatorsRoot = Bytes32.ZERO; + @Spec private CommandSpec commandSpec; // injected by picocli @CommandLine.Option( @@ -39,50 +48,76 @@ public class PicoCommitBoostApiParameters implements KeystoresParameters { description = "The path to a writeable directory to store v3 and v4 proxy keystores for commit boost API.", paramLabel = PATH_FORMAT_HELP) - private Path keystoresPath; + private Path proxyKeystoresPath; @Option( names = {"--proxy-keystores-password-file"}, description = "The path to the password file used to encrypt/decrypt proxy keystores for commit boost API.", paramLabel = PATH_FORMAT_HELP) - private Path keystoresPasswordFile; + private Path proxyKeystoresPasswordFile; @Override - public Path getKeystoresPath() { - return keystoresPath; + public boolean isEnabled() { + return isCommitBoostApiEnabled; } @Override - public Path getKeystoresPasswordsPath() { - return null; + public Path getProxyKeystoresPath() { + return proxyKeystoresPath; } @Override - public Path getKeystoresPasswordFile() { - return keystoresPasswordFile; + public Path getProxyKeystoresPasswordFile() { + return proxyKeystoresPasswordFile; } @Override - public boolean isEnabled() { - return isCommitBoostApiEnabled; + public Bytes32 getGenesisValidatorsRoot() { + return genesisValidatorsRoot; } - public void validateParameters() throws ParameterException { + /** + * Validate the parameters for the commit boost API and initialize parameters which will be used + * during run operation. + */ + public void validateParameters(final Eth2NetworkConfiguration eth2NetworkConfig) + throws ParameterException { if (!isCommitBoostApiEnabled) { return; } - if (keystoresPath == null) { + if (proxyKeystoresPath == null) { throw new ParameterException( commandSpec.commandLine(), "Commit boost API is enabled, but --proxy-keystores-path not set"); } - if (keystoresPasswordFile == null) { + if (proxyKeystoresPasswordFile == null) { throw new ParameterException( commandSpec.commandLine(), "Commit boost API is enabled, but --proxy-keystores-password-file not set"); } + + loadGenesisValidatorsRoot(eth2NetworkConfig); + } + + /** Load genesis state and obtain genesis validators root. */ + private void loadGenesisValidatorsRoot(final Eth2NetworkConfiguration eth2NetworkConfig) { + if (USE_ZERO_GENESIS_VALIDATORS_ROOT) { + return; + } + try { + final String genesisState = + eth2NetworkConfig.getNetworkBoostrapConfig().getGenesisState().orElseThrow(); + + final BeaconState beaconState = + ChainDataLoader.loadState(eth2NetworkConfig.getSpec(), genesisState); + this.genesisValidatorsRoot = beaconState.getGenesisValidatorsRoot(); + } catch (final Exception e) { + throw new ParameterException( + commandSpec.commandLine(), + "Unable to load genesis state to determine genesis validators root. Please provide custom genesis state using --Xgenesis-state"); + } } } diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java index 92bf6ef19..ea4d2d8e8 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java @@ -28,6 +28,7 @@ import tech.pegasys.web3signer.commandline.PicoCliAwsSecretsManagerParameters; import tech.pegasys.web3signer.commandline.PicoCliEth2AzureKeyVaultParameters; import tech.pegasys.web3signer.commandline.PicoCliGcpSecretManagerParameters; +import tech.pegasys.web3signer.commandline.PicoCliNetworkOverrides; import tech.pegasys.web3signer.commandline.PicoCliSlashingProtectionParameters; import tech.pegasys.web3signer.commandline.config.PicoCommitBoostApiParameters; import tech.pegasys.web3signer.commandline.config.PicoKeystoresParameters; @@ -91,60 +92,6 @@ private static class NetworkCliCompletionCandidates extends ArrayList { arity = "1") private String network; - @CommandLine.Option( - names = {"--Xnetwork-altair-fork-epoch"}, - hidden = true, - paramLabel = "", - description = "Override the Altair fork activation epoch.", - arity = "1", - converter = UInt64Converter.class) - private UInt64 altairForkEpoch; - - @CommandLine.Option( - names = {"--Xnetwork-bellatrix-fork-epoch"}, - hidden = true, - paramLabel = "", - description = "Override the Bellatrix fork activation epoch.", - arity = "1", - converter = UInt64Converter.class) - private UInt64 bellatrixForkEpoch; - - @CommandLine.Option( - names = {"--Xnetwork-capella-fork-epoch"}, - hidden = true, - paramLabel = "", - description = "Override the Capella fork activation epoch.", - arity = "1", - converter = UInt64Converter.class) - private UInt64 capellaForkEpoch; - - @CommandLine.Option( - names = {"--Xnetwork-deneb-fork-epoch"}, - hidden = true, - paramLabel = "", - description = "Override the Deneb fork activation epoch.", - arity = "1", - converter = UInt64Converter.class) - private UInt64 denebForkEpoch; - - @CommandLine.Option( - names = {"--Xnetwork-electra-fork-epoch"}, - hidden = true, - paramLabel = "", - description = "Override the Electra fork activation epoch.", - arity = "1", - converter = UInt64Converter.class) - private UInt64 electraForkEpoch; - - @CommandLine.Option( - names = {"--Xtrusted-setup"}, - hidden = true, - paramLabel = "", - description = - "The trusted setup which is needed for KZG commitments. Only required when creating a custom network. This value should be a file or URL pointing to a trusted setup.", - arity = "1") - private String trustedSetup = null; // Depends on network configuration - @CommandLine.Option( names = {"--key-manager-api-enabled", "--enable-key-manager-api"}, paramLabel = "", @@ -160,13 +107,16 @@ private static class NetworkCliCompletionCandidates extends ArrayList { hidden = true) private boolean signingExtEnabled = false; + @Mixin private PicoCliNetworkOverrides networkOverrides; @Mixin private PicoCliSlashingProtectionParameters slashingProtectionParameters; @Mixin private PicoCliEth2AzureKeyVaultParameters azureKeyVaultParameters; @Mixin private PicoKeystoresParameters keystoreParameters; @Mixin private PicoCliAwsSecretsManagerParameters awsSecretsManagerParameters; @Mixin private PicoCliGcpSecretManagerParameters gcpSecretManagerParameters; @Mixin private PicoCommitBoostApiParameters commitBoostApiParameters; - private tech.pegasys.teku.spec.Spec eth2Spec; + + // eth2 network configuration is initialized during validation + private Eth2NetworkConfiguration eth2NetworkConfig; public Eth2SubCommand() { network = "mainnet"; @@ -183,14 +133,14 @@ public Runner createRunner() { keystoreParameters, awsSecretsManagerParameters, gcpSecretManagerParameters, - eth2Spec, + eth2NetworkConfig.getSpec(), isKeyManagerApiEnabled, signingExtEnabled, commitBoostApiParameters); } private void logNetworkSpecInformation() { - final ForkSchedule forkSchedule = eth2Spec.getForkSchedule(); + final ForkSchedule forkSchedule = eth2NetworkConfig.getSpec().getForkSchedule(); final Map milestoneSlotMap = forkSchedule .streamMilestoneBoundarySlots() @@ -212,41 +162,9 @@ private void logNetworkSpecInformation() { LOG.info(logString); } - private Eth2NetworkConfiguration createEth2NetworkConfig() { - Eth2NetworkConfiguration.Builder builder = Eth2NetworkConfiguration.builder(); - builder.applyNetworkDefaults(network); - if (altairForkEpoch != null) { - builder.altairForkEpoch(altairForkEpoch); - } - if (bellatrixForkEpoch != null) { - builder.bellatrixForkEpoch(bellatrixForkEpoch); - } - if (capellaForkEpoch != null) { - builder.capellaForkEpoch(capellaForkEpoch); - } - if (denebForkEpoch != null) { - builder.denebForkEpoch(denebForkEpoch); - } - if (electraForkEpoch != null) { - builder.electraForkEpoch(electraForkEpoch); - } - if (trustedSetup != null) { - builder.trustedSetup(trustedSetup); - } - return builder.build(); - } - @Override protected void validateArgs() { - try { - Eth2NetworkConfiguration eth2NetworkConfig = createEth2NetworkConfig(); - eth2Spec = eth2NetworkConfig.getSpec(); - } catch (final IllegalArgumentException e) { - throw new ParameterException( - commandSpec.commandLine(), - "Failed to load network " + network + " due to " + e.getMessage(), - e); - } + this.eth2NetworkConfig = createEth2NetworkConfiguration(); if (slashingProtectionParameters.isEnabled() && slashingProtectionParameters.getDbUrl() == null) { @@ -264,7 +182,22 @@ protected void validateArgs() { validateKeystoreParameters(keystoreParameters); validateAwsSecretsManageParameters(); validateGcpSecretManagerParameters(); - commitBoostApiParameters.validateParameters(); + + commitBoostApiParameters.validateParameters(this.eth2NetworkConfig); + } + + private Eth2NetworkConfiguration createEth2NetworkConfiguration() { + try { + final Eth2NetworkConfiguration.Builder builder = Eth2NetworkConfiguration.builder(); + builder.applyNetworkDefaults(network); + networkOverrides.applyOverrides(builder); // custom fork epochs + return builder.build(); + } catch (final IllegalArgumentException e) { + throw new ParameterException( + commandSpec.commandLine(), + "Failed to load network " + network + " due to " + e.getMessage(), + e); + } } private void validateGcpSecretManagerParameters() { diff --git a/core/build.gradle b/core/build.gradle index edba2f10f..90d2b4663 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'tech.pegasys.teku.internal:bls-keystore' implementation 'tech.pegasys.teku.internal:serializer' implementation 'tech.pegasys.teku.internal:spec' + implementation 'tech.pegasys.teku.internal:networks' implementation 'tech.pegasys.teku.internal:unsigned' implementation 'tech.pegasys.teku.internal:jackson' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl' diff --git a/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java b/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java index 4a1ad0892..14f711301 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java @@ -13,6 +13,7 @@ package tech.pegasys.web3signer.core; import static org.web3j.crypto.Keys.getAddress; +import static tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils.ecPublicKeyToBigInteger; import static tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils.toHexString; import static tech.pegasys.web3signer.signing.secp256k1.util.AddressUtil.remove0xPrefix; @@ -31,11 +32,7 @@ public Eth1AddressSignerIdentifier(final String address) { } public static SignerIdentifier fromPublicKey(final ECPublicKey publicKey) { - return new Eth1AddressSignerIdentifier(getAddress(toHexString(publicKey))); - } - - public static SignerIdentifier fromPublicKey(final String publicKey) { - return new Eth1AddressSignerIdentifier(getAddress(publicKey)); + return new Eth1AddressSignerIdentifier(getAddress(ecPublicKeyToBigInteger(publicKey))); } @Override 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 dd1a3abdf..de12fbfa6 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java @@ -25,7 +25,9 @@ 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.CommitBoostRequestSignatureRoute; import tech.pegasys.web3signer.core.routes.eth2.Eth2SignExtensionRoute; import tech.pegasys.web3signer.core.routes.eth2.Eth2SignRoute; import tech.pegasys.web3signer.core.routes.eth2.HighWatermarkRoute; @@ -43,6 +45,7 @@ import tech.pegasys.web3signer.signing.config.AwsVaultParameters; import tech.pegasys.web3signer.signing.config.AzureKeyVaultFactory; import tech.pegasys.web3signer.signing.config.AzureKeyVaultParameters; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; import tech.pegasys.web3signer.signing.config.GcpSecretManagerParameters; import tech.pegasys.web3signer.signing.config.KeystoresParameters; @@ -89,7 +92,7 @@ public class Eth2Runner extends Runner { private final Spec eth2Spec; private final boolean isKeyManagerApiEnabled; private final boolean signingExtEnabled; - private final KeystoresParameters commitBoostApiParameters; + private final CommitBoostParameters commitBoostParameters; public Eth2Runner( final BaseConfig baseConfig, @@ -101,7 +104,7 @@ public Eth2Runner( final Spec eth2Spec, final boolean isKeyManagerApiEnabled, final boolean signingExtEnabled, - final KeystoresParameters commitBoostApiParameters) { + final CommitBoostParameters commitBoostParameters) { super(baseConfig); this.slashingProtectionContext = createSlashingProtection(slashingProtectionParameters); this.azureKeyVaultParameters = azureKeyVaultParameters; @@ -113,7 +116,7 @@ public Eth2Runner( this.awsVaultParameters = awsVaultParameters; this.gcpSecretManagerParameters = gcpSecretManagerParameters; this.signingExtEnabled = signingExtEnabled; - this.commitBoostApiParameters = commitBoostApiParameters; + this.commitBoostParameters = commitBoostParameters; } private Optional createSlashingProtection( @@ -141,8 +144,10 @@ public void populateRouter(final Context context) { if (isKeyManagerApiEnabled) { new KeyManagerApiRoute(context, baseConfig, slashingProtectionContext).register(); } - if (commitBoostApiParameters.isEnabled()) { + if (commitBoostParameters.isEnabled()) { new CommitBoostPublicKeysRoute(context).register(); + new CommitBoostGenerateProxyKeyRoute(context, commitBoostParameters, eth2Spec).register(); + new CommitBoostRequestSignatureRoute(context, commitBoostParameters, eth2Spec).register(); } } @@ -174,7 +179,7 @@ protected List createArtifactSignerProvider( return signers; } }, - Optional.of(commitBoostApiParameters))); + Optional.of(commitBoostParameters))); } private MappedResults loadSignersFromKeyConfigFiles( @@ -235,13 +240,12 @@ private MappedResults bulkLoadSigners( if (keystoresParameters.isEnabled()) { LOG.info("Bulk loading keys from local keystores ... "); - final BlsKeystoreBulkLoader blsKeystoreBulkLoader = new BlsKeystoreBulkLoader(); final MappedResults keystoreSignersResult = keystoresParameters.hasKeystoresPasswordsPath() - ? blsKeystoreBulkLoader.loadKeystoresUsingPasswordDir( + ? BlsKeystoreBulkLoader.loadKeystoresUsingPasswordDir( keystoresParameters.getKeystoresPath(), keystoresParameters.getKeystoresPasswordsPath()) - : blsKeystoreBulkLoader.loadKeystoresUsingPasswordFile( + : BlsKeystoreBulkLoader.loadKeystoresUsingPasswordFile( keystoresParameters.getKeystoresPath(), keystoresParameters.getKeystoresPasswordFile()); LOG.info( diff --git a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/Eth1SignRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/Eth1SignRoute.java index cdd1db7f1..ba4b3d9cd 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/Eth1SignRoute.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/Eth1SignRoute.java @@ -20,7 +20,6 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.SignerForIdentifier; import tech.pegasys.web3signer.core.service.http.metrics.HttpApiMetrics; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.SecpArtifactSignature; import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; import java.util.Optional; @@ -33,7 +32,7 @@ public class Eth1SignRoute implements Web3SignerRoute { private final Context context; private final ArtifactSignerProvider signerProvider; - private final SignerForIdentifier secpSigner; + private final SignerForIdentifier secpSigner; public Eth1SignRoute(final Context context) { this.context = context; @@ -46,9 +45,7 @@ public Eth1SignRoute(final Context context) { if (first.isPresent()) { signerProvider = first.get(); - secpSigner = - new SignerForIdentifier<>( - signerProvider, sig -> SecpArtifactSignature.toBytes(sig).toHexString(), SECP256K1); + secpSigner = new SignerForIdentifier(signerProvider); } else { throw new IllegalStateException( "No DefaultArtifactSignerProvider found in Context for eth1 mode"); diff --git a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/JsonRpcRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/JsonRpcRoute.java index ffefa21b3..f78a72a2f 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/JsonRpcRoute.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth1/JsonRpcRoute.java @@ -12,8 +12,6 @@ */ package tech.pegasys.web3signer.core.routes.eth1; -import static tech.pegasys.web3signer.signing.KeyType.SECP256K1; - import tech.pegasys.web3signer.core.Context; import tech.pegasys.web3signer.core.Runner; import tech.pegasys.web3signer.core.WebClientOptionsFactory; @@ -38,11 +36,8 @@ import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.SendTransactionHandler; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.SecpArtifactSignature; import tech.pegasys.web3signer.signing.config.SecpArtifactSignerProviderAdapter; -import java.util.Optional; - import io.vertx.core.Vertx; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpMethod; @@ -62,18 +57,17 @@ public class JsonRpcRoute implements Web3SignerRoute { public JsonRpcRoute(final Context context, final Eth1Config eth1Config) { this.context = context; - // we need signerProvider which is an instance of SecpArtifactSignerProviderAdapter - final Optional first = + // we need signerProvider which is an instance of SecpArtifactSignerProviderAdapter which uses + // eth1 address as identifier + final ArtifactSignerProvider signerProvider = context.getArtifactSignerProviders().stream() .filter(provider -> provider instanceof SecpArtifactSignerProviderAdapter) - .findFirst(); - final ArtifactSignerProvider signerProvider; - if (first.isPresent()) { - signerProvider = first.get(); - } else { - throw new IllegalStateException( - "No SecpArtifactSignerProviderAdapter found in Context for eth1 mode"); - } + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + "No SecpArtifactSignerProviderAdapter found in Context for eth1 mode")); + // use same instance of downstreamHttpClient and path calculator for all requests final HttpClient downstreamHttpClient = createDownstreamHttpClient(eth1Config, context.getVertx()); @@ -126,11 +120,8 @@ private static RequestMapper createRequestMapper( final long chainId) { final PassThroughHandler defaultHandler = new PassThroughHandler(transmitterFactory, JSON_DECODER); - final SignerForIdentifier secpSigner = - new SignerForIdentifier<>( - signerProviderMappedToEth1Address, - sig -> SecpArtifactSignature.toBytes(sig).toHexString(), - SECP256K1); + final SignerForIdentifier secpSigner = + new SignerForIdentifier(signerProviderMappedToEth1Address); final TransactionFactory transactionFactory = new TransactionFactory(chainId, JSON_DECODER, transmitterFactory); final SendTransactionHandler sendTransactionHandler = 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..69d1e8330 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostGenerateProxyKeyRoute.java @@ -0,0 +1,91 @@ +/* + * 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.CommitBoostParameters; +import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; + +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 CommitBoostParameters commitBoostParameters; + private final Spec eth2Spec; + + public CommitBoostGenerateProxyKeyRoute( + final Context context, + final CommitBoostParameters 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/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/routes/eth2/CommitBoostRequestSignatureRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostRequestSignatureRoute.java new file mode 100644 index 000000000..820040851 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/CommitBoostRequestSignatureRoute.java @@ -0,0 +1,94 @@ +/* + * 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.CommitBoostRequestSignatureHandler; +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; +import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; + +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; + +public class CommitBoostRequestSignatureRoute implements Web3SignerRoute { + private static final String PATH = "/signer/v1/request_signature"; + private final Context context; + private final CommitBoostParameters commitBoostParameters; + private final Spec eth2Spec; + private final ArtifactSignerProvider artifactSignerProvider; + + public CommitBoostRequestSignatureRoute( + final Context context, + final CommitBoostParameters 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( + () -> + new IllegalStateException( + "No DefaultArtifactSignerProvider found in Context for eth2 mode")); + } + + @Override + public void register() { + context + .getRouter() + .route(HttpMethod.POST, PATH) + .blockingHandler( + new CommitBoostRequestSignatureHandler( + 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/routes/eth2/Eth2SignExtensionRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignExtensionRoute.java index c5caac682..a338b95d8 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignExtensionRoute.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignExtensionRoute.java @@ -12,14 +12,11 @@ */ package tech.pegasys.web3signer.core.routes.eth2; -import static tech.pegasys.web3signer.signing.KeyType.BLS; - import tech.pegasys.web3signer.core.Context; import tech.pegasys.web3signer.core.routes.Web3SignerRoute; import tech.pegasys.web3signer.core.service.http.handlers.signing.SignerForIdentifier; import tech.pegasys.web3signer.core.service.http.handlers.signing.SigningExtensionHandler; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.BlsArtifactSignature; import io.vertx.core.http.HttpMethod; import io.vertx.core.json.JsonObject; @@ -28,7 +25,7 @@ public class Eth2SignExtensionRoute implements Web3SignerRoute { public static final String SIGN_EXT_PATH = "/api/v1/eth2/ext/sign/:identifier"; private final Context context; - private final SignerForIdentifier blsSigner; + private final SignerForIdentifier blsSigner; public Eth2SignExtensionRoute(final Context context) { this.context = context; @@ -36,11 +33,14 @@ public Eth2SignExtensionRoute(final Context context) { // there should be only one ArtifactSignerProvider in eth2 mode at the moment which is of BLS // types. final ArtifactSignerProvider artifactSignerProvider = - context.getArtifactSignerProviders().stream().findFirst().orElseThrow(); + context.getArtifactSignerProviders().stream() + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + "No ArtifactSignerProvider found in Context for eth2 mode")); - blsSigner = - new SignerForIdentifier<>( - artifactSignerProvider, sig -> sig.getSignatureData().toString(), BLS); + blsSigner = new SignerForIdentifier(artifactSignerProvider); } @Override diff --git a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignRoute.java b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignRoute.java index c54e42da6..27aaa2c38 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignRoute.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/routes/eth2/Eth2SignRoute.java @@ -23,7 +23,6 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.Eth2SignForIdentifierHandler; import tech.pegasys.web3signer.core.service.http.metrics.HttpApiMetrics; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.BlsArtifactSignature; import tech.pegasys.web3signer.slashingprotection.SlashingProtection; import tech.pegasys.web3signer.slashingprotection.SlashingProtectionContext; @@ -36,7 +35,7 @@ public class Eth2SignRoute implements Web3SignerRoute { private static final String SIGN_PATH = "/api/v1/eth2/sign/:identifier"; private final Context context; - private final SignerForIdentifier blsSigner; + private final SignerForIdentifier blsSigner; private final ObjectMapper objectMapper = SigningObjectMapperFactory.createObjectMapper(); private final Spec eth2Spec; private final Optional slashingProtection; @@ -52,11 +51,14 @@ public Eth2SignRoute( // there should be only one ArtifactSignerProvider in eth2 mode at the moment which is of BLS // types. final ArtifactSignerProvider artifactSignerProvider = - context.getArtifactSignerProviders().stream().findFirst().orElseThrow(); + context.getArtifactSignerProviders().stream() + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + "No ArtifactSignerProvider found in Context for eth2 mode")); - blsSigner = - new SignerForIdentifier<>( - artifactSignerProvider, sig -> sig.getSignatureData().toString(), BLS); + blsSigner = new SignerForIdentifier(artifactSignerProvider); } @Override 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..8d88bd62d --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostGenerateProxyKeyHandler.java @@ -0,0 +1,112 @@ +/* + * 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.GenerateProxyKeyBody; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.ProxyDelegation; +import tech.pegasys.web3signer.core.service.http.handlers.commitboost.json.SignRequestType; +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.CommitBoostParameters; + +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 ProxyKeyGenerator proxyKeyGenerator; + private final SigningRootGenerator signingRootGenerator; + private final CommitBoostSignerProvider commitBoostSignerProvider; + + public CommitBoostGenerateProxyKeyHandler( + final ArtifactSignerProvider artifactSignerProvider, + final CommitBoostParameters commitBoostParameters, + final Spec eth2Spec) { + commitBoostSignerProvider = new CommitBoostSignerProvider(artifactSignerProvider); + proxyKeyGenerator = new ProxyKeyGenerator(commitBoostParameters); + signingRootGenerator = + new SigningRootGenerator(eth2Spec, commitBoostParameters.getGenesisValidatorsRoot()); + } + + @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, SignRequestType.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, SignRequestType.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/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/CommitBoostRequestSignatureHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostRequestSignatureHandler.java new file mode 100644 index 000000000..4354c8742 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostRequestSignatureHandler.java @@ -0,0 +1,88 @@ +/* + * 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.RequestSignatureBody; +import tech.pegasys.web3signer.signing.ArtifactSignerProvider; +import tech.pegasys.web3signer.signing.config.CommitBoostParameters; + +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 CommitBoostRequestSignatureHandler 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 commitBoostSigner; + private final SigningRootGenerator signingRootGenerator; + + public CommitBoostRequestSignatureHandler( + final ArtifactSignerProvider artifactSignerProvider, + final CommitBoostParameters commitBoostParameters, + final Spec eth2Spec) { + commitBoostSigner = new CommitBoostSignerProvider(artifactSignerProvider); + signingRootGenerator = + new SigningRootGenerator(eth2Spec, commitBoostParameters.getGenesisValidatorsRoot()); + } + + @Override + public void handle(final RoutingContext context) { + final String body = context.body().asString(); + + // read and validate incoming json body + final RequestSignatureBody requestSignatureBody; + try { + requestSignatureBody = JSON_MAPPER.readValue(body, RequestSignatureBody.class); + } catch (final JsonProcessingException | IllegalArgumentException e) { + context.fail(BAD_REQUEST); + return; + } + try { + // Check for pubkey based on signing type, if not exist, fail with 404 + final String identifier = normaliseIdentifier(requestSignatureBody.publicKey()); + if (!commitBoostSigner.isSignerAvailable(identifier, requestSignatureBody.type())) { + context.fail(NOT_FOUND); + return; + } + + // Calculate Signing root and sign the request + final Bytes32 signingRoot = + signingRootGenerator.computeSigningRoot(requestSignatureBody.objectRoot()); + final Optional optionalSig = + commitBoostSigner.sign(identifier, requestSignatureBody.type(), signingRoot); + if (optionalSig.isEmpty()) { + context.fail(NOT_FOUND); + return; + } + + // Encode and send response + final String jsonEncoded = JSON_MAPPER.writeValueAsString(optionalSig.get()); + 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..9fbc7d1eb --- /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.SignRequestType; +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 SignRequestType 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 SignRequestType type, final Bytes32 signingRoot) { + final Optional optionalArtifactSigner = + type == SignRequestType.CONSENSUS + ? artifactSignerProvider.getSigner(identifier) + : artifactSignerProvider.getProxySigner(identifier); + + return optionalArtifactSigner.map(signer -> signer.sign(signingRoot).asHex()); + } + + /** + * Add a proxy signer to the provider + * + * @param artifactSigner The proxy signer to add + * @param consensusPubKey The consensus public key to associate with the proxy signer + */ + public void addProxySigner(final ArtifactSigner artifactSigner, final String consensusPubKey) { + artifactSignerProvider.addProxySigner(artifactSigner, consensusPubKey); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeyGenerator.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeyGenerator.java new file mode 100644 index 000000000..09c49fe92 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeyGenerator.java @@ -0,0 +1,146 @@ +/* + * Copyright 2024 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.web3signer.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.CommitBoostParameters; +import tech.pegasys.web3signer.signing.config.metadata.SignerOrigin; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.interfaces.ECPublicKey; + +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.Keys; +import org.web3j.crypto.Wallet; +import org.web3j.crypto.WalletFile; +import org.web3j.crypto.exception.CipherException; + +/** Proxy Key Generator class to generate proxy keys for CommitBoost API */ +public class ProxyKeyGenerator { + private static final Logger LOG = LogManager.getLogger(); + private static final ObjectMapper JSON_MAPPER = SigningObjectMapperFactory.createObjectMapper(); + + private final SecureRandom secureRandom = new SecureRandom(); + private final CommitBoostParameters commitBoostParameters; + + public ProxyKeyGenerator(final CommitBoostParameters commitBoostParameters) { + this.commitBoostParameters = commitBoostParameters; + } + + public ArtifactSigner generateECProxyKey(final String identifier) { + try { + final ECKeyPair ecKeyPair = Keys.createEcKeyPair(secureRandom); + final Path ecWalletFile = createECWalletFile(ecKeyPair, identifier); + LOG.debug("Created proxy EC wallet file {} for identifier: {}", ecWalletFile, identifier); + return new K256ArtifactSigner(ecKeyPair); + } catch (final GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public ArtifactSigner generateBLSProxyKey(final String identifier) throws UncheckedIOException { + final BLSKeyPair blsKeyPair = BLSKeyPair.random(secureRandom); + final Path blsKeystoreFile = createBLSKeystoreFile(blsKeyPair, identifier); + LOG.debug("Created proxy BLS keystore file {} for identifier: {}", blsKeystoreFile, identifier); + return new BlsArtifactSigner(blsKeyPair, SignerOrigin.FILE_KEYSTORE); + } + + private Path createBLSKeystoreFile(final BLSKeyPair keyPair, final String identifier) { + final Bytes salt = Bytes.random(32, secureRandom); + final Bytes iv = Bytes.random(16, secureRandom); + 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.getProxyKeystoresPasswordFile()); + final KeyStoreData keyStoreData = + KeyStore.encrypt( + keyPair.getSecretKey().toBytes(), publicKey, password, "", kdfParam, cipher); + try { + final Path keystoreDir = + createSubDirectories( + commitBoostParameters.getProxyKeystoresPath(), identifier, 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 identifier) { + final String password = readFile(commitBoostParameters.getProxyKeystoresPasswordFile()); + final Path keystoreDir = + createSubDirectories( + commitBoostParameters.getProxyKeystoresPath(), identifier, KeyType.SECP256K1); + final ECPublicKey ecPublicKey = + EthPublicKeyUtils.bigIntegerToECPublicKey(ecKeyPair.getPublicKey()); + final String compressedPubHex = EthPublicKeyUtils.getEncoded(ecPublicKey, true).toHexString(); + + 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) throws UncheckedIOException { + final String password; + try { + password = Files.readString(file, StandardCharsets.UTF_8); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + return password; + } + + 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..1e302bbb4 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/SigningRootGenerator.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.core.service.http.handlers.commitboost; + +import tech.pegasys.teku.infrastructure.bytes.Bytes4; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.web3signer.core.util.Web3SignerSigningRootUtil; + +import com.google.common.annotations.VisibleForTesting; +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 final Bytes32 domain; + + public SigningRootGenerator(final Spec eth2Spec, final Bytes32 genesisValidatorsRoot) { + final Bytes4 genesisForkVersion = eth2Spec.getGenesisSpec().getConfig().getGenesisForkVersion(); + domain = + Web3SignerSigningRootUtil.computeDomain( + COMMIT_BOOST_DOMAIN, genesisForkVersion, genesisValidatorsRoot); + } + + /** + * 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 root + */ + public Bytes32 computeSigningRoot(final Bytes32 objectRoot) { + return Web3SignerSigningRootUtil.computeSigningRoot(objectRoot, domain); + } + + @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..d23f606b2 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/datastructure/SszSECPPublicKey.java @@ -0,0 +1,46 @@ +/* + * 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; + +public class SszSECPPublicKey extends SszByteVectorImpl { + + private final Supplier publicKey; + + public SszSECPPublicKey(final ECPublicKey publicKey) { + super(SszSECPPublicKeySchema.INSTANCE, EthPublicKeyUtils.getEncoded(publicKey, true)); + 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/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/signing/SignatureFormatter.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyKeySignatureScheme.java similarity index 66% rename from core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SignatureFormatter.java rename to core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyKeySignatureScheme.java index 103c531c5..3f33902d9 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SignatureFormatter.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/ProxyKeySignatureScheme.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 ConsenSys AG. + * 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 @@ -10,12 +10,9 @@ * 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.signing; +package tech.pegasys.web3signer.core.service.http.handlers.commitboost.json; -import tech.pegasys.web3signer.signing.ArtifactSignature; - -@FunctionalInterface -public interface SignatureFormatter { - - String format(T signature); +public enum ProxyKeySignatureScheme { + BLS, + ECDSA } 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/commitboost/json/RequestSignatureBody.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/RequestSignatureBody.java new file mode 100644 index 000000000..f8087d224 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/RequestSignatureBody.java @@ -0,0 +1,21 @@ +/* + * 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; +import org.apache.tuweni.bytes.Bytes32; + +public record RequestSignatureBody( + @JsonProperty(value = "type", required = true) SignRequestType type, + @JsonProperty(value = "pubkey", required = true) String publicKey, + @JsonProperty(value = "object_root", required = true) Bytes32 objectRoot) {} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/SignRequestType.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/SignRequestType.java new file mode 100644 index 000000000..c5b413d2a --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/json/SignRequestType.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; + +public enum SignRequestType { + CONSENSUS, + PROXY_BLS, + PROXY_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/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/Eth1SignForIdentifierHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/Eth1SignForIdentifierHandler.java index 2c459976c..becfac852 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/Eth1SignForIdentifierHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/Eth1SignForIdentifierHandler.java @@ -14,7 +14,6 @@ import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; import static tech.pegasys.web3signer.core.service.http.handlers.ContentTypes.TEXT_PLAIN_UTF_8; -import static tech.pegasys.web3signer.core.service.http.handlers.signing.SignerForIdentifier.toBytes; import static tech.pegasys.web3signer.signing.util.IdentifierUtils.normaliseIdentifier; import tech.pegasys.web3signer.core.service.http.metrics.HttpApiMetrics; @@ -23,6 +22,7 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RequestBody; import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; @@ -31,11 +31,11 @@ public class Eth1SignForIdentifierHandler implements Handler { private static final Logger LOG = LogManager.getLogger(); - private final SignerForIdentifier signerForIdentifier; + private final SignerForIdentifier signerForIdentifier; private final HttpApiMetrics metrics; public Eth1SignForIdentifierHandler( - final SignerForIdentifier signerForIdentifier, final HttpApiMetrics metrics) { + final SignerForIdentifier signerForIdentifier, final HttpApiMetrics metrics) { this.signerForIdentifier = signerForIdentifier; this.metrics = metrics; } @@ -47,7 +47,7 @@ public void handle(final RoutingContext routingContext) { final Bytes data; try { data = getDataToSign(routingContext.body()); - } catch (final IllegalArgumentException e) { + } catch (final RuntimeException e) { metrics.getMalformedRequestCounter().inc(); LOG.debug("Invalid signing request", e); routingContext.fail(400); @@ -72,6 +72,14 @@ private void respondWithSignature(final RoutingContext routingContext, final Str private Bytes getDataToSign(final RequestBody requestBody) { final JsonObject jsonObject = requestBody.asJsonObject(); - return toBytes(jsonObject.getString("data")); + + if (!jsonObject.containsKey("data")) { + throw new IllegalArgumentException("Request must contain a 'data' field"); + } + if (StringUtils.isBlank(jsonObject.getString("data"))) { + throw new IllegalArgumentException("Data field must not be empty"); + } + + return Bytes.fromHexString(jsonObject.getString("data")); } } 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 7148fb6fd..d2b717451 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 @@ -14,28 +14,20 @@ import tech.pegasys.web3signer.signing.ArtifactSignature; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.KeyType; import java.util.Optional; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; -public class SignerForIdentifier { - private static final Logger LOG = LogManager.getLogger(); +/** + * This class wraps the {@link ArtifactSignerProvider} and provides a way to check if a signer is + * available for a given identifier and to sign a message. + */ +public class SignerForIdentifier { private final ArtifactSignerProvider signerProvider; - private final SignatureFormatter signatureFormatter; - private final KeyType type; - public SignerForIdentifier( - final ArtifactSignerProvider signerProvider, - final SignatureFormatter signatureFormatter, - final KeyType type) { + public SignerForIdentifier(final ArtifactSignerProvider signerProvider) { this.signerProvider = signerProvider; - this.signatureFormatter = signatureFormatter; - this.type = type; } /** @@ -48,43 +40,19 @@ public SignerForIdentifier( * @throws IllegalArgumentException if data is invalid i.e. not a valid hex string, null or empty. */ public Optional sign(final String identifier, final Bytes data) { - return signerProvider.getSigner(identifier).map(signer -> formatSignature(signer.sign(data))); - } - - @SuppressWarnings("unchecked") - public Optional signAndGetArtifactSignature(final String identifier, final Bytes data) { - return signerProvider.getSigner(identifier).map(signer -> (T) signer.sign(data)); + return signerProvider.getSigner(identifier).map(signer -> signer.sign(data).asHex()); } /** - * Converts hex string to bytes + * Sign data for given identifier and return ArtifactSignature. Useful for SECP signing. * - * @param data hex string - * @return Bytes - * @throws IllegalArgumentException if data is invalid i.e. not a valid hex string, null or empty + * @param identifier The identifier for which to sign data. + * @param data Bytes which is signed + * @return Optional ArtifactSignature. Empty if no signer available for given identifier */ - public static Bytes toBytes(final String data) { - final Bytes dataToSign; - try { - if (StringUtils.isBlank(data)) { - throw new IllegalArgumentException("Blank data"); - } - dataToSign = Bytes.fromHexString(data); - } catch (final IllegalArgumentException e) { - LOG.debug("Invalid hex string {}", data, e); - throw e; - } - return dataToSign; - } - - @SuppressWarnings("unchecked") - private String formatSignature(final ArtifactSignature signature) { - if (signature.getType() == type) { - final T artifactSignature = (T) signature; - return signatureFormatter.format(artifactSignature); - } else { - throw new IllegalStateException("Invalid signature type"); - } + public Optional signAndGetArtifactSignature( + final String identifier, final Bytes data) { + return signerProvider.getSigner(identifier).map(signer -> signer.sign(data)); } /** diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionHandler.java index be9100a2a..596d15e5d 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionHandler.java @@ -36,9 +36,9 @@ public class SigningExtensionHandler implements Handler { .copy() .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - private final SignerForIdentifier signerForIdentifier; + private final SignerForIdentifier signerForIdentifier; - public SigningExtensionHandler(final SignerForIdentifier signerForIdentifier) { + public SigningExtensionHandler(final SignerForIdentifier signerForIdentifier) { this.signerForIdentifier = signerForIdentifier; } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java index ffca43dd7..4298dfb0a 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java @@ -16,7 +16,7 @@ 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.core.service.http.handlers.ContentTypes.TEXT_PLAIN_UTF_8; -import static tech.pegasys.web3signer.core.util.DepositSigningRootUtil.computeDomain; +import static tech.pegasys.web3signer.core.util.Web3SignerSigningRootUtil.computeDomain; import static tech.pegasys.web3signer.signing.util.IdentifierUtils.normaliseIdentifier; import tech.pegasys.teku.api.schema.AttestationData; @@ -31,7 +31,7 @@ import tech.pegasys.web3signer.core.service.http.ArtifactType; import tech.pegasys.web3signer.core.service.http.handlers.signing.SignerForIdentifier; import tech.pegasys.web3signer.core.service.http.metrics.HttpApiMetrics; -import tech.pegasys.web3signer.core.util.DepositSigningRootUtil; +import tech.pegasys.web3signer.core.util.Web3SignerSigningRootUtil; import tech.pegasys.web3signer.slashingprotection.SlashingProtection; import java.util.List; @@ -56,7 +56,7 @@ public class Eth2SignForIdentifierHandler implements Handler { private static final Logger LOG = LogManager.getLogger(); - private final SignerForIdentifier signerForIdentifier; + private final SignerForIdentifier signerForIdentifier; private final HttpApiMetrics httpMetrics; private final SlashingProtectionMetrics slashingMetrics; private final Optional slashingProtection; @@ -69,7 +69,7 @@ public class Eth2SignForIdentifierHandler implements Handler { public static final int SLASHING_PROTECTION_ENFORCED = 412; public Eth2SignForIdentifierHandler( - final SignerForIdentifier signerForIdentifier, + final SignerForIdentifier signerForIdentifier, final HttpApiMetrics httpMetrics, final SlashingProtectionMetrics slashingMetrics, final Optional slashingProtection, @@ -281,7 +281,7 @@ private Bytes computeSigningRoot(final Eth2SigningRequestBody body) { checkArgument(body.deposit() != null, "deposit must be specified"); final Bytes32 depositDomain = computeDomain(Domain.DEPOSIT, body.deposit().getGenesisForkVersion(), Bytes32.ZERO); - return DepositSigningRootUtil.computeSigningRoot( + return Web3SignerSigningRootUtil.computeSigningRoot( body.deposit().asInternalDepositMessage(), depositDomain); case SYNC_COMMITTEE_MESSAGE: final SyncCommitteeMessage syncCommitteeMessage = body.syncCommitteeMessage(); diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignResultProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignResultProvider.java index 80701b0f0..f14b26e5b 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignResultProvider.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignResultProvider.java @@ -21,7 +21,6 @@ import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.ResultProvider; -import tech.pegasys.web3signer.signing.SecpArtifactSignature; import java.util.List; @@ -33,10 +32,9 @@ public class EthSignResultProvider implements ResultProvider { private static final Logger LOG = LogManager.getLogger(); - private final SignerForIdentifier transactionSignerProvider; + private final SignerForIdentifier transactionSignerProvider; - public EthSignResultProvider( - final SignerForIdentifier transactionSignerProvider) { + public EthSignResultProvider(final SignerForIdentifier transactionSignerProvider) { this.transactionSignerProvider = transactionSignerProvider; } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java index 2b4b0f700..8043644fc 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTransactionResultProvider.java @@ -25,7 +25,6 @@ import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.EthTransaction; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.TransactionSerializer; -import tech.pegasys.web3signer.signing.SecpArtifactSignature; import java.util.List; @@ -40,12 +39,10 @@ public class EthSignTransactionResultProvider implements ResultProvider private final long chainId; private final JsonDecoder decoder; - private final SignerForIdentifier secpSigner; + private final SignerForIdentifier secpSigner; public EthSignTransactionResultProvider( - final long chainId, - final SignerForIdentifier secpSigner, - final JsonDecoder decoder) { + final long chainId, final SignerForIdentifier secpSigner, final JsonDecoder decoder) { this.chainId = chainId; this.decoder = decoder; this.secpSigner = secpSigner; diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTypedDataResultProvider.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTypedDataResultProvider.java index e3f6be396..a7e1d2cf4 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTypedDataResultProvider.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/internalresponse/EthSignTypedDataResultProvider.java @@ -20,7 +20,6 @@ import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest; import tech.pegasys.web3signer.core.service.jsonrpc.exceptions.JsonRpcException; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.ResultProvider; -import tech.pegasys.web3signer.signing.SecpArtifactSignature; import java.io.IOException; import java.util.List; @@ -34,10 +33,9 @@ public class EthSignTypedDataResultProvider implements ResultProvider { private static final Logger LOG = LogManager.getLogger(); - private final SignerForIdentifier transactionSignerProvider; + private final SignerForIdentifier transactionSignerProvider; - public EthSignTypedDataResultProvider( - final SignerForIdentifier transactionSignerProvider) { + public EthSignTypedDataResultProvider(final SignerForIdentifier transactionSignerProvider) { this.transactionSignerProvider = transactionSignerProvider; } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java index a090f67a0..2aedfda5b 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/jsonrpc/handlers/sendtransaction/SendTransactionHandler.java @@ -26,7 +26,6 @@ import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.Transaction; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory; import tech.pegasys.web3signer.core.service.jsonrpc.handlers.signing.TransactionSerializer; -import tech.pegasys.web3signer.signing.SecpArtifactSignature; import io.vertx.core.json.DecodeException; import io.vertx.ext.web.RoutingContext; @@ -41,14 +40,14 @@ public class SendTransactionHandler implements JsonRpcRequestHandler { private final TransactionFactory transactionFactory; private final VertxRequestTransmitterFactory vertxTransmitterFactory; - private final SignerForIdentifier secpSigner; + private final SignerForIdentifier secpSigner; private static final int MAX_NONCE_RETRIES = 10; public SendTransactionHandler( final long chainId, final TransactionFactory transactionFactory, final VertxRequestTransmitterFactory vertxTransmitterFactory, - final SignerForIdentifier secpSigner) { + final SignerForIdentifier secpSigner) { this.chainId = chainId; this.transactionFactory = transactionFactory; this.vertxTransmitterFactory = vertxTransmitterFactory; @@ -89,7 +88,7 @@ public void handle(final RoutingContext context, final JsonRpcRequest request) { private void sendTransaction( final Transaction transaction, final RoutingContext routingContext, - final SignerForIdentifier secpSigner, + final SignerForIdentifier secpSigner, final JsonRpcRequest request) { final TransactionSerializer transactionSerializer = 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 233fad7f9..186a17b45 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 @@ -32,11 +32,10 @@ public class TransactionSerializer { - protected final SignerForIdentifier secpSigner; + protected final SignerForIdentifier secpSigner; protected final long chainId; - public TransactionSerializer( - final SignerForIdentifier secpSigner, final long chainId) { + public TransactionSerializer(final SignerForIdentifier secpSigner, final long chainId) { this.secpSigner = secpSigner; this.chainId = chainId; } @@ -79,9 +78,11 @@ private static byte[] prependEip1559TransactionType(byte[] bytesToSign) { private SignatureData sign(final String eth1Address, final byte[] bytesToSign) { final SecpArtifactSignature artifactSignature = - secpSigner - .signAndGetArtifactSignature(normaliseIdentifier(eth1Address), Bytes.of(bytesToSign)) - .orElseThrow(() -> new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); + (SecpArtifactSignature) + secpSigner + .signAndGetArtifactSignature( + normaliseIdentifier(eth1Address), Bytes.of(bytesToSign)) + .orElseThrow(() -> new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT)); final Signature signature = artifactSignature.getSignatureData(); diff --git a/core/src/main/java/tech/pegasys/web3signer/core/util/DepositSigningRootUtil.java b/core/src/main/java/tech/pegasys/web3signer/core/util/Web3SignerSigningRootUtil.java similarity index 85% rename from core/src/main/java/tech/pegasys/web3signer/core/util/DepositSigningRootUtil.java rename to core/src/main/java/tech/pegasys/web3signer/core/util/Web3SignerSigningRootUtil.java index 103fed68a..485fdba12 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/util/DepositSigningRootUtil.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/util/Web3SignerSigningRootUtil.java @@ -20,11 +20,15 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; -public class DepositSigningRootUtil { - public static Bytes computeSigningRoot(final Merkleizable object, final Bytes32 domain) { +public class Web3SignerSigningRootUtil { + public static Bytes32 computeSigningRoot(final Merkleizable object, final Bytes32 domain) { return new SigningData(object.hashTreeRoot(), domain).hashTreeRoot(); } + public static Bytes32 computeSigningRoot(final Bytes32 objectRoot, final Bytes32 domain) { + return new SigningData(objectRoot, domain).hashTreeRoot(); + } + public static Bytes32 computeDomain( final Bytes4 domainType, final Bytes4 forkVersion, final Bytes32 genesisValidatorsRoot) { final Bytes32 forkDataRoot = computeForkDataRoot(forkVersion, genesisValidatorsRoot); diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java index b33695eb4..cc7b5e687 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java @@ -15,53 +15,89 @@ import static org.assertj.core.api.Assertions.assertThat; import tech.pegasys.web3signer.core.Eth1AddressSignerIdentifier; -import tech.pegasys.web3signer.core.util.PublicKeyUtils; import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; import tech.pegasys.web3signer.signing.secp256k1.SignerIdentifier; -import tech.pegasys.web3signer.signing.secp256k1.util.AddressUtil; +import java.security.KeyPair; +import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; -import java.util.Locale; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.web3j.crypto.Keys; class Eth1AddressSignerIdentifierTest { + private static KeyPair secp256k1KeyPair; + private static KeyPair secp256k1KeyPair2; + + @BeforeAll + static void generateKeyPair() throws Exception { + final SecureRandom random = new SecureRandom(); + secp256k1KeyPair = EthPublicKeyUtils.createSecp256k1KeyPair(random); + secp256k1KeyPair2 = EthPublicKeyUtils.createSecp256k1KeyPair(random); + } @Test void prefixIsRemovedFromAddress() { - final Eth1AddressSignerIdentifier signerIdentifier = new Eth1AddressSignerIdentifier("0xAb"); - assertThat(signerIdentifier.toStringIdentifier()).isEqualTo("ab"); + // web3j.crypto.Keys.getAddress() returns lower case address without 0x prefix + final String address = + Keys.getAddress( + EthPublicKeyUtils.ecPublicKeyToBigInteger((ECPublicKey) secp256k1KeyPair.getPublic())); + // forcefully convert first two alphabets to uppercase and add prefix + final String mixCaseAddress = "0X" + convertAlphabetsToUpperCase(address); + + final Eth1AddressSignerIdentifier signerIdentifier = + new Eth1AddressSignerIdentifier(mixCaseAddress); + assertThat(signerIdentifier.toStringIdentifier()).isEqualTo(address); + assertThat(signerIdentifier.toStringIdentifier()).doesNotStartWithIgnoringCase("0x"); + assertThat(signerIdentifier.toStringIdentifier()).isLowerCase(); } @Test void validateWorksForSamePrimaryKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); assertThat(signerIdentifier.validate(publicKey)).isTrue(); } @Test void validateFailsForDifferentPrimaryKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); - assertThat(signerIdentifier.validate(PublicKeyUtils.createKeyFrom("0xbb"))).isFalse(); + assertThat(signerIdentifier.validate((ECPublicKey) secp256k1KeyPair2.getPublic())).isFalse(); } @Test void validateFailsForNullPrimaryKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); assertThat(signerIdentifier.validate(null)).isFalse(); } @Test void correctEth1AddressIsGeneratedFromPublicKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); - final String prefixRemovedAddress = - AddressUtil.remove0xPrefix( - Keys.getAddress(EthPublicKeyUtils.toHexString(publicKey)).toLowerCase(Locale.US)); - assertThat(signerIdentifier.toStringIdentifier()).isEqualTo(prefixRemovedAddress); + + // web3j.crypto.Keys.getAddress() returns lower case address without 0x prefix + final String expectedAddress = + Keys.getAddress(EthPublicKeyUtils.ecPublicKeyToBigInteger(publicKey)); + assertThat(signerIdentifier.toStringIdentifier()).isEqualTo(expectedAddress); + assertThat(signerIdentifier.toStringIdentifier()).doesNotStartWithIgnoringCase("0x"); + assertThat(signerIdentifier.toStringIdentifier()).isLowerCase(); + } + + public static String convertAlphabetsToUpperCase(final String input) { + final char[] chars = input.toCharArray(); + int count = 0; + + for (int i = 0; i < chars.length && count < 2; i++) { + if (Character.isLetter(chars[i]) && Character.isLowerCase(chars[i])) { + chars[i] = Character.toUpperCase(chars[i]); + count++; + } + } + + return new String(chars); } } diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostSigningRootTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostSigningRootTest.java new file mode 100644 index 000000000..ff1214f49 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/CommitBoostSigningRootTest.java @@ -0,0 +1,192 @@ +/* + * 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 CommitBoostSigningRootTest { + 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.bigIntegerToECPublicKey(SECP_PROXY_KEY_PAIR.getPublicKey()); + private static final Bytes SECP_PROXY_PUB_KEY_ENC = + EthPublicKeyUtils.getEncoded(SECP_PROXY_EC_PUB_KEY, true); + + @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, GVR); + 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, GVR); + + 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, GVR); + + 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(); + builder.applyNetworkDefaults(network); + Eth2NetworkConfiguration eth2NetworkConfiguration = builder.build(); + return eth2NetworkConfiguration.getSpec(); + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeyGeneratorTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeyGeneratorTest.java new file mode 100644 index 000000000..a6bacdbb0 --- /dev/null +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/http/handlers/commitboost/ProxyKeyGeneratorTest.java @@ -0,0 +1,97 @@ +/* + * 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.CommitBoostParameters; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +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 ProxyKeyGeneratorTest { + @TempDir private Path commitBoostKeystoresPath; + + @TempDir private Path commitBoostPasswordDir; + + private ProxyKeyGenerator proxyKeyGenerator; + + @BeforeEach + void init() { + final CommitBoostParameters commitBoostParameters = + new TestCommitBoostParameters(commitBoostKeystoresPath, commitBoostPasswordDir); + proxyKeyGenerator = new ProxyKeyGenerator(commitBoostParameters); + } + + @Test + void generateBLSProxyKey() { + final ArtifactSigner artifactSigner = proxyKeyGenerator.generateBLSProxyKey("pubkey"); + assertThat( + commitBoostKeystoresPath + .resolve("pubkey") + .resolve(KeyType.BLS.name()) + .resolve(artifactSigner.getIdentifier() + ".json")) + .exists(); + } + + @Test + void generateECProxyKey() throws IOException { + final ArtifactSigner artifactSigner = proxyKeyGenerator.generateECProxyKey("pubkey"); + assertThat( + commitBoostKeystoresPath + .resolve("pubkey") + .resolve(KeyType.SECP256K1.name()) + .resolve(artifactSigner.getIdentifier() + ".json")) + .exists(); + } + + private static class TestCommitBoostParameters implements CommitBoostParameters { + 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 boolean isEnabled() { + return true; + } + + @Override + public Path getProxyKeystoresPath() { + return keystorePath; + } + + @Override + public Path getProxyKeystoresPasswordFile() { + return passwordFile; + } + } +} diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java index 6923303dd..ba761cf6a 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java @@ -22,30 +22,37 @@ import tech.pegasys.web3signer.core.service.jsonrpc.handlers.internalresponse.EthAccountsResultProvider; import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; import java.util.List; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.collect.Sets; -import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; import org.web3j.crypto.Keys; -@SuppressWarnings("unchecked") public class EthAccountsResultProviderTest { - - final ECPublicKey publicKeyA = createKeyFrom("A".repeat(128)); - final ECPublicKey publicKeyB = createKeyFrom("B".repeat(128)); - final ECPublicKey publicKeyC = createKeyFrom("C".repeat(128)); - - final String addressA = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyA)); - final String addressB = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyB)); - final String addressC = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyC)); - - final ECPublicKey createKeyFrom(final String hexString) { - return EthPublicKeyUtils.createPublicKey(Bytes.fromHexString(hexString)); + private final ECPublicKey publicKeyA; + private final ECPublicKey publicKeyB; + private final ECPublicKey publicKeyC; + + private final String addressA; + private final String addressB; + private final String addressC; + + public EthAccountsResultProviderTest() throws GeneralSecurityException { + SecureRandom secureRandom = new SecureRandom(); + publicKeyA = (ECPublicKey) EthPublicKeyUtils.createSecp256k1KeyPair(secureRandom).getPublic(); + publicKeyB = (ECPublicKey) EthPublicKeyUtils.createSecp256k1KeyPair(secureRandom).getPublic(); + publicKeyC = (ECPublicKey) EthPublicKeyUtils.createSecp256k1KeyPair(secureRandom).getPublic(); + + addressA = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyA)); + addressB = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyB)); + addressC = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyC)); } @Test @@ -103,9 +110,7 @@ public void missingParametersIsOk() { final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_accounts"); request.setId(new JsonRpcRequestId(id)); - final Object body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - final List addressses = (List) body; + final List addressses = resultProvider.createResponseResult(request); assertThat(addressses).containsExactly("0x" + addressA); } @@ -120,10 +125,7 @@ public void multipleValueFromBodyProviderInsertedToResult() { request.setId(new JsonRpcRequestId(id)); request.setParams(emptyList()); - final Object body = resultProvider.createResponseResult(request); - - assertThat(body).isInstanceOf(List.class); - final List reportedAddresses = (List) body; + final List reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyInAnyOrder("0x" + addressA, "0x" + addressB, "0x" + addressC); } @@ -139,25 +141,19 @@ public void accountsReturnedAreDynamicallyFetchedFromProvider() { request.setId(new JsonRpcRequestId(1)); request.setParams(emptyList()); - Object body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - List reportedAddresses = (List) body; + List reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyElementsOf( - List.of("0x" + addressA, "0x" + addressB, "0x" + addressC).stream() + Stream.of("0x" + addressA, "0x" + addressB, "0x" + addressC) .sorted() .collect(Collectors.toList())); addresses.remove(publicKeyA); - body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - reportedAddresses = (List) body; + reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyElementsOf( - List.of("0x" + addressB, "0x" + addressC).stream() - .sorted() - .collect(Collectors.toList())); + Stream.of("0x" + addressB, "0x" + addressC).sorted().collect(Collectors.toList())); } @Test @@ -169,12 +165,10 @@ public void accountsReturnedAreSortedAlphabetically() { request.setId(new JsonRpcRequestId(1)); request.setParams(emptyList()); - final Object body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - List reportedAddresses = (List) body; + List reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyElementsOf( - List.of("0x" + addressA, "0x" + addressB, "0x" + addressC).stream() + Stream.of("0x" + addressA, "0x" + addressB, "0x" + addressC) .sorted() .collect(Collectors.toList())); } diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignResultProviderTest.java index 78dbe5bcc..77ae13f18 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignResultProviderTest.java @@ -55,7 +55,7 @@ @ExtendWith(MockitoExtension.class) public class EthSignResultProviderTest { - @Mock SignerForIdentifier transactionSignerProvider; + @Mock SignerForIdentifier transactionSignerProvider; @ParameterizedTest @ArgumentsSource(InvalidParamsProvider.class) @@ -97,7 +97,7 @@ public void signatureHasTheExpectedFormat() { final SecpArtifactSignature secpArtifactSignature = new SecpArtifactSignature(new Signature(v, r, s)); - doReturn(Optional.of(SecpArtifactSignature.toBytes(secpArtifactSignature).toHexString())) + doReturn(Optional.of(secpArtifactSignature.asHex())) .when(transactionSignerProvider) .sign(any(), any(Bytes.class)); @@ -138,13 +138,12 @@ public void returnsExpectedSignature(final String message) { Bytes data = answer.getArgument(1, Bytes.class); final Sign.SignatureData signature = Sign.signMessage(data.toArrayUnsafe(), keyPair); return Optional.of( - SecpArtifactSignature.toBytes( - new SecpArtifactSignature( - new Signature( - new BigInteger(signature.getV()), - new BigInteger(1, signature.getR()), - new BigInteger(1, signature.getS())))) - .toHexString()); + new SecpArtifactSignature( + new Signature( + new BigInteger(signature.getV()), + new BigInteger(1, signature.getR()), + new BigInteger(1, signature.getS()))) + .asHex()); }) .when(transactionSignerProvider) .sign(anyString(), any(Bytes.class)); 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 e0aab9d00..4e37487bf 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 @@ -61,7 +61,7 @@ public class EthSignTransactionResultProviderTest { private static JsonDecoder jsonDecoder; private static long chainId; - @Mock SignerForIdentifier mockSignerForIdentifier; + @Mock SignerForIdentifier mockSignerForIdentifier; @BeforeAll static void beforeAll() { @@ -108,7 +108,8 @@ public void ifAddressIsNotUnlockedExceptionIsThrownWithSigningNotUnlocked() { public void signatureHasTheExpectedFormat() { final Credentials cs = Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); - final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + final ECPublicKey key = + EthPublicKeyUtils.bigIntegerToECPublicKey(cs.getEcKeyPair().getPublicKey()); final String addr = Keys.getAddress(EthPublicKeyUtils.toHexString(key)); final BigInteger v = BigInteger.ONE; @@ -169,7 +170,8 @@ public void returnsExpectedSignatureForEip1559Transaction() { private String executeEthSignTransaction(final JsonObject params) { final Credentials cs = Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); - final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + final ECPublicKey key = + EthPublicKeyUtils.bigIntegerToECPublicKey(cs.getEcKeyPair().getPublicKey()); final String addr = Keys.getAddress(EthPublicKeyUtils.toHexString(key)); doAnswer( diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTypedDataResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTypedDataResultProviderTest.java index a73a90139..65fe11e1a 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTypedDataResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTypedDataResultProviderTest.java @@ -71,7 +71,7 @@ public class EthSignTypedDataResultProviderTest { private static final ECKeyPair KEY_PAIR = new ECKeyPair(PRIVATE_KEY, PUBLIC_KEY); - @Mock SignerForIdentifier transactionSignerProvider; + @Mock SignerForIdentifier transactionSignerProvider; @ParameterizedTest @ArgumentsSource(InvalidParamsProvider.class) @@ -147,12 +147,11 @@ public Stream provideArguments(final ExtensionContext conte } private String hexFromSignatureData(Sign.SignatureData signature) { - return SecpArtifactSignature.toBytes( - new SecpArtifactSignature( - new Signature( - new BigInteger(signature.getV()), - new BigInteger(1, signature.getR()), - new BigInteger(1, signature.getS())))) - .toHexString(); + return new SecpArtifactSignature( + new Signature( + new BigInteger(signature.getV()), + new BigInteger(1, signature.getR()), + new BigInteger(1, signature.getS()))) + .asHex(); } } diff --git a/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java b/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java index 7f639069f..344299f16 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java @@ -22,6 +22,6 @@ public class PublicKeyUtils { public static ECPublicKey createKeyFrom(final String hexString) { Bytes bytes = Bytes.fromHexString(hexString, 64); - return EthPublicKeyUtils.createPublicKey(bytes); + return EthPublicKeyUtils.bytesToECPublicKey(bytes); } } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignature.java b/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignature.java index 00232fef0..2b9b5afd4 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignature.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/ArtifactSignature.java @@ -14,5 +14,17 @@ public interface ArtifactSignature { + /** + * Returns the type of key used to sign the artifact + * + * @return the type of key used to sign the artifact + */ KeyType getType(); + + /** + * Returns the signature data in hex format + * + * @return the signature data in hex format + */ + String asHex(); } 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..87d1698c5 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; @@ -36,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. * @@ -44,12 +53,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. @@ -67,6 +78,17 @@ public interface ArtifactSignerProvider extends Closeable { */ Future removeSigner(final String identifier); + /** + * Add a proxy signer to the signer provider. + * + * @param signer Instance of ArtifactSigner + * @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 signer, 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/BlsArtifactSignature.java b/signing/src/main/java/tech/pegasys/web3signer/signing/BlsArtifactSignature.java index 00ce131ce..1aec75556 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/BlsArtifactSignature.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/BlsArtifactSignature.java @@ -29,4 +29,9 @@ public KeyType getType() { public BLSSignature getSignatureData() { return blsSignature; } + + @Override + public String asHex() { + return blsSignature.toString(); + } } 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..c416acb69 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/K256ArtifactSigner.java @@ -0,0 +1,140 @@ +/* + * 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.getEncoded( + EthPublicKeyUtils.bigIntegerToECPublicKey(ecKeyPair.getPublicKey()), true) + .toHexString(); + return IdentifierUtils.normaliseIdentifier(hexString); + } + + @Override + public ArtifactSignature 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(); + + // 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); + + return new K256ArtifactSignature(concatenated.toArray()); + } 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 (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not found", e); + } + } + + public static class K256ArtifactSignature implements ArtifactSignature { + final Bytes signature; + + public K256ArtifactSignature(final byte[] signature) { + this.signature = Bytes.of(signature); + } + + @Override + public KeyType getType() { + return KeyType.SECP256K1; + } + + @Override + public String asHex() { + return signature.toHexString(); + } + + @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/SecpArtifactSignature.java b/signing/src/main/java/tech/pegasys/web3signer/signing/SecpArtifactSignature.java index 236288642..667291917 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/SecpArtifactSignature.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/SecpArtifactSignature.java @@ -35,6 +35,15 @@ public Signature getSignatureData() { return signature; } + @Override + public String asHex() { + return Bytes.concatenate( + Bytes32.leftPad(Bytes.wrap(ByteUtils.bigIntegerToBytes(signature.getR()))), + Bytes32.leftPad(Bytes.wrap(ByteUtils.bigIntegerToBytes(signature.getS()))), + Bytes.wrap(ByteUtils.bigIntegerToBytes(signature.getV()))) + .toHexString(); + } + public static SecpArtifactSignature fromBytes(final Bytes signature) { final Bytes r = signature.slice(0, 32); final Bytes s = signature.slice(32, 32); @@ -45,12 +54,4 @@ public static SecpArtifactSignature fromBytes(final Bytes signature) { Numeric.toBigInt(r.toArrayUnsafe()), Numeric.toBigInt(s.toArrayUnsafe()))); } - - public static Bytes toBytes(final SecpArtifactSignature signature) { - final Signature signatureData = signature.getSignatureData(); - return Bytes.concatenate( - Bytes32.leftPad(Bytes.wrap(ByteUtils.bigIntegerToBytes(signatureData.getR()))), - Bytes32.leftPad(Bytes.wrap(ByteUtils.bigIntegerToBytes(signatureData.getS()))), - Bytes.wrap(ByteUtils.bigIntegerToBytes(signatureData.getV()))); - } } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/BlsKeystoreBulkLoader.java b/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/BlsKeystoreBulkLoader.java index 93c66e2f8..53e3267ef 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/BlsKeystoreBulkLoader.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/bulkloading/BlsKeystoreBulkLoader.java @@ -39,7 +39,7 @@ public class BlsKeystoreBulkLoader { private static final Logger LOG = LogManager.getLogger(); - public MappedResults loadKeystoresUsingPasswordDir( + public static MappedResults loadKeystoresUsingPasswordDir( final Path keystoresDirectory, final Path passwordsDirectory) { final List keystoreFiles; try { @@ -59,7 +59,7 @@ public MappedResults loadKeystoresUsingPasswordDir( .reduce(MappedResults.newSetInstance(), MappedResults::merge); } - public MappedResults loadKeystoresUsingPasswordFile( + public static MappedResults loadKeystoresUsingPasswordFile( final Path keystoresDirectory, final Path passwordFile) { final List keystoreFiles; try { @@ -82,7 +82,7 @@ public MappedResults loadKeystoresUsingPasswordFile( .reduce(MappedResults.newSetInstance(), MappedResults::merge); } - private MappedResults createSignerForKeystore( + private static MappedResults createSignerForKeystore( final Path keystoreFile, final PasswordReader passwordReader) { try { LOG.debug("Loading keystore {}", keystoreFile); 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..2c6b3fdc2 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; @@ -36,6 +37,25 @@ public class SecpV3KeystoresBulkLoader { public static MappedResults loadV3KeystoresUsingPasswordFileOrDir( final Path keystoresPath, final Path pwrdFileOrDirPath) { + return loadV3KeystoresUsingPasswordFileOrDir(keystoresPath, pwrdFileOrDirPath, false); + } + + /** + * Loads K256ArtifactSigners that are used in Commit Boost API. It uses compressed identifier and + * perform SHA256-SECP256K1 digest 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, true); + } + + private static MappedResults loadV3KeystoresUsingPasswordFileOrDir( + final Path keystoresPath, final Path pwrdFileOrDirPath, final boolean isCompressed) { if (!Files.exists(pwrdFileOrDirPath)) { LOG.error("Password file or directory doesn't exist."); return MappedResults.errorResult(); @@ -67,12 +87,12 @@ public static MappedResults loadV3KeystoresUsingPasswordFileOrDi } return keystoresFiles.parallelStream() - .map(keystoreFile -> createSecpArtifactSigner(keystoreFile, passwordReader)) + .map(keystoreFile -> createSecpArtifactSigner(keystoreFile, passwordReader, isCompressed)) .reduce(MappedResults.newSetInstance(), MappedResults::merge); } private static MappedResults createSecpArtifactSigner( - final Path v3KeystorePath, final PasswordReader passwordReader) { + final Path v3KeystorePath, final PasswordReader passwordReader, final boolean isCompressed) { try { final String fileNameWithoutExt = FilenameUtils.removeExtension(v3KeystorePath.getFileName().toString()); @@ -81,8 +101,10 @@ private static MappedResults createSecpArtifactSigner( final Credentials credentials = WalletUtils.loadCredentials(password, v3KeystorePath.toFile()); - final EthSecpArtifactSigner artifactSigner = - new EthSecpArtifactSigner(new CredentialSigner(credentials)); + final ArtifactSigner artifactSigner = + isCompressed + ? new K256ArtifactSigner(credentials.getEcKeyPair()) + : new EthSecpArtifactSigner(new CredentialSigner(credentials)); 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/CommitBoostParameters.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/CommitBoostParameters.java new file mode 100644 index 000000000..e72883fee --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/CommitBoostParameters.java @@ -0,0 +1,51 @@ +/* + * 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.signing.config; + +import java.nio.file.Path; + +import org.apache.tuweni.bytes.Bytes32; + +/** Configuration parameters for the commit boost API. */ +public interface CommitBoostParameters { + /** + * Whether the commit boost API is enabled. + * + * @return true if enabled, false otherwise + */ + boolean isEnabled(); + + /** + * The path to a writeable directory to store proxy BLS and SECP keystores for commit boost API. + * + * @return the path to the directory + */ + Path getProxyKeystoresPath(); + + /** + * The path to the password file used to encrypt/decrypt proxy keystores for commit boost API. + * + * @return the path to the password file + */ + Path getProxyKeystoresPasswordFile(); + + /** + * The Genesis Validators Root for the network used by the commit boost signing operations. The + * Commit Boost Client implementation uses ZERO as the default value. + * + * @return Genesis Validators Root as Bytes32 + */ + default Bytes32 getGenesisValidatorsRoot() { + return Bytes32.ZERO; + } +} 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 788cbc7b4..c87bf52b3 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,15 +46,15 @@ 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; + private final Optional commitBoostParameters; public DefaultArtifactSignerProvider( final Supplier> artifactSignerCollectionSupplier, - final Optional commitBoostKeystoresParameters) { + final Optional commitBoostParameters) { this.artifactSignerCollectionSupplier = artifactSignerCollectionSupplier; - this.commitBoostKeystoresParameters = commitBoostKeystoresParameters; + this.commitBoostParameters = commitBoostParameters; } @Override @@ -76,8 +76,8 @@ public Future load() { .forEach(signers::putIfAbsent); // for each loaded signer, load commit boost proxy signers (if any) - commitBoostKeystoresParameters - .filter(KeystoresParameters::isEnabled) + commitBoostParameters + .filter(CommitBoostParameters::isEnabled) .ifPresent( keystoreParameter -> signers @@ -86,14 +86,17 @@ public Future load() { signerIdentifier -> { 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); - } + loadProxySigners( + keystoreParameter, + signerIdentifier, + SECP256K1.name(), + SecpV3KeystoresBulkLoader::loadECDSAProxyKeystores); + + loadProxySigners( + keystoreParameter, + signerIdentifier, + BLS.name(), + BlsKeystoreBulkLoader::loadKeystoresUsingPasswordFile); })); LOG.info("Total signers (keys) currently loaded in memory: {}", signers.size()); @@ -111,20 +114,28 @@ 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()); } @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,11 +153,25 @@ public Future removeSigner(final String identifier) { return executorService.submit( () -> { signers.remove(identifier); + proxySigners.remove(identifier); LOG.info("Removed signer with identifier '{}'", identifier); return null; }); } + @Override + public Future addProxySigner(final ArtifactSigner signer, final String consensusPubKey) { + return executorService.submit( + () -> { + proxySigners.computeIfAbsent(consensusPubKey, k -> new HashSet<>()).add(signer); + LOG.info( + "Loaded new proxy signer {} for consensus public key '{}'", + signer.getIdentifier(), + consensusPubKey); + return null; + }); + } + @Override public void close() { executorService.shutdownNow(); @@ -157,35 +182,21 @@ private static boolean canReadFromDirectory(final Path path) { return file.canRead() && file.isDirectory(); } - private void loadSecpProxySigners( - 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, + private void loadProxySigners( + final CommitBoostParameters keystoreParameter, final String identifier, - final Path identifierPath) { - final Path proxyBlsDir = identifierPath.resolve(BLS.name()); - - if (canReadFromDirectory(proxyBlsDir)) { - // load bls proxy signers - final BlsKeystoreBulkLoader blsKeystoreBulkLoader = new BlsKeystoreBulkLoader(); - final MappedResults blsSignersResult = - blsKeystoreBulkLoader.loadKeystoresUsingPasswordFile( - proxyBlsDir, keystoreParameter.getKeystoresPasswordFile()); - final Collection blsSigners = blsSignersResult.getValues(); - proxySigners.computeIfAbsent(identifier, k -> new ArrayList<>()).addAll(blsSigners); + final String keyType, + final BiFunction> loaderFunction) { + + // Calculate identifierPath from keystoreParameter + final Path identifierPath = keystoreParameter.getProxyKeystoresPath().resolve(identifier); + final Path proxyDir = identifierPath.resolve(keyType); + + if (canReadFromDirectory(proxyDir)) { + final MappedResults signersResult = + loaderFunction.apply(proxyDir, keystoreParameter.getProxyKeystoresPasswordFile()); + final Collection signers = signersResult.getValues(); + proxySigners.computeIfAbsent(identifier, 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/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java index 2ae294201..ea63f3d38 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,63 +12,220 @@ */ package tech.pegasys.web3signer.signing.secp256k1; -import static com.google.common.base.Preconditions.checkArgument; -import static org.bouncycastle.util.BigIntegers.asUnsignedByteArray; - import java.math.BigInteger; -import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; import java.security.KeyFactory; +import java.security.KeyPair; +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.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.EllipticCurve; import java.security.spec.InvalidKeySpecException; -import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.web3j.utils.Numeric; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; +import org.web3j.crypto.ECKeyPair; public class EthPublicKeyUtils { - private static final int PUBLIC_KEY_SIZE = 64; + private static final BouncyCastleProvider BC_PROVIDER = new BouncyCastleProvider(); + private static final ECDomainParameters SECP256K1_DOMAIN; + private static final ECParameterSpec BC_SECP256K1_SPEC; + private static final java.security.spec.ECParameterSpec JAVA_SECP256K1_SPEC; + private static final String SECP256K1_CURVE = "secp256k1"; + private static final String EC_ALGORITHM = "EC"; + + static { + final X9ECParameters params = CustomNamedCurves.getByName(SECP256K1_CURVE); + SECP256K1_DOMAIN = + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + BC_SECP256K1_SPEC = + new ECParameterSpec(params.getCurve(), params.getG(), params.getN(), params.getH()); + final ECCurve bcCurve = BC_SECP256K1_SPEC.getCurve(); + JAVA_SECP256K1_SPEC = + new java.security.spec.ECParameterSpec( + new EllipticCurve( + new java.security.spec.ECFieldFp(bcCurve.getField().getCharacteristic()), + bcCurve.getA().toBigInteger(), + bcCurve.getB().toBigInteger()), + new java.security.spec.ECPoint( + BC_SECP256K1_SPEC.getG().getAffineXCoord().toBigInteger(), + BC_SECP256K1_SPEC.getG().getAffineYCoord().toBigInteger()), + BC_SECP256K1_SPEC.getN(), + BC_SECP256K1_SPEC.getH().intValue()); + } + + /** + * Create a new secp256k1 key pair. + * + * @param random The random number generator to use + * @return The generated key pair + * @throws GeneralSecurityException If there is an issue generating the key pair + */ + public static KeyPair createSecp256k1KeyPair(final SecureRandom random) + throws GeneralSecurityException { + final KeyPairGenerator keyPairGenerator = + KeyPairGenerator.getInstance(EC_ALGORITHM, BC_PROVIDER); + final ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(SECP256K1_CURVE); + if (random != null) { + keyPairGenerator.initialize(ecGenParameterSpec, random); + } else { + keyPairGenerator.initialize(ecGenParameterSpec); + } + + return keyPairGenerator.generateKeyPair(); + } - public static ECPublicKey createPublicKey(final ECPoint publicPoint) { + public static KeyPair web3JECKeypairToJavaKeyPair(final ECKeyPair web3JECKeypair) { try { - final AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); - parameters.init(new ECGenParameterSpec("secp256k1")); - final ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); - final ECPublicKeySpec pubSpec = new ECPublicKeySpec(publicPoint, ecParameters); - final KeyFactory kf = KeyFactory.getInstance("EC"); - return (ECPublicKey) kf.generatePublic(pubSpec); - } catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) { - throw new IllegalStateException("Unable to create Ethereum public key", e); + PrivateKey ecPrivateKey = + KeyFactory.getInstance("EC", BC_PROVIDER) + .generatePrivate( + new ECPrivateKeySpec(web3JECKeypair.getPrivateKey(), JAVA_SECP256K1_SPEC)); + return new KeyPair(bigIntegerToECPublicKey(web3JECKeypair.getPublicKey()), ecPrivateKey); + } catch (final Exception e) { + throw new RuntimeException("Unable to convert web3j to Java EC keypair", e); } } - public static ECPublicKey createPublicKey(final Bytes value) { - checkArgument(value.size() == PUBLIC_KEY_SIZE, "Invalid public key size must be 64 bytes"); - final Bytes x = value.slice(0, 32); - final Bytes y = value.slice(32, 32); - final ECPoint ecPoint = - new ECPoint(Numeric.toBigInt(x.toArrayUnsafe()), Numeric.toBigInt(y.toArrayUnsafe())); - return createPublicKey(ecPoint); + /** + * Convert a public key in bytes format to an ECPublicKey. + * + * @param value The public key in bytes format + * @return The ECPublicKey + */ + public static ECPublicKey bytesToECPublicKey(final Bytes value) { + return bcECPointToECPublicKey(bytesToBCECPoint(value)); } - public static ECPublicKey createPublicKey(final BigInteger value) { - final Bytes ethBytes = Bytes.wrap(Numeric.toBytesPadded(value, PUBLIC_KEY_SIZE)); - return createPublicKey(ethBytes); + public static ECPoint bytesToBCECPoint(final Bytes value) { + if (value.size() != 33 && value.size() != 65 && value.size() != 64) { + throw new IllegalArgumentException( + "Invalid public key length. Expected 33, 64, or 65 bytes."); + } + + final ECPoint point; + final byte[] key; + if (value.size() == 64) { + // For 64-byte input, we need to prepend the 0x04 prefix for uncompressed format + key = new byte[65]; + key[0] = 0x04; + System.arraycopy(value.toArrayUnsafe(), 0, key, 1, 64); + } else { + key = value.toArrayUnsafe(); + } + point = SECP256K1_DOMAIN.getCurve().decodePoint(key); + + return point; } - public static byte[] toByteArray(final ECPublicKey publicKey) { - final ECPoint ecPoint = publicKey.getW(); - final Bytes xBytes = Bytes32.wrap(asUnsignedByteArray(32, ecPoint.getAffineX())); - final Bytes yBytes = Bytes32.wrap(asUnsignedByteArray(32, ecPoint.getAffineY())); - return Bytes.concatenate(xBytes, yBytes).toArray(); + private static ECPublicKey bcECPointToECPublicKey(final ECPoint point) { + try { + // Convert Bouncy Castle ECPoint to Java ECPoint + final java.security.spec.ECPoint ecPoint = + new java.security.spec.ECPoint( + point.getAffineXCoord().toBigInteger(), point.getAffineYCoord().toBigInteger()); + + final java.security.spec.ECPublicKeySpec pubSpec = + new java.security.spec.ECPublicKeySpec(ecPoint, JAVA_SECP256K1_SPEC); + return (ECPublicKey) + KeyFactory.getInstance(EC_ALGORITHM, BC_PROVIDER).generatePublic(pubSpec); + } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unable to create EC public key", e); + } } + /** + * Create an ECPublicKey from a BigInteger representation of the public key. + * + * @param publicKeyValue The BigInteger representation of the public key (64 bytes, without + * prefix) + * @return The created ECPublicKey + * @throws IllegalArgumentException if the input is invalid + */ + public static ECPublicKey bigIntegerToECPublicKey(final BigInteger publicKeyValue) { + if (publicKeyValue == null) { + throw new IllegalArgumentException("Public key value cannot be null"); + } + + byte[] publicKeyBytes = publicKeyValue.toByteArray(); + + // Ensure we have exactly 64 bytes + if (publicKeyBytes.length < 64) { + byte[] temp = new byte[64]; + System.arraycopy(publicKeyBytes, 0, temp, 64 - publicKeyBytes.length, publicKeyBytes.length); + publicKeyBytes = temp; + } else if (publicKeyBytes.length > 64) { + publicKeyBytes = + Arrays.copyOfRange(publicKeyBytes, publicKeyBytes.length - 64, publicKeyBytes.length); + } + + // Create a new byte array with the uncompressed prefix + byte[] fullPublicKeyBytes = new byte[65]; + fullPublicKeyBytes[0] = 0x04; // Uncompressed point prefix + System.arraycopy(publicKeyBytes, 0, fullPublicKeyBytes, 1, 64); + + // Use the existing createPublicKey method + return bytesToECPublicKey(Bytes.wrap(fullPublicKeyBytes)); + } + + /** + * Convert a java ECPublicKey to an uncompressed (64 bytes) hex string. + * + * @param publicKey The public key to convert + * @return The public key as a hex string + */ public static String toHexString(final ECPublicKey publicKey) { - return Bytes.wrap(toByteArray(publicKey)).toHexString(); + return getEncoded(publicKey, false).toHexString(); + } + + /** + * Convert a java ECPublicKey to a BigInteger. + * + * @param publicKey The public key to convert + * @return The public key as a BigInteger + */ + public static BigInteger ecPublicKeyToBigInteger(final ECPublicKey publicKey) { + // Get the uncompressed public key without prefix (64 bytes) + final Bytes publicKeyBytes = EthPublicKeyUtils.getEncoded(publicKey, false); + + // Convert to BigInteger + return new BigInteger(1, publicKeyBytes.toArrayUnsafe()); + } + + /** + * Convert java ECPublicKey to Bytes. + * + * @param publicKey The public key to convert + * @param compressed Whether to return the compressed form 33 bytes or the uncompressed form 64 + * bytes + * @return The encoded public key. + */ + public static Bytes getEncoded(final ECPublicKey publicKey, boolean compressed) { + final ECPoint point; + if (publicKey instanceof BCECPublicKey) { + // If it's already a Bouncy Castle key, we can get the ECPoint directly + point = ((BCECPublicKey) publicKey).getQ(); + } else { + // If it's not a BC key, we need to create the ECPoint from the coordinates + final BigInteger x = publicKey.getW().getAffineX(); + final BigInteger y = publicKey.getW().getAffineY(); + point = BC_SECP256K1_SPEC.getCurve().createPoint(x, y); + } + + return compressed + ? Bytes.wrap(point.getEncoded(true)) + : Bytes.wrap(point.getEncoded(false), 1, 64); } } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java index 83e9edfbd..18a9ab749 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java @@ -51,7 +51,7 @@ public class AzureKeyVaultSigner implements Signer { final AzureKeyVault azureKeyVault, final AzureHttpClientFactory azureHttpClientFactory) { this.config = config; - this.publicKey = EthPublicKeyUtils.createPublicKey(publicKey); + this.publicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKey); this.needsToHash = needsToHash; this.signingAlgo = useDeprecatedSignatureAlgorithm diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java index 090f77747..b9f87275c 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java @@ -31,7 +31,8 @@ public class CredentialSigner implements Signer { public CredentialSigner(final Credentials credentials, final boolean needToHash) { this.credentials = credentials; - this.publicKey = EthPublicKeyUtils.createPublicKey(credentials.getEcKeyPair().getPublicKey()); + this.publicKey = + EthPublicKeyUtils.bigIntegerToECPublicKey(credentials.getEcKeyPair().getPublicKey()); this.needToHash = needToHash; } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java index 0e81f85d9..f17dafeb9 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java @@ -28,7 +28,6 @@ import org.bouncycastle.asn1.DLSequence; import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.Sign; -import org.web3j.utils.Numeric; public class Eth1SignatureUtil { private static final Logger LOG = LogManager.getLogger(); @@ -92,7 +91,7 @@ private static Signature deriveSignature( private static int recoverKeyIndex( final ECPublicKey ecPublicKey, final ECDSASignature sig, final byte[] hash) { - final BigInteger publicKey = Numeric.toBigInt(EthPublicKeyUtils.toByteArray(ecPublicKey)); + final BigInteger publicKey = EthPublicKeyUtils.ecPublicKeyToBigInteger(ecPublicKey); for (int i = 0; i < 4; i++) { final BigInteger k = Sign.recoverFromSignature(i, sig, hash); LOG.trace("recovered key: {}", k); diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignatureTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignatureTest.java index f76bd0901..9f81f379d 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignatureTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignatureTest.java @@ -28,7 +28,7 @@ void hexEncodedSignatureIsReturned() { final BLSSignature blsSignature = BLSSignature.fromBytesCompressed(Bytes.fromHexString(SIGNATURE)); final BlsArtifactSignature blsArtifactSignature = new BlsArtifactSignature(blsSignature); - assertThat(blsArtifactSignature.getSignatureData().toString()).isEqualTo(SIGNATURE); + assertThat(blsArtifactSignature.asHex()).isEqualTo(SIGNATURE); assertThat(blsSignature.toBytesCompressed().toHexString()).isEqualTo(SIGNATURE); assertThat(blsSignature.toString()).isEqualTo(SIGNATURE); } diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignerTest.java index 0abfa331b..3919fc0a4 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignerTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/BlsArtifactSignerTest.java @@ -45,6 +45,6 @@ void signsData() { new BlsArtifactSigner(keyPair, SignerOrigin.FILE_RAW); final BlsArtifactSignature signature = blsArtifactSigner.sign(message); - assertThat(signature.getSignatureData().toString()).isEqualTo(expectedSignature.toString()); + assertThat(signature.asHex()).isEqualTo(expectedSignature.toString()); } } 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 c28ced922..36900d8f2 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 @@ -29,17 +29,18 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; +import java.security.KeyPair; import java.security.SecureRandom; 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; 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; import org.web3j.crypto.exception.CipherException; @@ -112,9 +113,13 @@ void proxySignersAreLoadedCorrectly() throws IOException { final SecureRandom secureRandom = new SecureRandom(); // create random proxy signers - final KeystoresParameters commitBoostParameters = + final CommitBoostParameters commitBoostParameters = new TestCommitBoostParameters(commitBoostKeystoresPath, commitBoostPasswordDir); + assertThat(commitBoostParameters.getProxyKeystoresPasswordFile()) + .exists() + .hasFileName("password.txt"); + // create random BLS key pairs as proxy keys for public key1 and public key2 final List key1ProxyKeyPairs = randomBLSV4Keystores(secureRandom, PUBLIC_KEY1); final List key2ProxyKeyPairs = randomBLSV4Keystores(secureRandom, PUBLIC_KEY2); @@ -137,27 +142,27 @@ 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)); 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 void emptyProxySignersAreLoadedSuccessfully() { // enable commit boost without existing proxy keys - final KeystoresParameters commitBoostParameters = + final CommitBoostParameters commitBoostParameters = new TestCommitBoostParameters(commitBoostKeystoresPath, commitBoostPasswordDir); // set up mock signers @@ -174,7 +179,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(); } @@ -205,7 +210,10 @@ private List randomSecpV3Keystores( .mapToObj( i -> { try { - final ECKeyPair ecKeyPair = Keys.createEcKeyPair(secureRandom); + final KeyPair secp256k1KeyPair = + EthPublicKeyUtils.createSecp256k1KeyPair(secureRandom); + final ECKeyPair ecKeyPair = ECKeyPair.create(secp256k1KeyPair); + WalletUtils.generateWalletFile("password", ecKeyPair, v3Dir.toFile(), false); return ecKeyPair; } catch (GeneralSecurityException | CipherException | IOException e) { @@ -222,17 +230,19 @@ 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.createPublicKey(keyPair.getPublicKey()))) + EthPublicKeyUtils.getEncoded( + EthPublicKeyUtils.bigIntegerToECPublicKey(keyPair.getPublicKey()), true) + .toHexString()) .toList() .toArray(String[]::new); } - private static class TestCommitBoostParameters implements KeystoresParameters { + private static class TestCommitBoostParameters implements CommitBoostParameters { private final Path keystorePath; private final Path passwordFile; @@ -249,23 +259,18 @@ public TestCommitBoostParameters(final Path keystorePath, final Path passwordDir } @Override - public Path getKeystoresPath() { - return keystorePath; + public boolean isEnabled() { + return true; } @Override - public Path getKeystoresPasswordsPath() { - return null; + public Path getProxyKeystoresPath() { + return keystorePath; } @Override - public Path getKeystoresPasswordFile() { + public Path getProxyKeystoresPasswordFile() { return passwordFile; } - - @Override - public boolean isEnabled() { - return true; - } } } diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java index c75cfb9a8..b007c9c40 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java @@ -14,22 +14,29 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.Fail.fail; import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; +import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X962Parameters; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.ec.CustomNamedCurves; -import org.bouncycastle.math.ec.ECFieldElement; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.web3j.utils.Numeric; @@ -41,19 +48,10 @@ class EthPublicKeyUtilsTest { "0xaf80b90d25145da28c583359beb47b21796b2fe1a23c1511e443e7a64dfdb27d7434c380f0aa4c500e220aa1a9d068514b1ff4d5019e624e7ba1efe82b340a59"; private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); - @Test - public void createsPublicKeyFromECPoint() { - final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPoint expectedEcPoint = createEcPoint(publicKeyBytes); - - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(expectedEcPoint); - verifyPublicKey(ecPublicKey, publicKeyBytes, expectedEcPoint); - } - @Test public void createsPublicKeyFromBytes() { final Bytes expectedPublicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(expectedPublicKeyBytes); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(expectedPublicKeyBytes); final ECPoint expectedEcPoint = createEcPoint(expectedPublicKeyBytes); verifyPublicKey(ecPublicKey, expectedPublicKeyBytes, expectedEcPoint); @@ -62,26 +60,75 @@ public void createsPublicKeyFromBytes() { @Test public void createsPublicKeyFromBigInteger() { final BigInteger publicKey = Numeric.toBigInt(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(publicKey); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bigIntegerToECPublicKey(publicKey); final Bytes expectedPublicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); final ECPoint expectedEcPoint = createEcPoint(expectedPublicKeyBytes); verifyPublicKey(ecPublicKey, expectedPublicKeyBytes, expectedEcPoint); } + private static Stream validPublicKeys() { + final KeyPair keyPair; + try { + keyPair = EthPublicKeyUtils.createSecp256k1KeyPair(new SecureRandom()); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + return Stream.of( + // Compressed (33 bytes) + EthPublicKeyUtils.getEncoded((ECPublicKey) keyPair.getPublic(), true), + // Uncompressed without prefix (64 bytes) + EthPublicKeyUtils.getEncoded((ECPublicKey) keyPair.getPublic(), false), + // Uncompressed with prefix (65 bytes) + Bytes.concatenate( + Bytes.of(0x04), + EthPublicKeyUtils.getEncoded((ECPublicKey) keyPair.getPublic(), false))); + } + + @ParameterizedTest + @MethodSource("validPublicKeys") + void acceptsValidPublicKeySizes(final Bytes publicKey) { + assertThatCode(() -> EthPublicKeyUtils.bytesToECPublicKey(publicKey)) + .doesNotThrowAnyException(); + } + @ParameterizedTest - @ValueSource(ints = {0, 63, 65}) - public void throwsInvalidArgumentExceptionForInvalidPublicKeySize(final int size) { - assertThatThrownBy(() -> EthPublicKeyUtils.createPublicKey(Bytes.random(size))) + @ValueSource(ints = {0, 32, 34, 63, 66}) + void throwsIllegalArgumentExceptionForInvalidPublicKeySize(final int size) { + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(Bytes.random(size))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid public key length. Expected 33, 64, or 65 bytes."); + } + + @Test + void throwsIllegalArgumentExceptionForInvalid33ByteKey() { + Bytes invalidCompressedKey = Bytes.concatenate(Bytes.of(0x00), Bytes.random(32)); + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(invalidCompressedKey)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Incorrect length for infinity encoding"); + } + + @Test + void throwsIllegalArgumentExceptionForInvalid65ByteKey() { + Bytes invalidUncompressedKey = Bytes.concatenate(Bytes.of(0x00), Bytes.random(64)); + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(invalidUncompressedKey)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid public key size must be 64 bytes"); + .hasMessageContaining("Incorrect length for infinity encoding"); + } + + @Test + void throwsIllegalArgumentExceptionForInvalidCompressedKeyPrefix() { + Bytes invalidCompressedKey = Bytes.concatenate(Bytes.of(0x04), Bytes.random(32)); + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(invalidCompressedKey)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Incorrect length for uncompressed encoding"); } @Test public void publicKeyIsConvertedToEthHexString() { final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(publicKeyBytes); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKeyBytes); final String hexString = EthPublicKeyUtils.toHexString(ecPublicKey); assertThat(hexString).isEqualTo(PUBLIC_KEY); } @@ -90,33 +137,71 @@ public void publicKeyIsConvertedToEthHexString() { public void publicKeyIsConvertedToEthBytes() { final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(publicKeyBytes); - final Bytes bytes = Bytes.wrap(EthPublicKeyUtils.toByteArray(ecPublicKey)); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKeyBytes); + final Bytes bytes = EthPublicKeyUtils.getEncoded(ecPublicKey, false); assertThat(bytes).isEqualTo(publicKeyBytes); assertThat(bytes.size()).isEqualTo(64); assertThat(bytes.get(0)).isNotEqualTo(0x4); } + @Test + public void encodePublicKey() { + final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKeyBytes); + + final Bytes uncompressedWithoutPrefix = EthPublicKeyUtils.getEncoded(ecPublicKey, false); + final Bytes compressed = EthPublicKeyUtils.getEncoded(ecPublicKey, true); + + assertThat(uncompressedWithoutPrefix.size()).isEqualTo(64); + assertThat(compressed.size()).isEqualTo(33); + } + private void verifyPublicKey( final ECPublicKey ecPublicKey, final Bytes publicKeyBytes, final ECPoint ecPoint) { + // verify public point assertThat(ecPublicKey.getW()).isEqualTo(ecPoint); + + // verify algorithm assertThat(ecPublicKey.getAlgorithm()).isEqualTo("EC"); + // verify curve parameters final ECParameterSpec params = ecPublicKey.getParams(); assertThat(params.getCofactor()).isEqualTo(CURVE_PARAMS.getCurve().getCofactor().intValue()); - assertThat(params.getOrder()).isEqualTo(CURVE_PARAMS.getCurve().getOrder()); - assertThat(params.getGenerator()).isEqualTo(fromBouncyCastleECPoint(CURVE_PARAMS.getG())); - + assertThat(params.getOrder()).isEqualTo(CURVE_PARAMS.getN()); + assertThat(params.getGenerator().getAffineX()) + .isEqualTo(CURVE_PARAMS.getG().getAffineXCoord().toBigInteger()); + assertThat(params.getGenerator().getAffineY()) + .isEqualTo(CURVE_PARAMS.getG().getAffineYCoord().toBigInteger()); + assertThat(params.getCurve().getA()).isEqualTo(CURVE_PARAMS.getCurve().getA().toBigInteger()); + assertThat(params.getCurve().getB()).isEqualTo(CURVE_PARAMS.getCurve().getB().toBigInteger()); + assertThat(params.getCurve().getField().getFieldSize()).isEqualTo(256); + + // Verify format assertThat(ecPublicKey.getFormat()).isEqualTo("X.509"); + // Verify encoded form SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(ecPublicKey.getEncoded())); assertThat(subjectPublicKeyInfo.getPublicKeyData().getBytes()) .isEqualTo(Bytes.concatenate(Bytes.of(0x4), publicKeyBytes).toArray()); + // Verify algorithm identifier final AlgorithmIdentifier algorithm = subjectPublicKeyInfo.getAlgorithm(); assertThat(algorithm.getAlgorithm().getId()).isEqualTo(EC_OID); - assertThat(algorithm.getParameters().toASN1Primitive().toString()).isEqualTo(SECP_OID); + + // Verify curve identifier + X962Parameters x962Params = X962Parameters.getInstance(algorithm.getParameters()); + if (x962Params.isNamedCurve()) { + assertThat(x962Params.getParameters()).isEqualTo(new ASN1ObjectIdentifier(SECP_OID)); + } else if (x962Params.isImplicitlyCA()) { + fail("Implicitly CA parameters are not expected for secp256k1"); + } else { + X9ECParameters ecParams = X9ECParameters.getInstance(x962Params.getParameters()); + assertThat(ecParams.getCurve()).isEqualTo(CURVE_PARAMS.getCurve()); + assertThat(ecParams.getG()).isEqualTo(CURVE_PARAMS.getG()); + assertThat(ecParams.getN()).isEqualTo(CURVE_PARAMS.getN()); + assertThat(ecParams.getH()).isEqualTo(CURVE_PARAMS.getH()); + } } private ECPoint createEcPoint(final Bytes publicKeyBytes) { @@ -124,18 +209,4 @@ private ECPoint createEcPoint(final Bytes publicKeyBytes) { final Bytes y = publicKeyBytes.slice(32, 32); return new ECPoint(Numeric.toBigInt(x.toArrayUnsafe()), Numeric.toBigInt(y.toArrayUnsafe())); } - - private ECPoint fromBouncyCastleECPoint( - final org.bouncycastle.math.ec.ECPoint bouncyCastleECPoint) { - final ECFieldElement xCoord = bouncyCastleECPoint.getAffineXCoord(); - final ECFieldElement yCoord = bouncyCastleECPoint.getAffineYCoord(); - - final Bytes32 xEncoded = Bytes32.wrap(xCoord.getEncoded()); - final Bytes32 yEncoded = Bytes32.wrap(yCoord.getEncoded()); - - final BigInteger x = xEncoded.toUnsignedBigInteger(); - final BigInteger y = yEncoded.toUnsignedBigInteger(); - - return new ECPoint(x, y); - } } diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java index 527c3e854..5e7dd5d12 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java @@ -116,8 +116,7 @@ void awsSignatureCanBeVerified() throws SignatureException { final Signer signer = new AwsKmsSignerFactory(cachedAwsKmsClientFactory, applySha3Hash) .createSigner(awsKmsMetadata); - final BigInteger publicKey = - Numeric.toBigInt(EthPublicKeyUtils.toByteArray(signer.getPublicKey())); + final BigInteger publicKey = EthPublicKeyUtils.ecPublicKeyToBigInteger(signer.getPublicKey()); final byte[] dataToSign = "Hello".getBytes(UTF_8); diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java index 3fa551e11..c9549088a 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java @@ -71,7 +71,7 @@ void azureSignerCanSign() throws SignatureException { new AzureKeyVaultSignerFactory(new AzureKeyVaultFactory(), new AzureHttpClientFactory()) .createSigner(config); final BigInteger publicKey = - Numeric.toBigInt(EthPublicKeyUtils.toByteArray(azureNonHashedDataSigner.getPublicKey())); + EthPublicKeyUtils.ecPublicKeyToBigInteger(azureNonHashedDataSigner.getPublicKey()); final byte[] dataToSign = "Hello World".getBytes(UTF_8); 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); + } + } +}