From 9dfed5ab74cd84ac666b1367d08cab84af80e29b Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 4 Feb 2020 15:59:06 +1000 Subject: [PATCH] [BESU-80] TLS to orion (#324) * enable SSL on connection to enclave Signed-off-by: Sally MacFarlane --- .../org/hyperledger/besu/cli/BesuCommand.java | 47 +++++- .../hyperledger/besu/cli/BesuCommandTest.java | 31 ++++ .../src/test/resources/everything_config.toml | 6 + enclave/build.gradle | 10 ++ .../enclave/TlsCertificateDefinition.java | 53 +++++++ .../besu/enclave/TlsEnabledEnclaveTest.java | 145 ++++++++++++++++++ .../enclave/TlsEnabledHttpServerFactory.java | 101 ++++++++++++ .../hyperledger/besu/enclave/TlsHelpers.java | 95 ++++++++++++ .../integration-test/resources/tls/cert1.pfx | Bin 0 -> 2517 bytes .../integration-test/resources/tls/cert2.pfx | Bin 0 -> 2517 bytes .../besu/enclave/EnclaveFactory.java | 73 +++++++++ .../besu/ethereum/core/PrivacyParameters.java | 30 +++- 12 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java create mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java create mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java create mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java create mode 100644 enclave/src/integration-test/resources/tls/cert1.pfx create mode 100644 enclave/src/integration-test/resources/tls/cert2.pfx 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 0000000000000000000000000000000000000000..84033238a8a029f793862a76b2796dfe37af2454 GIT binary patch literal 2517 zcmY+^cQhM{9tZFwlE^SytB6vxMiHY{HK9t>=4#aDQsW|KwN%lBqOldFwz{?!DXG?8 zp#wD^wb#A&jM9?pp7Y+j@BQ&RzjMCdbH0Cm&_wtk2*`vc!ZV<7si%5R4_SfCzsW=C@PlP9?j>OzTZeD>G3N zY^~b9SeaL_vQ8Egc)4*^QC&ZF>VjCC z(Ol>dH;|Io`%SUKKDfl`TZZ8fqyHsKfScP@6A!pONv z2xi%iHDDB&ZoR8&5#EYH+01=Hr1P6OJ%cYP3Nx=D_#NP*NSM^&zR! zTnNXxPUyERHwXAFgakX;k~&VgzghK3$WG(6^f*0Tp}^J&UtMM}lIMW!D{nv{b_vM?K$t@<(6lT7aKW zGzvC%QufOo%SPr`4HNnmZB5!lyw=#wiEC8M_S;%`^dzX0sM6O9Y{1{k9BKyG8$ip| zXG1yAmhn|y9FGb&zLGJ*#Ex`28X=4ayA z<9z+426bbIo}I<&&wf!nNROv z7bVskA;`Y=0tIg}Bn}Ob(#XK2-5-l#=5#Gi6!|0}J~N&*I^e8lVt2J<{SsI3Ho+sUlrjC|YZylV{LHfXGt*a+$_MM9I`)PB?73)YAT4Ry< zRXaNKbyBSOy?E}dDaXv{Vc$MV61%QRr?Dm*bEvg=Kyv6rrx;UI5Fs`3 zyKe~eIk~_*J4Lv5>WW-ZGU4?H9q_RAQ@h42QJ)?$XJ{ zU9vw<O3wO>lc7BaS(S`9WZ_-c!T6F<(Is*GB!MD0Y|KoZM?FV(}Et=N{Mu$C^!( z{x$do>K}(ZB{)Ys%79&J;!a?5KQcPrLYt56&b5-r$~W3PwQX}Vz2j3C+jy#9OS@=# zMfB|a@KCN@d7Bx4;`1_n~m#9eoJzJz z&-W3qe?`dRv<;I1W>5HXU7V?8B=L~lJa;osY{37kIQ!I-847VZCL}7Cm&;r*XnC{<8V-d>voV4AApkH}+g*jn@})wT9u5!x Z66y0Wh5q(kIv6Gv1Sew${cZnN@-JGSnJxeT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bbf08a0440dc90e691523f2a8075394256a2a509 GIT binary patch literal 2517 zcmY+^cQh3a9|mxDy4RLU*`Q?qUZ4O#kyK#V76z+YnV+5p1j0zLIM6;oY$!Z5 zCl<64QI#YaLbzK`?o5UF{v*P*-%rP97yURcfH)F-*MUopAxh%b$6{_Y^_=xDZ6qlA zjU81}m$5G|BI;X7pq`*}Lt@+Txw|{17oEqe@on`~Q^mLY#QvZQy0IzynBbPK2Ylc9_q>*%pI?9Nr#xF=L+rzJ11;Wd>Xp2vrA_=S|j~j zCJq#B(FEIV4keen5|AQU_AvVCHeUnIlJSyTFqv}xk~S-4k7m$fNHgj^cGy1lEHANp zf5P%$12DSfGb=S2u%UdiSu>lPB`gWYjxjXu_LVNcV{Aq(Dg$=VblJYGO z{@FbW6@lV4<`TU3t_U(0RZ|dZPsOT3^uJp|m#^W+)yjd}kp;Obso$EVfH20q6Cd*m zf8*kCjQRj-XLscM6jb%dMfZnZXXaFY+RS!3xuHE;G>m^lt#`5KIb)O5mkg_EZaL4% zh!?jvDKUeJu1X#BPmi9=@$r#j4_)9LOL|^+PYM;qlacX5={sT!{LkVFzdXqve#5#z zFj}{GFlU=0$7JumW1~oNnC9QruT-fA{6%eJGorQf}zU+*|c8gwLo-+N+RdN%93?E zXL`>a=?vACG_MNG9?uLwKtP5*HAxPwYjX`xbE_)uy@|L4o|5z&@n@(bIXW>ui+PzN zcp5K9ZyDsfFJB(a7+CI6*DHKcXFTHWsX#~zmIzul$Qs{h?5aViE)X>xSIICwsTa-;mVEk%GZUTrVJOIdV~D$##! zqgSgT#+1vi%*4brG6^|XQrmHK3KJ+FO!!zPepap*Gd>>jeG;>j{Nt~<>A%C+Ha?uBEP6=?N{ zy5onV((*Jlc3QC`pFW)zdP@Tv=qCGEgSx8qk zDUEp3_sS+q=80qiHD@xcutg~?9nj(B4Az0lULavnSUM(KS>rNmBbgNaRc0mM$#3%L zz0dUuyIN{Jfuw(k&wH1eGH>O_cFNo~b82KK*|5Z1sMQ%>B{aUC|m!h2%ST2KXW+;qAGS~ZWa%nsL=(p$rI6Xy2O+4iINjVmg(KD zo73bh57hsZa5sddP?T<7upIHzZsh@*-?Bp&2JXDd(EfXx}*BjKJK@X1$8TdSBrj!tJBIS ze`RP`7*N)IAn6~sj(KyVe5|tDq%gIC-eI;Un_jWuF`2=k=S$D+fm%)qa;*pxzHBH7dyqeRLa8F|F|o@_HHp+s{mF zk|Btljb|o9;*=Ym!2N!t39>TojP(|WjW(oI8wIcVpGpb*UbiZXMEH@2Cr6%sdFkSm z6Bk~8*Q#DrM3Z^y-hq8VrOV_wB9oq-pw41+4pKOYOdz2x?a{` zvfi%Y4BszzH0k3FF4HHAu5TO}ce~FR&R*)U8%4B0FO=NR{~6dK%eN*C9iTZ?zmlC} zJFrRt)^ly_wu{H{tGD~%1IN*bL$*VYc=VG}PcIcCl*DoLJ-%R|9A6s2eYDRj49-r% z=%X=6V^~VuZVE>B37&*To1siAI8xNN->qi*X&$ozZ`{X-Stn{DU5jJiL7dr5S?vxh z<n6JAd3lP$~zm!vaJaih@&!l)Agj$l2m5i9N!!@ zd}lB+I>g8k<7w2eomb822vPF>bJa@YSMv>)5O`jlL}gj22v*_d;w11?n*$5~A;LuB z{SM)WmnU3^n`}^jD=UR|+X;JKDtomi$_~JzelGVsnCV(vylv}ifAQk zd-R?b&j#fLAA5sPt*?|}jjF;Q=OWC4XSTAnFJypeQi1Ry<4)Y#3vqrV?|*fKGl(;S z>0zNs300cC44UIi;sLg~4(&PA#z=7_HxdqoT%`g5Ss)}}`jxLJ{b#S~rVCR&*Uc*X W&++4Tlff_z^F2Mv${5UFCI1EadyuC9 literal 0 HcmV?d00001 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()));