diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index 398212e98b5a82..4c6b828f85fd39 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -180,6 +180,18 @@ public class RestClientConfig { @ConfigItem public Optional keyStoreType; + /** + * The classpath path or file path to a server certificate or certificate chain in PEM format. + */ + @ConfigItem + public Optional certificate; + + /** + * The classpath path or file path to the corresponding certificate private key file in PEM format. + */ + @ConfigItem + public Optional key; + /** * The class name of the host name verifier. The class must have a public no-argument constructor. */ diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index 2721eae19cbc59..035f959d722a7e 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -288,6 +288,22 @@ public class RestClientsConfig { @ConfigItem public Optional keyStoreType; + /** + * The classpath path or file path to a server certificate or certificate chain in PEM format. + *

+ * Can be overwritten by client-specific settings. + */ + @ConfigItem + public Optional certificate; + + /** + * The classpath path or file path to the corresponding certificate private key file in PEM format. + *

+ * Can be overwritten by client-specific settings. + */ + @ConfigItem + public Optional key; + /** * If this is true then HTTP/2 will be enabled. */ diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java index a7c66165bea7f4..f2ee25a6db0acf 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java @@ -2,6 +2,7 @@ import java.net.URI; import java.net.URL; +import java.nio.file.Path; import java.security.KeyStore; import java.util.ServiceLoader; import java.util.concurrent.TimeUnit; @@ -151,6 +152,16 @@ static QuarkusRestClientBuilder newBuilder() { */ QuarkusRestClientBuilder trustStore(KeyStore trustStore, String trustStorePassword); + /** + * The classpath path or file path to a server certificate or certificate chain in PEM format. + */ + QuarkusRestClientBuilder certificate(Path certificate); + + /** + * The classpath path or file path to the corresponding certificate private key file in PEM format. + */ + QuarkusRestClientBuilder key(Path key); + /** * Set the client-side key store. * diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java index 3ba9ad115f9619..36fadde916a8e5 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java @@ -2,6 +2,7 @@ import java.net.URI; import java.net.URL; +import java.nio.file.Path; import java.security.KeyStore; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -84,6 +85,18 @@ public QuarkusRestClientBuilder keyStore(KeyStore keyStore, String keystorePassw return this; } + @Override + public QuarkusRestClientBuilder certificate(Path certificate) { + proxy.certificate(certificate); + return this; + } + + @Override + public QuarkusRestClientBuilder key(Path key) { + proxy.key(key); + return this; + } + @Override public QuarkusRestClientBuilder hostnameVerifier(HostnameVerifier hostnameVerifier) { proxy.hostnameVerifier(hostnameVerifier); diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 3920c8db594c16..caf288f1772ae0 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -6,6 +6,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Path; import java.security.KeyStore; import java.util.ArrayList; import java.util.Comparator; @@ -75,6 +76,8 @@ public class RestClientBuilderImpl implements RestClientBuilder { private Boolean trustAll; private String userAgent; + private Path certificate; + private Path key; @Override public RestClientBuilderImpl baseUrl(URL url) { @@ -126,6 +129,16 @@ public RestClientBuilderImpl keyStore(KeyStore keyStore, String keystorePassword return this; } + public RestClientBuilderImpl certificate(Path certificate) { + this.certificate = certificate; + return this; + } + + public RestClientBuilderImpl key(Path key) { + this.key = key; + return this; + } + @Override public RestClientBuilderImpl hostnameVerifier(HostnameVerifier hostnameVerifier) { clientBuilder.hostnameVerifier(hostnameVerifier); diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index 1424900d8a16cd..dfde1d21af62f5 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -10,6 +10,7 @@ import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Paths; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -229,6 +230,18 @@ private void configureSsl(QuarkusRestClientBuilder builder) { registerKeyStore(maybeKeyStore.get(), builder); } + Optional maybeCertificate = oneOf(clientConfigByClassName().certificate, clientConfigByConfigKey().certificate, + configRoot.certificate); + if (maybeCertificate.isPresent()) { + builder.certificate(Paths.get(maybeCertificate.get())); + } + + Optional maybeKey = oneOf(clientConfigByClassName().key, clientConfigByConfigKey().key, + configRoot.key); + if (maybeKey.isPresent()) { + builder.key(Paths.get(maybeKey.get())); + } + Optional maybeHostnameVerifier = oneOf(clientConfigByClassName().hostnameVerifier, clientConfigByConfigKey().hostnameVerifier, configRoot.hostnameVerifier); if (maybeHostnameVerifier.isPresent()) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 028165eb19d731..8bfeda506c925f 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -5,6 +5,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -39,6 +43,7 @@ import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpVersion; import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PemKeyCertOptions; import io.vertx.core.net.PfxOptions; import io.vertx.core.net.ProxyOptions; @@ -57,6 +62,9 @@ public class ClientBuilderImpl extends ClientBuilder { private SSLContext sslContext; private KeyStore trustStore; private char[] trustStorePassword; + + private Path certificate; + private Path key; private boolean http2; private boolean alpn; @@ -114,6 +122,16 @@ public ClientBuilder trustStore(KeyStore trustStore, char[] password) { return this; } + public ClientBuilder certificate(Path certificate) { + this.certificate = certificate; + return this; + } + + public ClientBuilder key(Path key) { + this.key = key; + return this; + } + @Override public ClientBuilder hostnameVerifier(HostnameVerifier verifier) { // TODO @@ -222,43 +240,58 @@ public ClientImpl build() { options.setVerifyHost(false); } - char[] effectiveTrustStorePassword = trustStorePassword == null ? EMPTY_CHAR_ARARAY : trustStorePassword; - Buffer keyStore = asBuffer(this.keyStore, keystorePassword); - Buffer trustStore = asBuffer(this.trustStore, effectiveTrustStorePassword); - if (keyStore != null || trustStore != null) { - options = options.setSsl(true); - if (keyStore != null) { - String keyStoreType = this.keyStore.getType(); - if ("PKCS12".equals(keyStoreType)) { - PfxOptions pks = new PfxOptions(); - pks.setValue(keyStore); - pks.setPassword(new String(keystorePassword)); - options = options.setKeyCertOptions(pks); - } else if ("JKS".equals(keyStoreType)) { - JksOptions jks = new JksOptions(); - jks.setValue(keyStore); - jks.setPassword(new String(keystorePassword)); - options = options.setKeyCertOptions(jks); - } else { - throw new IllegalStateException("Unsupported key store type " + keyStoreType); - } - + if (certificate != null && key != null) { + try { + byte[] certBytes = getFileContent(certificate); + byte[] keyBytes = getFileContent(key); + + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setCertValue(Buffer.buffer(certBytes)) + .setKeyValue(Buffer.buffer(keyBytes)); + options.setKeyCertOptions(pemKeyCertOptions); + } catch (IOException e) { + throw new UncheckedIOException(e); } - if (trustStore != null) { - String trustStoreType = this.keyStore.getType(); - if ("PKCS12".equals(trustStoreType)) { - PfxOptions pks = new PfxOptions(); - pks.setValue(trustStore); - pks.setPassword(new String(effectiveTrustStorePassword)); - } else if ("JKS".equals(trustStoreType)) { - JksOptions jks = new JksOptions(); - jks.setValue(trustStore); - jks.setPassword(new String(effectiveTrustStorePassword)); - options.setTrustOptions(jks); - } else { - throw new IllegalStateException("Unsupported trust store type " + trustStoreType); + + } else { + char[] effectiveTrustStorePassword = trustStorePassword == null ? EMPTY_CHAR_ARARAY : trustStorePassword; + Buffer keyStore = asBuffer(this.keyStore, keystorePassword); + Buffer trustStore = asBuffer(this.trustStore, effectiveTrustStorePassword); + if (keyStore != null || trustStore != null) { + options = options.setSsl(true); + if (keyStore != null) { + String keyStoreType = this.keyStore.getType(); + if ("PKCS12".equals(keyStoreType)) { + PfxOptions pks = new PfxOptions(); + pks.setValue(keyStore); + pks.setPassword(new String(keystorePassword)); + options = options.setKeyCertOptions(pks); + } else if ("JKS".equals(keyStoreType)) { + JksOptions jks = new JksOptions(); + jks.setValue(keyStore); + jks.setPassword(new String(keystorePassword)); + options = options.setKeyCertOptions(jks); + } else { + throw new IllegalStateException("Unsupported key store type " + keyStoreType); + } + } + if (trustStore != null) { + String trustStoreType = this.keyStore.getType(); + if ("PKCS12".equals(trustStoreType)) { + PfxOptions pks = new PfxOptions(); + pks.setValue(trustStore); + pks.setPassword(new String(effectiveTrustStorePassword)); + } else if ("JKS".equals(trustStoreType)) { + JksOptions jks = new JksOptions(); + jks.setValue(trustStore); + jks.setPassword(new String(effectiveTrustStorePassword)); + options.setTrustOptions(jks); + } else { + throw new IllegalStateException("Unsupported trust store type " + trustStoreType); + } + } } } @@ -446,4 +479,41 @@ public ClientBuilderImpl nonProxyHosts(String nonProxyHosts) { this.nonProxyHosts = nonProxyHosts; return this; } + + private static byte[] getFileContent(Path path) throws IOException { + byte[] data; + final InputStream resource = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(toResourceName(path)); + if (resource != null) { + try (InputStream is = resource) { + data = doRead(is); + } + } else { + try (InputStream is = Files.newInputStream(path)) { + data = doRead(is); + } + } + return data; + } + + /** + * Translates a file system-specific path to a Java classpath resource name + * that uses '/' as a separator. + * + * @param path file system path + * @return Java classpath resource name + */ + public static String toResourceName(Path path) { + if (path == null) { + return null; + } + if (path.getFileSystem().getSeparator().equals("/")) { + return path.toString(); + } + return path.toString().replace(path.getFileSystem().getSeparator(), "/"); + } + + private static byte[] doRead(InputStream is) throws IOException { + return is.readAllBytes(); + } }