org.postgresql.ssl.PGjdbcHostnameVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of postgresql Show documentation
Show all versions of postgresql Show documentation
PostgreSQL JDBC Driver JDBC4
/*
* Copyright (c) 2018, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.ssl;
import org.postgresql.util.GT;
import java.net.IDN;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
public class PGjdbcHostnameVerifier implements HostnameVerifier {
private static final Logger LOGGER = Logger.getLogger(PGjdbcHostnameVerifier.class.getName());
public static final PGjdbcHostnameVerifier INSTANCE = new PGjdbcHostnameVerifier();
private static final int TYPE_DNS_NAME = 2;
private static final int TYPE_IP_ADDRESS = 7;
public static Comparator HOSTNAME_PATTERN_COMPARATOR = new Comparator() {
private int countChars(String value, char ch) {
int count = 0;
int pos = -1;
while (true) {
pos = value.indexOf(ch, pos + 1);
if (pos == -1) {
break;
}
count++;
}
return count;
}
@Override
public int compare(String o1, String o2) {
// The more the dots the better: a.b.c.postgresql.org is more specific than postgresql.org
int d1 = countChars(o1, '.');
int d2 = countChars(o2, '.');
if (d1 != d2) {
return d1 > d2 ? 1 : -1;
}
// The less the stars the better: postgresql.org is more specific than *.*.postgresql.org
int s1 = countChars(o1, '*');
int s2 = countChars(o2, '*');
if (s1 != s2) {
return s1 < s2 ? 1 : -1;
}
// The longer the better: postgresql.org is more specific than sql.org
int l1 = o1.length();
int l2 = o2.length();
if (l1 != l2) {
return l1 > l2 ? 1 : -1;
}
return 0;
}
};
@Override
public boolean verify(String hostname, SSLSession session) {
X509Certificate[] peerCerts;
try {
peerCerts = (X509Certificate[]) session.getPeerCertificates();
} catch (SSLPeerUnverifiedException e) {
LOGGER.log(Level.SEVERE,
GT.tr("Unable to parse X509Certificate for hostname {0}", hostname), e);
return false;
}
if (peerCerts == null || peerCerts.length == 0) {
LOGGER.log(Level.SEVERE,
GT.tr("No certificates found for hostname {0}", hostname));
return false;
}
String canonicalHostname;
if (hostname.startsWith("[") && hostname.endsWith("]")) {
// IPv6 address like [2001:db8:0:1:1:1:1:1]
canonicalHostname = hostname.substring(1, hostname.length() - 1);
} else {
// This converts unicode domain name to ASCII
try {
canonicalHostname = IDN.toASCII(hostname);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "Canonical host name for {0} is {1}",
new Object[]{hostname, canonicalHostname});
}
} catch (IllegalArgumentException e) {
// e.g. hostname is invalid
LOGGER.log(Level.SEVERE,
GT.tr("Hostname {0} is invalid", hostname), e);
return false;
}
}
X509Certificate serverCert = peerCerts[0];
// Check for Subject Alternative Names (see RFC 6125)
Collection> subjectAltNames;
try {
subjectAltNames = serverCert.getSubjectAlternativeNames();
if (subjectAltNames == null) {
subjectAltNames = Collections.emptyList();
}
} catch (CertificateParsingException e) {
LOGGER.log(Level.SEVERE,
GT.tr("Unable to parse certificates for hostname {0}", hostname), e);
return false;
}
boolean anyDnsSan = false;
/*
* Each item in the SAN collection is a 2-element list.
* See {@link X509Certificate#getSubjectAlternativeNames}
* The first element in each list is a number indicating the type of entry.
*/
for (List> sanItem : subjectAltNames) {
if (sanItem.size() != 2) {
continue;
}
Integer sanType = (Integer) sanItem.get(0);
if (sanType == null) {
// just in case
continue;
}
if (sanType != TYPE_IP_ADDRESS && sanType != TYPE_DNS_NAME) {
continue;
}
String san = (String) sanItem.get(1);
if (sanType == TYPE_IP_ADDRESS && san.startsWith("*")) {
// Wildcards should not be present in the IP Address field
continue;
}
anyDnsSan |= sanType == TYPE_DNS_NAME;
if (verifyHostName(canonicalHostname, san)) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.SEVERE,
GT.tr("Server name validation pass for {0}, subjectAltName {1}", hostname, san));
}
return true;
}
}
if (anyDnsSan) {
/*
* RFC2818, section 3.1 (I bet you won't recheck :)
* If a subjectAltName extension of type dNSName is present, that MUST
* be used as the identity. Otherwise, the (most specific) Common Name
* field in the Subject field of the certificate MUST be used. Although
* the use of the Common Name is existing practice, it is deprecated and
* Certification Authorities are encouraged to use the dNSName instead.
*/
LOGGER.log(Level.SEVERE,
GT.tr("Server name validation failed: certificate for host {0} dNSName entries subjectAltName,"
+ " but none of them match. Assuming server name validation failed", hostname));
return false;
}
// Last attempt: no DNS Subject Alternative Name entries detected, try common name
LdapName DN;
try {
DN = new LdapName(serverCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
} catch (InvalidNameException e) {
LOGGER.log(Level.SEVERE,
GT.tr("Server name validation failed: unable to extract common name"
+ " from X509Certificate for hostname {0}", hostname), e);
return false;
}
List commonNames = new ArrayList(1);
for (Rdn rdn : DN.getRdns()) {
if ("CN".equals(rdn.getType())) {
commonNames.add((String) rdn.getValue());
}
}
if (commonNames.isEmpty()) {
LOGGER.log(Level.SEVERE,
GT.tr("Server name validation failed: certificate for hostname {0} has no DNS subjectAltNames,"
+ " and it CommonName is missing as well",
hostname));
return false;
}
if (commonNames.size() > 1) {
/*
* RFC2818, section 3.1
* If a subjectAltName extension of type dNSName is present, that MUST
* be used as the identity. Otherwise, the (most specific) Common Name
* field in the Subject field of the certificate MUST be used
*
* The sort is from less specific to most specific.
*/
Collections.sort(commonNames, HOSTNAME_PATTERN_COMPARATOR);
}
String commonName = commonNames.get(commonNames.size() - 1);
boolean result = verifyHostName(canonicalHostname, commonName);
if (!result) {
LOGGER.log(Level.SEVERE,
GT.tr("Server name validation failed: hostname {0} does not match common name {1}",
hostname, commonName));
}
return result;
}
public boolean verifyHostName(String hostname, String pattern) {
if (hostname == null || pattern == null) {
return false;
}
int lastStar = pattern.lastIndexOf('*');
if (lastStar == -1) {
// No wildcard => just compare hostnames
return hostname.equalsIgnoreCase(pattern);
}
if (lastStar > 0) {
// Wildcards like foo*.com are not supported yet
return false;
}
if (pattern.indexOf('.') == -1) {
// Wildcard certificates should contain at least one dot
return false;
}
// pattern starts with *, so hostname should be at least (pattern.length-1) long
if (hostname.length() < pattern.length() - 1) {
return false;
}
// Use case insensitive comparison
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);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy