mirror of https://github.com/hyperledger/besu
[BESU-80] TLS to orion (#324)
* enable SSL on connection to enclave Signed-off-by: Sally MacFarlane <sally.macfarlane@consensys.net>pull/364/head
parent
f12c92ec58
commit
9dfed5ab74
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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.
Loading…
Reference in new issue