Skip to content

Accept SSL certificates by providing a URL to use cert from within a jar #318

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

Closed
wants to merge 2 commits into from
Closed
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
Expand Up @@ -34,7 +34,9 @@

import javax.net.ssl.HostnameVerifier;
import java.io.File;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -331,20 +333,20 @@ public static final class Builder {
private String socket;

@Nullable
private String sslCert = null;
private URL sslCert = null;

private HostnameVerifier sslHostnameVerifier = DefaultHostnameVerifier.INSTANCE;

@Nullable
private String sslKey = null;
private URL sslKey = null;

private SSLMode sslMode = SSLMode.DISABLE;

@Nullable
private CharSequence sslPassword = null;

@Nullable
private String sslRootCert = null;
private URL sslRootCert = null;

private Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer = Function.identity();

Expand Down Expand Up @@ -638,7 +640,7 @@ public Builder sslContextBuilderCustomizer(Function<SslContextBuilder, SslContex
* @return this {@link Builder}
*/
public Builder sslCert(String sslCert) {
this.sslCert = Assert.requireFileExistsOrNull(sslCert, "sslCert must not be null and must exist");
this.sslCert = Assert.requireUrlExistsOrNull(sslCert, "sslCert must not be null");
return this;
}

Expand All @@ -660,7 +662,7 @@ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) {
* @return this {@link Builder}
*/
public Builder sslKey(String sslKey) {
this.sslKey = Assert.requireFileExistsOrNull(sslKey, "sslKey must not be null and must exist");
this.sslKey = Assert.requireUrlExistsOrNull(sslKey, "sslKey must not be null");
return this;
}

Expand Down Expand Up @@ -693,7 +695,7 @@ public Builder sslPassword(@Nullable CharSequence sslPassword) {
* @return this {@link Builder}
*/
public Builder sslRootCert(String sslRootCert) {
this.sslRootCert = Assert.requireFileExistsOrNull(sslRootCert, "sslRootCert must not be null and must exist");
this.sslRootCert = Assert.requireUrlExistsOrNull(sslRootCert, "sslRootCert must not be null");
return this;
}

Expand Down Expand Up @@ -779,14 +781,14 @@ private Supplier<SslProvider> createSslProvider() {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
if (this.sslMode.verifyCertificate()) {
if (this.sslRootCert != null) {
sslContextBuilder.trustManager(new File(this.sslRootCert));
sslContextBuilder.trustManager(new File(this.sslRootCert.getFile()));
}
} else {
sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
}

String sslKey = this.sslKey;
String sslCert = this.sslCert;
URL sslKey = this.sslKey;
URL sslCert = this.sslCert;

// Emulate Libpq behavior
// Determining the default file location
Expand All @@ -800,21 +802,17 @@ private Supplier<SslProvider> createSslProvider() {

if (sslCert == null) {
String pathname = defaultDir + "postgresql.crt";
if (new File(pathname).exists()) {
sslCert = pathname;
}
sslCert = getUrlFromPath(pathname);
}

if (sslKey == null) {
String pathname = defaultDir + "postgresql.pk8";
if (new File(pathname).exists()) {
sslKey = pathname;
}
sslKey = getUrlFromPath(pathname);
}

if (sslKey != null && sslCert != null) {
String sslPassword = this.sslPassword == null ? null : this.sslPassword.toString();
sslContextBuilder.keyManager(new File(sslCert), new File(sslKey), sslPassword);
sslContextBuilder.keyManager(new File(sslCert.getFile()), new File(sslKey.getFile()), sslPassword);
}

return () -> SslProvider.builder()
Expand All @@ -823,6 +821,20 @@ private Supplier<SslProvider> createSslProvider() {
.build();
}

private URL getUrlFromPath(String pathname) {
final File file = new File(pathname);

if (file.exists()) {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException(String.format("Malformed error occurred during creating URL from %s", pathname));
}
}

return null;
}

}

static class FixedFetchSize implements ToIntFunction<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.r2dbc.spi.Option;

import javax.net.ssl.HostnameVerifier;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
Expand Down Expand Up @@ -107,7 +108,7 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
public static final Option<Function<SslContextBuilder, SslContextBuilder>> SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer");

/**
* Full path for the certificate file.
* File path for the certificate file.
*/
public static final Option<String> SSL_CERT = Option.valueOf("sslCert");

Expand All @@ -132,7 +133,7 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
public static final Option<String> SSL_PASSWORD = Option.valueOf("sslPassword");

/**
* File name of the SSL root certificate.
* File path of the SSL root certificate.
*/
public static final Option<String> SSL_ROOT_CERT = Option.valueOf("sslRootCert");

Expand Down
63 changes: 44 additions & 19 deletions src/main/java/io/r2dbc/postgresql/util/Assert.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
import reactor.util.annotation.Nullable;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

/**
* Assertion library for the implementation.
*/
public final class Assert {

private static final String CLASSPATH_PREFIX = "classpath:";

private Assert() {
}

Expand Down Expand Up @@ -114,25 +118,6 @@ public static <T> T requireType(Object value, Class<T> type, String message) {
return (T) value;
}

/**
* Checks that the provided file exists or null.
*
* @param file file to check
* @param message the message to use in exception if type is not as required
* @return existing file or null
* @throws IllegalArgumentException if {@code value} is not of the required type
* @throws IllegalArgumentException if {@code value}, {@code type}, or {@code message} is {@code null}
*/
public static String requireFileExistsOrNull(@Nullable String file, String message) {
if (file == null) {
throw new IllegalArgumentException(message);
}
if (!new File(file).exists()) {
throw new IllegalArgumentException(message);
}
return file;
}

/**
* Assert a boolean expression, throwing an {@link IllegalArgumentException}
* if the expression evaluates to {@code false}.
Expand All @@ -147,4 +132,44 @@ public static void isTrue(boolean expression, String message) {
}
}

/**
* Checks that the provided URL exists or null.
*
* @param path path to check
* @param message the message to use in exception if type is not as required
* @return existing url
* @throws IllegalArgumentException if {@code value} is not of the required type
* @throws IllegalArgumentException if {@code value}, {@code type}, or {@code message} is {@code null}
*/
public static URL requireUrlExistsOrNull(@Nullable String path, String message) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should revert changes to the Assert class and instead, introduce rather an utility method to figure out, whether the given ConnectionFactoryOption tries to point to a real file or to a URL. Checking for a scheme would be a good approach if the configured string doesn't point to a file.

We might still run into an ambiguity on windows (c:\foo\bar.crt vs. classpath:foo.crt or classpath:/foo). Not sure how to resolve that.

if (path == null) {
throw new IllegalArgumentException(message);
}

if (path.startsWith(CLASSPATH_PREFIX)) {
path = path.substring(CLASSPATH_PREFIX.length());
}

ClassLoader classLoader = Assert.class.getClassLoader();
URL url = classLoader != null ? classLoader.getResource(path) : ClassLoader.getSystemResource(path);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the classloader supposed to be null?


if (url == null) {
File file = new File(path);

if (file.exists()) {
try {
url = file.toURI().toURL();
} catch (MalformedURLException ignored) {

}
}
}

if (url == null) {
throw new IllegalArgumentException(String.format("URL resource %s does not exist", path));
}

return url;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,64 @@ void constructorNoSslCustomizer() {
.withMessage("sslContextBuilderCustomizer must not be null");
}

@Test
void constructorNoSslCert() {
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
.host("test-host")
.password("test-password")
.sslCert(null)
.build())
.withMessage("sslCert must not be null");
}

@Test
void constructorNotExistSslCert() {
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
.host("test-host")
.password("test-password")
.sslCert("no-client.crt")
.build())
.withMessage("URL resource no-client.crt does not exist");
}

@Test
void constructorNoSslKey() {
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
.host("test-host")
.password("test-password")
.sslKey(null)
.build())
.withMessage("sslKey must not be null");
}

@Test
void constructorNotExistSslKey() {
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
.host("test-host")
.password("test-password")
.sslKey("no-client.key")
.build())
.withMessage("URL resource no-client.key does not exist");
}

