Skip to content
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

Support Subject Alternative Names for SSL connections #952

Merged
merged 7 commits into from Oct 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 59 additions & 12 deletions pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LibPQFactory.java
Expand Up @@ -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;
Expand All @@ -48,6 +51,8 @@
*/
public class LibPQFactory extends WrappedFactory implements HostnameVerifier {

private static final int ALT_DNS_NAME = 2;

LazyKeyManager km = null;
String sslmode;

Expand Down Expand Up @@ -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 (*),
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

Should the exception be logged? (e.g. log.warning)

}

LdapName DN;
try {
DN = new LdapName(serverCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
Expand All @@ -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);
}
}
Expand Up @@ -24,6 +24,7 @@
BinaryStreamTest.class,
CharacterStreamTest.class,
UUIDTest.class,
LibPQFactoryHostNameTest.class,
XmlTest.class
})
public class Jdbc4TestSuite {
Expand Down
@@ -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));
}
}