Skip to content

Commit 7cbdbc7

Browse files
committed
fix: Support IP address validation in certificate SAN (#2529)
Fix SSL certificate validation to check for IP address entries (type 7) in Subject Alternative Name extension, in addition to DNS names (type 2). This allows connections via IP address when the certificate's SAN contains that IP, without requiring the hostNameInCertificate workaround.
1 parent 303aeb9 commit 7cbdbc7

File tree

2 files changed

+171
-27
lines changed

2 files changed

+171
-27
lines changed

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -194,37 +194,67 @@ static void validateServerNameInCertificate(X509Certificate cert, String hostNam
194194
// more than one name of the same type."
195195
// So, more than one entry of dnsNameType can be present.
196196
// Java docs guarantee that the first entry in the list will be an integer.
197-
// 2 is the sequence no of a dnsName
198-
if ((key != null) && (key instanceof Integer) && ((Integer) key == 2)) {
199-
// As per RFC2459, the DNSName will be in the
200-
// "preferred name syntax" as specified by RFC
201-
// 1034 and the name can be in upper or lower case.
202-
// And no significance is attached to case.
203-
// Java docs guarantee that the second entry in the list
204-
// will be a string for dnsName
205-
if (value != null && value instanceof String) {
206-
dnsNameInSANCert = (String) value;
207-
208-
// Use English locale to avoid Turkish i issues.
209-
// Note that, this conversion was not necessary for
210-
// cert.getSubjectX500Principal().getName("canonical");
211-
// as the above API already does this by default as per documentation.
212-
dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);
213-
214-
isServerNameValidated = validateServerName(dnsNameInSANCert, hostName);
215-
if (isServerNameValidated) {
197+
// Per RFC 5280, GeneralName types: 2 = dNSName, 7 = iPAddress
198+
if ((key != null) && (key instanceof Integer)) {
199+
int sanType = (Integer) key;
200+
201+
if (sanType == 2) {
202+
// As per RFC2459, the DNSName will be in the
203+
// "preferred name syntax" as specified by RFC
204+
// 1034 and the name can be in upper or lower case.
205+
// And no significance is attached to case.
206+
// Java docs guarantee that the second entry in the list
207+
// will be a string for dnsName
208+
if (value != null && value instanceof String) {
209+
dnsNameInSANCert = (String) value;
210+
211+
// Use English locale to avoid Turkish i issues.
212+
// Note that, this conversion was not necessary for
213+
// cert.getSubjectX500Principal().getName("canonical");
214+
// as the above API already does this by default as per documentation.
215+
dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);
216+
217+
isServerNameValidated = validateServerName(dnsNameInSANCert, hostName);
218+
if (isServerNameValidated) {
219+
if (logger.isLoggable(Level.FINER)) {
220+
logger.finer(
221+
logContext + " found a valid name in certificate: " + dnsNameInSANCert);
222+
}
223+
break;
224+
}
225+
}
226+
227+
if (logger.isLoggable(Level.FINER)) {
228+
logger.finer(logContext
229+
+ " the following name in certificate does not match the serverName: " + value);
230+
logger.finer(logContext + " certificate:\n" + cert.toString());
231+
}
232+
} else if (sanType == 7) {
233+
// iPAddress SAN entry - per RFC 5280, IP addresses in SAN are stored
234+
// as octet strings but Java's getSubjectAlternativeNames() returns
235+
// them as String representation of the IP address
236+
if (value != null && value instanceof String) {
237+
String ipInCert = (String) value;
238+
216239
if (logger.isLoggable(Level.FINER)) {
217-
logger.finer(
218-
logContext + " found a valid name in certificate: " + dnsNameInSANCert);
240+
logger.finer(logContext + " found IP address in SAN: " + ipInCert);
241+
}
242+
243+
// IP addresses must match exactly (no wildcard support)
244+
if (ipInCert.equalsIgnoreCase(hostName)) {
245+
isServerNameValidated = true;
246+
if (logger.isLoggable(Level.FINER)) {
247+
logger.finer(
248+
logContext + " IP address in certificate matches: " + ipInCert);
249+
}
250+
break;
219251
}
220-
break;
221252
}
222-
}
223253

224-
if (logger.isLoggable(Level.FINER)) {
225-
logger.finer(logContext
226-
+ " the following name in certificate does not match the serverName: " + value);
227-
logger.finer(logContext + " certificate:\n" + cert.toString());
254+
if (logger.isLoggable(Level.FINER)) {
255+
logger.finer(logContext
256+
+ " the following IP in certificate does not match the serverName: " + value);
257+
}
228258
}
229259
}
230260
} else {

src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidationTest.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import java.lang.reflect.Method;
1313
import java.security.cert.CertificateException;
1414
import java.security.cert.X509Certificate;
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
1517
import java.util.Collection;
1618
import java.util.List;
1719
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -271,4 +273,116 @@ public void testSecureCNParsing_preventsHostnameSpoofing() throws Exception {
271273
assertDoesNotThrow(
272274
() -> SQLServerCertificateUtils.validateServerNameInCertificate(spoofedCert, "attacker.com"));
273275
}
276+
277+
/**
278+
* Helper to create a SAN entry list.
279+
* Per X509Certificate.getSubjectAlternativeNames() spec:
280+
* - First element is an Integer representing the type (2 = dNSName, 7 = iPAddress)
281+
* - Second element is a String for dNSName and iPAddress types
282+
*/
283+
private static List<?> sanEntry(int type, String value) {
284+
return Arrays.asList(Integer.valueOf(type), value);
285+
}
286+
287+
@Test
288+
public void testIPAddressInSAN_IPv4Match() throws Exception {
289+
// Certificate with IPv4 address in SAN (type 7)
290+
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
291+
Collection<List<?>> sans = new ArrayList<>();
292+
sans.add(sanEntry(7, "192.168.1.100"));
293+
294+
X509Certificate cert = mockCert(subject, sans);
295+
296+
// Should succeed when connecting via the IP address in SAN
297+
assertDoesNotThrow(
298+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.100"));
299+
}
300+
301+
@Test
302+
public void testIPAddressInSAN_IPv4Mismatch() throws Exception {
303+
// Certificate with IPv4 address in SAN (type 7)
304+
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
305+
Collection<List<?>> sans = new ArrayList<>();
306+
sans.add(sanEntry(7, "192.168.1.100"));
307+
308+
X509Certificate cert = mockCert(subject, sans);
309+
310+
// Should fail when connecting via a different IP address
311+
assertThrows(CertificateException.class,
312+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.101"));
313+
}
314+
315+
@Test
316+
public void testIPAddressInSAN_IPv6Match() throws Exception {
317+
// Certificate with IPv6 address in SAN (type 7)
318+
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
319+
Collection<List<?>> sans = new ArrayList<>();
320+
sans.add(sanEntry(7, "2001:db8::1"));
321+
322+
X509Certificate cert = mockCert(subject, sans);
323+
324+
// Should succeed when connecting via the IPv6 address in SAN
325+
assertDoesNotThrow(
326+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "2001:db8::1"));
327+
}
328+
329+
@Test
330+
public void testIPAddressInSAN_MultipleEntries() throws Exception {
331+
// Certificate with both DNS name and IP address in SAN
332+
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
333+
Collection<List<?>> sans = new ArrayList<>();
334+
sans.add(sanEntry(2, "sqlserver.example.com"));
335+
sans.add(sanEntry(7, "10.0.0.50"));
336+
sans.add(sanEntry(7, "192.168.1.100"));
337+
338+
X509Certificate cert = mockCert(subject, sans);
339+
340+
// Should succeed for DNS name
341+
assertDoesNotThrow(
342+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "sqlserver.example.com"));
343+
344+
// Should succeed for first IP
345+
assertDoesNotThrow(
346+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "10.0.0.50"));
347+
348+
// Should succeed for second IP
349+
assertDoesNotThrow(
350+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.100"));
351+
352+
// Should fail for unlisted IP
353+
assertThrows(CertificateException.class,
354+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "172.16.0.1"));
355+
}
356+
357+
@Test
358+
public void testIPAddressInSAN_CaseInsensitive() throws Exception {
359+
// Certificate with IPv6 address in SAN - test case insensitivity
360+
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
361+
Collection<List<?>> sans = new ArrayList<>();
362+
sans.add(sanEntry(7, "2001:DB8::1"));
363+
364+
X509Certificate cert = mockCert(subject, sans);
365+
366+
// Should succeed with different case (IPv6 addresses are case-insensitive)
367+
assertDoesNotThrow(
368+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "2001:db8::1"));
369+
}
370+
371+
@Test
372+
public void testIPAddressInSAN_FallbackToCN() throws Exception {
373+
// Certificate with only DNS SAN, no IP - should still work via CN fallback
374+
X500Principal subject = new X500Principal("CN=sqlserver.example.com");
375+
Collection<List<?>> sans = new ArrayList<>();
376+
sans.add(sanEntry(2, "sqlserver.example.com"));
377+
378+
X509Certificate cert = mockCert(subject, sans);
379+
380+
// Should succeed for hostname matching CN/SAN
381+
assertDoesNotThrow(
382+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "sqlserver.example.com"));
383+
384+
// Should fail for IP when no IP SAN is present
385+
assertThrows(CertificateException.class,
386+
() -> SQLServerCertificateUtils.validateServerNameInCertificate(cert, "192.168.1.100"));
387+
}
274388
}

0 commit comments

Comments
 (0)