@Test
void constructorNullSslRootCert() {
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
.host("test-host")
.password("test-password")
.sslRootCert(null)
.build())
.withMessage("sslRootCert must not be null");
}

@Test
void constructorNotExistSslRootCert() {
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
.host("test-host")
.password("test-password")
.sslRootCert("no-server.crt")
.build())
.withMessage("URL resource no-server.crt does not exist");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.POSTGRESQL_DRIVER;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.PREPARED_STATEMENT_CACHE_QUERIES;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.SOCKET;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.SSL_CERT;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.SSL_CONTEXT_BUILDER_CUSTOMIZER;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.SSL_KEY;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.SSL_MODE;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.SSL_ROOT_CERT;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.TCP_KEEPALIVE;
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.TCP_NODELAY;
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
Expand Down Expand Up @@ -151,6 +154,38 @@ void supportsSsl() {
assertThat(sslConfig.getSslMode()).isEqualTo(SSLMode.VERIFY_FULL);
}

@Test
void supportsSslCertificates() {
PostgresqlConnectionFactory factory = this.provider.create(builder()
.option(DRIVER, POSTGRESQL_DRIVER)
.option(HOST, "test-host")
.option(PASSWORD, "test-password")
.option(USER, "test-user")
.option(SSL, true)
.option(SSL_KEY, "client.key")
.option(SSL_CERT, "client.crt")
.option(SSL_ROOT_CERT, "server.crt")
.build());

assertThat(factory).isNotNull();
}

@Test
void supportsSslCertificatesByClasspath() {
PostgresqlConnectionFactory factory = this.provider.create(builder()
.option(DRIVER, POSTGRESQL_DRIVER)
.option(HOST, "test-host")
.option(PASSWORD, "test-password")
.option(USER, "test-user")
.option(SSL, true)
.option(SSL_KEY, "classpath:client.key")
.option(SSL_CERT, "classpath:client.crt")
.option(SSL_ROOT_CERT, "classpath:server.crt")
.build());

assertThat(factory).isNotNull();
}

@Test
void supportsSslMode() {
PostgresqlConnectionFactory factory = this.provider.create(builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,20 @@ void require() {
.verifyComplete());
}

@Test
void requireCertificatesWithClasspath() {
client(
c -> c
.sslMode(SSLMode.REQUIRE)
.sslRootCert("classpath:server.crt")
.sslKey("classpath:client.key")
.sslCert("classpath:client.crt"),
c -> c
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete());
}

@Test
void requireConnectsWithoutCertificate() {
client(
Expand Down Expand Up @@ -726,10 +740,8 @@ void verifyCa() {
@Test
void verifyCaWithCustomizer() {
client(
c -> c.sslContextBuilderCustomizer(sslContextBuilder -> {
return sslContextBuilder.trustManager(new File(SERVER.getServerCrt()))
.keyManager(new File(SERVER.getClientCrt()), new File(SERVER.getClientKey()));
})
c -> c.sslContextBuilderCustomizer(sslContextBuilder -> sslContextBuilder.trustManager(SERVER.getServerCrtFile())
.keyManager(SERVER.getClientCrtFile(), SERVER.getClientKeyFile()))
.sslMode(SSLMode.VERIFY_CA),
c -> c
.as(StepVerifier::create)
Expand Down
Loading