From 3e4f6aa1e2cd2c2cede17d4fa0d2e18523cc6162 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Mon, 6 Jul 2026 12:08:36 -0600 Subject: [PATCH 01/13] F-4359: validate peer DH public key in WolfCryptKeyAgreement.engineDoPhase() --- .../provider/jce/WolfCryptKeyAgreement.java | 9 +++ src/main/java/com/wolfssl/wolfcrypt/Dh.java | 22 ++++++ .../jce/test/WolfCryptKeyAgreementTest.java | 75 +++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java index c1f90273..1f5cefc0 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java @@ -47,6 +47,7 @@ import com.wolfssl.wolfcrypt.Dh; import com.wolfssl.wolfcrypt.Ecc; +import com.wolfssl.wolfcrypt.WolfCryptException; /** * wolfCrypt JCE Key Agreement wrapper @@ -133,6 +134,14 @@ protected Key engineDoPhase(Key key, boolean lastPhase) "Failed to get DH public key from Key object"); } + /* Validate peer public key (SP 800-56A) before use */ + try { + this.dh.checkPublicKey(pubKey); + } catch (WolfCryptException e) { + throw new InvalidKeyException( + "DH public key failed validation", e); + } + this.dh.setPublicKey(pubKey); break; diff --git a/src/main/java/com/wolfssl/wolfcrypt/Dh.java b/src/main/java/com/wolfssl/wolfcrypt/Dh.java index 7b27cec1..22968a62 100644 --- a/src/main/java/com/wolfssl/wolfcrypt/Dh.java +++ b/src/main/java/com/wolfssl/wolfcrypt/Dh.java @@ -377,6 +377,28 @@ public synchronized byte[] makeSharedSecret(byte[] pubKey) } } + /** + * Validate a DH public key against the parameters stored in this + * object, using native wc_DhCheckPubKey(). + * + * @param pub public key value to validate, as byte array + * + * @throws WolfCryptException if the public key is invalid or the native + * operation fails + * @throws IllegalStateException if object fails to initialize, or if + * releaseNativeStruct() has been called and object has been + * released. + */ + public synchronized void checkPublicKey(byte[] pub) + throws WolfCryptException, IllegalStateException { + + checkStateAndInitialize(); + + synchronized (pointerLock) { + wc_DhCheckPubKey(pub); + } + } + /** * Get named DH parameters (FFDHE groups from RFC 7919). * diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java index a8ec56ad..c7e918b2 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java @@ -48,6 +48,9 @@ import javax.crypto.SecretKey; import javax.crypto.Cipher; import javax.crypto.spec.DHParameterSpec; +import javax.crypto.interfaces.DHPublicKey; + +import java.math.BigInteger; import java.security.Security; import java.security.Provider; @@ -68,6 +71,7 @@ import com.wolfssl.wolfcrypt.Ecc; import com.wolfssl.wolfcrypt.Fips; +import com.wolfssl.wolfcrypt.Dh; import com.wolfssl.provider.jce.WolfCryptProvider; import com.wolfssl.wolfcrypt.test.TimedTestWatcher; @@ -338,6 +342,77 @@ public void testDHKeyAgreementWithUpdateArgument() assertArrayEquals(secretA2, secretC); } + /* Minimal DHPublicKey holding a caller-chosen Y, for feeding malicious + * peer public key values into engineDoPhase() during testing. */ + private static DHPublicKey makeDHPublicKey(final BigInteger y, + final DHParameterSpec params) { + + return new DHPublicKey() { + public BigInteger getY() { + return y; + } + public DHParameterSpec getParams() { + return params; + } + public String getAlgorithm() { + return "DH"; + } + public String getFormat() { + return "X.509"; + } + public byte[] getEncoded() { + return null; + } + }; + } + + @Test + public void testDHKeyAgreementRejectsInvalidPublicKey() + throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidParameterSpecException, InvalidKeyException, + InvalidAlgorithmParameterException { + + /* engineDoPhase() must reject small-subgroup and out-of-range peer + * DH public keys (SP 800-56A) before use. */ + + /* Use FFDHE-2048 parameters so the test is deterministic and does + * not depend on runtime DH parameter generation support. */ + byte[][] pg = Dh.getNamedDhParams(Dh.WC_FFDHE_2048); + BigInteger p = new BigInteger(1, pg[0]); + BigInteger g = new BigInteger(1, pg[1]); + DHParameterSpec dhParams = new DHParameterSpec(p, g); + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH", "wolfJCE"); + keyGen.initialize(dhParams, secureRandom); + KeyPair aPair = keyGen.generateKeyPair(); + + /* Degenerate / small-order peer public keys that must be rejected */ + BigInteger[] badY = new BigInteger[] { + BigInteger.ZERO, /* 0 */ + BigInteger.ONE, /* 1, order 1 */ + p.subtract(BigInteger.ONE), /* p-1, order 2 */ + p /* p */ + }; + + for (BigInteger y : badY) { + KeyAgreement ka = KeyAgreement.getInstance("DH", "wolfJCE"); + ka.init(aPair.getPrivate()); + try { + ka.doPhase(makeDHPublicKey(y, dhParams), true); + fail("doPhase should reject invalid DH public key Y=" + y); + } catch (InvalidKeyException e) { + /* expected */ + } + } + + /* Positive control: a valid peer public key still agrees */ + KeyPair bPair = keyGen.generateKeyPair(); + KeyAgreement ka = KeyAgreement.getInstance("DH", "wolfJCE"); + ka.init(aPair.getPrivate()); + ka.doPhase(bPair.getPublic(), true); + assertNotNull(ka.generateSecret()); + } + @Test public void testDHKeyAgreementInterop() throws NoSuchProviderException, NoSuchAlgorithmException, From 5e9578dd871fe919b7bba631d9c76b602ed6058a Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Mon, 6 Jul 2026 14:12:24 -0600 Subject: [PATCH 02/13] F-4366: fix use-after-free race in CertManager nativeVerifyCallback --- jni/jni_wolfssl_cert_manager.c | 48 ++- .../wolfcrypt/test/WolfCryptTestSuite.java | 1 + .../WolfSSLCertManagerVerifyCallbackTest.java | 290 ++++++++++++++++++ 3 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/wolfssl/wolfcrypt/test/WolfSSLCertManagerVerifyCallbackTest.java diff --git a/jni/jni_wolfssl_cert_manager.c b/jni/jni_wolfssl_cert_manager.c index c7e7265a..768474a6 100644 --- a/jni/jni_wolfssl_cert_manager.c +++ b/jni/jni_wolfssl_cert_manager.c @@ -210,7 +210,9 @@ static int nativeVerifyCallback(int preverify, WOLFSSL_X509_STORE_CTX* store) int errorDepth = 0; jint result = 0; JNIEnv* jenv = NULL; + JavaVM* jvm = NULL; VerifyCallbackCtx* ctx = NULL; + jobject localCallback = NULL; jbyteArray certDer = NULL; jclass callbackClass = NULL; jmethodID verifyMethod = NULL; @@ -234,31 +236,50 @@ static int nativeVerifyCallback(int preverify, WOLFSSL_X509_STORE_CTX* store) ctx = g_callbackList->ctx; } - wc_UnLockMutex(&g_callbackMutex); - /* No callback registered, use preverify result */ if (ctx == NULL || ctx->callback == NULL) { + wc_UnLockMutex(&g_callbackMutex); return preverify; } + /* Capture jvm and take our own local reference to the callback while + * still holding g_callbackMutex. A concurrent CertManagerClearVerify() + * deletes the global reference and frees ctx after releasing the mutex, + * so nothing below dereferences ctx or the global reference. */ + jvm = ctx->jvm; + /* Get JNIEnv for current thread. Native callback may be called from * different thread than original Java thread. */ - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&jenv, + if ((*jvm)->GetEnv(jvm, (void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) { #ifdef __ANDROID__ - result = (*ctx->jvm)->AttachCurrentThread( - ctx->jvm, &jenv, NULL); + result = (*jvm)->AttachCurrentThread(jvm, &jenv, NULL); #else - result = (*ctx->jvm)->AttachCurrentThread( - ctx->jvm, (void**)&jenv, NULL); + result = (*jvm)->AttachCurrentThread(jvm, (void**)&jenv, NULL); #endif if (result != 0) { /* Failed to attach, reject */ + wc_UnLockMutex(&g_callbackMutex); return 0; } needsDetach = 1; } + /* Local reference keeps the callback object alive independent of ctx's + * global reference, which a concurrent ClearVerify may delete. */ + localCallback = (*jenv)->NewLocalRef(jenv, ctx->callback); + + wc_UnLockMutex(&g_callbackMutex); + + /* ctx must not be dereferenced past this point, it may have been freed + * by a concurrent CertManagerClearVerify(). */ + if (localCallback == NULL) { + if (needsDetach) { + (*jvm)->DetachCurrentThread(jvm); + } + return 0; + } + /* Get error code and depth from store */ error = store->error; errorDepth = store->error_depth; @@ -267,13 +288,14 @@ static int nativeVerifyCallback(int preverify, WOLFSSL_X509_STORE_CTX* store) certDer = getCertDerAtDepth(jenv, store, errorDepth); /* Find verify() method on callback object */ - callbackClass = (*jenv)->GetObjectClass(jenv, ctx->callback); + callbackClass = (*jenv)->GetObjectClass(jenv, localCallback); if (callbackClass == NULL) { if (certDer != NULL) { (*jenv)->DeleteLocalRef(jenv, certDer); } + (*jenv)->DeleteLocalRef(jenv, localCallback); if (needsDetach) { - (*ctx->jvm)->DetachCurrentThread(ctx->jvm); + (*jvm)->DetachCurrentThread(jvm); } return 0; } @@ -285,14 +307,15 @@ static int nativeVerifyCallback(int preverify, WOLFSSL_X509_STORE_CTX* store) (*jenv)->DeleteLocalRef(jenv, certDer); } (*jenv)->DeleteLocalRef(jenv, callbackClass); + (*jenv)->DeleteLocalRef(jenv, localCallback); if (needsDetach) { - (*ctx->jvm)->DetachCurrentThread(ctx->jvm); + (*jvm)->DetachCurrentThread(jvm); } return 0; } /* Call Java callback.verify(preverify, error, errorDepth, certDer) */ - result = (*jenv)->CallIntMethod(jenv, ctx->callback, verifyMethod, + result = (*jenv)->CallIntMethod(jenv, localCallback, verifyMethod, (jint)preverify, (jint)error, (jint)errorDepth, certDer); /* Check for Java exceptions, reject on exception */ @@ -313,8 +336,9 @@ static int nativeVerifyCallback(int preverify, WOLFSSL_X509_STORE_CTX* store) (*jenv)->DeleteLocalRef(jenv, certDer); } (*jenv)->DeleteLocalRef(jenv, callbackClass); + (*jenv)->DeleteLocalRef(jenv, localCallback); if (needsDetach) { - (*ctx->jvm)->DetachCurrentThread(ctx->jvm); + (*jvm)->DetachCurrentThread(jvm); } return (int)result; diff --git a/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java b/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java index 9b03a67a..d20eeb3f 100644 --- a/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java +++ b/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java @@ -59,6 +59,7 @@ LmsTest.class, WolfObjectTest.class, WolfSSLCertManagerTest.class, + WolfSSLCertManagerVerifyCallbackTest.class, WolfSSLCertManagerOCSPTest.class, WolfSSLX509StoreCtxTest.class, WolfCryptTest.class diff --git a/src/test/java/com/wolfssl/wolfcrypt/test/WolfSSLCertManagerVerifyCallbackTest.java b/src/test/java/com/wolfssl/wolfcrypt/test/WolfSSLCertManagerVerifyCallbackTest.java new file mode 100644 index 00000000..56bb709d --- /dev/null +++ b/src/test/java/com/wolfssl/wolfcrypt/test/WolfSSLCertManagerVerifyCallbackTest.java @@ -0,0 +1,290 @@ +/* WolfSSLCertManagerVerifyCallbackTest.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.wolfcrypt.test; + +import static org.junit.Assert.*; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runners.model.Statement; +import org.junit.runner.Description; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import com.wolfssl.wolfcrypt.WolfCrypt; +import com.wolfssl.wolfcrypt.WolfSSLCertManager; +import com.wolfssl.wolfcrypt.WolfSSLCertManagerVerifyCallback; +import com.wolfssl.wolfcrypt.WolfCryptException; + +/** + * Tests for the WolfSSLCertManager verify callback, including concurrent + * verification while callbacks are registered and cleared. + */ +public class WolfSSLCertManagerVerifyCallbackTest { + + private static String certPre = ""; + private static String caCertDer = null; + private static String serverCertDer = null; + + @Rule(order = Integer.MIN_VALUE) + public TestRule testWatcher = TimedTestWatcher.create(); + + /* Skip tests if example cert files are not available. */ + @Rule(order = Integer.MIN_VALUE + 1) + public TestRule certFilesAvailable = new TestRule() { + + @Override + public Statement apply(final Statement base, Description description) { + + return new Statement() { + @Override + public void evaluate() throws Throwable { + File f = new File(caCertDer); + Assume.assumeTrue("Test cert files not available: " + + caCertDer, f.exists()); + base.evaluate(); + } + }; + } + }; + + /* Skip tests if WolfSSLCertManager or the verify callback are not + * compiled into the native library. */ + @Rule(order = Integer.MIN_VALUE + 2) + public TestRule verifyCallbackAvailable = new TestRule() { + + @Override + public Statement apply(final Statement base, Description description) { + + return new Statement() { + @Override + public void evaluate() throws Throwable { + WolfSSLCertManager cm = null; + try { + cm = new WolfSSLCertManager(); + cm.setVerifyCallback( + new WolfSSLCertManagerVerifyCallback() { + public int verify(int preverify, int error, + int errorDepth) { + return 1; + } + }); + } catch (WolfCryptException e) { + Assume.assumeTrue( + "WolfSSLCertManager verify callback not " + + "available, skipping", false); + } finally { + if (cm != null) { + cm.free(); + } + } + base.evaluate(); + } + }; + } + }; + + private static boolean isAndroid() { + if (System.getProperty("java.runtime.name").contains("Android")) { + return true; + } + return false; + } + + @BeforeClass + public static void testSetup() throws Exception { + + System.out.println("JNI WolfSSLCertManagerVerifyCallback Class"); + + if (isAndroid()) { + /* On Android, example certs/keys are on SD card */ + certPre = "/data/local/tmp/"; + } + + caCertDer = certPre.concat("examples/certs/ca-cert.der"); + serverCertDer = certPre.concat("examples/certs/server-cert.der"); + } + + private static byte[] readFile(String path) throws IOException { + return Files.readAllBytes(Paths.get(path)); + } + + /** + * Test invoking the verify callback during CertManagerVerifyBuffer(). + */ + @Test + public void testVerifyCallbackInvoked() throws Exception { + + final AtomicBoolean invoked = new AtomicBoolean(false); + WolfSSLCertManager cm = null; + + byte[] caDer = readFile(caCertDer); + byte[] serverDer = readFile(serverCertDer); + + try { + cm = new WolfSSLCertManager(); + cm.CertManagerLoadCABuffer(caDer, caDer.length, + WolfCrypt.SSL_FILETYPE_ASN1); + + cm.setVerifyCallback(new WolfSSLCertManagerVerifyCallback() { + public int verify(int preverify, int error, int errorDepth) { + invoked.set(true); + return 1; + } + }); + + try { + cm.CertManagerVerifyBuffer(serverDer, serverDer.length, + WolfCrypt.SSL_FILETYPE_ASN1); + } catch (WolfCryptException e) { + /* Verification result itself is not what we assert here */ + } + + assertTrue("verify callback should have been invoked", + invoked.get()); + + } finally { + if (cm != null) { + cm.free(); + } + } + } + + /** + * Test concurrent certificate verification while callbacks are registered + * and cleared. The native callback list is global across all CertManager + * instances, so this runs many managers verifying. + */ + @Test + public void testConcurrentVerifyAndClearCallback() + throws Exception { + + final int numPairs = 8; + final long runMillis = 1500; + final byte[] caDer = readFile(caCertDer); + final byte[] serverDer = readFile(serverCertDer); + + final AtomicBoolean stop = new AtomicBoolean(false); + final AtomicLong verifies = new AtomicLong(0); + final List errors = + Collections.synchronizedList(new ArrayList()); + + List threads = new ArrayList(); + + for (int i = 0; i < numPairs; i++) { + + /* Register a callback and verify in a loop */ + threads.add(new Thread(new Runnable() { + public void run() { + WolfSSLCertManager cm = null; + try { + cm = new WolfSSLCertManager(); + cm.CertManagerLoadCABuffer(caDer, caDer.length, + WolfCrypt.SSL_FILETYPE_ASN1); + cm.setVerifyCallback( + new WolfSSLCertManagerVerifyCallback() { + public int verify(int preverify, int error, + int errorDepth) { + return 1; + } + }); + + while (!stop.get()) { + try { + cm.CertManagerVerifyBuffer(serverDer, + serverDer.length, + WolfCrypt.SSL_FILETYPE_ASN1); + } catch (WolfCryptException e) { + /* Verification result itself is not + * what we assert here */ + } + verifies.incrementAndGet(); + } + } catch (Throwable t) { + errors.add(t); + } finally { + if (cm != null) { + cm.free(); + } + } + } + })); + + /* Churner: repeatedly registers then clears its callback, + * freeing and reallocating its ctx node in the shared global + * callback list. */ + threads.add(new Thread(new Runnable() { + public void run() { + WolfSSLCertManager cm = null; + try { + cm = new WolfSSLCertManager(); + cm.CertManagerLoadCABuffer(caDer, caDer.length, + WolfCrypt.SSL_FILETYPE_ASN1); + while (!stop.get()) { + cm.setVerifyCallback( + new WolfSSLCertManagerVerifyCallback() { + public int verify(int preverify, int error, + int errorDepth) { + return 1; + } + }); + cm.setVerifyCallback(null); + } + } catch (Throwable t) { + errors.add(t); + } finally { + if (cm != null) { + cm.free(); + } + } + } + })); + } + + for (Thread t : threads) { + t.start(); + } + Thread.sleep(runMillis); + stop.set(true); + for (Thread t : threads) { + t.join(); + } + + if (!errors.isEmpty()) { + fail("Concurrent verify/clear raised " + errors.size() + + " error(s), first: " + errors.get(0)); + } + assertTrue("expected at least one verify to run", + verifies.get() > 0); + } +} From 10e50c18e6cc921ad61708bbdd72909f7b256761 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Mon, 6 Jul 2026 14:55:18 -0600 Subject: [PATCH 03/13] F-4855: validate peer ECC public key in WolfCryptKeyAgreement.engineDoPhase() --- .../provider/jce/WolfCryptKeyAgreement.java | 10 ++- .../jce/test/WolfCryptKeyAgreementTest.java | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java index 1f5cefc0..6d9070d5 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptKeyAgreement.java @@ -158,7 +158,15 @@ protected Key engineDoPhase(Key key, boolean lastPhase) "Failed to get ECC public key from Key object"); } - this.ecPublic.publicKeyDecode(pubKey); + /* Decode then validate peer public key is a valid point on + * the curve before use. */ + try { + this.ecPublic.publicKeyDecode(pubKey); + this.ecPublic.checkKey(); + } catch (WolfCryptException e) { + throw new InvalidKeyException( + "ECC public key failed validation", e); + } break; }; diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java index c7e918b2..3898fd55 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptKeyAgreementTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.*; import org.junit.Test; import org.junit.Rule; +import org.junit.Assume; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; @@ -66,8 +67,15 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidParameterSpecException; import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.ECFieldFp; import com.wolfssl.wolfcrypt.Ecc; import com.wolfssl.wolfcrypt.Fips; @@ -599,6 +607,68 @@ public void testECDHKeyAgreement() assertArrayEquals(secretA2, secretC); } + /** + * Test that engineDoPhase() rejects ECC peer public keys that are not + * valid points on the curve. + */ + @Test + public void testECDHKeyAgreementRejectsInvalidPublicKey() + throws Exception { + + + /* P-256 domain parameters */ + BigInteger p = new BigInteger("FFFFFFFF00000001000000000000000000" + + "000000FFFFFFFFFFFFFFFFFFFFFFFF", 16); + BigInteger a = new BigInteger("FFFFFFFF00000001000000000000000000" + + "000000FFFFFFFFFFFFFFFFFFFFFFFC", 16); + BigInteger b = new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC65" + + "1D06B0CC53B0F63BCE3C3E27D2604B", 16); + BigInteger order = new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFF" + + "BCE6FAADA7179E84F3B9CAC2FC632551", 16); + BigInteger gx = new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277" + + "037D812DEB33A0F4A13945D898C296", 16); + BigInteger gy = new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162B" + + "CE33576B315ECECBB6406837BF51F5", 16); + + EllipticCurve curve = new EllipticCurve(new ECFieldFp(p), a, b); + ECParameterSpec spec = + new ECParameterSpec(curve, new ECPoint(gx, gy), order, 1); + KeyFactory kf = KeyFactory.getInstance("EC"); + + /* Off-curve point (gx, gy+1): does not satisfy the curve equation. */ + ECPublicKey offCurve = null; + try { + offCurve = (ECPublicKey) kf.generatePublic( + new ECPublicKeySpec( + new ECPoint(gx, gy.add(BigInteger.ONE)), spec)); + } catch (Exception e) { + Assume.assumeNoException( + "Platform EC provider rejects off-curve point", e); + } + + KeyPairGenerator keyGen = + KeyPairGenerator.getInstance("EC", "wolfJCE"); + keyGen.initialize(new ECGenParameterSpec("secp256r1")); + KeyPair aPair = keyGen.generateKeyPair(); + + KeyAgreement ka = KeyAgreement.getInstance("ECDH", "wolfJCE"); + ka.init(aPair.getPrivate()); + + try { + ka.doPhase(offCurve, true); + fail("doPhase should reject off-curve ECC public key"); + } catch (InvalidKeyException e) { + /* expected */ + } + + /* Make sure valid peer public key still agrees */ + KeyPair bPair = keyGen.generateKeyPair(); + KeyAgreement ka2 = KeyAgreement.getInstance("ECDH", "wolfJCE"); + ka2.init(aPair.getPrivate()); + ka2.doPhase(bPair.getPublic(), true); + assertNotNull(ka2.generateSecret()); + } + @Test public void testECDHKeyAgreementWithUpdateArgument() throws NoSuchProviderException, NoSuchAlgorithmException, From abb9ae8071e85a4a486664bb0d4be7cc00959b59 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Mon, 6 Jul 2026 17:20:37 -0600 Subject: [PATCH 04/13] F-5029: reject AES-GCM key and IV reuse for encryption in WolfCryptCipher --- .../wolfssl/provider/jce/WolfCryptCipher.java | 40 +++++++++++ .../jce/test/WolfCryptCipherTest.java | 66 +++++++++++++++++-- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java index be44bcae..db1d7b52 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java @@ -39,6 +39,7 @@ import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; +import java.security.MessageDigest; import java.security.KeyFactory; import java.security.spec.MGF1ParameterSpec; import java.security.AlgorithmParameters; @@ -151,6 +152,11 @@ enum RsaKeyType { /* AAD data for AES-GCM, populated via engineUpdateAAD() */ private byte[] aadData = null; + /* Last (key, IV) used for a completed AES-GCM encryption, tracked to + * reject GCM nonce reuse. */ + private byte[] lastGcmEncryptKey = null; + private byte[] lastGcmEncryptIv = null; + /* Has update/final been called yet, gates setting of AAD for GCM */ private boolean operationStarted = false; @@ -1389,10 +1395,42 @@ private byte[] wolfCryptFinal(byte[] input, int inputOffset, int len) case WC_AES: if (cipherMode == CipherMode.WC_GCM) { if (this.direction == OpMode.WC_ENCRYPT) { + + byte[] keyEnc; + if (this.storedKey == null) { + keyEnc = null; + } else { + keyEnc = this.storedKey.getEncoded(); + } + + /* Prevent reusing the same key and IV for GCM + * encryption to protect against catastrophic security + * degradation. */ + if (this.lastGcmEncryptIv != null && + MessageDigest.isEqual( + this.lastGcmEncryptIv, this.iv) && + MessageDigest.isEqual( + this.lastGcmEncryptKey, keyEnc)) { + zeroArray(keyEnc); + throw new IllegalStateException( + "Cannot reuse the same key and IV for " + + "AES-GCM encryption, re-initialize with a " + + "fresh IV"); + } + byte[] tag = new byte[this.gcmTagLen]; tmpOut = this.aesGcm.encrypt(tmpIn, this.iv, tag, this.aadData); + /* Record this (key, IV) as used for GCM encryption */ + zeroArray(this.lastGcmEncryptKey); + this.lastGcmEncryptKey = keyEnc; + if (this.iv == null) { + this.lastGcmEncryptIv = null; + } else { + this.lastGcmEncryptIv = this.iv.clone(); + } + /* Concatenate auth tag to end of ciphertext */ byte[] totalOut = new byte[tmpOut.length + tag.length]; System.arraycopy(tmpOut, 0, totalOut, 0, tmpOut.length); @@ -2082,6 +2120,8 @@ protected void finalize() throws Throwable { } zeroArray(this.iv); + zeroArray(this.lastGcmEncryptKey); + zeroArray(this.lastGcmEncryptIv); this.storedKey = null; this.storedSpec = null; diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java index e520b029..d823afb7 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java @@ -6196,8 +6196,11 @@ public void testByteBufferWithGCM() assertEquals("GCM should include auth tag", plaintext.length + 16, bytesWritten); - /* Compare with byte array result */ - byte[] expected = cipher.doFinal(plaintext); + /* Compare with byte array result. Use separate Cipher instance, since + * GCM forbids reusing same key/IV for second encryption. */ + Cipher cipher2 = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); + cipher2.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] expected = cipher2.doFinal(plaintext); outputBuf.flip(); byte[] result = new byte[bytesWritten]; outputBuf.get(result); @@ -6835,6 +6838,9 @@ public void testAESGCMExplicitParamsWithGetParameters() int[] tagLengths = {96, 104, 112, 120, 128}; for (int tagLen : tagLengths) { + /* Use a fresh IV for each encryption, GCM forbids reusing same + * key+IV pair for more than one encryption. */ + secureRandom.nextBytes(iv); GCMParameterSpec gcmSpec = new GCMParameterSpec(tagLen, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); byte[] ciphertext = cipher.doFinal(plaintext); @@ -7306,8 +7312,12 @@ public void testGCMAlgorithmParametersWithCipher() assertArrayEquals("Decrypted text should match original", plaintext, decrypted); - /* Verify cipher returns compatible parameters */ - cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + /* Verify cipher returns compatible parameters. Use a fresh IV, GCM + * forbids reusing same key+IV pair for a second encryption */ + byte[] iv2 = new byte[12]; + new SecureRandom().nextBytes(iv2); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, + new GCMParameterSpec(128, iv2)); AlgorithmParameters cipherParams = cipher.getParameters(); assertNotNull("Cipher should return parameters", cipherParams); @@ -7320,6 +7330,54 @@ public void testGCMAlgorithmParametersWithCipher() plaintext, decrypted2); } + /** + * Test that AES-GCM refuses to reuse the same key and IV for more than + * one encryption. + */ + @Test + public void testGCMEncryptRejectsNonceReuse() throws Exception { + + if (!enabledJCEAlgos.contains("AES/GCM/NoPadding")) { + return; + } + + byte[] keyBytes = new byte[16]; + byte[] iv = new byte[12]; + byte[] pt; + + secureRandom.nextBytes(keyBytes); + SecretKeySpec key = new SecretKeySpec(keyBytes, "AES"); + secureRandom.nextBytes(iv); + pt = "GCM nonce reuse test".getBytes(); + + /* Encrypting a second time with the same key and IV, without + * re-initializing, must be rejected. */ + Cipher c = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); + c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + c.doFinal(pt); + try { + c.doFinal(pt); + fail("Second GCM encryption with same key and IV should throw"); + } catch (IllegalStateException e) { + /* expected */ + } + + /* Re-initializing with a fresh IV must succeed. */ + byte[] iv2 = new byte[12]; + secureRandom.nextBytes(iv2); + c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv2)); + assertNotNull(c.doFinal(pt)); + + /* Decryption may reuse the same key and IV without restriction. */ + Cipher enc = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); + enc.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] ct = enc.doFinal(pt); + Cipher dec = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); + dec.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + assertArrayEquals(pt, dec.doFinal(ct)); + assertArrayEquals(pt, dec.doFinal(ct)); + } + /** * Test AlgorithmParameters.getInstance("AES") basic functionality */ From 1e6bcb17cc2e527eeccb6c5b739a1dc7a86e1975 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Mon, 6 Jul 2026 17:42:00 -0600 Subject: [PATCH 05/13] F-5030: use constant-time PKCS#7 unpadding and uniform BadPaddingException --- .../wolfssl/provider/jce/WolfCryptCipher.java | 12 +++- .../com/wolfssl/wolfcrypt/BlockCipher.java | 50 +++++++------- .../jce/test/WolfCryptCipherTest.java | 69 +++++++++++++++++++ .../com/wolfssl/wolfcrypt/test/AesTest.java | 19 +++++ 4 files changed, 125 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java index db1d7b52..272a5d8c 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java @@ -1536,7 +1536,11 @@ else if (cipherMode == CipherMode.WC_OFB) { cipherMode != CipherMode.WC_CTR && cipherMode != CipherMode.WC_CTS && cipherMode != CipherMode.WC_OFB) { - tmpOut = Aes.unPadPKCS7(tmpOut, Aes.BLOCK_SIZE); + try { + tmpOut = Aes.unPadPKCS7(tmpOut, Aes.BLOCK_SIZE); + } catch (WolfCryptException e) { + throw new BadPaddingException("Decryption error"); + } } } @@ -1552,7 +1556,11 @@ else if (cipherMode == CipherMode.WC_OFB) { if (tmpOut != null && tmpOut.length > 0) { if (this.direction == OpMode.WC_DECRYPT && this.paddingType == PaddingType.WC_PKCS5) { - tmpOut = Des3.unPadPKCS7(tmpOut, Des3.BLOCK_SIZE); + try { + tmpOut = Des3.unPadPKCS7(tmpOut, Des3.BLOCK_SIZE); + } catch (WolfCryptException e) { + throw new BadPaddingException("Decryption error"); + } } } diff --git a/src/main/java/com/wolfssl/wolfcrypt/BlockCipher.java b/src/main/java/com/wolfssl/wolfcrypt/BlockCipher.java index d22ff616..f28fd774 100644 --- a/src/main/java/com/wolfssl/wolfcrypt/BlockCipher.java +++ b/src/main/java/com/wolfssl/wolfcrypt/BlockCipher.java @@ -354,42 +354,46 @@ public static synchronized byte[] padPKCS7(byte[] in, int blockSize) */ public static synchronized byte[] unPadPKCS7(byte[] in, int blockSize) { - byte padValue = 0; + int len = 0; + int count = 0; + int position = 0; + int failed = 0; byte[] unpadded = null; - boolean valid = true; if (in == null || in.length == 0) { - throw new WolfCryptException( - "Input array is null or zero length"); + throw new WolfCryptException("Input array is null or zero length"); } - if (blockSize == 0) { - throw new WolfCryptException("Block size is 0"); + if (blockSize <= 0) { + throw new WolfCryptException("Block size must be positive"); } - padValue = in[in.length - 1]; - - /* verify pad value is less than or equal to block size */ - if ((padValue & 0xff) > blockSize) { - throw new WolfCryptException( - "Invalid pad value, larger than block size"); + if (in.length < blockSize || (in.length % blockSize) != 0) { + throw new WolfCryptException("Invalid PKCS#7 padding"); } - /* verify pad bytes are consistent */ - for (int i = in.length; i > in.length - (padValue & 0xff); i--) { - if (in[i - 1] != padValue) { - valid = false; - } - } + len = in.length; + count = in[len - 1] & 0xff; - unpadded = new byte[in.length - (padValue & 0xff)]; - System.arraycopy(in, 0, unpadded, 0, in.length - (padValue & 0xff)); + /* Validate PKCS#7 padding in constant time */ + position = blockSize - count; - if (!valid) { - throw new WolfCryptException( - "Invalid PKCS#7 padding, pad bytes not consistent"); + /* pad value must be in the range [1, blockSize] */ + failed = ((count - 1) | (blockSize - count)) >> 31; + + /* each of the final 'count' bytes must equal count */ + for (int i = 0; i < blockSize; i++) { + int b = in[len - blockSize + i] & 0xff; + failed |= (b ^ count) & ~((i - position) >> 31); } + if (failed != 0) { + throw new WolfCryptException("Invalid PKCS#7 padding"); + } + + unpadded = new byte[len - count]; + System.arraycopy(in, 0, unpadded, 0, len - count); + return unpadded; } } diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java index d823afb7..2ac145bd 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java @@ -7378,6 +7378,75 @@ public void testGCMEncryptRejectsNonceReuse() throws Exception { assertArrayEquals(pt, dec.doFinal(ct)); } + /** + * Test that AES-CBC/PKCS5 decryption reports every padding failure with + * the same generic BadPaddingException. + */ + @Test + public void testCBCPaddingFailureMessagesConsistent() + throws Exception { + + if (!enabledJCEAlgos.contains("AES/CBC/PKCS5Padding")) { + return; + } + + byte[] keyBytes = new byte[16]; + byte[] iv = new byte[16]; + SecretKeySpec key; + IvParameterSpec ivSpec; + + secureRandom.nextBytes(keyBytes); + key = new SecretKeySpec(keyBytes, "AES"); + secureRandom.nextBytes(iv); + ivSpec = new IvParameterSpec(iv); + + /* A 16-byte plaintext produces a full 0x10 padding block, so second + * ciphertext block decrypts to the padding. Flipping bytes of + * the first ciphertext block deterministically corrupts padding */ + byte[] pt = new byte[16]; + secureRandom.nextBytes(pt); + + Cipher enc = Cipher.getInstance("AES/CBC/PKCS5Padding", jceProvider); + enc.init(Cipher.ENCRYPT_MODE, key, ivSpec); + byte[] ct = enc.doFinal(pt); + assertEquals(32, ct.length); + + /* Corrupt a non-terminal padding byte, so the pad bytes are + * inconsistent while the pad length stays in range. */ + byte[] bad1 = ct.clone(); + bad1[0] ^= (byte)0xFF; + + /* Corrupt the pad-length byte so the pad value exceeds block size */ + byte[] bad2 = ct.clone(); + bad2[15] ^= (byte)0x30; + + String msg1 = decryptExpectingBadPadding(key, ivSpec, bad1); + String msg2 = decryptExpectingBadPadding(key, ivSpec, bad2); + + /* Both failures must throw an identical message. */ + assertEquals("Padding failures must not be distinguishable", + msg1, msg2); + + /* Valid ciphertext must still decrypt correctly. */ + Cipher dec = Cipher.getInstance("AES/CBC/PKCS5Padding", jceProvider); + dec.init(Cipher.DECRYPT_MODE, key, ivSpec); + assertArrayEquals(pt, dec.doFinal(ct)); + } + + private String decryptExpectingBadPadding(SecretKeySpec key, + IvParameterSpec ivSpec, byte[] ct) throws Exception { + + Cipher dec = Cipher.getInstance("AES/CBC/PKCS5Padding", jceProvider); + dec.init(Cipher.DECRYPT_MODE, key, ivSpec); + try { + dec.doFinal(ct); + fail("Corrupted CBC padding must throw BadPaddingException"); + return null; + } catch (BadPaddingException e) { + return e.getMessage(); + } + } + /** * Test AlgorithmParameters.getInstance("AES") basic functionality */ diff --git a/src/test/java/com/wolfssl/wolfcrypt/test/AesTest.java b/src/test/java/com/wolfssl/wolfcrypt/test/AesTest.java index cc51c3f7..0347ffef 100644 --- a/src/test/java/com/wolfssl/wolfcrypt/test/AesTest.java +++ b/src/test/java/com/wolfssl/wolfcrypt/test/AesTest.java @@ -535,6 +535,25 @@ public void testPadPKCS7() { /* expected */ } + /* Test unPadPKCS7() with negative block size */ + try { + input = new byte[10]; + unpadded = Aes.unPadPKCS7(input, -1); + fail("negative block size should throw WolfCryptException"); + } catch (WolfCryptException e) { + /* expected */ + } + + /* Test unPadPKCS7() with length not a multiple of block size. + * PKCS#7 padded data must end on a block boundary. */ + try { + input = Util.h2b("000102030405060708090A0B0C0D0E0F01"); + unpadded = Aes.unPadPKCS7(input, Aes.BLOCK_SIZE); + fail("non block size multiple should throw WolfCryptException"); + } catch (WolfCryptException e) { + /* expected */ + } + /* Test unPadPKCS7() with inconsistent pad value */ try { padded = Util.h2b("00112233445566778899AABBCC020303"); From d6a4e0d9a2a59e197c89408c75b0a71fc56db0ba Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 10:12:59 -0600 Subject: [PATCH 06/13] F-5066: use a uniform exception for RSA PKCS#1 v1.5 unwrap failures --- .../wolfssl/provider/jce/WolfCryptCipher.java | 9 +-- .../jce/test/WolfCryptCipherTest.java | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java index 272a5d8c..fb9115a0 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java @@ -1996,13 +1996,8 @@ protected Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgo, try { unwrappedKey = wolfCryptFinal(wrappedKey, 0, wrappedKey.length); - } catch (BadPaddingException e) { - throw new InvalidKeyException("Failed to unwrap key: " + - e.getMessage(), e); - - } catch (IllegalBlockSizeException e) { - throw new InvalidKeyException("Failed to unwrap key: " + - e.getMessage(), e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new InvalidKeyException("Failed to unwrap key"); } switch (wrappedKeyType) { diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java index 2ac145bd..2a231d70 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java @@ -34,6 +34,8 @@ import java.util.Random; import java.util.Arrays; import java.util.Iterator; +import java.util.Set; +import java.util.HashSet; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.CountDownLatch; @@ -5013,6 +5015,68 @@ public void testRSAWrapUnwrapMultipleKeySizes() } } + /** + * Test that RSA PKCS#1 v1.5 unwrap reports every failure with the same + * exception type and message. + */ + @Test + public void testRSAUnwrapFailuresAreNotDistinguishable() + throws Exception { + + if (!enabledJCEAlgos.contains("RSA/ECB/PKCS1Padding")) { + return; + } + + assertNotNull("RSA key pair was not generated", rsaPair); + + byte[] raw = new byte[16]; + secureRandom.nextBytes(raw); + SecretKeySpec key = new SecretKeySpec(raw, "AES"); + + Cipher wrap = Cipher.getInstance("RSA/ECB/PKCS1Padding", jceProvider); + wrap.init(Cipher.WRAP_MODE, rsaPair.getPublic()); + byte[] wrapped = wrap.wrap(key); + int modSize = wrapped.length; + + /* Confirm a valid wrapped key still unwraps. */ + Cipher ok = Cipher.getInstance("RSA/ECB/PKCS1Padding", jceProvider); + ok.init(Cipher.UNWRAP_MODE, rsaPair.getPrivate()); + Key unwrapped = ok.unwrap(wrapped, "AES", Cipher.SECRET_KEY); + assertArrayEquals(raw, unwrapped.getEncoded()); + + /* Distinct, deterministic failure inputs: an over-length input that + * fails structurally, a modulus-sized input that fails the padding + * check, and an under-length input. */ + byte[] over = new byte[modSize + 1]; + byte[] padFail = new byte[modSize]; + byte[] under = new byte[modSize - 1]; + + /* Every failure must surface the identical exception type and msg */ + Set outcomes = new HashSet(); + outcomes.add(rsaUnwrapFailure(over)); + outcomes.add(rsaUnwrapFailure(padFail)); + outcomes.add(rsaUnwrapFailure(under)); + + assertEquals("RSA unwrap failures must not be distinguishable: " + + outcomes, 1, outcomes.size()); + } + + private String rsaUnwrapFailure(byte[] wrapped) throws Exception { + + Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding", jceProvider); + c.init(Cipher.UNWRAP_MODE, rsaPair.getPrivate()); + + try { + c.unwrap(wrapped, "AES", Cipher.SECRET_KEY); + fail("Malformed wrapped key must not unwrap successfully"); + + return "SUCCESS"; + + } catch (InvalidKeyException e) { + return e.getClass().getName() + ":" + e.getMessage(); + } + } + @Test public void testRSAWrapUnwrapResetsState() throws NoSuchProviderException, NoSuchAlgorithmException, From 8c7abe133c650c7ac1e77da28901d41cdb902af9 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 10:29:28 -0600 Subject: [PATCH 07/13] F-5222: reject AES-GMAC key and IV reuse in WolfCryptMac --- .../wolfssl/provider/jce/WolfCryptMac.java | 25 ++++++- .../provider/jce/test/WolfCryptMacTest.java | 72 +++++++++++++++++-- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptMac.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptMac.java index 48c37543..a37a9f25 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptMac.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptMac.java @@ -166,19 +166,36 @@ private WolfCryptMac(MacType type) } } + /** + * Reject AES-GMAC operations that would reuse a key and IV. The IV is + * cleared after each GMAC tag is produced, so a further update() or + * doFinal() without re-initializing with a fresh IV is refused. + */ + private void requireGmacIv() { + + if ((macType == MacType.WC_AES_GMAC) && (gmacIv == null)) { + throw new IllegalStateException( + "AES-GMAC requires re-init with fresh IV before each MAC op"); + } + } + @Override protected byte[] engineDoFinal() { byte[] out = null; + requireGmacIv(); + if (macType == MacType.WC_AES_CMAC) { out = this.aesCmac.doFinal(); } else if (macType == MacType.WC_AES_GMAC) { /* Compute GMAC using accumulated auth data */ byte[] authData = gmacAuthData.toByteArray(); out = this.aesGmac.update(gmacIv, authData, gmacTagLen); - /* Reset for next operation */ + + /* Clear IV and buffered data */ gmacAuthData.reset(); + this.gmacIv = null; } else { out = this.hmac.doFinal(); } @@ -258,6 +275,9 @@ protected void engineReset() { @Override protected void engineUpdate(byte input) { + + requireGmacIv(); + if (macType == MacType.WC_AES_CMAC) { this.aesCmac.update(input); } else if (macType == MacType.WC_AES_GMAC) { @@ -272,6 +292,9 @@ protected void engineUpdate(byte input) { @Override protected void engineUpdate(byte[] input, int offset, int len) { + + requireGmacIv(); + if (macType == MacType.WC_AES_CMAC) { this.aesCmac.update(input, offset, len); } else if (macType == MacType.WC_AES_GMAC) { diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptMacTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptMacTest.java index eab3f44c..58220219 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptMacTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptMacTest.java @@ -2187,17 +2187,75 @@ public void testAesGmacReset() /* 128 bits == 16 bytes */ GCMParameterSpec gmacSpec = new GCMParameterSpec(128, iv); - /* First computation */ + /* reset() before doFinal() must discard accumulated data without + * consuming IV, so tag computed after reset matches the known vector */ mac.init(keyspec, gmacSpec); + mac.update(new byte[] { (byte)0xAA, (byte)0xBB, (byte)0xCC }); + mac.reset(); mac.update(authIn); - byte[] computedTag1 = mac.doFinal(); - assertArrayEquals(expectedTag, computedTag1); + byte[] computedTag = mac.doFinal(); + assertArrayEquals(expectedTag, computedTag); + } - /* Reset and compute again - should get same result */ - mac.reset(); + @Test + public void testAesGmacRejectsIvReuse() + throws InvalidKeyException, NoSuchAlgorithmException, + NoSuchProviderException, InvalidAlgorithmParameterException { + + byte[] key = new byte[16]; + byte[] iv = new byte[12]; + byte[] authIn = "authenticated data".getBytes(); + + if (!enabledAlgos.contains("AESGMAC")) { + return; + } + + for (int i = 0; i < key.length; i++) { + key[i] = (byte)i; + } + for (int i = 0; i < iv.length; i++) { + iv[i] = (byte)(0x20 + i); + } + + SecretKeySpec keyspec = new SecretKeySpec(key, "AES"); + GCMParameterSpec gmacSpec = new GCMParameterSpec(128, iv); + + /* A second GMAC over same key and IV without re-init reuses the nonce + * and must be rejected */ + Mac mac = Mac.getInstance("AESGMAC", "wolfJCE"); + mac.init(keyspec, gmacSpec); + mac.update(authIn); + mac.doFinal(); + + try { + mac.update(authIn); + mac.doFinal(); + fail("GMAC must reject a second MAC with the same key and IV"); + } catch (IllegalStateException e) { + /* expected */ + } + + /* The same reuse via reset() after doFinal() must also be rejected. */ + Mac mac2 = Mac.getInstance("AESGMAC", "wolfJCE"); + mac2.init(keyspec, gmacSpec); + mac2.update(authIn); + mac2.doFinal(); + mac2.reset(); + + try { + mac2.update(authIn); + mac2.doFinal(); + fail("GMAC must reject reuse after reset()"); + } catch (IllegalStateException e) { + /* expected */ + } + + /* Re-initializing with a fresh IV must succeed. */ + byte[] iv2 = iv.clone(); + iv2[0] ^= (byte)0xFF; + mac.init(keyspec, new GCMParameterSpec(128, iv2)); mac.update(authIn); - byte[] computedTag2 = mac.doFinal(); - assertArrayEquals(expectedTag, computedTag2); + assertNotNull(mac.doFinal()); } @Test From a6e69def1c51c39633f931ea0ddd76b42be9249d Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 10:46:11 -0600 Subject: [PATCH 08/13] F-5794: synchronize FIPS error callback access in jni_fips.c --- jni/jni_fips.c | 105 ++++++++++++++++++++++++++++++++-------- jni/jni_native_struct.c | 9 +++- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/jni/jni_fips.c b/jni/jni_fips.c index 866f97a7..8d2f99e3 100644 --- a/jni/jni_fips.c +++ b/jni/jni_fips.c @@ -29,6 +29,7 @@ #ifdef HAVE_FIPS #include + #include #include #include #include @@ -54,7 +55,28 @@ #ifdef HAVE_FIPS extern JavaVM* g_vm; static jobject g_errCb; + +/* Mutex protecting g_errCb against concurrent access from NativeErrorCallback, + * wolfCrypt_1SetCb_1fips, and the cleanup path. */ +static wolfSSL_Mutex g_fipsCbMutex; +static int g_fipsCbMutexInit = 0; +#endif + +/* Initialize the FIPS callback mutex. Called from JNI_OnLoad() so it is set + * up before any FIPS callback operations. Returns 0 on success, negative on + * error. No-op in non-FIPS builds. */ +int wolfCrypt_JNI_FipsCb_init(void) +{ +#ifdef HAVE_FIPS + if (!g_fipsCbMutexInit) { + if (wc_InitMutex(&g_fipsCbMutex) != 0) { + return -1; + } + g_fipsCbMutexInit = 1; + } #endif + return 0; +} /* Deregister native FIPS callback and free global ref. Called from * JNI_OnUnload in jni_native_struct.c. Logic here so we can access g_errCb. */ @@ -63,9 +85,18 @@ void wolfCrypt_JNI_FipsCb_cleanup(JNIEnv* env) #ifdef HAVE_FIPS wolfCrypt_SetCb_fips(NULL); - if (env != NULL && g_errCb != NULL) { - (*env)->DeleteGlobalRef(env, g_errCb); - g_errCb = NULL; + if (g_fipsCbMutexInit) { + if (wc_LockMutex(&g_fipsCbMutex) == 0) { + if (env != NULL && g_errCb != NULL) { + (*env)->DeleteGlobalRef(env, g_errCb); + } + g_errCb = NULL; + wc_UnLockMutex(&g_fipsCbMutex); + } + + /* Do not wc_FreeMutex(&g_fipsCbMutex) here. NativeErrorCallback + * may still lock this process-wide static mutex concurrently + * during JNI_OnUnload(), so leave it initialized for process life. */ } #else (void)env; @@ -76,6 +107,7 @@ void NativeErrorCallback(const int ok, const int err, const char * const hash) { #ifdef HAVE_FIPS JNIEnv* env; + jobject localCb = NULL; jclass class; jmethodID method; jint ret; @@ -102,12 +134,21 @@ void NativeErrorCallback(const int ok, const int err, const char * const hash) return; } - /* Return silently if stored callback ref is NULL or invalid */ - if (g_errCb == NULL) { + /* Take our own local reference to the callback while holding the mutex, + * so a concurrent wolfCrypt_1SetCb_1fips() cannot free the global + * reference while we use it. The rest of this function uses only the + * local reference and never touches g_errCb again. */ + if (!g_fipsCbMutexInit || wc_LockMutex(&g_fipsCbMutex) != 0) { return; } + if (g_errCb != NULL && + (*env)->GetObjectRefType(env, g_errCb) == JNIGlobalRefType) { + localCb = (*env)->NewLocalRef(env, g_errCb); + } + wc_UnLockMutex(&g_fipsCbMutex); - if (JNIGlobalRefType != (*env)->GetObjectRefType(env, g_errCb)) { + /* Return silently if no valid callback was registered */ + if (localCb == NULL) { if ((*env)->ExceptionOccurred(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); @@ -115,12 +156,13 @@ void NativeErrorCallback(const int ok, const int err, const char * const hash) return; } - class = (*env)->GetObjectClass(env, g_errCb); + class = (*env)->GetObjectClass(env, localCb); if (!class) { if ((*env)->ExceptionOccurred(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } + (*env)->DeleteLocalRef(env, localCb); return; } @@ -131,6 +173,7 @@ void NativeErrorCallback(const int ok, const int err, const char * const hash) (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } + (*env)->DeleteLocalRef(env, localCb); return; } @@ -140,12 +183,14 @@ void NativeErrorCallback(const int ok, const int err, const char * const hash) (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } + (*env)->DeleteLocalRef(env, localCb); return; } - (*env)->CallVoidMethod(env, g_errCb, method, ok, err, hashStr); + (*env)->CallVoidMethod(env, localCb, method, ok, err, hashStr); (*env)->DeleteLocalRef(env, hashStr); + (*env)->DeleteLocalRef(env, localCb); if ((*env)->ExceptionOccurred(env)) { (*env)->ExceptionDescribe(env); @@ -158,25 +203,47 @@ JNIEXPORT void JNICALL Java_com_wolfssl_wolfcrypt_Fips_wolfCrypt_1SetCb_1fips( JNIEnv* env, jclass class, jobject callback) { #ifdef HAVE_FIPS - /* Unregister native callback */ - wolfCrypt_SetCb_fips(NULL); + jobject newRef = NULL; - /* Free previous callback global ref if set */ - if (g_errCb != NULL) { - (*env)->DeleteGlobalRef(env, g_errCb); - g_errCb = NULL; + /* Ensure mutex is initialized, normally done in JNI_OnLoad() */ + if (!g_fipsCbMutexInit && wolfCrypt_JNI_FipsCb_init() != 0) { + throwWolfCryptException(env, + "Failed to initialize FIPS callback mutex"); + return; } + /* Unregister native callback */ + wolfCrypt_SetCb_fips(NULL); + + /* Create the new global ref before taking the lock */ if (callback != NULL) { - g_errCb = (*env)->NewGlobalRef(env, callback); - if (g_errCb != NULL) { - wolfCrypt_SetCb_fips(NativeErrorCallback); - } - else { + newRef = (*env)->NewGlobalRef(env, callback); + if (newRef == NULL) { throwWolfCryptException(env, "Failed to store global error callback"); + return; } } + + /* Swap in the new ref under the mutex, freeing the previous one. Doing + * this as one critical section prevents a double free when two callers + * race. */ + if (wc_LockMutex(&g_fipsCbMutex) != 0) { + if (newRef != NULL) { + (*env)->DeleteGlobalRef(env, newRef); + } + throwWolfCryptException(env, "Failed to lock FIPS callback mutex"); + return; + } + if (g_errCb != NULL) { + (*env)->DeleteGlobalRef(env, g_errCb); + } + g_errCb = newRef; + wc_UnLockMutex(&g_fipsCbMutex); + + if (newRef != NULL) { + wolfCrypt_SetCb_fips(NativeErrorCallback); + } #endif } diff --git a/jni/jni_native_struct.c b/jni/jni_native_struct.c index 0e5a6eca..1643acd1 100644 --- a/jni/jni_native_struct.c +++ b/jni/jni_native_struct.c @@ -44,7 +44,8 @@ JavaVM* g_vm = NULL; extern int wolfSSL_CertManager_init(void); extern void wolfSSL_CertManager_cleanup(void); -/* Forward declaration for FIPS callback cleanup */ +/* Forward declarations for FIPS callback mutex init/cleanup */ +extern int wolfCrypt_JNI_FipsCb_init(void); extern void wolfCrypt_JNI_FipsCb_cleanup(JNIEnv* env); /* called when native library is loaded */ @@ -61,6 +62,12 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) return JNI_ERR; } + /* Initialize FIPS callback mutex for thread-safe error callback handling. + * No-op in non-FIPS builds. */ + if (wolfCrypt_JNI_FipsCb_init() != 0) { + return JNI_ERR; + } + return JNI_VERSION_1_6; } From 866cca671f863cdf730562ec3cfaf80f882ba75b Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 11:54:17 -0600 Subject: [PATCH 09/13] F-5795: match qualified jdk.certpath.disabledAlgorithms entries by algorithm name --- .../wolfssl/provider/jce/WolfCryptUtil.java | 63 ++++++++++++++++--- .../provider/jce/test/WolfCryptUtilTest.java | 43 +++++++++++++ 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java index f73cdb0e..e44da593 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java @@ -439,13 +439,6 @@ public static boolean isAlgorithmDisabled(String algorithm, disabledAlgos = disabledAlgos.replaceAll(", ", ","); disabledList = Arrays.asList(disabledAlgos.split(",")); - /* Check full algorithm name first (case-insensitive) */ - for (String disabled : disabledList) { - if (disabled.equalsIgnoreCase(algorithm)) { - return true; - } - } - /* Decompose composite algorithm names like "MD2withRSA" into * constituent parts and check each. Common formats: * - "MD2withRSA" - ["MD2", "RSA"] @@ -453,9 +446,31 @@ public static boolean isAlgorithmDisabled(String algorithm, * - "SHA256withRSA" - ["SHA256", "RSA"] * Use case-insensitive matching to match SunJCE behavior */ String[] parts = decomposeAlgorithmName(algorithm); - for (String part : parts) { - for (String disabled : disabledList) { - if (disabled.equalsIgnoreCase(part)) { + + for (String disabled : disabledList) { + /* Entries may carry qualifiers, for example the JDK 11+ default + * "SHA1 jdkCA & denyAfter 2019-01-01". Compare against the + * leading algorithm name only. */ + String disabledName = extractDisabledAlgorithmName(disabled); + if (disabledName == null || disabledName.isEmpty()) { + continue; + } + + /* Skip key-size constraints such as "RSA keySize < 1024". Those + * are size limits enforced by isKeyAllowed(), not signature name + * disables. */ + if (disabled.toLowerCase().contains("keysize")) { + continue; + } + + /* Check the full algorithm name (case-insensitive) */ + if (disabledName.equalsIgnoreCase(algorithm)) { + return true; + } + + /* Check each decomposed part */ + for (String part : parts) { + if (disabledName.equalsIgnoreCase(part)) { return true; } } @@ -464,6 +479,34 @@ public static boolean isAlgorithmDisabled(String algorithm, return false; } + /** + * Extract the leading algorithm name from a single + * jdk.certpath.disabledAlgorithms list entry, dropping any qualifiers. + * + * For example "SHA1 jdkCA ..." returns "SHA1" and + * "RSA keySize ..." returns "RSA". + * + * @param entry a single disabled-algorithms list entry + * + * @return the leading algorithm name, or null if none could be extracted + */ + private static String extractDisabledAlgorithmName(String entry) { + + String[] tokens = null; + + if (entry == null) { + return null; + } + + /* Split on the first whitespace or '&' qualifier separator */ + tokens = entry.trim().split("[\\s&]"); + if (tokens.length == 0) { + return null; + } + + return tokens[0].trim(); + } + /** * Decompose a composite algorithm name into constituent parts. * diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java index 17f926b4..790bc8aa 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java @@ -568,6 +568,49 @@ public void testIsAlgorithmDisabledComposite() { } } + @Test + public void testIsAlgorithmDisabledQualifiedEntries() { + + String origProperty = Security.getProperty( + "jdk.certpath.disabledAlgorithms"); + + try { + /* JDK 11+ factory default ships SHA1 only in qualified form and + * key algorithms only as keySize constraints. */ + Security.setProperty("jdk.certpath.disabledAlgorithms", + "MD2, MD5, SHA1 jdkCA & denyAfter 2019-01-01, " + + "RSA keySize < 1024, DSA keySize < 1024, EC keySize < 224"); + + /* Qualified SHA1 entry must still disable SHA1 signatures */ + assertTrue("SHA1withRSA should be disabled (qualified SHA1)", + WolfCryptUtil.isAlgorithmDisabled( + "SHA1withRSA", "jdk.certpath.disabledAlgorithms")); + assertTrue("SHA1withECDSA should be disabled (qualified SHA1)", + WolfCryptUtil.isAlgorithmDisabled( + "SHA1withECDSA", "jdk.certpath.disabledAlgorithms")); + + /* Bare entries must still work */ + assertTrue("MD5withRSA should be disabled (bare MD5)", + WolfCryptUtil.isAlgorithmDisabled( + "MD5withRSA", "jdk.certpath.disabledAlgorithms")); + + /* keySize constraints must not disable the signature algorithm. + * "RSA keySize < 1024" must not reject a SHA256withRSA + * signature. */ + assertFalse("SHA256withRSA must not be disabled by keySize entry", + WolfCryptUtil.isAlgorithmDisabled( + "SHA256withRSA", "jdk.certpath.disabledAlgorithms")); + assertFalse("SHA384withECDSA must not be disabled", + WolfCryptUtil.isAlgorithmDisabled( + "SHA384withECDSA", "jdk.certpath.disabledAlgorithms")); + } finally { + if (origProperty != null) { + Security.setProperty("jdk.certpath.disabledAlgorithms", + origProperty); + } + } + } + @Test public void testIsAlgorithmDisabledPBE() { String origProperty = Security.getProperty( From 21cfcc0bae4ab1579f8963cef0c53b45e7157a2d Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 12:11:49 -0600 Subject: [PATCH 10/13] F-5887: close Curve25519 release split-lock race by freeing under one lock --- src/main/java/com/wolfssl/wolfcrypt/Curve25519.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wolfssl/wolfcrypt/Curve25519.java b/src/main/java/com/wolfssl/wolfcrypt/Curve25519.java index a314a405..a6e625b6 100644 --- a/src/main/java/com/wolfssl/wolfcrypt/Curve25519.java +++ b/src/main/java/com/wolfssl/wolfcrypt/Curve25519.java @@ -58,8 +58,8 @@ public void releaseNativeStruct() { synchronized (pointerLock) { wc_curve25519_free(); + super.releaseNativeStruct(); } - super.releaseNativeStruct(); state = WolfCryptState.RELEASED; } } From ac82b660c4c30fc0b897c3b3c493de2da8016a1f Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 12:37:13 -0600 Subject: [PATCH 11/13] F-5888: hard-fail OCSP revoked status regardless of SOFT_FAIL --- .../jce/WolfCryptPKIXRevocationChecker.java | 13 +++++ .../WolfCryptPKIXRevocationCheckerTest.java | 55 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptPKIXRevocationChecker.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptPKIXRevocationChecker.java index 9f7bff70..e48adc43 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptPKIXRevocationChecker.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptPKIXRevocationChecker.java @@ -41,6 +41,7 @@ import com.wolfssl.wolfcrypt.WolfCrypt; import com.wolfssl.wolfcrypt.WolfSSLCertManager; import com.wolfssl.wolfcrypt.WolfCryptException; +import com.wolfssl.wolfcrypt.WolfCryptError; import java.security.cert.TrustAnchor; import javax.security.auth.x500.X500Principal; @@ -329,6 +330,13 @@ private void checkOcsp(X509Certificate cert) "Failed to encode certificate", e); } catch (WolfCryptException e) { + /* A definitive OCSP "revoked" status must always be a hard fail, + * with BasicReason.REVOKED. Other codes SOFT_FAIL may suppress. */ + if (e.getCode() == WolfCryptError.OCSP_CERT_REVOKED.getCode()) { + throw new CertPathValidatorException( + "Certificate revoked (OCSP): " + e.getMessage(), e, + null, -1, BasicReason.REVOKED); + } throw new CertPathValidatorException( "OCSP check failed: " + e.getMessage(), e, null, -1, BasicReason.UNDETERMINED_REVOCATION_STATUS); @@ -510,6 +518,11 @@ else if (ocspStatus < 0) { private void handleException(CertPathValidatorException e) throws CertPathValidatorException { + /* A confirmed revoked status is always a hard failure */ + if (e.getReason() == BasicReason.REVOKED) { + throw e; + } + if (options.contains(Option.SOFT_FAIL)) { softFailExceptions.add(e); } else { diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptPKIXRevocationCheckerTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptPKIXRevocationCheckerTest.java index 004f0d35..20bde5b8 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptPKIXRevocationCheckerTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptPKIXRevocationCheckerTest.java @@ -37,6 +37,7 @@ import java.security.Provider; import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidatorException; +import java.security.cert.CertPathValidatorException.BasicReason; import java.security.cert.CertificateFactory; import java.security.cert.PKIXRevocationChecker; import java.security.cert.PKIXRevocationChecker.Option; @@ -52,6 +53,7 @@ import com.wolfssl.wolfcrypt.WolfCrypt; import com.wolfssl.wolfcrypt.WolfSSLCertManager; import com.wolfssl.wolfcrypt.WolfCryptException; +import com.wolfssl.wolfcrypt.WolfCryptError; import com.wolfssl.wolfcrypt.test.TimedTestWatcher; import com.wolfssl.provider.jce.WolfCryptProvider; import com.wolfssl.provider.jce.WolfCryptPKIXRevocationChecker; @@ -818,6 +820,59 @@ public void testRevocationCheckerSoftFailCollectsExceptions() cm.free(); } + /* CertManager that simulates a definitive OCSP "revoked" response by + * throwing WolfCryptException(OCSP_CERT_REVOKED) from CertManagerCheckOCSP, + * exactly as native wolfSSL does for a revoked certificate. */ + private static class RevokedOcspCertManager extends WolfSSLCertManager { + + @Override + public synchronized void CertManagerCheckOCSP(byte[] cert, int sz) { + throw new WolfCryptException( + WolfCryptError.OCSP_CERT_REVOKED.getCode()); + } + } + + @Test + public void testOcspRevokedNotSuppressedBySoftFail() throws Exception { + + if (!WolfCrypt.OcspEnabled()) { + /* Skip test if OCSP not compiled in */ + return; + } + + CertPathValidator cpv = CertPathValidator.getInstance("PKIX", provider); + WolfCryptPKIXRevocationChecker checker = + (WolfCryptPKIXRevocationChecker)cpv.getRevocationChecker(); + + FileInputStream fis = new FileInputStream(caCertDer); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate caCert = (X509Certificate)cf.generateCertificate(fis); + fis.close(); + + fis = new FileInputStream(serverCertDer); + X509Certificate serverCert = + (X509Certificate)cf.generateCertificate(fis); + fis.close(); + + /* SOFT_FAIL must not suppress a definitive revoked status. */ + checker.setOptions(EnumSet.of(Option.SOFT_FAIL)); + + WolfSSLCertManager cm = new RevokedOcspCertManager(); + cm.CertManagerLoadCA(caCert); + checker.setCertManager(cm); + checker.init(false); + + try { + checker.check(serverCert, null); + fail("A revoked certificate must hard-fail even under SOFT_FAIL"); + } catch (CertPathValidatorException e) { + assertEquals("Revoked cert must fail with BasicReason.REVOKED", + BasicReason.REVOKED, e.getReason()); + } + + cm.free(); + } + @Test public void testRevocationCheckerCheckWithCertChain() throws Exception { From 9094a1f274ff77e3057c02d577ab0b4aab632334 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 12:49:08 -0600 Subject: [PATCH 12/13] F-5905: enforce 1 < Y < p-1 range on DH public key in KeyFactory import --- .../provider/jce/WolfCryptDHKeyFactory.java | 10 +++++ .../jce/test/WolfCryptDHKeyFactoryTest.java | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptDHKeyFactory.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptDHKeyFactory.java index 3eb3efed..27603fb8 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptDHKeyFactory.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptDHKeyFactory.java @@ -393,6 +393,16 @@ private PublicKey generatePublicFromDHSpec(DHPublicKeySpec keySpec) "Public key value must be positive"); } + /* Validate public key is in range 1 < Y < p-1. Values of 1 or + * p-1 are degenerate and yield a weak shared secret. Mirrors native + * wc_DhCheckPubKey range check. */ + if (keySpec.getY().compareTo(BigInteger.ONE) <= 0 || + keySpec.getY().compareTo( + keySpec.getP().subtract(BigInteger.ONE)) >= 0) { + throw new InvalidKeySpecException( + "Public key out of valid range: must satisfy 1 < Y < p-1"); + } + try { /* Create DHParameterSpec from p and g */ DHParameterSpec paramSpec = new DHParameterSpec( diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java index 9c45e05c..e5cd6c91 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java @@ -448,6 +448,50 @@ public void testInvalidKeySpecs() throws Exception { } } + @Test + public void testDHPublicKeySpecRangeValidation() throws Exception { + + Assume.assumeTrue(FeatureDetect.DhEnabled()); + Assume.assumeTrue(enabledKeySizes.contains(2048)); + + /* Generate reference key pair to obtain valid DH parameters (P, G) + * and a valid public value Y */ + KeyPairGenerator kpg = KeyPairGenerator.getInstance("DH", "wolfJCE"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + DHPublicKey pubKey = (DHPublicKey) kp.getPublic(); + DHParameterSpec params = pubKey.getParams(); + BigInteger p = params.getP(); + BigInteger g = params.getG(); + + KeyFactory kf = KeyFactory.getInstance("DH", "wolfJCE"); + + /* Y = 1 is degenerate and must be rejected */ + try { + kf.generatePublic(new DHPublicKeySpec(BigInteger.ONE, p, g)); + fail("Should reject DH public key Y = 1"); + + } catch (InvalidKeySpecException e) { + /* Expected */ + } + + /* Y = p-1 is degenerate and must be rejected */ + try { + kf.generatePublic( + new DHPublicKeySpec(p.subtract(BigInteger.ONE), p, g)); + fail("Should reject DH public key Y = p-1"); + + } catch (InvalidKeySpecException e) { + /* Expected */ + } + + /* Valid Y in range 1 < Y < p-1 is accepted */ + PublicKey validKey = kf.generatePublic( + new DHPublicKeySpec(pubKey.getY(), p, g)); + assertNotNull("Valid DH public key should be created", validKey); + assertTrue("Should be DHPublicKey", validKey instanceof DHPublicKey); + } + @Test public void testDHPrivateKeySpecConversionWithoutSunJCE() throws Exception { From fee50d8b4f9820bfc642e697fd29a671b1b8cb93 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 7 Jul 2026 12:58:03 -0600 Subject: [PATCH 13/13] F-5906: enforce 1 < Y < p-1 range on DH public key in X.509 import --- .../provider/jce/WolfCryptDHPublicKey.java | 10 +++++ .../jce/test/WolfCryptDHKeyFactoryTest.java | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptDHPublicKey.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptDHPublicKey.java index a333ee19..7bf4f22c 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptDHPublicKey.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptDHPublicKey.java @@ -228,6 +228,16 @@ private void parseX509Der(byte[] derData) System.arraycopy(derData, idx, pubBytes, 0, pubLen); publicVal = new BigInteger(1, pubBytes); + /* Validate public key is in range 1 < Y < p-1. Values of 0, + * 1, or p-1 are degenerate and yield a weak or constant shared + * secret. Matches native wc_DhCheckPubKey range check. */ + if (publicVal.compareTo(BigInteger.ONE) <= 0 || + publicVal.compareTo(p.subtract(BigInteger.ONE)) >= 0) { + throw new IllegalArgumentException( + "DH public key value out of range: " + + "must satisfy 1 < Y < p-1"); + } + /* Store extracted values */ this.publicValue = publicVal; this.paramSpec = new DHParameterSpec(p, g); diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java index e5cd6c91..e3a10cbb 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptDHKeyFactoryTest.java @@ -51,6 +51,7 @@ import org.junit.Test; import com.wolfssl.provider.jce.WolfCryptProvider; +import com.wolfssl.provider.jce.WolfCryptDHPublicKey; import com.wolfssl.wolfcrypt.FeatureDetect; import com.wolfssl.wolfcrypt.test.TimedTestWatcher; @@ -187,6 +188,47 @@ public void testX509PublicKeyConversion() throws Exception { convertedEncoded); } + @Test + public void testX509PublicKeyRangeValidation() throws Exception { + + Assume.assumeTrue(FeatureDetect.DhEnabled()); + Assume.assumeTrue(enabledKeySizes.contains(2048)); + + /* Generate reference key pair to obtain valid DH parameters (P, G) + * and a valid encoded public key */ + KeyPairGenerator kpg = KeyPairGenerator.getInstance("DH", "wolfJCE"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + DHPublicKey refPub = (DHPublicKey) kp.getPublic(); + DHParameterSpec params = refPub.getParams(); + BigInteger p = params.getP(); + + KeyFactory kf = KeyFactory.getInstance("DH", "wolfJCE"); + + /* Build X.509 encodings carrying degenerate Y values (0, 1, p-1) + * using the raw public key constructor, which does not validate, + * then confirm the X.509 import path rejects each one */ + BigInteger[] badY = new BigInteger[] { + BigInteger.ZERO, BigInteger.ONE, p.subtract(BigInteger.ONE) }; + + for (BigInteger y : badY) { + byte[] der = new WolfCryptDHPublicKey(y, params).getEncoded(); + try { + kf.generatePublic(new X509EncodedKeySpec(der)); + fail("Should reject X.509 DH public key Y = " + y); + + } catch (InvalidKeySpecException e) { + /* Expected */ + } + } + + /* Valid Y round-trips through X.509 import */ + PublicKey validKey = + kf.generatePublic(new X509EncodedKeySpec(refPub.getEncoded())); + assertNotNull("Valid DH public key should be created", validKey); + assertTrue("Should be DHPublicKey", validKey instanceof DHPublicKey); + } + @Test public void testDHPrivateKeySpecConversion() throws Exception {