F-4852: match IP address peer identities against iPAddress SANs only

pull/382/head
Chris Conlon 2026-07-17 14:22:33 -06:00
parent 536069f33f
commit db65aac73f
10 changed files with 502 additions and 17 deletions

View File

@ -1099,10 +1099,9 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1check_1host
/* peerNamePtr not used */
ret = wolfSSL_X509_check_host(x509, hostname,
XSTRLEN(hostname), (unsigned int)flags, NULL);
(*jenv)->ReleaseStringUTFChars(jenv, chk, hostname);
}
(*jenv)->ReleaseStringUTFChars(jenv, chk, hostname);
return (jint)ret;
#else
@ -1116,6 +1115,39 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1check_1host
#endif
}
JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1check_1ip_1asc
(JNIEnv* jenv, jclass jcl, jlong x509Ptr, jstring ipasc, jlong flags)
{
#ifndef NO_ASN
int ret = WOLFSSL_FAILURE;
const char* ip = NULL;
WOLFSSL_X509* x509 = (WOLFSSL_X509*)(uintptr_t)x509Ptr;
(void)jcl;
if (jenv == NULL || ipasc == NULL) {
return WOLFSSL_FAILURE;
}
/* Matches only iPAddress SAN entries. Does not fall back to Subject CN or
a dNSName, required for IP address ref identities (RFC 6125/2818). */
ip = (*jenv)->GetStringUTFChars(jenv, ipasc, 0);
if (ip != NULL) {
ret = wolfSSL_X509_check_ip_asc(x509, ip, (unsigned int)flags);
(*jenv)->ReleaseStringUTFChars(jenv, ipasc, ip);
}
return (jint)ret;
#else
(void)jenv;
(void)jcl;
(void)x509Ptr;
(void)ipasc;
(void)flags;
return (jint)NOT_COMPILED_IN;
#endif
}
JNIEXPORT jbyteArray JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1get_1der
(JNIEnv* jenv, jclass jcl, jlong x509Ptr)
{

View File

@ -285,6 +285,14 @@ JNIEXPORT jlong JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1load_1certific
JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1check_1host
(JNIEnv *, jclass, jlong, jstring, jlong, jlong);
/*
* Class: com_wolfssl_WolfSSLCertificate
* Method: X509_check_ip_asc
* Signature: (JLjava/lang/String;J)I
*/
JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLCertificate_X509_1check_1ip_1asc
(JNIEnv *, jclass, jlong, jstring, jlong);
/*
* Class: com_wolfssl_WolfSSLCertificate
* Method: X509_get_ext_d2i_name_constraints

View File

@ -114,6 +114,7 @@ public class WolfSSLCertificate implements Serializable {
static native long X509_load_certificate_file(String path, int format);
static native int X509_check_host(long x509, String chk, long flags,
long peerName);
static native int X509_check_ip_asc(long x509, String ipasc, long flags);
static native long X509_get_ext_d2i_name_constraints(long x509);
/* native functions used for X509v3 certificate generation */
@ -2142,6 +2143,38 @@ public class WolfSSLCertificate implements Serializable {
}
}
/**
* Checks that the given IP address literal matches an iPAddress entry in
* this certificate's SubjectAltName extension.
*
* Unlike {@link #checkHost(String)}, this only matches iPAddress SAN
* entries. It never falls back to the Subject CommonName or a dNSName
* SAN, as required for IP address reference identities by RFC 6125 and
* RFC 2818.
*
* @param ipAddress IP address literal to check certificate against, in
* text form (ex: "192.0.2.1" or "::1")
*
* @return WolfSSL.SSL_SUCCESS on successful iPAddress match,
* WolfSSL.SSL_FAILURE on invalid match or error, or
* WolfSSL.NOT_COMPILED_IN if native wolfSSL has been compiled
* with NO_ASN defined and native API is not available.
*
* @throws IllegalStateException if WolfSSLCertificate has been freed.
*/
public int checkIpAddress(String ipAddress) throws IllegalStateException {
confirmObjectIsActive();
synchronized (x509Lock) {
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
WolfSSLDebug.INFO, this.x509Ptr,
() -> "entering checkIpAddress(" + ipAddress + ")");
return X509_check_ip_asc(this.x509Ptr, ipAddress, 0);
}
}
/**
* Returns an immutable Collection of subject alternative names from this
* certificate's SubjectAltName extension.

View File

@ -288,22 +288,23 @@ public class WolfSSLInternalVerifyCb implements WolfSSLVerifyCallback {
return 0;
}
/* Verify hostname against certificate SAN/CN using native
* wolfSSL X509_check_host() */
/* Verify hostname against certificate SAN/CN using native wolfSSL.
* IP address literals are matched against iPAddress SANs only, never
* CN or a dNSName, per RFC 6125 / RFC 2818. */
final String tmpHost = peerHost;
WolfSSLCertificate wCert = null;
try {
wCert = new WolfSSLCertificate(peer.getEncoded());
int ret = wCert.checkHost(peerHost);
int ret = WolfSSLUtil.verifyHostnameOrIp(wCert, peerHost, 0);
if (ret == WolfSSL.SSL_SUCCESS) {
WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO,
() -> "Provider-level hostname verification " +
"passed for: " + tmpHost);
() -> "Provider-level hostname verification passed for: " +
tmpHost);
return 1;
} else {
WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO,
() -> "Provider-level hostname verification " +
"FAILED for: " + tmpHost);
() -> "Provider-level hostname verification FAILED for: " +
tmpHost);
this.verifyException = new CertificateException(
"Hostname verification failed for: " + tmpHost);
return 0;

