Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSL documentation and related fixes #1039

Merged
merged 3 commits into from
Feb 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.deployment.builditem;

import org.jboss.builder.item.MultiBuildItem;

public final class JavaLibraryPathAdditionalPathBuildItem extends MultiBuildItem {

private final String path;

public JavaLibraryPathAdditionalPathBuildItem(String path) {
this.path = path;
}

public String getPath() {
return path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.deployment.builditem;

import org.jboss.builder.item.SimpleBuildItem;

public final class SslTrustStoreSystemPropertyBuildItem extends SimpleBuildItem {

private final String path;

public SslTrustStoreSystemPropertyBuildItem(String path) {
this.path = path;
}

public String getPath() {
return path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.graalvm.nativeimage.ImageInfo;
import org.jboss.builder.Version;
import org.jboss.protean.gizmo.BytecodeCreator;
import org.jboss.protean.gizmo.CatchBlockCreator;
import org.jboss.protean.gizmo.ClassCreator;
import org.jboss.protean.gizmo.FieldCreator;
Expand All @@ -42,9 +44,11 @@
import io.quarkus.deployment.builditem.ClassOutputBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.HttpServerBuildItem;
import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem;
import io.quarkus.deployment.builditem.MainBytecodeRecorderBuildItem;
import io.quarkus.deployment.builditem.MainClassBuildItem;
import io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem;
import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem;
import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.recording.BytecodeRecorderImpl;
Expand All @@ -58,6 +62,8 @@ class MainClassBuildStep {
private static final String APP_CLASS = "io.quarkus.runner.ApplicationImpl";
private static final String MAIN_CLASS = "io.quarkus.runner.GeneratedMain";
private static final String STARTUP_CONTEXT = "STARTUP_CONTEXT";
private static final String JAVA_LIBRARY_PATH = "java.library.path";
private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore";

private static final AtomicInteger COUNT = new AtomicInteger();

Expand All @@ -66,6 +72,8 @@ MainClassBuildItem build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
List<ObjectSubstitutionBuildItem> substitutions,
List<MainBytecodeRecorderBuildItem> mainMethod,
List<SystemPropertyBuildItem> properties,
List<JavaLibraryPathAdditionalPathBuildItem> javaLibraryPathAdditionalPaths,
Optional<SslTrustStoreSystemPropertyBuildItem> sslTrustStoreSystemProperty,
Optional<HttpServerBuildItem> httpServer,
List<FeatureBuildItem> features,
BuildProducer<ApplicationClassNameBuildItem> appClassNameProducer,
Expand Down Expand Up @@ -133,8 +141,54 @@ MainClassBuildItem build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
mv.load(i.getKey()), mv.load(i.getValue()));
}

// Set the SSL system properties
if (!javaLibraryPathAdditionalPaths.isEmpty()) {
// FIXME: this is the code we should use but I couldn't get GraalVM to work with a java.library.path containing multiple paths.
// We need to dig further but for now, we need this to work.
// ResultHandle javaLibraryPath = mv.newInstance(ofConstructor(StringBuilder.class, String.class),
// mv.invokeStaticMethod(ofMethod(System.class, "getProperty", String.class, String.class), mv.load(JAVA_LIBRARY_PATH)));
// for (JavaLibraryPathAdditionalPathBuildItem javaLibraryPathAdditionalPath : javaLibraryPathAdditionalPaths) {
// ResultHandle javaLibraryPathLength = mv.invokeVirtualMethod(ofMethod(StringBuilder.class, "length", int.class), javaLibraryPath);
// mv.ifNonZero(javaLibraryPathLength).trueBranch()
// .invokeVirtualMethod(ofMethod(StringBuilder.class, "append", StringBuilder.class, String.class), javaLibraryPath, mv.load(File.pathSeparator));
// mv.invokeVirtualMethod(ofMethod(StringBuilder.class, "append", StringBuilder.class, String.class), javaLibraryPath,
// mv.load(javaLibraryPathAdditionalPath.getPath()));
// }
// mv.invokeStaticMethod(ofMethod(System.class, "setProperty", String.class, String.class, String.class),
// mv.load(JAVA_LIBRARY_PATH), mv.invokeVirtualMethod(ofMethod(StringBuilder.class, "toString", String.class), javaLibraryPath));

ResultHandle isJavaLibraryPathEmpty = mv.invokeVirtualMethod(ofMethod(String.class, "isEmpty", boolean.class),
mv.invokeStaticMethod(ofMethod(System.class, "getProperty", String.class, String.class),
mv.load(JAVA_LIBRARY_PATH)));

BytecodeCreator inGraalVMCode = mv
.ifNonZero(mv.invokeStaticMethod(ofMethod(ImageInfo.class, "inImageRuntimeCode", boolean.class)))
.trueBranch();

inGraalVMCode.ifNonZero(isJavaLibraryPathEmpty).trueBranch().invokeStaticMethod(
ofMethod(System.class, "setProperty", String.class, String.class, String.class),
inGraalVMCode.load(JAVA_LIBRARY_PATH),
inGraalVMCode.load(javaLibraryPathAdditionalPaths.iterator().next().getPath()));
}

if (sslTrustStoreSystemProperty.isPresent()) {
ResultHandle alreadySetTrustStore = mv.invokeStaticMethod(
ofMethod(System.class, "getProperty", String.class, String.class),
mv.load(JAVAX_NET_SSL_TRUST_STORE));

BytecodeCreator inGraalVMCode = mv
.ifNonZero(mv.invokeStaticMethod(ofMethod(ImageInfo.class, "inImageRuntimeCode", boolean.class)))
.trueBranch();

inGraalVMCode.ifNull(alreadySetTrustStore).trueBranch().invokeStaticMethod(
ofMethod(System.class, "setProperty", String.class, String.class, String.class),
inGraalVMCode.load(JAVAX_NET_SSL_TRUST_STORE),
inGraalVMCode.load(sslTrustStoreSystemProperty.get().getPath()));
}

mv.invokeStaticMethod(ofMethod(Timing.class, "mainStarted", void.class));
startupContext = mv.readStaticField(scField.getFieldDescriptor());

tryBlock = mv.tryBlock();
for (MainBytecodeRecorderBuildItem holder : mainMethod) {
final BytecodeRecorderImpl recorder = holder.getBytecodeRecorder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.JavaLibraryPathAdditionalPathBuildItem;
import io.quarkus.deployment.builditem.SslNativeConfigBuildItem;
import io.quarkus.deployment.builditem.SslTrustStoreSystemPropertyBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.builditem.substrate.RuntimeInitializedClassBuildItem;
import io.quarkus.deployment.builditem.substrate.RuntimeReinitializedClassBuildItem;
Expand All @@ -52,7 +54,9 @@ void build(List<SubstrateConfigBuildItem> substrateConfigBuildItems,
BuildProducer<RuntimeInitializedClassBuildItem> runtimeInit,
BuildProducer<RuntimeReinitializedClassBuildItem> runtimeReinit,
BuildProducer<SubstrateSystemPropertyBuildItem> nativeImage,
BuildProducer<SystemPropertyBuildItem> systemProperty) {
BuildProducer<SystemPropertyBuildItem> systemProperty,
BuildProducer<JavaLibraryPathAdditionalPathBuildItem> javaLibraryPathAdditionalPath,
BuildProducer<SslTrustStoreSystemPropertyBuildItem> sslTrustStoreSystemProperty) {
for (SubstrateConfigBuildItem substrateConfigBuildItem : substrateConfigBuildItems) {
for (String i : substrateConfigBuildItem.getRuntimeInitializedClasses()) {
runtimeInit.produce(new RuntimeInitializedClassBuildItem(i));
Expand Down Expand Up @@ -86,21 +90,30 @@ void build(List<SubstrateConfigBuildItem> substrateConfigBuildItems,
Path graalVmLibDirectory = Paths.get(graalVmHome, "jre", "lib");
Path linuxLibDirectory = graalVmLibDirectory.resolve("amd64");

// We add . as it might be useful in a containerized world
// FIXME: it seems GraalVM does not support having multiple paths in java.library.path
//javaLibraryPathAdditionalPath.produce(new JavaLibraryPathAdditionalPathBuildItem("."));
if (Files.exists(linuxLibDirectory)) {
// On Linux, the SunEC library is in jre/lib/amd64/
systemProperty.produce(new SystemPropertyBuildItem("java.library.path", linuxLibDirectory.toString()));
// This is useful for testing or if you have a similar environment in production
javaLibraryPathAdditionalPath
.produce(new JavaLibraryPathAdditionalPathBuildItem(linuxLibDirectory.toString()));
} else {
// On MacOS, the SunEC library is directly in jre/lib/
// This is useful for testing or if you have a similar environment in production
systemProperty.produce(new SystemPropertyBuildItem("java.library.path", graalVmLibDirectory.toString()));
}
systemProperty.produce(
new SystemPropertyBuildItem("javax.net.ssl.trustStore",

// This is useful for testing but the user will have to override it.
sslTrustStoreSystemProperty.produce(
new SslTrustStoreSystemPropertyBuildItem(
graalVmLibDirectory.resolve(Paths.get("security", "cacerts")).toString()));
} else {
// only warn if we're building a native image
if (ImageInfo.inImageBuildtimeCode())
if (ImageInfo.inImageBuildtimeCode()) {
log.warn(
"SSL is enabled but the GRAALVM_HOME environment variable is not set. The java.library.path property has not been set and will need to be set manually.");
}
}
}

Expand Down
195 changes: 195 additions & 0 deletions docs/src/main/asciidoc/native-and-ssl-guide.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
= Quarkus - Using SSL With Native Images

We are quickly moving to an SSL-everywhere world so being able to use SSL is crucial.

In this guide, we will discuss how you can get your native images to support SSL,
as native images don't support it out of the box.

NOTE: If you don't plan on using native images, you can pass your way as in JDK mode, SSL is supported without further manipulations.

== Prerequisites

To complete this guide, you need:

* less than 20 minutes
* an IDE
* GraalVM installed with `JAVA_HOME` and `GRAALVM_HOME` configured appropriately
* Apache Maven 3.5.3+

This guide is based on the REST client guide so you should get this Maven project first.

Clone the Git repository: `git clone https://github.com/jbossas/protean-quickstarts.git`, or download an https://github.com/jbossas/protean-quickstarts/archive/master.zip[archive].

The project is located in the `rest-client` https://github.com/jbossas/protean-quickstarts/tree/master/rest-client[directory].

== Looks like it works out of the box?!?

If you open the application's configuration file (`src/main/resources/META-INF/microprofile-config.properties`), you can see the following line:
```
org.acme.restclient.CountriesService/mp-rest/url=https://restcountries.eu/rest
```
which configures our REST client to connect to an SSL REST service.

Now let's build the application as a native image and run the tests:
```
mvn clean install -Pnative
```

And we obtain the following result:
```
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
```

So, yes, it appears it works out of the box and this guide is pretty useless.

It's not. The magic happens when building the native image:
```
[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] /opt/graalvm/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dcom.sun.xml.internal.bind.v2.bytecode.ClassTailor.noOptimize=true -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -jar rest-client-1.0-SNAPSHOT-runner.jar -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 -H:+PrintAnalysisCallTree -H:EnableURLProtocols=http,https --enable-all-security-services -H:-SpawnIsolates -H:+JNI --no-server -H:-UseServiceLoaderFeature -H:+StackTrace
```

The important elements are these 3 options:
```
-H:EnableURLProtocols=http,https --enable-all-security-services -H:+JNI
```

They enable the native SSL support for your native image.

As SSL is ipso facto the standard nowadays, we decided to enable its support automatically for some of our extensions:

* the SmallRye REST client extension,
* the Agroal connection pooling extension,
* the Jaeger extension.

As long as you have one of those extensions in your project, the SSL support will be enabled by default.

Now, let's just check the size of our native image as it will be useful later:
```
$ ls -lh target/rest-client-1.0-SNAPSHOT-runner
-rwxrwxr-x. 1 gsmet gsmet 34M Feb 22 15:27 target/rest-client-1.0-SNAPSHOT-runner
```

== Let's disable SSL and see how it goes

Quarkus has an option to disable the SSL support entirely.
Why? Because it comes at a certain cost.
So if you are sure you don't need it, you can disable it entirely.

First, let's disable it without changing the REST service URL and see how it goes.

Open `src/main/resources/META-INF/microprofile-config.properties` and add the following line:
```
quarkus.ssl.native=false
```

And let's try to build again:
```
mvn clean install -Pnative
```

The native image tests will fail with the following error:
```
Exception handling request to /country/name/greece: com.oracle.svm.core.jdk.UnsupportedFeatureError: Accessing an URL protocol that was not enabled. The URL protocol https is supported but not enabled by default. It must be enabled by adding the --enable-url-protocols=https option to the native-image command.
```

This error is the one you obtain when trying to use SSL while it was not explicitly enabled in your native image.

Now, let's change the REST service URL to **not** use SSL in `src/main/resources/META-INF/microprofile-config.properties`:
```
org.acme.restclient.CountriesService/mp-rest/url=http://restcountries.eu/rest
```

And build again:
```
mvn clean install -Pnative
```

If you check carefully the native image build options, you can see that the SSL related options are gone:
```
[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] /opt/graalvm/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dcom.sun.xml.internal.bind.v2.bytecode.ClassTailor.noOptimize=true -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -jar rest-client-1.0-SNAPSHOT-runner.jar -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 -H:+PrintAnalysisCallTree -H:EnableURLProtocols=http -H:-SpawnIsolates -H:-JNI --no-server -H:-UseServiceLoaderFeature -H:+StackTrace
```

And we end up with:
```
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
```

You remember we checked the size of the native image with SSL enabled?
Let's check again with SSL support entirely disabled:
```
$ ls -lh target/rest-client-1.0-SNAPSHOT-runner
-rwxrwxr-x. 1 gsmet gsmet 25M Feb 22 15:19 target/rest-client-1.0-SNAPSHOT-runner
```

Yes, it is now **25 MB** whereas it used to be **34 MB**. SSL comes with a 9 MB overhead in native image size.

And there's more to it.

== Let's start again with a clean slate

Let's revert the changes we made to the configuration file and go back to SSL with the following command:
```
git checkout -- src/main/resources/META-INF/microprofile-config.properties
```

And let's build the native image again:
```
mvn clean install -Pnative
```

== The SunEC library and friends

You haven't noticed anything but, while building the image,
Quarkus has automatically set `java.library.path` to point to the GraalVM library folder (the one containing the SunEC library).

It has also set `javax.net.ssl.trustStore` to point to the `cacerts` file bundled in the GraalVM distribution.
This file contains the root certificates.

This is useful when running tests but, obviously, it is not portable as these paths are hardcoded.

You can check that pretty easily:

* move your GraalVM directory to another place (let's call it `<new-graalvm-home>`)
* run the native image `./target/rest-client-1.0-SNAPSHOT-runner`
* in a browser, go to `http://localhost:8080/country/name/greece`
* you will have an Internal Server Error
* in your terminal, you should have a warning `WARNING: The sunec native library, required by the SunEC provider, could not be loaded.`
and an exception too: `java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty`
* hit `Ctrl+C` to stop the application

To make it work, you need to manually set `java.library.path` and `javax.net.ssl.trustStore` to point to the new GraalVM home:
```
./target/rest-client-1.0-SNAPSHOT-runner -Djava.library.path=<new-graalvm-home>/jre/lib/amd64 -Djavax.net.ssl.trustStore=<new-graalvm-home>/jre/lib/security/cacerts
```

Now, the application should work as expected:

* in a browser, go to `http://localhost:8080/country/name/greece`
* you should see a JSON output with some information about Greece
* hit `Ctrl+C` to stop the application

When working with containers, the idea is to bundle both the SunEC library and the certificates in the container and to point your binary to them using the system properties mentioned above.

[TIP]
====
The root certificates file of GraalVM might not be totally up to date.
If you have issues with some certificates, your best bet is to include the `cacerts` file of a regular JDK instead.
====

[WARNING]
====
Don't forget to move your GraalVM directory back to where it was.
====

== Conclusion

We make building native images easy and, even if the SSL support in GraalVM is still requiring some serious thinking,
it should be mostly transparent when using Quarkus.

Hopefully, the situation will improve in the future:
the native image size overhead will be reduced and the SunCE library might not be needed anymore.

We track GraalVM progress on a regular basis so we will promptly integrate in Quarkus any improvement with respect to SSL support.