Skip to content

Commit f5a2c63

Browse files
committed
Allow explicit server certificate whitelisting #27
1 parent bb8c7a9 commit f5a2c63

File tree

7 files changed

+150
-21
lines changed

7 files changed

+150
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
[\#86](https://github.com/osiegmar/logback-gelf/issues/86)
1111
- Add another method for adding static field to GelfEncoder
1212
[\#80](https://github.com/osiegmar/logback-gelf/issues/80)
13+
- Add server certificate whitelisting (`trustedServerCertificate`).
1314

1415
### Changed
1516
- Upgrade to Java 11 (Premier Support of Java 8 ended in March 2022).

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ Simple TCP with TLS configuration:
8080
<appender name="GELF" class="de.siegmar.logbackgelf.GelfTcpTlsAppender">
8181
<graylogHost>localhost</graylogHost>
8282
<graylogPort>12201</graylogPort>
83+
<trustedServerCertificate>
84+
-----BEGIN CERTIFICATE-----
85+
...
86+
-----END CERTIFICATE-----
87+
</trustedServerCertificate>
88+
<insecure>false</insecure>
8389
</appender>
8490

8591
<!-- Use AsyncAppender to prevent slowdowns -->
@@ -141,6 +147,12 @@ Find more advanced examples in the [examples directory](examples).
141147
`de.siegmar.logbackgelf.GelfTcpTlsAppender`
142148

143149
* Everything from GelfTcpAppender
150+
* **trustedServerCertificate**: An optionally configured X.509 server certificate (PEM encoded) to
151+
trust (whitelist). Can be configured multiple times to ease certification renewal.
152+
If configured, the server needs to offer one of these certificates in order to allow
153+
communication. The certificate offered by the server needs to be valid (matching hostname and
154+
not expired). If this property is configured, the server's certificate chain is not validated in
155+
order to allow self-signed certificates. Default: none.
144156
* **insecure**: If true, skip the TLS certificate validation.
145157
You should not use this in production! Default: false.
146158

src/main/java/de/siegmar/logbackgelf/CustomX509TrustManager.java

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Collection;
2727
import java.util.Collections;
2828
import java.util.List;
29+
import java.util.Objects;
2930

3031
import javax.naming.InvalidNameException;
3132
import javax.naming.ldap.LdapName;
@@ -38,10 +39,17 @@ class CustomX509TrustManager implements X509TrustManager {
3839

3940
private final X509TrustManager trustManager;
4041
private final String hostname;
42+
private final List<X509Certificate> trustedServerCertificates;
4143

4244
CustomX509TrustManager(final X509TrustManager trustManager, final String hostname) {
43-
this.trustManager = trustManager;
44-
this.hostname = hostname;
45+
this(trustManager, hostname, Collections.emptyList());
46+
}
47+
48+
CustomX509TrustManager(final X509TrustManager trustManager, final String hostname,
49+
final List<X509Certificate> trustedServerCertificates) {
50+
this.trustManager = Objects.requireNonNull(trustManager);
51+
this.hostname = Objects.requireNonNull(hostname);
52+
this.trustedServerCertificates = Objects.requireNonNull(trustedServerCertificates);
4553
}
4654

4755
@Override
@@ -53,34 +61,43 @@ public void checkClientTrusted(final X509Certificate[] chain, final String authT
5361
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
5462
throws CertificateException {
5563

56-
// First, check the chain via the trust manager
57-
trustManager.checkServerTrusted(chain, authType);
58-
64+
// The first certificate is the server certificate
5965
final X509Certificate serverCert = chain[0];
6066

61-
if (checkAlternativeNames(serverCert)) {
62-
return;
67+
if (trustedServerCertificates.isEmpty()) {
68+
// If not explicitly trusted, check via the trust manager (chain validation)
69+
trustManager.checkServerTrusted(chain, authType);
70+
} else {
71+
if (!trustedServerCertificates.contains(serverCert)) {
72+
throw new CertificateException("Server did not offer a whitelisted certificate");
73+
}
74+
75+
// Check if the certificate is valid. This is also done by the trust manager.
76+
serverCert.checkValidity();
6377
}
6478

65-
// Check the deprecated common name
66-
checkCommonName(serverCert);
79+
if (!checkAlternativeNames(serverCert)) {
80+
// Check the deprecated common name
81+
checkCommonName(serverCert);
82+
}
6783
}
6884

6985
private boolean checkAlternativeNames(final X509Certificate serverCert)
7086
throws CertificateException {
7187

7288
final List<String> alternativeNames = getAlternativeNames(serverCert);
73-
if (!alternativeNames.isEmpty()) {
74-
for (final String alternativeName : alternativeNames) {
75-
if (HostnameVerifier.verify(hostname, alternativeName)) {
76-
return true;
77-
}
78-
}
89+
if (alternativeNames.isEmpty()) {
90+
return false;
91+
}
7992

80-
throw new CertificateException(String.format("Server certificate mismatch. Tried to "
81-
+ "verify %s against subject alternative names: %s", hostname, alternativeNames));
93+
for (final String alternativeName : alternativeNames) {
94+
if (HostnameVerifier.verify(hostname, alternativeName)) {
95+
return true;
96+
}
8297
}
83-
return false;
98+
99+
throw new CertificateException(String.format("Server certificate mismatch. Tried to "
100+
+ "verify %s against subject alternative names: %s", hostname, alternativeNames));
84101
}
85102

86103
private static List<String> getAlternativeNames(final X509Certificate cert)

src/main/java/de/siegmar/logbackgelf/GelfTcpTlsAppender.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@
1919

2020
package de.siegmar.logbackgelf;
2121

22+
import java.io.ByteArrayInputStream;
23+
import java.nio.charset.StandardCharsets;
2224
import java.security.GeneralSecurityException;
2325
import java.security.KeyManagementException;
2426
import java.security.KeyStore;
2527
import java.security.KeyStoreException;
2628
import java.security.NoSuchAlgorithmException;
2729
import java.security.SecureRandom;
30+
import java.security.cert.CertificateException;
31+
import java.security.cert.CertificateFactory;
32+
import java.security.cert.X509Certificate;
33+
import java.util.ArrayList;
34+
import java.util.List;
2835

2936
import javax.net.ssl.SSLContext;
3037
import javax.net.ssl.SSLSocketFactory;
@@ -39,6 +46,8 @@ public class GelfTcpTlsAppender extends GelfTcpAppender {
3946
*/
4047
private boolean insecure;
4148

49+
private final List<X509Certificate> trustedServerCertificates = new ArrayList<>();
50+
4251
public boolean isInsecure() {
4352
return insecure;
4453
}
@@ -47,6 +56,22 @@ public void setInsecure(final boolean insecure) {
4756
this.insecure = insecure;
4857
}
4958

59+
public List<X509Certificate> getTrustedServerCertificates() {
60+
return trustedServerCertificates;
61+
}
62+
63+
public void addTrustedServerCertificate(final String trustedServerCertificate)
64+
throws CertificateException {
65+
66+
trustedServerCertificates.add(readCert(trustedServerCertificate));
67+
}
68+
69+
private X509Certificate readCert(final String cert) throws CertificateException {
70+
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
71+
return (X509Certificate) certificateFactory.generateCertificate(
72+
new ByteArrayInputStream(cert.getBytes(StandardCharsets.US_ASCII)));
73+
}
74+
5075
@Override
5176
protected SSLSocketFactory initSocketFactory() {
5277
try {
@@ -60,13 +85,20 @@ private TrustManager newTrustManager() throws NoSuchAlgorithmException, KeyStore
6085
if (insecure) {
6186
addWarn("Enabled insecure mode (skip TLS certificate validation)"
6287
+ " - don't use this in production!");
88+
89+
if (!trustedServerCertificates.isEmpty()) {
90+
throw new IllegalStateException("Configuration options 'insecure' and "
91+
+ "'trustedServerCertificates' are mutually exclusive!");
92+
}
93+
6394
return new NoopX509TrustManager();
6495
}
6596

66-
return new CustomX509TrustManager(findDefaultX509TrustManager(), getGraylogHost());
97+
return new CustomX509TrustManager(defaultTrustManager(), getGraylogHost(),
98+
trustedServerCertificates);
6799
}
68100

69-
private static X509TrustManager findDefaultX509TrustManager()
101+
private static X509TrustManager defaultTrustManager()
70102
throws NoSuchAlgorithmException, KeyStoreException {
71103

72104
final TrustManagerFactory trustManagerFactory =

src/test/java/de/siegmar/logbackgelf/CustomX509TrustManagerTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.security.cert.X509Certificate;
3434
import java.time.LocalDate;
3535
import java.util.Arrays;
36+
import java.util.List;
3637

3738
import javax.net.ssl.TrustManager;
3839
import javax.net.ssl.TrustManagerFactory;
@@ -142,6 +143,21 @@ public void expired() throws Exception {
142143
() -> validate(cert, caBuilder.getCaCertificate()));
143144
}
144145

146+
@Test
147+
public void caSignedNotWhitelisted() throws Exception {
148+
final X509Util.CABuilder caBuilder = new X509Util.CABuilder();
149+
final X509Certificate cert = prepareCaSigned(caBuilder)
150+
.build(HOSTNAME);
151+
152+
tm = new CustomX509TrustManager(defaultTrustManager(null), HOSTNAME,
153+
List.of(c.build(null, HOSTNAME)));
154+
155+
final CertificateException e = assertThrows(CertificateException.class,
156+
() -> validate(cert, caBuilder.getCaCertificate()));
157+
158+
assertEquals("Server did not offer a whitelisted certificate", e.getMessage());
159+
}
160+
145161
private void validate(final X509Certificate... certificates) throws CertificateException {
146162
tm.checkServerTrusted(certificates, "RSA");
147163
}

src/test/java/de/siegmar/logbackgelf/GelfTcpTlsAppenderTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
import java.io.IOException;
2929
import java.io.UncheckedIOException;
3030
import java.net.Socket;
31+
import java.security.cert.X509Certificate;
3132
import java.util.concurrent.Callable;
3233
import java.util.concurrent.ExecutionException;
3334
import java.util.concurrent.Executors;
3435
import java.util.concurrent.Future;
3536
import java.util.concurrent.TimeUnit;
3637
import java.util.concurrent.TimeoutException;
38+
import java.util.concurrent.atomic.AtomicReference;
3739

3840
import javax.net.ssl.SSLServerSocket;
3941
import javax.net.ssl.SSLServerSocketFactory;
@@ -47,6 +49,7 @@
4749

4850
import ch.qos.logback.classic.Logger;
4951
import ch.qos.logback.classic.LoggerContext;
52+
import ch.qos.logback.core.status.Status;
5053

5154
public class GelfTcpTlsAppenderTest {
5255

@@ -75,6 +78,40 @@ void defaultValues() {
7578
assertFalse(appender.isInsecure());
7679
}
7780

81+
@Test
82+
public void configurationError() throws Exception {
83+
final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
84+
85+
final AtomicReference<Status> lastStatus = new AtomicReference<>();
86+
lc.getStatusManager().add(lastStatus::set);
87+
88+
final GelfEncoder gelfEncoder = new GelfEncoder();
89+
gelfEncoder.setContext(lc);
90+
gelfEncoder.setOriginHost("localhost");
91+
gelfEncoder.start();
92+
93+
final Logger logger = (Logger) LoggerFactory.getLogger(LOGGER_NAME);
94+
logger.addAppender(buildAppender(lc, gelfEncoder));
95+
logger.setAdditive(false);
96+
97+
final X509Certificate cert = new X509Util.CertBuilder()
98+
.build(null, "foo.example.com");
99+
100+
final GelfTcpTlsAppender gelfAppender = new GelfTcpTlsAppender();
101+
gelfAppender.setContext(lc);
102+
gelfAppender.setName("GELF");
103+
gelfAppender.setEncoder(gelfEncoder);
104+
gelfAppender.setGraylogHost("localhost");
105+
gelfAppender.setGraylogPort(port);
106+
gelfAppender.setInsecure(true);
107+
gelfAppender.addTrustedServerCertificate(X509Util.toPEM(cert));
108+
gelfAppender.start();
109+
110+
final IllegalStateException e = (IllegalStateException) lastStatus.get().getThrowable();
111+
assertEquals("Configuration options 'insecure' and 'trustedServerCertificates' "
112+
+ "are mutually exclusive!", e.getMessage());
113+
}
114+
78115
@Test
79116
public void simple() {
80117
final Logger logger = setupLogger();

src/test/java/de/siegmar/logbackgelf/X509Util.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package de.siegmar.logbackgelf;
2121

2222
import java.io.IOException;
23+
import java.io.StringWriter;
2324
import java.math.BigInteger;
2425
import java.security.KeyPair;
2526
import java.security.KeyPairGenerator;
@@ -54,6 +55,7 @@
5455
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
5556
import org.bouncycastle.crypto.util.PrivateKeyFactory;
5657
import org.bouncycastle.jce.provider.BouncyCastleProvider;
58+
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
5759
import org.bouncycastle.operator.ContentSigner;
5860
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
5961
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
@@ -62,7 +64,7 @@
6264
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
6365

6466
@SuppressWarnings({"checkstyle:classdataabstractioncoupling", "checkstyle:classfanoutcomplexity"})
65-
public class X509Util {
67+
final class X509Util {
6668

6769
private static final String ALGORITHM = "RSA";
6870
private static final String SIG_ALGORITHM = "SHA256withRSA";
@@ -73,12 +75,24 @@ public class X509Util {
7375
Security.addProvider(new BouncyCastleProvider());
7476
}
7577

78+
private X509Util() {
79+
}
80+
7681
private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
7782
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
7883
keyGen.initialize(KEY_SIZE);
7984
return keyGen.genKeyPair();
8085
}
8186

87+
static String toPEM(final X509Certificate certificate) throws IOException {
88+
final StringWriter sw = new StringWriter();
89+
try (JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(sw)) {
90+
jcaPEMWriter.writeObject(certificate);
91+
}
92+
93+
return sw.toString();
94+
}
95+
8296
static class CABuilder {
8397

8498
private final KeyPair keyPair;

0 commit comments

Comments
 (0)