View File

@ -751,7 +751,7 @@ public final class WolfSSLTrustX509 extends X509ExtendedTrustManager {
() -> "trying hostname verification against SNI: " +
tmpSniName);
ret = peerCert.checkHost(sniHostName);
ret = WolfSSLUtil.verifyHostnameOrIp(peerCert, sniHostName, 0);
if (ret == WolfSSL.SSL_SUCCESS) {
/* Hostname successfully verified against SNI name */
WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO,
@ -776,13 +776,12 @@ public final class WolfSSLTrustX509 extends X509ExtendedTrustManager {
() -> "trying hostname verification against peer host: " +
peerHost);
if (type == HOSTNAME_TYPE_LDAPS) {
/* LDAPS requires wildcard left-most matching only */
ret = peerCert.checkHost(peerHost,
WolfSSL.WOLFSSL_LEFT_MOST_WILDCARD_ONLY);
} else {
ret = peerCert.checkHost(peerHost);
}
/* LDAPS requires wildcard left-most matching only. IP literals
* are matched against iPAddress SANs only, handled inside
* verifyHostnameOrIp(). */
long flags = (type == HOSTNAME_TYPE_LDAPS) ?
WolfSSL.WOLFSSL_LEFT_MOST_WILDCARD_ONLY : 0;
ret = WolfSSLUtil.verifyHostnameOrIp(peerCert, peerHost, flags);
if (ret == WolfSSL.SSL_SUCCESS) {
/* Hostname successfully verified against peer host name */
WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO,

View File

@ -35,6 +35,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import com.wolfssl.WolfSSL;
import com.wolfssl.WolfSSLCertificate;
import com.wolfssl.WolfSSLDebug;
import com.wolfssl.WolfSSLException;
@ -782,5 +783,134 @@ public class WolfSSLUtil {
return ks;
}
/**
* Verify a reference identity (host name or IP address literal) against a
* peer certificate.
*
* IP address literals are matched only against iPAddress SAN entries, via
* wolfSSL_X509_check_ip_asc(). They are never matched against the Subject
* CommonName or a dNSName SAN, as required by RFC 6125 and RFC 2818. Host
* names use wolfSSL_X509_check_host(), which matches dNSName SANs with a
* CN fallback.
*
* @param peerCert peer certificate to check against
* @param name reference identity (host name or IP literal) to verify
* @param flags checkHost flags, applied only to host name matching
*
* @return WolfSSL.SSL_SUCCESS on match, otherwise WolfSSL.SSL_FAILURE
*/
protected static int verifyHostnameOrIp(WolfSSLCertificate peerCert,
String name, long flags) {
if (peerCert == null || name == null) {
return WolfSSL.SSL_FAILURE;
}
if (isIpAddress(name)) {
/* Match against iPAddress SANs using the bracket-free form. A
* cert stores IPv6 addresses without the "[...]" brackets that
* can wrap an IPv6 literal in a URL or host:port string. */
return peerCert.checkIpAddress(stripIpv6Brackets(name));
}
return peerCert.checkHost(name, flags);
}
/**
* Return true if the given string is an IPv4 or IPv6 address literal.
*
* Does not perform DNS resolution. Used to route IP reference identities
* to iPAddress SAN matching, since a host name and an IP literal are
* verified against different certificate fields (RFC 6125).
*
* @param host string to test, may be null
*
* @return true if host is an IP address literal, otherwise false
*/
protected static boolean isIpAddress(String host) {
String h;
if (host == null || host.isEmpty()) {
return false;
}
h = stripIpv6Brackets(host);
/* A DNS host name never contains ':', so any ':' means IPv6 literal. */
if (h.indexOf(':') != -1) {
return true;
}
return isIPv4Literal(h);
}
/**
* Strip surrounding "[...]" brackets from an IPv6 address literal, ex:
* "[::1]" becomes "::1". Returns the input unchanged if not bracketed.
* A certificate stores IPv6 iPAddress SANs without brackets, so they must
* be removed before matching an IP reference identity against the cert.
*
* @param host string to strip, may be null
*
* @return host with any surrounding IPv6 brackets removed
*/
private static String stripIpv6Brackets(String host) {
boolean bracketed;
if (host == null || host.length() < 2) {
return host;
}
bracketed = (host.charAt(0) == '[') &&
(host.charAt(host.length() - 1) == ']');
if (bracketed) {
return host.substring(1, host.length() - 1);
}
return host;
}
/**
* Return true if the given string is a strict IPv4 dotted-quad literal,
* meaning four octets each in the range 0 to 255.
*
* @param s string to test
*
* @return true if s is an IPv4 literal, otherwise false
*/
private static boolean isIPv4Literal(String s) {
int octets = 0;
int start = 0;
int len = s.length();
for (int i = 0; i <= len; i++) {
if (i == len || s.charAt(i) == '.') {
int fieldLen = i - start;
int val = 0;
if (fieldLen < 1 || fieldLen > 3) {
return false;
}
for (int j = start; j < i; j++) {
char c = s.charAt(j);
if (c < '0' || c > '9') {
return false;
}
val = (val * 10) + (c - '0');
}
if (val > 255) {
return false;
}
octets++;
start = i + 1;
}
}
return (octets == 4);
}
}

View File

@ -41,6 +41,7 @@ import org.junit.runners.Suite;
WolfSSLServiceLoaderTest.class,
WolfSSLParametersPskTest.class,
WolfSSLNamedGroupsTest.class,
WolfSSLUtilTest.class,
WolfSSLPQCKeyExchangeTest.class,
WolfSSLPQCAuthenticationTest.class,
WolfSSLPQCAuthKeyStoreTest.class,

View File

@ -2028,6 +2028,16 @@ public class WolfSSLSocketTest {
* matched the certificate IP SAN, so this subcase confirms the
* hostname, not the resolved IP, is what verification runs against. */
connectHttpsAndCheckVerification("wolfssl.invalid", false);
/* IP literal matching the certificate iPAddress SAN (IP:127.0.0.1)
* must verify. Confirms IP reference identities are still matched
* against iPAddress SANs after routing them away from checkHost. */
connectHttpsAndCheckVerification("127.0.0.1", true);
/* IP literal not present in the certificate iPAddress SAN must fail.
* It must not match the DNS SAN (example.com) or the Subject CN,
* which is what an IP identity is forbidden from doing (RFC 6125). */
connectHttpsAndCheckVerification("127.0.0.2", false);
}
@Test

View File

@ -0,0 +1,187 @@
/* WolfSSLUtilTest.java
*
* Copyright (C) 2006-2026 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
package com.wolfssl.provider.jsse.test;
import org.junit.Test;
import org.junit.Rule;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.rules.TestRule;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.Date;
import java.time.Instant;
import java.time.Duration;
import com.wolfssl.WolfSSL;
import com.wolfssl.WolfSSLCertificate;
import com.wolfssl.WolfSSLX509Name;
import com.wolfssl.provider.jsse.WolfSSLProvider;
import com.wolfssl.test.TimedTestWatcher;
import com.wolfssl.test.WolfSSLTestCommon;
/**
* Tests for com.wolfssl.provider.jsse.WolfSSLUtil helper methods.
*/
public class WolfSSLUtilTest {
private static String cliKeyPubDer = "examples/certs/client-keyPub.der";
private static String cliKeyDer = "examples/certs/client-key.der";
@Rule
public TestRule testWatcher = TimedTestWatcher.create();
@BeforeClass
public static void setup() throws Exception {
System.out.println("WolfSSLUtil Class");
/* Instantiate wolfJSSE, loads native wolfSSL library */
new WolfSSLProvider();
cliKeyPubDer = WolfSSLTestCommon.getPath(cliKeyPubDer);
cliKeyDer = WolfSSLTestCommon.getPath(cliKeyDer);
}
/* Invoke the package-private static WolfSSLUtil.isIpAddress() via
* reflection, since this test lives in a different package. */
private static boolean isIpAddress(String host) throws Exception {
Method m = com.wolfssl.provider.jsse.WolfSSLUtil.class
.getDeclaredMethod("isIpAddress", String.class);
m.setAccessible(true);
return (Boolean) m.invoke(null, host);
}
private static void check(String host, boolean expected) throws Exception {
assertEquals("isIpAddress(\"" + host + "\")", expected,
isIpAddress(host));
}
/* isIpAddress() decides whether a reference identity is matched against
* iPAddress SANs (IP literal) or dNSName SANs and the CN (host name). */
@Test
public void test_isIpAddress() throws Exception {
/* IPv4 literals */
check("192.0.2.1", true);
check("127.0.0.1", true);
check("255.255.255.255", true);
check("0.0.0.0", true);
/* IPv6 literals, plain and bracketed */
check("::1", true);
check("2001:db8::1", true);
check("[::1]", true);
check("fe80::1", true);
/* Host names, must not be treated as IP literals */
check("example.com", false);
check("wolfssl.com", false);
check("localhost", false);
check("host-1.example.org", false);
/* Not valid IPv4 dotted-quads, treated as host names */
check("192.0.2", false);
check("192.0.2.1.5", false);
check("256.0.0.1", false);
check("192.0.2.x", false);
check("192.0.2.", false);
/* null and empty */
check(null, false);
check("", false);
}
/* Invoke the package-private static WolfSSLUtil.verifyHostnameOrIp() via
* reflection. */
private static int verifyHostnameOrIp(WolfSSLCertificate cert, String name,
long flags) throws Exception {
Method m = com.wolfssl.provider.jsse.WolfSSLUtil.class
.getDeclaredMethod("verifyHostnameOrIp",
WolfSSLCertificate.class, String.class, long.class);
m.setAccessible(true);
return (Integer) m.invoke(null, cert, name, flags);
}
/* Generate a certificate with a single IPv6 iPAddress SAN. */
private static byte[] genIpv6SanCert(String sanIp) throws Exception {
WolfSSLCertificate x509 = new WolfSSLCertificate();
WolfSSLX509Name name = new WolfSSLX509Name();
Instant now = Instant.now();
x509.setNotBefore(Date.from(now));
x509.setNotAfter(Date.from(now.plus(Duration.ofDays(365))));
x509.setSerialNumber(BigInteger.valueOf(4321));
name.setCountryName("US");
name.setOrganizationName("wolfSSL Inc.");
name.setCommonName("wolfssl.com");
x509.setSubjectName(name);
x509.setPublicKey(cliKeyPubDer, WolfSSL.RSAk,
WolfSSL.SSL_FILETYPE_ASN1);
x509.addAltName(sanIp, WolfSSL.ASN_IP_TYPE);
x509.signCert(cliKeyDer, WolfSSL.RSAk,
WolfSSL.SSL_FILETYPE_ASN1, "SHA256");
byte[] der = x509.getDer();
name.free();
x509.free();
return der;
}
/* verifyHostnameOrIp() must strip surrounding IPv6 brackets before
* matching, so a bracketed literal like "[::1]" still matches a cert whose
* iPAddress SAN stores the address without brackets. Native wolfSSL
* normalizes IPv6 text forms internally, so only the brackets need
* removing at the Java layer. */
@Test
public void test_verifyHostnameOrIp_ipv6Brackets() throws Exception {
Assume.assumeTrue(WolfSSL.FileSystemEnabled());
/* Generating an IPv6 iPAddress SAN via wolfSSL_X509_add_altname()
* requires wolfSSL 5.9.2 or later. The bracket-stripping fix itself is
* version independent, but this test's cert generation is not. */
Assume.assumeTrue(WolfSSL.getLibVersionHex() >= 0x05009002L);
byte[] der = genIpv6SanCert("::1");
WolfSSLCertificate cert = new WolfSSLCertificate(der);
try {
assertEquals("bracketed IPv6 must match iPAddress SAN",
WolfSSL.SSL_SUCCESS, verifyHostnameOrIp(cert, "[::1]", 0));
assertEquals("bare IPv6 must match iPAddress SAN",
WolfSSL.SSL_SUCCESS, verifyHostnameOrIp(cert, "::1", 0));
assertEquals("non-matching IPv6 must be rejected",
WolfSSL.SSL_FAILURE,
verifyHostnameOrIp(cert, "[2001:db8::1]", 0));
} finally {
cert.free();
}
}
}

View File

@ -797,6 +797,90 @@ public class WolfSSLCertificateTest {
}
}
/* Generate a self-signed cert with the given CN and an optional single
* SubjectAltName entry, return its DER encoding. */
private byte[] genIpTestCertDer(String cn, String sanValue, int sanType)
throws WolfSSLException, WolfSSLJNIException, IOException {
WolfSSLCertificate x509 = new WolfSSLCertificate();
WolfSSLX509Name name = new WolfSSLX509Name();
Instant now = Instant.now();
x509.setNotBefore(Date.from(now));
x509.setNotAfter(Date.from(now.plus(Duration.ofDays(365))));
x509.setSerialNumber(BigInteger.valueOf(1122));
name.setCountryName("US");
name.setOrganizationName("wolfSSL Inc.");
name.setCommonName(cn);
x509.setSubjectName(name);
x509.setPublicKey(cliKeyPubDer, WolfSSL.RSAk,
WolfSSL.SSL_FILETYPE_ASN1);
if (sanValue != null) {
x509.addAltName(sanValue, sanType);
}
x509.signCert(cliKeyDer, WolfSSL.RSAk,
WolfSSL.SSL_FILETYPE_ASN1, "SHA256");
byte[] der = x509.getDer();
name.free();
x509.free();
return der;
}
/* An IP address reference identity must be matched only against an
* iPAddress SAN, never the Subject CN or a dNSName SAN (RFC 6125 /
* RFC 2818). checkIpAddress() must reject a cert whose only "match" is a
* CN equal to the IP or a dNSName SAN holding the IP as text, and accept
* a cert with a real iPAddress SAN. wolfSSL_X509_check_host() would accept
* the first two by CN or dNSName fallback, which is why IP identities use
* checkIpAddress() instead. */
@Test
public void test_checkIpAddress()
throws WolfSSLException, WolfSSLJNIException, IOException {
Assume.assumeTrue(WolfSSL.FileSystemEnabled());
/* The strict "IP identity never falls back to the Subject CN"
* behavior that checkIpAddress() relies on was added to native
* wolfSSL_X509_check_ip_asc() in wolfSSL 5.9.2. On older versions a
* CN=IP cert with no SAN still matches by CN. Cross-version IP
* matching is covered by the endpoint-identification test. */
Assume.assumeTrue(WolfSSL.getLibVersionHex() >= 0x05009002L);
final String ip = "192.0.2.1";
/* Case A: CN = IP, no SAN. Must be rejected (no CN fallback). */
byte[] derA = genIpTestCertDer(ip, null, 0);
WolfSSLCertificate certA = new WolfSSLCertificate(derA);
assertEquals("CN=IP with no SAN must not match by IP",
WolfSSL.SSL_FAILURE, certA.checkIpAddress(ip));
certA.free();
/* Case B: dNSName SAN = IP text. Must be rejected (a dNSName is not
* an iPAddress SAN). */
byte[] derB = genIpTestCertDer("wolfssl.com", ip, WolfSSL.ASN_DNS_TYPE);
WolfSSLCertificate certB = new WolfSSLCertificate(derB);
assertEquals("dNSName SAN holding an IP string must not match by IP",
WolfSSL.SSL_FAILURE, certB.checkIpAddress(ip));
certB.free();
/* Case C: iPAddress SAN = IP. Must be accepted, and a different IP
* must not match. */
byte[] derC = genIpTestCertDer("wolfssl.com", ip, WolfSSL.ASN_IP_TYPE);
WolfSSLCertificate certC = new WolfSSLCertificate(derC);
assertEquals("iPAddress SAN must match by IP",
WolfSSL.SSL_SUCCESS, certC.checkIpAddress(ip));
assertEquals("non-matching IP must be rejected",
WolfSSL.SSL_FAILURE, certC.checkIpAddress("198.51.100.9"));
certC.free();
}
@Test
public void testWolfSSLCertificateExtensionSetters()
throws WolfSSLException, WolfSSLJNIException, IOException,