Permalink
Browse files

fix: support Subject Alternative Names for SSL connections (#952)

  • Loading branch information...
davecramer authored and vlsi committed Oct 25, 2017
1 parent d28deff commit 2dcb91ef1fd8f0fe08f107c9c30cdc57d4c44b05
@@ -24,7 +24,10 @@
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import javax.naming.InvalidNameException;
@@ -48,6 +51,8 @@
*/
public class LibPQFactory extends WrappedFactory implements HostnameVerifier {
private static final int ALT_DNS_NAME = 2;
LazyKeyManager km = null;
String sslmode;
@@ -229,6 +234,39 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback
}
}
public static boolean verifyHostName(String hostname, String pattern) {
if (hostname == null || pattern == null) {
return false;
}
if (!pattern.startsWith("*")) {
// No wildcard => just compare hostnames
return hostname.equalsIgnoreCase(pattern);
}
// pattern starts with *, so hostname should be at least (pattern.length-1) long
if (hostname.length() < pattern.length() - 1) {
return false;
}
// Compare ignore case
final boolean ignoreCase = true;
// Below code is "hostname.endsWithIgnoreCase(pattern.withoutFirstStar())"
// E.g. hostname==sub.host.com; pattern==*.host.com
// We need to start the offset of ".host.com" in hostname
// For this we take hostname.length() - pattern.length()
// and +1 is required since pattern is known to start with *
int toffset = hostname.length() - pattern.length() + 1;
// Wildcard covers just one domain level
// a.b.c.com should not be covered by *.c.com
if (hostname.lastIndexOf('.', toffset - 1) >= 0) {
// If there's a dot in between 0..toffset
return false;
}
return hostname.regionMatches(ignoreCase, toffset,
pattern, 1, pattern.length() - 1);
}
/**
* Verifies the server certificate according to the libpq rules. The cn attribute of the
* certificate is matched against the hostname. If the cn attribute starts with an asterisk (*),
@@ -252,6 +290,26 @@ public boolean verify(String hostname, SSLSession session) {
}
// Extract the common name
X509Certificate serverCert = peerCerts[0];
try {
// Check for Subject Alternative Names (see RFC 6125)
Collection<List<?>> subjectAltNames = serverCert.getSubjectAlternativeNames();
if (subjectAltNames != null) {
for (List<?> sanit : subjectAltNames) {
Integer type = (Integer) sanit.get(0);
String san = (String) sanit.get(1);
// this mimics libpq check for ALT_DNS_NAME
if (type != null && type == ALT_DNS_NAME && verifyHostName(hostname, san)) {
return true;
}
}
}
} catch (CertificateParsingException e) {
return false;
}
LdapName DN;
try {
DN = new LdapName(serverCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
@@ -266,17 +324,6 @@ public boolean verify(String hostname, SSLSession session) {
break;
}
}
if (CN == null) {
return false;
} else if (CN.startsWith("*")) { // We have a wildcard
if (hostname.endsWith(CN.substring(1))) {
// Avoid IndexOutOfBoundsException because hostname already ends with CN
return !(hostname.substring(0, hostname.length() - CN.length() + 1).contains("."));
} else {
return false;
}
} else {
return CN.equals(hostname);
}
return verifyHostName(hostname, CN);
}
}
@@ -24,6 +24,7 @@
BinaryStreamTest.class,
CharacterStreamTest.class,
UUIDTest.class,
LibPQFactoryHostNameTest.class,
XmlTest.class
})
public class Jdbc4TestSuite {
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2017, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.test.jdbc4;
import org.postgresql.ssl.jdbc4.LibPQFactory;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
@RunWith(Parameterized.class)
public class LibPQFactoryHostNameTest {
private final String hostname;
private final String pattern;
private final boolean expected;
public LibPQFactoryHostNameTest(String hostname, String pattern, boolean expected) {
this.hostname = hostname;
this.pattern = pattern;
this.expected = expected;
}
@Parameterized.Parameters(name = "host={0}, pattern={1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{"host.com", "pattern.com", false},
{"host.com", ".pattern.com", false},
{"host.com", "*.pattern.com", false},
{"host.com", "*.host.com", false},
{"a.com", "*.host.com", false},
{".a.com", "*.host.com", false},
{"longhostname.com", "*.com", true},
{"longhostname.ru", "*.com", false},
{"host.com", "host.com", true},
{"sub.host.com", "host.com", false},
{"sub.host.com", "sub.host.com", true},
{"sub.host.com", "*.host.com", true},
{"Sub.host.com", "sub.host.com", true},
{"sub.host.com", "Sub.host.com", true},
{"sub.host.com", "*.hoSt.com", true},
{"*.host.com", "host.com", false},
{"sub.sub.host.com", "*.host.com", false}, // Wildcard should cover just one level
});
}
@Test
public void checkPattern() throws Exception {
Assert.assertEquals(expected, LibPQFactory.verifyHostName(hostname, pattern));
}
}

0 comments on commit 2dcb91e

Please sign in to comment.