Skip to content

Commit e44d737

Browse files
committed
Polishing
Simplify test setup by moving TrustStore assertions into unit tests instead of customizing the actual SQL server instance. Add configuration options to MssqlConnectionFactoryProvider and document these. Add author and since tags. [#148][#150]
1 parent 0532f67 commit e44d737

10 files changed

+204
-380
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Mono<Connection> connectionMono = Mono.from(connectionFactory.create());
7676
| `hostNameInCertificate` | Expected hostname in SSL certificate. Supports wildcards (e.g. `*.database.windows.net`). _(Optional)_
7777
| `preferCursoredExecution` | Whether to prefer cursors or direct execution for queries. Uses by default direct. Cursors require more round-trips but are more backpressure-friendly. Defaults to direct execution. Can be `boolean` or a `Predicate<String>` accepting the SQL query. _(Optional)_
7878
| `sendStringParametersAsUnicode` | Configure whether to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding, defaults to `true`. If disabled, `CharSequence` data is sent using the database-specific collation such as ASCII/MBCS instead of Unicode.
79+
| `trustStoreType` | Type of the TrustStore. Defaults to `KeyStore.getDefaultType()`. _(Optional)_
80+
| `trustStore` | Path to the certificate TrustStore file. _(Optional)_
81+
| `trustStorePassword` | Password used to check the integrity of the TrustStore data. _(Optional)_
82+
7983

8084
**Programmatic Configuration**
8185

pom.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
<slf4j.version>1.7.26</slf4j.version>
5252
<spring-framework.version>5.2.6.RELEASE</spring-framework.version>
5353
<testcontainers.version>1.14.2</testcontainers.version>
54-
<bouncycastle.version>1.64</bouncycastle.version>
5554
</properties>
5655

5756
<licenses>
@@ -211,12 +210,6 @@
211210
<version>${logback.version}</version>
212211
<scope>test</scope>
213212
</dependency>
214-
<dependency>
215-
<groupId>org.bouncycastle</groupId>
216-
<artifactId>bcpkix-jdk15on</artifactId>
217-
<version>${bouncycastle.version}</version>
218-
<scope>test</scope>
219-
</dependency>
220213
</dependencies>
221214

222215
<build>

src/main/java/io/r2dbc/mssql/MssqlConnectionConfiguration.java

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,28 @@
3131
import javax.net.ssl.TrustManager;
3232
import javax.net.ssl.TrustManagerFactory;
3333
import javax.net.ssl.X509TrustManager;
34-
import java.io.ByteArrayInputStream;
3534
import java.io.File;
35+
import java.io.FileInputStream;
3636
import java.io.IOException;
3737
import java.net.InetAddress;
3838
import java.net.UnknownHostException;
3939
import java.security.GeneralSecurityException;
4040
import java.security.KeyStore;
4141
import java.time.Duration;
42+
import java.util.Arrays;
4243
import java.util.Optional;
4344
import java.util.UUID;
4445
import java.util.function.Function;
4546
import java.util.function.Predicate;
4647

47-
import static java.nio.file.Files.readAllBytes;
4848
import static reactor.netty.tcp.SslProvider.DefaultConfigurationType.TCP;
4949

5050
/**
5151
* Connection configuration information for connecting to a Microsoft SQL database.
5252
* Allows configuration of the connection endpoint, login credentials, database and trace details such as application name and connection Id.
5353
*
5454
* @author Mark Paluch
55+
* @author Alex Stockinger
5556
*/
5657
public final class MssqlConnectionConfiguration {
5758

@@ -94,17 +95,17 @@ public final class MssqlConnectionConfiguration {
9495
private final String username;
9596

9697
@Nullable
97-
private final String trustStore;
98+
private final File trustStore;
9899

99100
@Nullable
100101
private final String trustStoreType;
101102

102103
@Nullable
103104
private final char[] trustStorePassword;
104105

105-
private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, Duration connectTimeout, @Nullable String database, String host, String hostNameInCertificate
106-
, CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode, boolean ssl,
107-
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer, @Nullable String trustStore, @Nullable String trustStoreType,
106+
private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, Duration connectTimeout, @Nullable String database, String host, String hostNameInCertificate,
107+
CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode, boolean ssl,
108+
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer, @Nullable File trustStore, @Nullable String trustStoreType,
108109
@Nullable char[] trustStorePassword, String username) {
109110

110111
this.applicationName = applicationName;
@@ -164,7 +165,7 @@ MssqlConnectionConfiguration withRedirect(Redirect redirect) {
164165
}
165166

166167
ClientConfiguration toClientConfiguration() {
167-
return new DefaultClientConfiguration(this.connectTimeout, this.host, this.hostNameInCertificate, this.port, this.ssl, sslContextBuilderCustomizer, this.trustStore, this.trustStoreType,
168+
return new DefaultClientConfiguration(this.connectTimeout, this.host, this.hostNameInCertificate, this.port, this.ssl, this.sslContextBuilderCustomizer, this.trustStore, this.trustStoreType,
168169
this.trustStorePassword);
169170
}
170171

@@ -323,10 +324,13 @@ public static final class Builder {
323324

324325
private String username;
325326

326-
private String trustStore;
327+
@Nullable
328+
private File trustStore;
327329

330+
@Nullable
328331
private String trustStoreType;
329332

333+
@Nullable
330334
private char[] trustStorePassword;
331335

332336
private Builder() {
@@ -505,22 +509,38 @@ public Builder username(String username) {
505509
/**
506510
* Configure the trust store type.
507511
*
508-
* @param trustStoreType the type of the trust store to be used for SSL certificate verification. Defaults to 'JKS' if not set.
512+
* @param trustStoreType the type of the trust store to be used for SSL certificate verification. Defaults to {@link KeyStore#getDefaultType()} if not set.
509513
* @return this {@link Builder}
514+
* @throws IllegalArgumentException if {@code trustStoreType} is {@code null}
515+
* @since 0.8.3
510516
*/
511-
public Builder withTrustStoreType(String trustStoreType) {
512-
this.trustStoreType = trustStoreType;
517+
public Builder trustStoreType(String trustStoreType) {
518+
this.trustStoreType = Assert.requireNonNull(trustStoreType, "trustStoreType must not be null");
513519
return this;
514520
}
515521

516522
/**
517-
* Configure the trust store.
523+
* Configure the file path to the trust store.
524+
*
525+
* @param trustStoreFile the path of the trust store to be used for SSL certificate verification.
526+
* @return this {@link Builder}
527+
* @throws IllegalArgumentException if {@code trustStore} is {@code null}
528+
* @since 0.8.3
529+
*/
530+
public Builder trustStore(String trustStoreFile) {
531+
return trustStore(new File(Assert.requireNonNull(trustStoreFile, "trustStore must not be null")));
532+
}
533+
534+
/**
535+
* Configure the path to the trust store.
518536
*
519537
* @param trustStore the path of the trust store to be used for SSL certificate verification.
520538
* @return this {@link Builder}
539+
* @throws IllegalArgumentException if {@code trustStore} is {@code null}
540+
* @since 0.8.3
521541
*/
522-
public Builder withTrustStore(String trustStore) {
523-
this.trustStore = trustStore;
542+
public Builder trustStore(File trustStore) {
543+
this.trustStore = Assert.requireNonNull(trustStore, "trustStore must not be null");
524544
return this;
525545
}
526546

@@ -529,9 +549,10 @@ public Builder withTrustStore(String trustStore) {
529549
*
530550
* @param trustStorePassword the password to read the trust store.
531551
* @return this {@link Builder}
552+
* @since 0.8.3
532553
*/
533-
public Builder withTrustStorePassword(char[] trustStorePassword) {
534-
this.trustStorePassword = trustStorePassword;
554+
public Builder trustStorePassword(char[] trustStorePassword) {
555+
this.trustStorePassword = Assert.requireNonNull(Arrays.copyOf(trustStorePassword, trustStorePassword.length), "trustStorePassword must not be null");
535556
return this;
536557
}
537558

@@ -552,7 +573,7 @@ public MssqlConnectionConfiguration build() {
552573
}
553574
}
554575

555-
private static class DefaultClientConfiguration implements ClientConfiguration {
576+
static class DefaultClientConfiguration implements ClientConfiguration {
556577

557578
private final Duration connectTimeout;
558579

@@ -567,7 +588,7 @@ private static class DefaultClientConfiguration implements ClientConfiguration {
567588
private final Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer;
568589

569590
@Nullable
570-
private final String trustStore;
591+
private final File trustStore;
571592

572593
@Nullable
573594
private final String trustStoreType;
@@ -576,7 +597,7 @@ private static class DefaultClientConfiguration implements ClientConfiguration {
576597
private final char[] trustStorePassword;
577598

578599
DefaultClientConfiguration(Duration connectTimeout, String host, String hostNameInCertificate, int port, boolean ssl,
579-
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer, @Nullable String trustStore,
600+
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer, @Nullable File trustStore,
580601
@Nullable String trustStoreType, @Nullable char[] trustStorePassword) {
581602

582603
this.connectTimeout = connectTimeout;
@@ -642,17 +663,19 @@ public SslProvider getSslProvider() throws GeneralSecurityException {
642663
}
643664

644665
@Nullable
645-
private KeyStore loadCustomTrustStore() throws GeneralSecurityException {
666+
KeyStore loadCustomTrustStore() throws GeneralSecurityException {
646667

647-
try {
648-
if (trustStore == null) {
649-
return null;
650-
}
651-
KeyStore trustStoreInstance = KeyStore.getInstance(trustStoreType == null ? "JKS" : trustStoreType);
652-
trustStoreInstance.load(new ByteArrayInputStream(readAllBytes(new File(trustStore).toPath())), trustStorePassword);
668+
if (this.trustStore == null) {
669+
return null;
670+
}
671+
672+
KeyStore trustStoreInstance = KeyStore.getInstance(this.trustStoreType == null ? KeyStore.getDefaultType() : this.trustStoreType);
673+
674+
try (FileInputStream fis = new FileInputStream(this.trustStore)) {
675+
trustStoreInstance.load(fis, this.trustStorePassword);
653676
return trustStoreInstance;
654677
} catch (IOException e) {
655-
throw new GeneralSecurityException("Could not load custom trust store", e);
678+
throw new GeneralSecurityException(String.format("Could not load custom trust store from %s", this.trustStore), e);
656679
}
657680
}
658681
}

src/main/java/io/r2dbc/mssql/MssqlConnectionFactoryProvider.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import reactor.util.Logger;
2525
import reactor.util.Loggers;
2626

27+
import java.io.File;
2728
import java.time.Duration;
2829
import java.util.UUID;
2930
import java.util.function.Function;
@@ -82,6 +83,27 @@ public final class MssqlConnectionFactoryProvider implements ConnectionFactoryPr
8283
*/
8384
public static final Option<Function<SslContextBuilder, SslContextBuilder>> SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer");
8485

86+
/**
87+
* Type of the TrustStore.
88+
*
89+
* @since 0.8.3
90+
*/
91+
public static final Option<String> TRUST_STORE_TYPE = Option.valueOf("trustStoreType");
92+
93+
/**
94+
* Path to the certificate TrustStore file.
95+
*
96+
* @since 0.8.3
97+
*/
98+
public static final Option<File> TRUST_STORE = Option.valueOf("trustStore");
99+
100+
/**
101+
* Password used to check the integrity of the TrustStore data.
102+
*
103+
* @since 0.8.3
104+
*/
105+
public static final Option<char[]> TRUST_STORE_PASSWORD = Option.valueOf("trustStorePassword");
106+
85107
/**
86108
* Driver option value.
87109
*/
@@ -149,7 +171,7 @@ public MssqlConnectionFactory create(ConnectionFactoryOptions connectionFactoryO
149171
} else {
150172

151173
try {
152-
Object predicate = Class.forName(value).getConstructor().newInstance();
174+
Object predicate = Class.forName(value).getDeclaredConstructor().newInstance();
153175
if (predicate instanceof Predicate) {
154176
builder.preferCursoredExecution((Predicate<String>) predicate);
155177
} else {
@@ -176,6 +198,18 @@ public MssqlConnectionFactory create(ConnectionFactoryOptions connectionFactoryO
176198
builder.username(connectionFactoryOptions.getRequiredValue(USER));
177199
builder.applicationName(connectionFactoryOptions.getRequiredValue(USER));
178200

201+
if (connectionFactoryOptions.hasOption(TRUST_STORE)) {
202+
builder.trustStore(connectionFactoryOptions.getRequiredValue(TRUST_STORE));
203+
}
204+
205+
if (connectionFactoryOptions.hasOption(TRUST_STORE_PASSWORD)) {
206+
builder.trustStorePassword(connectionFactoryOptions.getRequiredValue(TRUST_STORE_PASSWORD));
207+
}
208+
209+
if (connectionFactoryOptions.hasOption(TRUST_STORE_TYPE)) {
210+
builder.trustStoreType(connectionFactoryOptions.getRequiredValue(TRUST_STORE_TYPE));
211+
}
212+
179213
if (connectionFactoryOptions.hasOption(SSL_CONTEXT_BUILDER_CUSTOMIZER)) {
180214
builder.sslContextBuilderCustomizer(connectionFactoryOptions.getRequiredValue(SSL_CONTEXT_BUILDER_CUSTOMIZER));
181215
}

src/test/java/io/r2dbc/mssql/MssqlConnectionConfigurationUnitTests.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,25 @@
1818

1919
import io.r2dbc.mssql.message.tds.Redirect;
2020
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.io.TempDir;
22+
import org.testcontainers.shaded.org.bouncycastle.asn1.x500.X500Name;
23+
import org.testcontainers.shaded.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
24+
import org.testcontainers.shaded.org.bouncycastle.cert.X509CertificateHolder;
25+
import org.testcontainers.shaded.org.bouncycastle.cert.X509v3CertificateBuilder;
26+
import org.testcontainers.shaded.org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
27+
import org.testcontainers.shaded.org.bouncycastle.operator.ContentSigner;
28+
import org.testcontainers.shaded.org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
2129

30+
import java.io.File;
31+
import java.io.FileOutputStream;
32+
import java.math.BigInteger;
33+
import java.security.KeyPair;
34+
import java.security.KeyPairGenerator;
35+
import java.security.KeyStore;
36+
import java.security.SecureRandom;
37+
import java.security.cert.Certificate;
38+
import java.util.Calendar;
39+
import java.util.Date;
2240
import java.util.UUID;
2341
import java.util.function.Predicate;
2442

@@ -212,4 +230,68 @@ void redirectInDomain() {
212230
.hasFieldOrPropertyWithValue("sendStringParametersAsUnicode", true)
213231
.hasFieldOrPropertyWithValue("hostNameInCertificate", "*.target.windows.net");
214232
}
233+
234+
@Test
235+
void configureKeyStore(@TempDir File tempDir) throws Exception {
236+
237+
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
238+
keyGen.initialize(1024, new SecureRandom());
239+
KeyPair keypair = keyGen.generateKeyPair();
240+
241+
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
242+
keyStore.load(null, null);
243+
244+
Certificate selfSignedCertificate = selfSign(keypair, "CN=dummy");
245+
246+
KeyStore.Entry entry = new KeyStore.PrivateKeyEntry(keypair.getPrivate(),
247+
new Certificate[]{selfSignedCertificate});
248+
249+
keyStore.setEntry("dummy", entry, new KeyStore.PasswordProtection("key-password".toCharArray()));
250+
251+
File file = new File(tempDir, getClass().getName() + ".jks");
252+
keyStore.store(new FileOutputStream(file), "my-password".toCharArray());
253+
254+
255+
MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder()
256+
.database("test-database")
257+
.host("test-host.windows.net")
258+
.password("test-password")
259+
.username("test-username")
260+
.trustStore(file)
261+
.trustStorePassword("my-password".toCharArray())
262+
.build();
263+
264+
MssqlConnectionConfiguration.DefaultClientConfiguration clientConfiguration = (MssqlConnectionConfiguration.DefaultClientConfiguration) configuration.toClientConfiguration();
265+
266+
KeyStore loaded = clientConfiguration.loadCustomTrustStore();
267+
268+
KeyStore.Entry loadedEntry = loaded.getEntry("dummy", new KeyStore.PasswordProtection("key-password".toCharArray()));
269+
assertThat(loadedEntry).isInstanceOf(KeyStore.PrivateKeyEntry.class);
270+
}
271+
272+
private static Certificate selfSign(KeyPair keyPair, String subjectDN)
273+
throws Exception {
274+
275+
Date startDate = new Date();
276+
X500Name dnName = new X500Name(subjectDN);
277+
278+
Calendar calendar = Calendar.getInstance();
279+
calendar.setTime(startDate);
280+
calendar.add(Calendar.YEAR, 1);
281+
Date endDate = calendar.getTime();
282+
283+
284+
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair
285+
.getPublic().getEncoded());
286+
287+
X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(dnName,
288+
BigInteger.valueOf(1), startDate, endDate, dnName, subjectPublicKeyInfo);
289+
290+
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
291+
292+
X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner);
293+
294+
return new JcaX509CertificateConverter()
295+
.getCertificate(certificateHolder);
296+
}
215297
}

0 commit comments

Comments
 (0)