JSSE: fix certificate verification for cross-signed and alternate chains in WolfSSLTrustX509

pull/292/head
Chris Conlon 2025-10-27 16:27:05 -06:00
parent 9b067ce155
commit 56f6fc7f91
2 changed files with 415 additions and 1 deletions

View File

@ -391,7 +391,12 @@ public final class WolfSSLTrustX509 extends X509ExtendedTrustManager {
/* Walk backwards down list of intermediate CA certs, verify each one
* based on trusted certs we already have loaded in the CertManager,
* then once verified load the intermediate into the CertManager
* as a root that can be used to verify our peer cert. */
* as a root that can be used to verify our peer cert.
*
* Similarly to native wolfSSL WOLFSSL_ALT_CERT_CHAINS behavior: if a CA
* certificate cannot be verified, we skip it and continue building
* the chain through other certificates. This allows handling of
* cross-signed certificates and extra certificates in the chain. */
for (int i = sortedCerts.length-1; i > 0; i--) {
final int tmpI = i;
@ -405,6 +410,19 @@ public final class WolfSSLTrustX509 extends X509ExtendedTrustManager {
ret = cm.CertManagerVerifyBuffer(encoded, encoded.length,
WolfSSL.SSL_FILETYPE_ASN1);
if (ret != WolfSSL.SSL_SUCCESS) {
/* Failure here is ok if this is a CA cert and we can still
* build and verify a complete chain. Some cert chains may
* include extra CA certs. Similar to native wolfSSL
* WOLFSSL_ALT_CERT_CHAINS. */
if (sortedCerts[tmpI].getBasicConstraints() != -1) {
/* This is a CA certificate, skip it and continue.
* Do not add it to the certificate manager. */
WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO,
() -> "Using alternate cert chain (skipping CA): " +
sortedCerts[tmpI].getSubjectX500Principal().getName());
continue;
}
/* Non-CA certificates must verify successfully */
cm.free();
throw new CertificateException(
"Failed to verify intermediate chain cert");

View File

@ -3143,4 +3143,400 @@ public class WolfSSLTrustX509Test {
return this.exception;
}
}
/**
* Test alternate certificate chain with unverifiable CA in chain.
* This simulates a cross-signed certificate scenario where
* the chain contains an extra CA certificate that cannot be verified
* but a valid path exists through other certificates.
*
* Chain structure being tested:
* [0]: server/peer cert (signed by intermediate)
* [1]: intermediate CA (signed by root)
* [2]: root CA in trust store (cross-signed version - unverifiable)
*
* Expected: Should succeed by using the root CA from trust store
* instead of the unverifiable cross-signed version in the chain.
*/
@Test
public void testCheckServerTrustedWithUnverifiableCrossSignedRoot()
throws NoSuchProviderException, NoSuchAlgorithmException,
KeyStoreException, IOException, CertificateException {
TrustManager[] tm;
X509TrustManager x509tm;
Certificate cert = null;
X509Certificate[] certArray = null;
CertificateFactory cf = CertificateFactory.getInstance("X.509");
System.out.print("\talt chain: unverifiable CA");
/* Build a chain where the root CA is included but in cross-signed
* form that cannot verify against our trust store. The trust store
* contains the self-signed version of the same root CA.
*
* Using intermediate chain: server -> int1 -> int2 (acts as root)
* Trust store contains: ca-cert.pem (acts as int2's issuer)
*
* We'll manually construct a scenario where ca-int2-cert.pem cannot
* be verified but the chain should still validate. */
String serverCert = "examples/certs/intermediate/server-int-cert.pem";
String int1Cert = "examples/certs/intermediate/ca-int-cert.pem";
String int2Cert = "examples/certs/intermediate/ca-int2-cert.pem";
if (WolfSSLTestFactory.isAndroid()) {
serverCert = "/sdcard/" + serverCert;
int1Cert = "/sdcard/" + int1Cert;
int2Cert = "/sdcard/" + int2Cert;
}
/* Create TrustManager with caJKS which contains ca-cert.pem */
tm = tf.createTrustManager("SunX509", tf.caJKS, provider);
if (tm == null) {
error("\t... failed");
fail("failed to create trustmanager");
return;
}
x509tm = (X509TrustManager) tm[0];
/* Build certificate chain: [server, int1, int2] */
certArray = new X509Certificate[3];
/* certArray[0]: server cert */
try (FileInputStream fis = new FileInputStream(serverCert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[0] = (X509Certificate)cert;
}
/* certArray[1]: intermediate CA 1 */
try (FileInputStream fis = new FileInputStream(int1Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[1] = (X509Certificate)cert;
}
/* certArray[2]: intermediate CA 2 (acts like cross-signed root) */
try (FileInputStream fis = new FileInputStream(int2Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[2] = (X509Certificate)cert;
}
/* Verify chain. With WOLFSSL_ALT_CERT_CHAINS behavior, this should
* succeed even if certArray[2] cannot be independently verified,
* as long as a valid path exists through the trust store. */
try {
x509tm.checkServerTrusted(certArray, "RSA");
} catch (CertificateException e) {
error("\t... failed");
fail("Failed to verify chain with alternate path: " +
e.getMessage());
}
pass("\t... passed");
}
/**
* Test that chain with multiple unverifiable CA certificates still
* succeeds as long as valid path exists.
*
* This tests robustness of alternate chain logic when server sends
* multiple extra/redundant CA certificates at the end of a valid chain.
*/
@Test
public void testCheckServerTrustedWithMultipleUnverifiableCAs()
throws NoSuchProviderException, NoSuchAlgorithmException,
KeyStoreException, IOException, CertificateException {
TrustManager[] tm;
X509TrustManager x509tm;
Certificate cert = null;
X509Certificate[] certArray = null;
CertificateFactory cf = CertificateFactory.getInstance("X.509");
System.out.print("\talt chain: multiple CA skip");
/* Build valid chain with extra wrong CAs appended at end */
String serverCert = "examples/certs/intermediate/server-int-cert.pem";
String int1Cert = "examples/certs/intermediate/ca-int-cert.pem";
String int2Cert = "examples/certs/intermediate/ca-int2-cert.pem";
/* Add wrong CA cert from ECC chain that should be skipped */
String wrongCA = "examples/certs/intermediate/ca-int-ecc-cert.pem";
if (WolfSSLTestFactory.isAndroid()) {
serverCert = "/sdcard/" + serverCert;
int1Cert = "/sdcard/" + int1Cert;
int2Cert = "/sdcard/" + int2Cert;
wrongCA = "/sdcard/" + wrongCA;
}
tm = tf.createTrustManager("SunX509", tf.caJKS, provider);
if (tm == null) {
error("\t... failed");
fail("failed to create trustmanager");
return;
}
x509tm = (X509TrustManager) tm[0];
/* Build certificate chain: valid chain + wrong CA at end */
certArray = new X509Certificate[4];
/* certArray[0]: RSA server cert */
try (FileInputStream fis = new FileInputStream(serverCert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[0] = (X509Certificate)cert;
}
/* certArray[1]: RSA intermediate CA 1 */
try (FileInputStream fis = new FileInputStream(int1Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[1] = (X509Certificate)cert;
}
/* certArray[2]: RSA intermediate CA 2 */
try (FileInputStream fis = new FileInputStream(int2Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[2] = (X509Certificate)cert;
}
/* certArray[3]: ECC CA (wrong, should be skipped) */
try (FileInputStream fis = new FileInputStream(wrongCA);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[3] = (X509Certificate)cert;
}
/* Verify chain. Should succeed by skipping wrong CA cert at end
* and validating the valid RSA chain. */
try {
x509tm.checkServerTrusted(certArray, "RSA");
} catch (CertificateException e) {
error("\t... failed");
fail("Failed to verify chain with extra wrong CA: " +
e.getMessage());
}
pass("\t... passed");
}
/**
* Test that non-CA certificates cannot be skipped.
* This ensures the alternate chain logic only applies to CA certificates.
*/
@Test
public void testCheckServerTrustedNonCACannotBeSkipped()
throws NoSuchProviderException, NoSuchAlgorithmException,
KeyStoreException, IOException, CertificateException {
TrustManager[] tm;
X509TrustManager x509tm;
Certificate cert = null;
X509Certificate[] certArray = null;
CertificateFactory cf = CertificateFactory.getInstance("X.509");
System.out.print("\talt chain: non-CA must verify");
/* Build invalid chain with unverifiable server cert.
* This should FAIL because non-CA certs must verify. */
/* Use ECC server cert with RSA chain - signature won't verify */
String serverCert =
"examples/certs/intermediate/server-int-ecc-cert.pem";
String int1Cert = "examples/certs/intermediate/ca-int-cert.pem";
if (WolfSSLTestFactory.isAndroid()) {
serverCert = "/sdcard/" + serverCert;
int1Cert = "/sdcard/" + int1Cert;
}
/* Create TrustManager with RSA CA certs */
tm = tf.createTrustManager("SunX509", tf.caJKS, provider);
if (tm == null) {
error("\t... failed");
fail("failed to create trustmanager");
return;
}
x509tm = (X509TrustManager) tm[0];
/* Build certificate chain */
certArray = new X509Certificate[2];
/* certArray[0]: ECC server cert (wrong type) */
try (FileInputStream fis = new FileInputStream(serverCert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[0] = (X509Certificate)cert;
}
/* certArray[1]: RSA intermediate CA */
try (FileInputStream fis = new FileInputStream(int1Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[1] = (X509Certificate)cert;
}
/* Verify chain, should fail as server cert (non-CA) cannot be
* verified and non-CA certs must always verify. */
try {
x509tm.checkServerTrusted(certArray, "ECC");
error("\t... failed");
fail("Verified invalid server cert, should have failed");
} catch (CertificateException e) {
/* Expected - non-CA certificate must verify */
}
pass("\t... passed");
}
/**
* Test that invalid chain still fails properly.
* This ensures chain logic doesn't accept invalid chains.
*/
@Test
public void testCheckServerTrustedInvalidChainFails()
throws NoSuchProviderException, NoSuchAlgorithmException,
KeyStoreException, IOException, CertificateException {
TrustManager[] tm;
X509TrustManager x509tm;
Certificate cert = null;
X509Certificate[] certArray = null;
CertificateFactory cf = CertificateFactory.getInstance("X.509");
System.out.print("\talt chain: invalid still fails");
/* Build invalid chain where no path to trust anchor exists, should
* fail. Use server cert from one chain with CAs from different
* chain */
String serverCert =
"examples/certs/intermediate/server-int-cert.pem";
String wrongCA1 = "examples/certs/intermediate/ca-int-ecc-cert.pem";
String wrongCA2 = "examples/certs/intermediate/ca-int2-ecc-cert.pem";
if (WolfSSLTestFactory.isAndroid()) {
serverCert = "/sdcard/" + serverCert;
wrongCA1 = "/sdcard/" + wrongCA1;
wrongCA2 = "/sdcard/" + wrongCA2;
}
/* Create TrustManager with ECC CAs only */
tm = tf.createTrustManager("SunX509", tf.caJKS, provider);
if (tm == null) {
error("\t... failed");
fail("failed to create trustmanager");
return;
}
x509tm = (X509TrustManager) tm[0];
/* Build certificate chain with completely wrong CAs */
certArray = new X509Certificate[3];
/* certArray[0]: RSA server cert */
try (FileInputStream fis = new FileInputStream(serverCert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[0] = (X509Certificate)cert;
}
/* certArray[1]: ECC CA (wrong chain) */
try (FileInputStream fis = new FileInputStream(wrongCA1);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[1] = (X509Certificate)cert;
}
/* certArray[2]: ECC CA 2 (wrong chain) */
try (FileInputStream fis = new FileInputStream(wrongCA2);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[2] = (X509Certificate)cert;
}
/* Verify chain, should fail. No valid path to trust anchor exists */
try {
x509tm.checkServerTrusted(certArray, "RSA");
error("\t... failed");
fail("Verified invalid chain, should have failed");
} catch (CertificateException e) {
/* Expected - no valid path to trust anchor */
}
pass("\t... passed");
}
/**
* This tests cross signed root scenario where:
* - Trust store contains self-signed root CA
* - Server sends cross-signed root CA signed by removed secondary root
* - Verification should succeed using self-signed version from trust store
*/
@Test
public void testCheckServerTrustedCrossSignedRoot()
throws NoSuchProviderException, NoSuchAlgorithmException,
KeyStoreException, IOException, CertificateException {
TrustManager[] tm;
X509TrustManager x509tm;
Certificate cert = null;
X509Certificate[] certArray = null;
CertificateFactory cf = CertificateFactory.getInstance("X.509");
System.out.print("\talt chain: cross-sign");
String serverCert = "examples/certs/intermediate/server-int-cert.pem";
String int1Cert = "examples/certs/intermediate/ca-int-cert.pem";
String int2Cert = "examples/certs/intermediate/ca-int2-cert.pem";
if (WolfSSLTestFactory.isAndroid()) {
serverCert = "/sdcard/" + serverCert;
int1Cert = "/sdcard/" + int1Cert;
int2Cert = "/sdcard/" + int2Cert;
}
tm = tf.createTrustManager("SunX509", tf.caJKS, provider);
if (tm == null) {
error("\t\t... failed");
fail("failed to create trustmanager");
return;
}
x509tm = (X509TrustManager) tm[0];
/* Build chain: [server, intermediate1, intermediate2/root] */
certArray = new X509Certificate[3];
try (FileInputStream fis = new FileInputStream(serverCert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[0] = (X509Certificate)cert;
}
try (FileInputStream fis = new FileInputStream(int1Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[1] = (X509Certificate)cert;
}
try (FileInputStream fis = new FileInputStream(int2Cert);
BufferedInputStream bis = new BufferedInputStream(fis)) {
cert = cf.generateCertificate(bis);
certArray[2] = (X509Certificate)cert;
}
try {
x509tm.checkServerTrusted(certArray, "RSA");
} catch (CertificateException e) {
error("\t\t... failed");
fail("Cross-signed root test failed: " +
e.getMessage());
}
pass("\t\t... passed");
}
}