diff --git a/.circleci/config.yml b/.circleci/config.yml index a4a2a4799..3213f7465 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -264,7 +264,8 @@ jobs: name: build and test Docker image command: | ./gradlew --no-daemon --parallel "-Pbranch=${CIRCLE_BRANCH}" testDocker - + - store_test_results: + path: docker/reports publishDockerAmd64: executor: machine_executor_amd64 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 46fabd981..2a977c9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Next Version +### Features Added +- Added endpoint `/api/v1/eth2/ext/sign/:identifier` which is enabled using cli option `--Xsigning-ext-enabled=true`. This endpoint allows signing of additional data not covered by the remoting API specs. [#982](https://github.com/Consensys/web3signer/pull/982) + ### Bugs fixed - Update transitive dependency threetenbp and google cloud secretmanager library to fix CVE-2024-23082, CVE-2024-23081 - Update bouncycastle libraries to fix CVE-2024-29857, CVE-2024-30171, CVE-2024-30172 @@ -11,6 +14,7 @@ - Update Postgresql JDBC driver to fix CVE-2024-1597 - Fix cached gvr to be thread-safe during first boot. [#978](https://github.com/Consensys/web3signer/issues/978) +--- ## 24.2.0 This is a required update for Mainnet users containing the configuration for the Deneb upgrade on March 13th. This update is required for Gnosis Deneb network upgrade on March 11th. For all other networks, this update is optional. 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 5f98f3d6c..52924740d 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 @@ -66,6 +66,8 @@ public class Signer extends FilecoinJsonRpcEndpoint { public static final String ETH2_PUBLIC_KEYS = "/api/v1/eth2/publicKeys"; // bls keys public static final String RELOAD_ENDPOINT = "/reload"; + public static final String SIGN_EXT_ENDPOINT = "/api/v1/eth2/ext/sign/{identifier}"; + public static final ObjectMapper ETH_2_INTERFACE_OBJECT_MAPPER = SigningObjectMapperFactory.createObjectMapper().setSerializationInclusion(Include.NON_NULL); private static final String METRICS_ENDPOINT = "/metrics"; @@ -175,6 +177,17 @@ public Response eth2Sign( .post(signPath(BLS)); } + public Response signExtensionPayload( + final String publicKey, final String payload, final ContentType acceptMediaType) { + return given() + .baseUri(getUrl()) + .contentType(ContentType.JSON) + .accept(acceptMediaType) + .pathParam("identifier", publicKey) + .body(payload) + .post(SIGN_EXT_ENDPOINT); + } + public Response callApiPublicKeys(final KeyType keyType) { return given().baseUri(getUrl()).get(publicKeysPath(keyType)); } 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 b84c9e9a8..ebfa5618d 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 @@ -79,6 +79,8 @@ public class SignerConfiguration { private final ChainIdProvider chainIdProvider; private final Optional v3KeystoresBulkloadParameters; + private final boolean signingExtEnabled; + public SignerConfiguration( final String hostname, final int httpRpcPort, @@ -123,7 +125,8 @@ public SignerConfiguration( final int downstreamHttpPort, final Optional downstreamTlsOptions, final ChainIdProvider chainIdProvider, - final Optional v3KeystoresBulkloadParameters) { + final Optional v3KeystoresBulkloadParameters, + final boolean signingExtEnabled) { this.hostname = hostname; this.logLevel = logLevel; this.httpRpcPort = httpRpcPort; @@ -168,6 +171,7 @@ public SignerConfiguration( this.downstreamTlsOptions = downstreamTlsOptions; this.chainIdProvider = chainIdProvider; this.v3KeystoresBulkloadParameters = v3KeystoresBulkloadParameters; + this.signingExtEnabled = signingExtEnabled; } public String hostname() { @@ -353,4 +357,8 @@ public ChainIdProvider getChainIdProvider() { public Optional getV3KeystoresBulkloadParameters() { return v3KeystoresBulkloadParameters; } + + public boolean isSigningExtEnabled() { + return signingExtEnabled; + } } 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 f28ccd49c..9c83c1d12 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 @@ -83,6 +83,8 @@ public class SignerConfigurationBuilder { private KeystoresParameters v3KeystoresBulkloadParameters; + private boolean signingExtEnabled; + public SignerConfigurationBuilder withLogLevel(final Level logLevel) { this.logLevel = logLevel; return this; @@ -318,6 +320,11 @@ public SignerConfigurationBuilder withV3KeystoresBulkloadParameters( return this; } + public SignerConfigurationBuilder withSigningExtEnabled(final boolean signingExtEnabled) { + this.signingExtEnabled = signingExtEnabled; + return this; + } + public SignerConfiguration build() { if (mode == null) { throw new IllegalArgumentException("Mode cannot be null"); @@ -366,6 +373,7 @@ public SignerConfiguration build() { downstreamHttpPort, Optional.ofNullable(downstreamTlsOptions), chainIdProvider, - Optional.ofNullable(v3KeystoresBulkloadParameters)); + Optional.ofNullable(v3KeystoresBulkloadParameters), + signingExtEnabled); } } 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 2da917caf..346fdf609 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 @@ -158,6 +158,11 @@ public List createCmdLineParams() { .getGcpParameters() .ifPresent(gcpParameters -> yamlConfig.append(gcpBulkLoadingOptions(gcpParameters))); + if (signerConfig.isSigningExtEnabled()) { + yamlConfig.append( + String.format(YAML_BOOLEAN_FMT, "eth2.Xsigning-ext-enabled", Boolean.TRUE)); + } + final CommandArgs subCommandArgs = createSubCommandArgs(); params.addAll(subCommandArgs.params); yamlConfig.append(subCommandArgs.yamlConfig); 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 419d9e181..c6b567984 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 @@ -134,6 +134,10 @@ public List createCmdLineParams() { signerConfig .getGcpParameters() .ifPresent(gcpParams -> params.addAll(gcpSecretManagerBulkLoadingOptions(gcpParams))); + + if (signerConfig.isSigningExtEnabled()) { + params.add("--Xsigning-ext-enabled=true"); + } } else if (signerConfig.getMode().equals("eth1")) { params.add("--downstream-http-port"); params.add(Integer.toString(signerConfig.getDownstreamHttpPort())); diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/ProofOfValidationSigningExtAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/ProofOfValidationSigningExtAcceptanceTest.java new file mode 100644 index 000000000..6629842f3 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/ProofOfValidationSigningExtAcceptanceTest.java @@ -0,0 +1,187 @@ +/* + * 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.signing; + +import static io.restassured.http.ContentType.JSON; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.teku.spec.SpecMilestone.DENEB; + +import tech.pegasys.teku.bls.BLS; +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSecretKey; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.web3signer.core.service.http.SigningObjectMapperFactory; +import tech.pegasys.web3signer.core.service.http.handlers.signing.ProofOfValidationBody; +import tech.pegasys.web3signer.core.service.http.handlers.signing.SigningExtensionType; +import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; +import tech.pegasys.web3signer.dsl.utils.MetadataFileHelpers; +import tech.pegasys.web3signer.signing.KeyType; + +import java.io.IOException; +import java.nio.file.Path; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.http.ContentType; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class ProofOfValidationSigningExtAcceptanceTest extends SigningAcceptanceTestBase { + + private static final String PRIVATE_KEY = + "3ee2224386c82ffea477e2adf28a2929f5c349165a4196158c7f3a2ecca40f35"; + private static final MetadataFileHelpers METADATA_FILE_HELPERS = new MetadataFileHelpers(); + private static final BLSSecretKey KEY = + BLSSecretKey.fromBytes(Bytes32.fromHexString(PRIVATE_KEY)); + private static final BLSKeyPair KEY_PAIR = new BLSKeyPair(KEY); + private static final BLSPublicKey PUBLIC_KEY = KEY_PAIR.getPublicKey(); + private static final ObjectMapper JSON_MAPPER = SigningObjectMapperFactory.createObjectMapper(); + + @BeforeEach + void setup() { + final String configFilename = PUBLIC_KEY.toString().substring(2); + final Path keyConfigFile = testDirectory.resolve(configFilename + ".yaml"); + METADATA_FILE_HELPERS.createUnencryptedYamlFileAt(keyConfigFile, PRIVATE_KEY, KeyType.BLS); + + setForkEpochsAndStartSigner( + new SignerConfigurationBuilder() + .withKeyStoreDirectory(testDirectory) + .withMode("eth2") + .withNetwork(Eth2Network.MINIMAL.configName()) + .withSigningExtEnabled(true), + DENEB); + } + + @ParameterizedTest(name = "{index} - Testing Accept Media Type: {0}") + @EnumSource( + value = ContentType.class, + names = {"ANY", "JSON", "TEXT"}) + void extensionSigningData(final ContentType acceptMediaType) throws Exception { + final var signingExtensionBody = + new ProofOfValidationBody( + SigningExtensionType.PROOF_OF_VALIDATION, + "AT", + UInt64.valueOf(System.currentTimeMillis())); + final var payload = JSON_MAPPER.writeValueAsString(signingExtensionBody); + + final var response = + signer.signExtensionPayload(PUBLIC_KEY.toString(), payload, acceptMediaType); + + response.then().statusCode(200).contentType(JSON); + + final var signatureResponse = + JSON_MAPPER.readValue(response.asByteArray(), ProofOfValidationResponse.class); + + // assert that the signature is valid + final var blsSignature = + BLSSignature.fromBytesCompressed(Bytes.fromHexString(signatureResponse.signature)); + + final var isValidBLSSig = + BLS.verify(PUBLIC_KEY, Bytes.wrap(payload.getBytes(UTF_8)), blsSignature); + assertThat(isValidBLSSig).isTrue(); + + // assert that Base64 encoded payload is correct + assertThat(signatureResponse.payload) + .isEqualTo(Bytes.wrap(payload.getBytes(UTF_8)).toBase64String()); + } + + @ParameterizedTest + @ValueSource(strings = {"1634025600000", "\"1634025600000\""}) + void timestampAsStringAndNumberResultsInValidSignature(final String timestampValue) + throws IOException { + final var payloadFormat = + """ + { + "type": "PROOF_OF_VALIDATION", + "platform": "AT", + "timestamp": %s + } + """; + final var payload = String.format(payloadFormat, timestampValue); + + final var response = signer.signExtensionPayload(PUBLIC_KEY.toString(), payload, JSON); + response.then().statusCode(200).contentType(JSON); + + final var signatureResponse = + JSON_MAPPER.readValue(response.asByteArray(), ProofOfValidationResponse.class); + + // assert that the signature is valid + final var blsSignature = + BLSSignature.fromBytesCompressed(Bytes.fromHexString(signatureResponse.signature)); + + final var isValidBLSSig = + BLS.verify(PUBLIC_KEY, Bytes.wrap(payload.getBytes(UTF_8)), blsSignature); + assertThat(isValidBLSSig).isTrue(); + + // assert that Base64 encoded payload is correct + assertThat(signatureResponse.payload) + .isEqualTo(Bytes.wrap(payload.getBytes(UTF_8)).toBase64String()); + } + + @Test + void invalidIdentifierCausesNotFound() throws Exception { + final ProofOfValidationBody proofOfValidationBody = + new ProofOfValidationBody( + SigningExtensionType.PROOF_OF_VALIDATION, + "AT", + UInt64.valueOf(System.currentTimeMillis())); + final String data = JSON_MAPPER.writeValueAsString(proofOfValidationBody); + + signer.signExtensionPayload("0x1234", data, JSON).then().statusCode(404); + } + + @ParameterizedTest(name = "{index} - Testing Invalid Body: {0}") + @ValueSource(strings = {"", "invalid", "{}", "{\"data\": \"invalid\"}"}) + void invalidBodyCausesBadRequestStatusCode(final String data) { + signer.signExtensionPayload(PUBLIC_KEY.toString(), data, JSON).then().statusCode(400); + } + + @Test + void invalidSignExtensionTypeCausesBadRequestStatusCode() throws Exception { + final ProofOfValidationBody proofOfValidationBody = + new ProofOfValidationBody( + SigningExtensionType.PROOF_OF_VALIDATION, + "AT", + UInt64.valueOf(System.currentTimeMillis())); + var payload = JSON_MAPPER.writeValueAsString(proofOfValidationBody); + payload = payload.replace("PROOF_OF_VALIDATION", "INVALID_TYPE"); + + signer.signExtensionPayload(PUBLIC_KEY.toString(), payload, JSON).then().statusCode(400); + } + + @Test + void extraJsonFieldsCausesBadRequestStatusCode() throws Exception { + final ProofOfValidationBody proofOfValidationBody = + new ProofOfValidationBody( + SigningExtensionType.PROOF_OF_VALIDATION, + "AT", + UInt64.valueOf(System.currentTimeMillis())); + var payload = JSON_MAPPER.writeValueAsString(proofOfValidationBody); + payload = payload.replace("}", ",\"extraField\": \"extraValue\"}"); + + signer.signExtensionPayload(PUBLIC_KEY.toString(), payload, JSON).then().statusCode(400); + } + + record ProofOfValidationResponse( + @JsonProperty(value = "payload", required = true) String payload, + @JsonProperty(value = "signature", required = true) String signature) {} +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SigningAcceptanceTestBase.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SigningAcceptanceTestBase.java index 7ed27f78d..37a516f24 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SigningAcceptanceTestBase.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SigningAcceptanceTestBase.java @@ -49,19 +49,13 @@ protected void setupEth2Signer(final Eth2Network eth2Network, final SpecMileston .withKeyStoreDirectory(testDirectory) .withMode("eth2") .withNetwork(eth2Network.configName()); - - setForkEpochs(specMilestone, builder); - - startSigner(builder.build()); + setForkEpochsAndStartSigner(builder, specMilestone); } protected void setupEth2Signer(final Path networkConfigFile, final SpecMilestone specMilestone) { final SignerConfigurationBuilder builder = new SignerConfigurationBuilder(); builder.withKeyStoreDirectory(testDirectory).withMode("eth2").withNetwork(networkConfigFile); - - setForkEpochs(specMilestone, builder); - - startSigner(builder.build()); + setForkEpochsAndStartSigner(builder, specMilestone); } protected void setupEth2SignerWithCustomNetworkConfig(final Path networkConfigFile) { @@ -73,6 +67,12 @@ protected void setupEth2SignerWithCustomNetworkConfig(final Path networkConfigFi startSigner(builder.build()); } + protected void setForkEpochsAndStartSigner( + final SignerConfigurationBuilder builder, final SpecMilestone specMilestone) { + setForkEpochs(specMilestone, builder); + startSigner(builder.build()); + } + private void setForkEpochs( final SpecMilestone specMilestone, final SignerConfigurationBuilder builder) { switch (specMilestone) { diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/Web3SignerBaseCommand.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/Web3SignerBaseCommand.java index c504f8399..70dbbe8f3 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/Web3SignerBaseCommand.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/Web3SignerBaseCommand.java @@ -213,7 +213,7 @@ public class Web3SignerBaseCommand implements BaseConfig, Runnable { paramLabel = INTEGER_FORMAT_HELP) private Integer vertxWorkerPoolSize = null; - @Deprecated + @Deprecated(forRemoval = true) @Option(names = "--Xworker-pool-size", hidden = true) private Integer deprecatedWorkerPoolSize = null; 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 cbf3f1e00..35ac5a6c0 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 @@ -133,6 +133,14 @@ private static class NetworkCliCompletionCandidates extends ArrayList { arity = "1") private boolean isKeyManagerApiEnabled = false; + @CommandLine.Option( + names = "--Xsigning-ext-enabled", + description = "Set to true to enable signing extensions.", + paramLabel = "", + arity = "1", + hidden = true) + private boolean signingExtEnabled = false; + @Mixin private PicoCliSlashingProtectionParameters slashingProtectionParameters; @Mixin private PicoCliEth2AzureKeyVaultParameters azureKeyVaultParameters; @Mixin private PicoKeystoresParameters keystoreParameters; @@ -156,7 +164,8 @@ public Runner createRunner() { awsSecretsManagerParameters, gcpSecretManagerParameters, eth2Spec, - isKeyManagerApiEnabled); + isKeyManagerApiEnabled, + signingExtEnabled); } private void logNetworkSpecInformation() { 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 d642cdaa0..e3ec76a12 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java @@ -32,6 +32,7 @@ import tech.pegasys.web3signer.core.service.http.handlers.keymanager.imports.ImportKeystoresHandler; import tech.pegasys.web3signer.core.service.http.handlers.keymanager.list.ListKeystoresHandler; import tech.pegasys.web3signer.core.service.http.handlers.signing.SignerForIdentifier; +import tech.pegasys.web3signer.core.service.http.handlers.signing.SigningExtensionHandler; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.Eth2SignForIdentifierHandler; import tech.pegasys.web3signer.core.service.http.metrics.HttpApiMetrics; import tech.pegasys.web3signer.keystorage.aws.AwsSecretsManagerProvider; @@ -94,6 +95,7 @@ public class Eth2Runner extends Runner { public static final String KEYSTORES_PATH = "/eth/v1/keystores"; public static final String PUBLIC_KEYS_PATH = "/api/v1/eth2/publicKeys"; public static final String SIGN_PATH = "/api/v1/eth2/sign/:identifier"; + public static final String SIGN_EXT_PATH = "/api/v1/eth2/ext/sign/:identifier"; public static final String HIGH_WATERMARK_PATH = "/api/v1/eth2/highWatermark"; private static final Logger LOG = LogManager.getLogger(); @@ -106,6 +108,7 @@ public class Eth2Runner extends Runner { private final KeystoresParameters keystoresParameters; private final Spec eth2Spec; private final boolean isKeyManagerApiEnabled; + private final boolean signingExtEnabled; public Eth2Runner( final BaseConfig baseConfig, @@ -115,7 +118,8 @@ public Eth2Runner( final AwsVaultParameters awsVaultParameters, final GcpSecretManagerParameters gcpSecretManagerParameters, final Spec eth2Spec, - final boolean isKeyManagerApiEnabled) { + final boolean isKeyManagerApiEnabled, + final boolean signingExtEnabled) { super(baseConfig); this.slashingProtectionContext = createSlashingProtection(slashingProtectionParameters); this.azureKeyVaultParameters = azureKeyVaultParameters; @@ -126,6 +130,7 @@ public Eth2Runner( this.isKeyManagerApiEnabled = isKeyManagerApiEnabled; this.awsVaultParameters = awsVaultParameters; this.gcpSecretManagerParameters = gcpSecretManagerParameters; + this.signingExtEnabled = signingExtEnabled; } private Optional createSlashingProtection( @@ -186,6 +191,8 @@ private void registerEth2Routes( .handler(new HighWatermarkHandler(protectionContext.getSlashingProtection())) .failureHandler(errorHandler)); + addSigningExtHandler(router, errorHandler, blsSigner); + if (isKeyManagerApiEnabled) { router .route(HttpMethod.GET, KEYSTORES_PATH) @@ -394,6 +401,35 @@ private void registerSignerLoadingHealthCheck( }); } + private void addSigningExtHandler( + final Router router, + final LogErrorHandler errorHandler, + final SignerForIdentifier signer) { + if (!signingExtEnabled) { + return; + } + + router + .route(HttpMethod.POST, SIGN_EXT_PATH) + .blockingHandler(new SigningExtensionHandler(signer), false) + .failureHandler(errorHandler) + .failureHandler( + ctx -> { + final int statusCode = ctx.statusCode(); + if (statusCode == 400) { + ctx.response() + .setStatusCode(statusCode) + .end(new JsonObject().put("error", "Bad Request").encode()); + } else if (statusCode == 404) { + ctx.response() + .setStatusCode(statusCode) + .end(new JsonObject().put("error", "Identifier not found.").encode()); + } else { + ctx.next(); // go to global failure handler + } + }); + } + @Override public void run() { super.run(); diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/OpenApiOperationsId.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/OpenApiOperationsId.java index 7f9c6cd0d..80233e06f 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/OpenApiOperationsId.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/OpenApiOperationsId.java @@ -13,6 +13,7 @@ package tech.pegasys.web3signer.core.service.http; /** Operation IDs as defined in web3signer.yaml */ +@Deprecated(forRemoval = true) public enum OpenApiOperationsId { ETH2_SIGN, ETH1_SIGN, diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/ProofOfValidationBody.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/ProofOfValidationBody.java new file mode 100644 index 000000000..0fede0460 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/ProofOfValidationBody.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.signing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.tuweni.units.bigints.UInt64; + +public record ProofOfValidationBody( + @JsonProperty(value = "type", required = true) SigningExtensionType type, + @JsonProperty(value = "platform", required = true) String platform, + @JsonProperty(value = "timestamp", required = true) UInt64 timestamp) {} 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 new file mode 100644 index 000000000..be9100a2a --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionHandler.java @@ -0,0 +1,77 @@ +/* + * 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.signing; + +import static java.nio.charset.StandardCharsets.UTF_8; +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.web3signer.core.service.http.SigningObjectMapperFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import org.apache.tuweni.bytes.Bytes; + +/** A Signing Extension to sign very specific messages for a given identifier */ +public class SigningExtensionHandler implements Handler { + public static final int NOT_FOUND = 404; + public static final int BAD_REQUEST = 400; + // custom copy of ObjectMapper that fails on unknown properties. + private static final ObjectMapper JSON_MAPPER = + SigningObjectMapperFactory.createObjectMapper() + .copy() + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + private final SignerForIdentifier signerForIdentifier; + + public SigningExtensionHandler(final SignerForIdentifier signerForIdentifier) { + this.signerForIdentifier = signerForIdentifier; + } + + @Override + public void handle(final RoutingContext routingContext) { + final String identifier = normaliseIdentifier(routingContext.pathParam("identifier")); + final String body = routingContext.body().asString(); + + // validate that we have correct incoming json body + try { + JSON_MAPPER.readValue(body, ProofOfValidationBody.class); + } catch (final JsonProcessingException | IllegalArgumentException e) { + routingContext.fail(BAD_REQUEST); + return; + } + + final Bytes payload = Bytes.wrap(body.getBytes(UTF_8)); + signerForIdentifier + .sign(identifier, payload) + .ifPresentOrElse( + blsSigHex -> respondWithSignature(routingContext, payload, blsSigHex), + () -> routingContext.fail(NOT_FOUND)); + } + + private void respondWithSignature( + final RoutingContext routingContext, final Bytes payload, final String blsSigHex) { + routingContext.response().putHeader("Content-Type", JSON_UTF_8); + routingContext + .response() + .end( + new JsonObject() + .put("payload", payload.toBase64String()) + .put("signature", blsSigHex) + .encode()); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionType.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionType.java new file mode 100644 index 000000000..9005fb0fb --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/SigningExtensionType.java @@ -0,0 +1,17 @@ +/* + * 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.signing; + +public enum SigningExtensionType { + PROOF_OF_VALIDATION +} diff --git a/docker/test.sh b/docker/test.sh index 0185e2f31..d7f3ff390 100755 --- a/docker/test.sh +++ b/docker/test.sh @@ -18,7 +18,7 @@ i=0 # Test for normal startup with ports opened GOSS_FILES_PATH=tests/01 \ bash tests/dgoss \ -run ${DOCKER_TEST_IMAGE} \ +run --sysctl net.ipv6.conf.all.disable_ipv6=1 ${DOCKER_TEST_IMAGE} \ --http-listen-host=0.0.0.0 \ eth2 \ --slashing-protection-enabled=false \ @@ -29,4 +29,5 @@ docker image rm ${DOCKER_TEST_IMAGE} # also check for security vulns with trivy docker run aquasec/trivy image $DOCKER_IMAGE +echo "test.sh Exit code: $i" exit $i