Skip to content

Commit 2960bc4

Browse files
committed
add hostname verification
1 parent 3ef7ebc commit 2960bc4

14 files changed

+759
-38
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
- Allow encoder subclasses to customize the message before it is converted to String
1010
[\#40](https://github.com/osiegmar/logback-gelf/issues/40)
11+
- Server certificate hostname verification in `GelfTcpTlsAppender`.
1112

1213
### Changed
1314
- Upgrade to Java 8 (Premier Support of Java 7 ended in July 2019)
15+
- Rename `trustAllCertificates` property of `GelfTcpTlsAppender` to `insecure`.
1416

1517
## [2.2.0] - 2019-12-14
1618
### Added

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ dependencies {
5151

5252
## Important notes
5353

54+
Some changes may require to update your configuration.
55+
5456
### Breaking changes in version 3
55-
Version 3.0.0 of this library upgraded from Java 7 to Java 8.
57+
* Version 3.0.0 of this library upgraded from Java 7 to Java 8.
58+
* The server's certificate hostname now gets verified by `GelfTcpTlsAppender`.
59+
* The `trustAllCertificates` property of `GelfTcpTlsAppender` was renamed to `insecure`.
5660

5761
### Breaking changes in version 2
58-
Version 2.0.0 of this library introduced a configuration change. If you were already using this library, update
59-
your configuration to keep it working!
62+
* Version 2.0.0 of this library introduced a configuration change.
6063

6164
**Old** format:
6265
```xml
@@ -173,7 +176,7 @@ Find more advanced examples in the [examples directory](examples).
173176
`de.siegmar.logbackgelf.GelfTcpTlsAppender`
174177

175178
* Everything from GelfTcpAppender
176-
* **trustAllCertificates**: If true, trust all TLS certificates (even self signed certificates).
179+
* **insecure**: If true, skip the TLS certificate validation.
177180
You should not use this in production! Default: false.
178181

179182
### Encoder

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies {
2525
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
2626
testImplementation 'com.google.guava:guava:28.1-jre'
2727
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.10.1'
28+
testImplementation 'org.bouncycastle:bcpkix-jdk15on:1.64'
2829
}
2930

3031
test {

config/checkstyle/import-control.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<allow pkg="java.text"/>
1010
<allow pkg="java.util"/>
1111

12+
<allow pkg="javax.naming"/>
1213
<allow pkg="javax.net"/>
1314

1415
<allow pkg="ch.qos.logback"/>

config/checkstyle/suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<suppress files=".*Test.java" checks="MagicNumber"/>
66
<suppress files=".*Test.java" checks="ImportControl"/>
77
<suppress files="CustomGelfEncoder.java" checks="ImportControl"/>
8+
<suppress files="X509Util.java" checks="ImportControl"/>
89

910
<suppress files="Example.java" checks=".*"/>
1011
<suppress files="Manual.java" checks=".*"/>

examples/advanced_tcp_tls.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<retryDelay>3000</retryDelay>
1010
<poolSize>2</poolSize>
1111
<poolMaxWaitTime>5000</poolMaxWaitTime>
12-
<trustAllCertificates>false</trustAllCertificates>
12+
<insecure>false</insecure>
1313
<encoder class="de.siegmar.logbackgelf.GelfEncoder">
1414
<originHost>localhost</originHost>
1515
<includeRawMessage>false</includeRawMessage>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Logback GELF - zero dependencies Logback GELF appender library.
3+
* Copyright (C) 2019 Oliver Siegmar
4+
*
5+
* This library is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 2.1 of the License, or (at your option) any later version.
9+
*
10+
* This library is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this library; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18+
*/
19+
20+
package de.siegmar.logbackgelf;
21+
22+
import java.security.cert.CertificateException;
23+
import java.security.cert.CertificateParsingException;
24+
import java.security.cert.X509Certificate;
25+
import java.util.ArrayList;
26+
import java.util.Collection;
27+
import java.util.Collections;
28+
import java.util.List;
29+
30+
import javax.naming.InvalidNameException;
31+
import javax.naming.ldap.LdapName;
32+
import javax.naming.ldap.Rdn;
33+
import javax.net.ssl.X509TrustManager;
34+
35+
class CustomX509TrustManager implements X509TrustManager {
36+
37+
private static final int TYPE_DNS_NAME = 2;
38+
39+
private final X509TrustManager defaultTrustManager;
40+
private final String hostname;
41+
42+
CustomX509TrustManager(final X509TrustManager defaultTrustManager, final String hostname) {
43+
this.defaultTrustManager = defaultTrustManager;
44+
this.hostname = hostname;
45+
}
46+
47+
@Override
48+
public void checkClientTrusted(final X509Certificate[] chain, final String authType) {
49+
throw new UnsupportedOperationException();
50+
}
51+
52+
@Override
53+
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
54+
throws CertificateException {
55+
56+
// First, check the chain via the default trust manager
57+
defaultTrustManager.checkServerTrusted(chain, authType);
58+
59+
final X509Certificate serverCert = chain[0];
60+
61+
if (checkAlternativeNames(serverCert)) {
62+
return;
63+
}
64+
65+
// Check the deprecated common name
66+
checkCommonName(serverCert);
67+
}
68+
69+
private boolean checkAlternativeNames(final X509Certificate serverCert)
70+
throws CertificateException {
71+
72+
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+
}
79+
80+
throw new CertificateException(String.format("Server certificate mismatch. Tried to "
81+
+ "verify %s against subject alternative names: %s", hostname, alternativeNames));
82+
}
83+
return false;
84+
}
85+
86+
private static List<String> getAlternativeNames(final X509Certificate cert)
87+
throws CertificateParsingException {
88+
89+
final Collection<List<?>> subjectAlternativeNames = cert.getSubjectAlternativeNames();
90+
if (subjectAlternativeNames == null) {
91+
return Collections.emptyList();
92+
}
93+
94+
final List<String> ret = new ArrayList<>();
95+
for (final List<?> entry : subjectAlternativeNames) {
96+
if (((Integer) entry.get(0)) == TYPE_DNS_NAME) {
97+
final String dNSName = (String) entry.get(1);
98+
if (dNSName != null) {
99+
ret.add(dNSName);
100+
}
101+
}
102+
}
103+
104+
return ret;
105+
}
106+
107+
private void checkCommonName(final X509Certificate serverCert) throws CertificateException {
108+
final String commonName;
109+
try {
110+
commonName = getCommonName(serverCert);
111+
if (HostnameVerifier.verify(hostname, commonName)) {
112+
return;
113+
}
114+
} catch (final InvalidNameException e) {
115+
throw new CertificateException("Could not read CN from certificate", e);
116+
}
117+
118+
throw new CertificateException(String.format("Server certificate mismatch. "
119+
+ "Tried to verify %s against common name: %s", hostname, commonName));
120+
}
121+
122+
private static String getCommonName(final X509Certificate cert) throws InvalidNameException {
123+
final LdapName ldapName = new LdapName(cert.getSubjectDN().getName());
124+
for (final Rdn rdn : ldapName.getRdns()) {
125+
if ("CN".equalsIgnoreCase(rdn.getType())) {
126+
return (String) rdn.getValue();
127+
}
128+
}
129+
130+
throw new InvalidNameException("No common name found");
131+
}
132+
133+
@Override
134+
public X509Certificate[] getAcceptedIssuers() {
135+
return defaultTrustManager.getAcceptedIssuers();
136+
}
137+
138+
}

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

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,63 +19,79 @@
1919

2020
package de.siegmar.logbackgelf;
2121

22+
import java.security.GeneralSecurityException;
2223
import java.security.KeyManagementException;
24+
import java.security.KeyStore;
25+
import java.security.KeyStoreException;
2326
import java.security.NoSuchAlgorithmException;
2427
import java.security.SecureRandom;
25-
import java.security.cert.X509Certificate;
2628

2729
import javax.net.ssl.SSLContext;
2830
import javax.net.ssl.SSLSocketFactory;
2931
import javax.net.ssl.TrustManager;
32+
import javax.net.ssl.TrustManagerFactory;
3033
import javax.net.ssl.X509TrustManager;
3134

3235
public class GelfTcpTlsAppender extends GelfTcpAppender {
3336

3437
/**
35-
* If {@code true}, trust all TLS certificates (even self signed certificates).
38+
* If {@code true}, skip the TLS certificate validation.
3639
*/
37-
private boolean trustAllCertificates;
40+
private boolean insecure;
3841

39-
public boolean isTrustAllCertificates() {
40-
return trustAllCertificates;
42+
public boolean isInsecure() {
43+
return insecure;
4144
}
4245

43-
public void setTrustAllCertificates(final boolean trustAllCertificates) {
44-
this.trustAllCertificates = trustAllCertificates;
46+
public void setInsecure(final boolean insecure) {
47+
this.insecure = insecure;
4548
}
4649

4750
@Override
4851
protected SSLSocketFactory initSocketFactory() {
49-
if (trustAllCertificates) {
50-
addWarn("Enable trustAllCertificates - don't use this in production!");
51-
try {
52-
final SSLContext context = SSLContext.getInstance("TLS");
53-
context.init(null, buildNoopTrustManagers(), new SecureRandom());
54-
return context.getSocketFactory();
55-
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
56-
throw new IllegalStateException(e);
52+
return configureSocket();
53+
}
54+
55+
private SSLSocketFactory configureSocket() {
56+
final TrustManager trustManager;
57+
58+
try {
59+
if (insecure) {
60+
addWarn("Enabled insecure mode (skip TLS certificate validation)"
61+
+ " - don't use this in production!");
62+
trustManager = new NoopX509TrustManager();
63+
} else {
64+
trustManager = new CustomX509TrustManager(defaultTrustManager(), getGraylogHost());
5765
}
66+
67+
return configureSslFactory(trustManager);
68+
} catch (final GeneralSecurityException e) {
69+
throw new IllegalStateException(e);
5870
}
71+
}
72+
73+
private static X509TrustManager defaultTrustManager()
74+
throws NoSuchAlgorithmException, KeyStoreException {
5975

60-
return (SSLSocketFactory) SSLSocketFactory.getDefault();
76+
final TrustManagerFactory trustManagerFactory =
77+
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
78+
trustManagerFactory.init((KeyStore) null);
79+
80+
for (final TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
81+
if (trustManager instanceof X509TrustManager) {
82+
return (X509TrustManager) trustManager;
83+
}
84+
}
85+
86+
return null;
6187
}
6288

63-
private static TrustManager[] buildNoopTrustManagers() {
64-
return new TrustManager[] {
65-
new X509TrustManager() {
66-
public X509Certificate[] getAcceptedIssuers() {
67-
return null;
68-
}
69-
70-
public void checkClientTrusted(final X509Certificate[] chain,
71-
final String authType) {
72-
}
73-
74-
public void checkServerTrusted(final X509Certificate[] chain,
75-
final String authType) {
76-
}
77-
},
78-
};
89+
private SSLSocketFactory configureSslFactory(final TrustManager trustManager)
90+
throws NoSuchAlgorithmException, KeyManagementException {
91+
92+
final SSLContext context = SSLContext.getInstance("TLS");
93+
context.init(null, new TrustManager[]{trustManager}, new SecureRandom());
94+
return context.getSocketFactory();
7995
}
8096

8197
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Logback GELF - zero dependencies Logback GELF appender library.
3+
* Copyright (C) 2019 Oliver Siegmar
4+
*
5+
* This library is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 2.1 of the License, or (at your option) any later version.
9+
*
10+
* This library is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this library; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18+
*/
19+
20+
package de.siegmar.logbackgelf;
21+
22+
import java.util.Locale;
23+
import java.util.Objects;
24+
25+
final class HostnameVerifier {
26+
27+
private HostnameVerifier() {
28+
}
29+
30+
/**
31+
* Hostname verification logic based on RFC 6125.
32+
* <p>
33+
* If using a wildcard, the wildcard has to be the left-most label (e.g. "*.foo.com").
34+
*
35+
* @param hostname the hostname to validate (e.g. "bar.foo.com")
36+
* @param certname the name (CN or SubjectAlternativeName) from the certificate to verify
37+
* against (e.g. "bar.foo.com" or "*.foo.com")
38+
* @return {@code true}, if the hostname is verified.
39+
* @throws NullPointerException if {@code hostname} or {@code certname} is null.
40+
*/
41+
public static boolean verify(final String hostname, final String certname) {
42+
final String host = Objects.requireNonNull(hostname).toLowerCase(Locale.ENGLISH);
43+
final String cert = Objects.requireNonNull(certname).toLowerCase(Locale.ENGLISH);
44+
45+
if (cert.startsWith("*.")) {
46+
// *.foo.com becomes foo.com
47+
final String cnSuffix = cert.substring(2);
48+
49+
// At least two labels -- no TLD alone
50+
if (!cnSuffix.contains(".")) {
51+
return false;
52+
}
53+
54+
// bar.foo.com becomes foo.com
55+
final String hostSuffix = host.substring(host.indexOf('.') + 1);
56+
57+
return cnSuffix.equals(hostSuffix);
58+
}
59+
60+
// Simple case, exact match
61+
return host.equals(cert);
62+
}
63+
64+
}

0 commit comments

Comments
 (0)