Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,37 +194,67 @@ static void validateServerNameInCertificate(X509Certificate cert, String hostNam
// more than one name of the same type."
// So, more than one entry of dnsNameType can be present.
// Java docs guarantee that the first entry in the list will be an integer.
// 2 is the sequence no of a dnsName
if ((key != null) && (key instanceof Integer) && ((Integer) key == 2)) {
// As per RFC2459, the DNSName will be in the
// "preferred name syntax" as specified by RFC
// 1034 and the name can be in upper or lower case.
// And no significance is attached to case.
// Java docs guarantee that the second entry in the list
// will be a string for dnsName
if (value != null && value instanceof String) {
dnsNameInSANCert = (String) value;

// Use English locale to avoid Turkish i issues.
// Note that, this conversion was not necessary for
// cert.getSubjectX500Principal().getName("canonical");
// as the above API already does this by default as per documentation.
dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);

isServerNameValidated = validateServerName(dnsNameInSANCert, hostName);
if (isServerNameValidated) {
// Per RFC 5280, GeneralName types: 2 = dNSName, 7 = iPAddress
if ((key != null) && (key instanceof Integer)) {
int sanType = (Integer) key;

if (sanType == 2) {
// As per RFC2459, the DNSName will be in the
// "preferred name syntax" as specified by RFC
// 1034 and the name can be in upper or lower case.
// And no significance is attached to case.
// Java docs guarantee that the second entry in the list
// will be a string for dnsName
if (value != null && value instanceof String) {
dnsNameInSANCert = (String) value;

// Use English locale to avoid Turkish i issues.
// Note that, this conversion was not necessary for
// cert.getSubjectX500Principal().getName("canonical");
// as the above API already does this by default as per documentation.
dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);

isServerNameValidated = validateServerName(dnsNameInSANCert, hostName);
if (isServerNameValidated) {
if (logger.isLoggable(Level.FINER)) {
logger.finer(
logContext + " found a valid name in certificate: " + dnsNameInSANCert);
}
break;
}
}

if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext
+ " the following name in certificate does not match the serverName: " + value);
logger.finer(logContext + " certificate:\n" + cert.toString());
}
} else if (sanType == 7) {
// iPAddress SAN entry - per RFC 5280, IP addresses in SAN are stored
// as octet strings but Java's getSubjectAlternativeNames() returns
// them as String representation of the IP address
if (value != null && value instanceof String) {
String ipInCert = (String) value;

if (logger.isLoggable(Level.FINER)) {
logger.finer(
logContext + " found a valid name in certificate: " + dnsNameInSANCert);
logger.finer(logContext + " found IP address in SAN: " + ipInCert);
}

// IP addresses must match exactly (no wildcard support)
if (ipInCert.equalsIgnoreCase(hostName)) {
isServerNameValidated = true;
if (logger.isLoggable(Level.FINER)) {
logger.finer(
logContext + " IP address in certificate matches: " + ipInCert);
}
break;
}
break;
}
}

if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext
+ " the following name in certificate does not match the serverName: " + value);
logger.finer(logContext + " certificate:\n" + cert.toString());
if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext
+ " the following IP in certificate does not match the serverName: " + value);
}
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import java.lang.reflect.Method;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -271,4 +273,116 @@ public void testSecureCNParsing_preventsHostnameSpoofing() throws Exception {
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(spoofedCert, "attacker.com"));
}

/**
* Helper to create a SAN entry list.
* Per X509Certificate.getSubjectAlternativeNames() spec:
* - First element is an Integer representing the type (2 = dNSName, 7 = iPAddress)
* - Second element is a String for dNSName and iPAddress types
*/
private static List<?> sanEntry(int type, String value) {
return Arrays.asList(Integer.valueOf(type), value);
}

@Test
public void testIPAddressInSAN_IPv4Match() throws Exception {
// Certificate with IPv4 address in SAN (type 7)
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
Collection<List<?>> sans = new ArrayList<>();
sans.add(sanEntry(7, "192.168.1.100"));

X509Certificate cert = mockCert(subject, sans);

// Should succeed when connecting via the IP address in SAN
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.100"));
}

@Test
public void testIPAddressInSAN_IPv4Mismatch() throws Exception {
// Certificate with IPv4 address in SAN (type 7)
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
Collection<List<?>> sans = new ArrayList<>();
sans.add(sanEntry(7, "192.168.1.100"));

X509Certificate cert = mockCert(subject, sans);

// Should fail when connecting via a different IP address
assertThrows(CertificateException.class,
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.101"));
}

@Test
public void testIPAddressInSAN_IPv6Match() throws Exception {
// Certificate with IPv6 address in SAN (type 7)
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
Collection<List<?>> sans = new ArrayList<>();
sans.add(sanEntry(7, "2001:db8::1"));

X509Certificate cert = mockCert(subject, sans);

// Should succeed when connecting via the IPv6 address in SAN
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "2001:db8::1"));
}

@Test
public void testIPAddressInSAN_MultipleEntries() throws Exception {
// Certificate with both DNS name and IP address in SAN
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
Collection<List<?>> sans = new ArrayList<>();
sans.add(sanEntry(2, "sqlserver.example.com"));
sans.add(sanEntry(7, "10.0.0.50"));
sans.add(sanEntry(7, "192.168.1.100"));

X509Certificate cert = mockCert(subject, sans);

// Should succeed for DNS name
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "sqlserver.example.com"));

// Should succeed for first IP
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "10.0.0.50"));

// Should succeed for second IP
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.100"));

// Should fail for unlisted IP
assertThrows(CertificateException.class,
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "172.16.0.1"));
}

@Test
public void testIPAddressInSAN_CaseInsensitive() throws Exception {
// Certificate with IPv6 address in SAN - test case insensitivity
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
Collection<List<?>> sans = new ArrayList<>();
sans.add(sanEntry(7, "2001:DB8::1"));

X509Certificate cert = mockCert(subject, sans);

// Should succeed with different case (IPv6 addresses are case-insensitive)
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "2001:db8::1"));
}

@Test
public void testIPAddressInSAN_FallbackToCN() throws Exception {
// Certificate with only DNS SAN, no IP - should still work via CN fallback
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
Collection<List<?>> sans = new ArrayList<>();
sans.add(sanEntry(2, "sqlserver.example.com"));

X509Certificate cert = mockCert(subject, sans);

// Should succeed for hostname matching CN/SAN
assertDoesNotThrow(
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "sqlserver.example.com"));

// Should fail for IP when no IP SAN is present
assertThrows(CertificateException.class,
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.100"));
}
}