[BESU-80] TLS to orion (#324)

* enable SSL on connection to enclave

Signed-off-by: Sally MacFarlane <sally.macfarlane@consensys.net>
This commit is contained in:
Sally MacFarlane
2020-02-04 15:59:06 +10:00
committed by GitHub
parent f12c92ec58
commit 9dfed5ab74
12 changed files with 590 additions and 3 deletions

View File

@@ -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'

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<HttpServer> 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<Boolean> 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);
}
}
}

View File

@@ -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<Integer> serverPortToAppendToHostname)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
final List<X509Certificate> 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<X509Certificate> getCertsFromPkcs12(final TlsCertificateDefinition certDef)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException {
final List<X509Certificate> results = Lists.newArrayList();
final KeyStore p12 = loadP12KeyStore(certDef.getPkcs12File(), certDef.getPassword());
final Enumeration<String> 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();
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -14,21 +14,42 @@
*/
package org.hyperledger.besu.enclave;
import java.net.URI;
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);
}
}