diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index f0fcb2c397..fc57772a0d 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -524,6 +524,31 @@ public class BesuCommand implements DefaultCommandValues, Runnable { "Require authentication for the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})") private final Boolean isRpcWsAuthenticationEnabled = false; + @Option( + names = {"--privacy-tls-enabled"}, + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "Enable TLS for connecting to privacy enclave (default: ${DEFAULT-VALUE})") + private final Boolean isPrivacyTlsEnabled = false; + + @Option( + names = "--privacy-tls-keystore-file", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = + "Path to a PKCS#12 formatted keystore; used to enable TLS on inbound connections.") + private final Path privacyKeyStoreFile = null; + + @Option( + names = "--privacy-tls-keystore-password-file", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "Path to a file containing the password used to decrypt the keystore.") + private final Path privacyKeyStorePasswordFile = null; + + @Option( + names = "--privacy-tls-known-enclave-file", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "Path to a file containing the fingerprints of the authorized privacy enclave.") + private final Path privacyTlsKnownEnclaveFile = null; + @Option( names = {"--metrics-enabled"}, description = "Set to start the metrics exporter (default: ${DEFAULT-VALUE})") @@ -1265,6 +1290,18 @@ public class BesuCommand implements DefaultCommandValues, Runnable { asList("--rpc-http-tls-known-clients-file", "--rpc-http-tls-ca-clients-enabled")); } + private void checkPrivacyTlsOptionsDependencies() { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--privacy-tls-enabled", + !isPrivacyTlsEnabled, + asList( + "--privacy-tls-keystore-file", + "--privacy-tls-keystore-password-file", + "--privacy-tls-known-enclave-file")); + } + private Optional rpcHttpTlsConfiguration() { if (!isRpcTlsConfigurationRequired()) { return Optional.empty(); @@ -1492,7 +1529,10 @@ public class BesuCommand implements DefaultCommandValues, Runnable { "--privacy-url", "--privacy-public-key-file", "--privacy-precompiled-address", - "--privacy-multi-tenancy-enabled")); + "--privacy-multi-tenancy-enabled", + "--privacy-tls-enabled")); + + checkPrivacyTlsOptionsDependencies(); final PrivacyParameters.Builder privacyParametersBuilder = new PrivacyParameters.Builder(); if (isPrivacyEnabled) { @@ -1538,6 +1578,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable { privacyParametersBuilder.setPrivateKeyPath(privacyMarkerTransactionSigningKeyPath); privacyParametersBuilder.setStorageProvider( privacyKeyStorageProvider(keyValueStorageName + "-privacy")); + if (isPrivacyTlsEnabled) { + privacyParametersBuilder.setPrivacyKeyStoreFile(privacyKeyStoreFile); + privacyParametersBuilder.setPrivacyKeyStorePasswordFile(privacyKeyStorePasswordFile); + privacyParametersBuilder.setPrivacyTlsKnownEnclaveFile(privacyTlsKnownEnclaveFile); + } privacyParametersBuilder.setEnclaveFactory(new EnclaveFactory(vertx)); } else { if (anyPrivacyApiEnabled()) { diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index d8e8ba3b27..45128ef975 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -1532,6 +1532,37 @@ public class BesuCommandTest extends CommandTestAbstract { assertThat(commandErrorOutput.toString()).isEmpty(); } + @Test + public void privacyTlsOptionsRequiresTlsToBeEnabled() { + when(storageService.getByName("rocksdb-privacy")) + .thenReturn(Optional.of(rocksDBSPrivacyStorageFactory)); + final URL configFile = this.getClass().getResource("/orion_publickey.pub"); + + parseCommand( + "--privacy-enabled", + "--privacy-url", + ENCLAVE_URI, + "--privacy-public-key-file", + configFile.getPath(), + "--privacy-tls-keystore-file", + "/Users/me/key"); + + verifyOptionsConstraintLoggerCall("--privacy-tls-enabled", "--privacy-tls-keystore-file"); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void privacyTlsOptionsRequiresPrivacyToBeEnabled() { + parseCommand("--privacy-tls-enabled", "--privacy-tls-keystore-file", "/Users/me/key"); + + verifyOptionsConstraintLoggerCall("--privacy-enabled", "--privacy-tls-enabled"); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + @Test public void fastSyncOptionsRequiresFastSyncModeToBeSet() { parseCommand("--fast-sync-min-peers", "5"); diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index 0c1d204730..56e60dc23b 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -58,6 +58,12 @@ rpc-http-tls-client-auth-enabled=false rpc-http-tls-known-clients-file="rpc_tls_clients.txt" rpc-http-tls-ca-clients-enabled=false +# PRIVACY TLS +privacy-tls-enabled=false +privacy-tls-keystore-file="none.pfx" +privacy-tls-keystore-password-file="none.passwd" +privacy-tls-known-enclave-file="privacy_tls_enclave.txt" + # GRAPHQL HTTP graphql-http-enabled=false graphql-http-host="6.7.8.9" diff --git a/enclave/build.gradle b/enclave/build.gradle index c2ef68b028..eebe14532f 100644 --- a/enclave/build.gradle +++ b/enclave/build.gradle @@ -1,8 +1,15 @@ dependencies { + api project(':util') + api project(':crypto') + implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'io.vertx:vertx-core' + implementation 'org.apache.tuweni:tuweni-net' implementation 'org.apache.logging.log4j:log4j-api' + runtimeOnly('org.bouncycastle:bcpkix-jdk15on') + + // test dependencies. testImplementation project(':testutil') @@ -10,6 +17,9 @@ dependencies { // integration test dependencies. integrationTestImplementation project(':testutil') + integrationTestImplementation 'org.bouncycastle:bcpkix-jdk15on' + + integrationTestImplementation 'junit:junit' integrationTestImplementation 'net.consensys:orion' diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java new file mode 100644 index 0000000000..4f09831b00 --- /dev/null +++ b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java @@ -0,0 +1,53 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.enclave; + +import java.io.File; +import java.net.URL; +import java.nio.file.Path; + +import com.google.common.io.Resources; + +public class TlsCertificateDefinition { + + private final File pkcs12File; + private final String password; + + public static TlsCertificateDefinition loadFromResource( + final String resourcePath, final String password) { + try { + final URL sslCertificate = Resources.getResource(resourcePath); + final Path keystorePath = Path.of(sslCertificate.getPath()); + + return new TlsCertificateDefinition(keystorePath.toFile(), password); + } catch (final Exception e) { + throw new RuntimeException("Failed to load TLS certificates", e); + } + } + + public TlsCertificateDefinition(final File pkcs12File, final String password) { + this.pkcs12File = pkcs12File; + this.password = password; + } + + public File getPkcs12File() { + return pkcs12File; + } + + public String getPassword() { + return password; + } +} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java new file mode 100644 index 0000000000..f1b1a639ed --- /dev/null +++ b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.enclave; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.hyperledger.besu.enclave.TlsHelpers.populateFingerprintFile; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Optional; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TlsEnabledEnclaveTest { + + private TlsEnabledHttpServerFactory serverFactory; + private Vertx vertx; + + final TlsCertificateDefinition httpServerCert = + TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "password"); + final TlsCertificateDefinition besuCert = + TlsCertificateDefinition.loadFromResource("tls/cert2.pfx", "password2"); + + public void shutdown() { + vertx.close(); + } + + @Before + public void setup() { + serverFactory = new TlsEnabledHttpServerFactory(); + this.vertx = Vertx.vertx(); + } + + @After + public void cleanup() { + serverFactory.shutdown(); + this.shutdown(); + } + + private Enclave createEnclave( + final int httpServerPort, final Path workDir, final boolean tlsEnabled) throws IOException { + + final Path serverFingerprintFile = workDir.resolve("server_known_clients"); + final Path besuCertPasswordFile = workDir.resolve("password_file"); + try { + populateFingerprintFile(serverFingerprintFile, httpServerCert, Optional.of(httpServerPort)); + Files.write(besuCertPasswordFile, besuCert.getPassword().getBytes(Charset.defaultCharset())); + + final EnclaveFactory factory = new EnclaveFactory(vertx); + if (tlsEnabled) { + final URI httpServerUri = new URI("https://localhost:" + httpServerPort); + return factory.createVertxEnclave( + httpServerUri, + besuCert.getPkcs12File().toPath(), + besuCertPasswordFile, + serverFingerprintFile); + } else { + return factory.createVertxEnclave(new URI("http://localhost:" + httpServerPort)); + } + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e) { + fail("unable to populate fingerprint file"); + return null; + } catch (URISyntaxException e) { + fail("unable to create URI"); + return null; + } + } + + @Test + public void nonTlsEnclaveCannotConnectToTlsServer() throws IOException { + + Path workDir = Files.createTempDirectory("test-certs"); + + // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up". + final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, true); + + final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, false); + + assertThat(enclave.upCheck()).isEqualTo(false); + } + + @Test + public void nonTlsEnclaveCanConnectToNonTlsServer() throws IOException { + + Path workDir = Files.createTempDirectory("test-certs"); + + // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up". + final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, false); + + final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, false); + + assertThat(enclave.upCheck()).isEqualTo(true); + } + + @Test + public void tlsEnclaveCannotConnectToNonTlsServer() throws IOException { + + Path workDir = Files.createTempDirectory("test-certs"); + + // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up!". + final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, false); + + final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, true); + + assertThat(enclave.upCheck()).isEqualTo(false); + } + + @Test + public void tlsEnclaveCanConnectToTlsServer() throws IOException { + + Path workDir = Files.createTempDirectory("test-certs"); + + // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up". + final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, true); + + final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, true); + + assertThat(enclave.upCheck()).isEqualTo(true); + } +} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java new file mode 100644 index 0000000000..1fd028e511 --- /dev/null +++ b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.enclave; + +import static org.hyperledger.besu.enclave.TlsHelpers.populateFingerprintFile; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import com.google.common.collect.Lists; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PfxOptions; +import io.vertx.ext.web.Router; +import org.apache.tuweni.net.tls.VertxTrustOptions; + +public class TlsEnabledHttpServerFactory { + + private final Vertx vertx; + private final List serversCreated = Lists.newArrayList(); + + public TlsEnabledHttpServerFactory() { + this.vertx = Vertx.vertx(); + } + + public void shutdown() { + serversCreated.forEach(HttpServer::close); + vertx.close(); + } + + public HttpServer create( + final TlsCertificateDefinition serverCert, + final TlsCertificateDefinition acceptedClientCerts, + final Path workDir, + final boolean tlsEnabled) { + try { + + final Path serverFingerprintFile = workDir.resolve("server_known_clients"); + populateFingerprintFile(serverFingerprintFile, acceptedClientCerts, Optional.empty()); + + final HttpServerOptions web3HttpServerOptions = new HttpServerOptions(); + web3HttpServerOptions.setPort(0); + if (tlsEnabled) { + web3HttpServerOptions.setSsl(true); + web3HttpServerOptions.setClientAuth(ClientAuth.REQUIRED); + web3HttpServerOptions.setTrustOptions( + VertxTrustOptions.whitelistClients(serverFingerprintFile)); + web3HttpServerOptions.setPfxKeyCertOptions( + new PfxOptions() + .setPath(serverCert.getPkcs12File().toString()) + .setPassword(serverCert.getPassword())); + } + final Router router = Router.router(vertx); + router + .route(HttpMethod.GET, "/upcheck") + .produces(HttpHeaderValues.APPLICATION_JSON.toString()) + .handler(context -> context.response().end("I'm up!")); + + final HttpServer mockOrionHttpServer = vertx.createHttpServer(web3HttpServerOptions); + + final CompletableFuture serverConfigured = new CompletableFuture<>(); + mockOrionHttpServer.requestHandler(router).listen(result -> serverConfigured.complete(true)); + + serverConfigured.get(); + + serversCreated.add(mockOrionHttpServer); + return mockOrionHttpServer; + } catch (KeyStoreException + | NoSuchAlgorithmException + | CertificateException + | IOException + | ExecutionException + | InterruptedException e) { + throw new RuntimeException("Failed to construct a TLS Enabled Server", e); + } + } +} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java new file mode 100644 index 0000000000..6e12660675 --- /dev/null +++ b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java @@ -0,0 +1,95 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.enclave; + +import org.hyperledger.besu.crypto.MessageDigestFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; + +import com.google.common.collect.Lists; + +public class TlsHelpers { + + private static KeyStore loadP12KeyStore(final File pkcsFile, final String password) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException { + final KeyStore store = KeyStore.getInstance("pkcs12"); + try (final InputStream keystoreStream = new FileInputStream(pkcsFile)) { + store.load(keystoreStream, password.toCharArray()); + } catch (IOException e) { + throw new RuntimeException("Unable to load keystore.", e); + } + return store; + } + + public static void populateFingerprintFile( + final Path knownClientsPath, + final TlsCertificateDefinition certDef, + final Optional serverPortToAppendToHostname) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { + + final List certs = getCertsFromPkcs12(certDef); + final StringBuilder fingerprintsToAdd = new StringBuilder(); + final String portFragment = serverPortToAppendToHostname.map(port -> ":" + port).orElse(""); + for (final X509Certificate cert : certs) { + final String fingerprint = generateFingerprint(cert); + fingerprintsToAdd.append(String.format("localhost%s %s%n", portFragment, fingerprint)); + fingerprintsToAdd.append(String.format("127.0.0.1%s %s%n", portFragment, fingerprint)); + } + Files.writeString(knownClientsPath, fingerprintsToAdd.toString()); + } + + public static List getCertsFromPkcs12(final TlsCertificateDefinition certDef) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException { + final List results = Lists.newArrayList(); + + final KeyStore p12 = loadP12KeyStore(certDef.getPkcs12File(), certDef.getPassword()); + final Enumeration aliases = p12.aliases(); + while (aliases.hasMoreElements()) { + results.add((X509Certificate) p12.getCertificate(aliases.nextElement())); + } + return results; + } + + private static String generateFingerprint(final X509Certificate cert) + throws NoSuchAlgorithmException, CertificateEncodingException { + final MessageDigest md = MessageDigestFactory.create("SHA-256"); + md.update(cert.getEncoded()); + final byte[] digest = md.digest(); + + final StringJoiner joiner = new StringJoiner(":"); + for (final byte b : digest) { + joiner.add(String.format("%02X", b)); + } + + return joiner.toString().toLowerCase(); + } +} diff --git a/enclave/src/integration-test/resources/tls/cert1.pfx b/enclave/src/integration-test/resources/tls/cert1.pfx new file mode 100644 index 0000000000..84033238a8 Binary files /dev/null and b/enclave/src/integration-test/resources/tls/cert1.pfx differ diff --git a/enclave/src/integration-test/resources/tls/cert2.pfx b/enclave/src/integration-test/resources/tls/cert2.pfx new file mode 100644 index 0000000000..bbf08a0440 Binary files /dev/null and b/enclave/src/integration-test/resources/tls/cert2.pfx differ diff --git a/enclave/src/main/java/org/hyperledger/besu/enclave/EnclaveFactory.java b/enclave/src/main/java/org/hyperledger/besu/enclave/EnclaveFactory.java index 1245cd4f65..66d56a76d8 100644 --- a/enclave/src/main/java/org/hyperledger/besu/enclave/EnclaveFactory.java +++ b/enclave/src/main/java/org/hyperledger/besu/enclave/EnclaveFactory.java @@ -14,21 +14,42 @@ */ package org.hyperledger.besu.enclave; +import org.hyperledger.besu.util.InvalidConfigurationException; + +import java.io.IOException; import java.net.URI; +import java.nio.file.AccessDeniedException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import com.google.common.base.Charsets; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.net.PfxOptions; +import org.apache.tuweni.net.tls.VertxTrustOptions; public class EnclaveFactory { private final Vertx vertx; private static final int CONNECT_TIMEOUT = 1000; + private static final boolean TRUST_CA = false; public EnclaveFactory(final Vertx vertx) { this.vertx = vertx; } public Enclave createVertxEnclave(final URI enclaveUri) { + final HttpClientOptions clientOptions = createNonTlsClientOptions(enclaveUri); + + final RequestTransmitter vertxTransmitter = + new VertxRequestTransmitter(vertx.createHttpClient(clientOptions)); + + return new Enclave(vertxTransmitter); + } + + private HttpClientOptions createNonTlsClientOptions(final URI enclaveUri) { + if (enclaveUri.getPort() == -1) { throw new EnclaveIOException("Illegal URI - no port specified"); } @@ -37,10 +58,62 @@ public class EnclaveFactory { clientOptions.setDefaultHost(enclaveUri.getHost()); clientOptions.setDefaultPort(enclaveUri.getPort()); clientOptions.setConnectTimeout(CONNECT_TIMEOUT); + return clientOptions; + } + + private HttpClientOptions createTlsClientOptions( + final URI enclaveUri, + final Path privacyKeyStoreFile, + final Path privacyKeyStorePasswordFile, + final Path privacyWhitelistFile) { + + final HttpClientOptions clientOptions = createNonTlsClientOptions(enclaveUri); + try { + if (privacyKeyStoreFile != null && privacyKeyStorePasswordFile != null) { + clientOptions.setSsl(true); + clientOptions.setPfxKeyCertOptions( + convertFrom(privacyKeyStoreFile, privacyKeyStorePasswordFile)); + } + clientOptions.setTrustOptions( + VertxTrustOptions.whitelistServers(privacyWhitelistFile, TRUST_CA)); + } catch (final NoSuchFileException e) { + throw new InvalidConfigurationException( + "Requested file " + e.getMessage() + " does not exist at specified location."); + } catch (final AccessDeniedException e) { + throw new InvalidConfigurationException( + "Current user does not have permissions to access " + e.getMessage()); + } catch (final IllegalArgumentException e) { + throw new InvalidConfigurationException("Illegally formatted client fingerprint file."); + } catch (final IOException e) { + throw new InvalidConfigurationException("Failed to load TLS files " + e.getMessage()); + } + return clientOptions; + } + + public Enclave createVertxEnclave( + final URI enclaveUri, + final Path privacyKeyStoreFile, + final Path privacyKeyStorePasswordFile, + final Path privacyWhitelistFile) { + + final HttpClientOptions clientOptions = + createTlsClientOptions( + enclaveUri, privacyKeyStoreFile, privacyKeyStorePasswordFile, privacyWhitelistFile); final RequestTransmitter vertxTransmitter = new VertxRequestTransmitter(vertx.createHttpClient(clientOptions)); return new Enclave(vertxTransmitter); } + + private static PfxOptions convertFrom(final Path keystoreFile, final Path keystorePasswordFile) + throws IOException { + final String password = readSecretFromFile(keystorePasswordFile); + return new PfxOptions().setPassword(password).setPath(keystoreFile.toString()); + } + + private static String readSecretFromFile(final Path path) throws IOException { + final byte[] fileContent = Files.readAllBytes(path); + return new String(fileContent, Charsets.UTF_8); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/PrivacyParameters.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/PrivacyParameters.java index 5c6807f408..013b739686 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/PrivacyParameters.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/PrivacyParameters.java @@ -165,6 +165,9 @@ public class PrivacyParameters { private PrivacyStorageProvider storageProvider; private EnclaveFactory enclaveFactory; private boolean multiTenancyEnabled; + private Path privacyKeyStoreFile; + private Path privacyKeyStorePasswordFile; + private Path privacyTlsKnownEnclaveFile; public Builder setPrivacyAddress(final Integer privacyAddress) { this.privacyAddress = privacyAddress; @@ -201,6 +204,21 @@ public class PrivacyParameters { return this; } + public Builder setPrivacyKeyStoreFile(final Path privacyKeyStoreFile) { + this.privacyKeyStoreFile = privacyKeyStoreFile; + return this; + } + + public Builder setPrivacyKeyStorePasswordFile(final Path privacyKeyStorePasswordFile) { + this.privacyKeyStorePasswordFile = privacyKeyStorePasswordFile; + return this; + } + + public Builder setPrivacyTlsKnownEnclaveFile(final Path privacyTlsKnownEnclaveFile) { + this.privacyTlsKnownEnclaveFile = privacyTlsKnownEnclaveFile; + return this; + } + public PrivacyParameters build() { final PrivacyParameters config = new PrivacyParameters(); if (enabled) { @@ -218,7 +236,17 @@ public class PrivacyParameters { config.setEnclavePublicKeyFile(enclavePublicKeyFile); config.setPrivateStorageProvider(storageProvider); config.setPrivateStateStorage(privateStateStorage); - config.setEnclave(enclaveFactory.createVertxEnclave(enclaveUrl)); + // pass TLS options to enclave factory if they are set + if (privacyKeyStoreFile != null) { + config.setEnclave( + enclaveFactory.createVertxEnclave( + enclaveUrl, + privacyKeyStoreFile, + privacyKeyStorePasswordFile, + privacyTlsKnownEnclaveFile)); + } else { + config.setEnclave(enclaveFactory.createVertxEnclave(enclaveUrl)); + } if (privateKeyPath != null) { config.setSigningKeyPair(KeyPairUtil.load(privateKeyPath.toFile()));