From a250fc879e06b1f041081c7c4964ce7ce609df9d Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Mon, 27 Jul 2026 15:01:24 -0600 Subject: [PATCH] F-5891: buffer KeyStore stream for type detection in convertKeyStoreToWKS --- .../wolfssl/provider/jce/WolfCryptUtil.java | 78 ++++--- .../provider/jce/test/WolfCryptUtilTest.java | 220 +++++++++++++++++- 2 files changed, 264 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java index 66011d37..e6dcb0a0 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java @@ -69,15 +69,6 @@ public class WolfCryptUtil { public WolfCryptUtil() { } - /** - * Maximum size of the keystore buffer to mark. We try to set this - * high enough to handle any large keystore. Although there is no - * upper limit on the size of a keystore, looking at the JDK 23 cacerts - * KeyStore file, that is 190kB. We leave ample room for growth here - * with 512kB. - */ - private static final int MAX_KEYSTORE_SIZE = 512 * 1024; - /** * Chunk size for reading the keystore. We use 4kB as a happy medium * between memory usage and performance. @@ -98,10 +89,10 @@ public class WolfCryptUtil { * format. * * This method detects the type of the input KeyStore (WKS, JKS, or PKCS12) - * and converts it to WKS format if needed. All certificates and keys from - * the source KeyStore are transferred to the destination KeyStore. If the - * input KeyStore is already of type WKS, the method will return the same - * InputStream. + * and converts it to WKS format. All certificates and keys from the source + * KeyStore are transferred to a newly created WKS KeyStore, including when + * the input is already WKS. The input stream is read to the end but not + * closed, and the returned stream is always a new InputStream. * * @param stream Input stream containing a WKS, JKS, or PKCS12 KeyStore * @param oldPassword Password used to decrypt KeyStore entries. @@ -129,6 +120,7 @@ public class WolfCryptUtil { boolean wksFound = false; boolean jksFound = false; KeyStore sourceStore = null; + IOException passwordError = null; log("converting KeyStore InputStream to WKS format"); @@ -169,17 +161,15 @@ public class WolfCryptUtil { log("JKS to WKS mapping enabled: " + mapJksToWks); log("PKCS12 to WKS mapping enabled: " + mapPkcs12ToWks); - /* Since we will be doing KeyStore type detection by trying to - * read the KeyStore, we want to make sure we have the ability - * to mark() the stream. If we don't have the ability, we copy - * the stream into a ByteArrayOutputStream and then into a - * ByteArrayInputStream which is markable. */ - if (!stream.markSupported()) { + /* Copy into a ByteArrayInputStream, which ignores the mark() + * read limit, so reset() works after type detection reads any + * amount. Other stream types drop the mark on large KeyStores. */ + if (!(stream instanceof ByteArrayInputStream)) { try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int numRead; byte[] data = new byte[KEYSTORE_CHUNK_SIZE]; - while ((numRead = stream.read(data, 0, data.length)) != -1) { + while ((numRead = stream.read(data)) != -1) { buffer.write(data, 0, numRead); } buffer.flush(); @@ -189,8 +179,8 @@ public class WolfCryptUtil { } } - /* Mark the current position in the stream */ - stream.mark(MAX_KEYSTORE_SIZE); + /* ByteArrayInputStream ignores the read limit */ + stream.mark(Integer.MAX_VALUE); /* Try WKS */ try { @@ -228,9 +218,11 @@ public class WolfCryptUtil { jksFound = true; log("Input KeyStore is in JKS format"); - } catch (IOException | NoSuchAlgorithmException | - CertificateException e) { - /* Not a JKS KeyStore, continue with other formats */ + } catch (KeyStoreException | IOException | + NoSuchAlgorithmException | CertificateException e) { + /* Not a JKS KeyStore, or no JKS on this platform as on + * Android, continue with other formats */ + passwordError = getPasswordError(e); } finally { stream.reset(); } @@ -258,7 +250,20 @@ public class WolfCryptUtil { log("Input KeyStore is in PKCS12 format"); } catch (KeyStoreException | NoSuchAlgorithmException | - CertificateException ex) { + CertificateException | IOException ex) { + /* A valid KeyStore opened with the wrong password also + * fails detection, report that over a format error */ + IOException pwError = getPasswordError(ex); + if (pwError == null) { + pwError = passwordError; + } + if (pwError != null) { + /* Keep PKCS12 failure visible */ + if (pwError != ex) { + pwError.addSuppressed(ex); + } + throw pwError; + } throw new IOException( "Input KeyStore is neither WKS, JKS nor " + "PKCS12 KeyStore format", ex); @@ -325,6 +330,27 @@ public class WolfCryptUtil { } } + /** + * Return the exception if it reports a wrong KeyStore password. + * + * KeyStore.load() signals a bad password with an IOException caused by + * UnrecoverableKeyException, otherwise indistinguishable from a format + * mismatch. + * + * @param e exception thrown by KeyStore.load() + * + * @return e if it reports a bad password, otherwise null + */ + private static IOException getPasswordError(Exception e) { + + if ((e instanceof IOException) && + (e.getCause() instanceof UnrecoverableKeyException)) { + return (IOException)e; + } + + return null; + } + /** * Get the Sun provider JKS KeyStore implementation using reflection. * This is used when wolfJCE has registered itself as the JKS provider 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 464042a0..79000306 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java @@ -31,17 +31,22 @@ import org.junit.runner.Description; import org.junit.BeforeClass; import org.junit.AfterClass; +import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.File; import java.math.BigInteger; import java.security.Security; import java.util.Arrays; import java.util.Locale; +import java.security.Key; import java.security.Provider; import java.security.KeyStore; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; import java.security.KeyPairGenerator; import java.security.PublicKey; import java.security.NoSuchAlgorithmException; @@ -75,6 +80,16 @@ public class WolfCryptUtilTest { private static String origMapPkcs12ToWks = null; private static String origIterationCount = null; + /* Minimum size for the large KeyStore fixtures, asserted so these + * tests keep covering conversion of multi hundred kB KeyStores */ + private static final int LARGE_KEYSTORE_MIN_SIZE = 512 * 1024; + + /* Chain length and entry count for buildLargeKeyStore(). Chain stays + * well under the WKS default max of 100 so a lower + * wolfjce.wks.maxCertChainLength does not fail these tests early. */ + private static final int TEST_CHAIN_LENGTH = 50; + private static final int TEST_ENTRY_COUNT = 10; + @Rule(order = Integer.MIN_VALUE) public TestRule testWatcher = TimedTestWatcher.create(); @@ -136,6 +151,31 @@ public class WolfCryptUtilTest { Assume.assumeTrue("Test file not available: " + path, file.exists()); } + /** + * Helper method to load a KeyStore file into a byte array + * @param path Path to the KeyStore file + * @return byte array containing the KeyStore data + * @throws Exception if file cannot be read + */ + private static synchronized byte[] loadKeyStoreBytes(String path) + throws Exception { + + int bytesRead; + byte[] buffer = new byte[1024]; + FileInputStream fis = new FileInputStream(path); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try { + while ((bytesRead = fis.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + } finally { + fis.close(); + } + + return baos.toByteArray(); + } + /** * Helper method to load a KeyStore file into a ByteArrayInputStream * @param path Path to the KeyStore file @@ -145,17 +185,49 @@ public class WolfCryptUtilTest { private static synchronized ByteArrayInputStream loadKeyStoreFile( String path) throws Exception { - FileInputStream fis = new FileInputStream(path); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); + return new ByteArrayInputStream(loadKeyStoreBytes(path)); + } - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = fis.read(buffer)) != -1) { - baos.write(buffer, 0, bytesRead); + /** + * Helper method to build a large KeyStore from repeated server.jks + * entries, at least LARGE_KEYSTORE_MIN_SIZE bytes when stored. + * + * @param type KeyStore type to create ("JKS" or "WKS") + * @return byte array holding the stored KeyStore + * @throws Exception on error building KeyStore + */ + private static byte[] buildLargeKeyStore(String type) throws Exception { + + KeyStore src = KeyStore.getInstance("JKS"); + FileInputStream fis = new FileInputStream(TEST_JKS_PATH); + try { + src.load(fis, PASSWORD); + } finally { + fis.close(); } - fis.close(); - return new ByteArrayInputStream(baos.toByteArray()); + Key key = src.getKey(TEST_ALIAS, PASSWORD); + Certificate[] chain = src.getCertificateChain(TEST_ALIAS); + + Certificate[] bigChain = new Certificate[TEST_CHAIN_LENGTH]; + for (int i = 0; i < bigChain.length; i++) { + bigChain[i] = chain[i % chain.length]; + } + + KeyStore big; + if (type.equals("WKS")) { + big = KeyStore.getInstance("WKS", WKS_PROVIDER); + } else { + big = KeyStore.getInstance(type); + } + big.load(null, PASSWORD); + for (int i = 0; i < TEST_ENTRY_COUNT; i++) { + big.setKeyEntry(TEST_ALIAS + i, key, PASSWORD, bigChain); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + big.store(baos, PASSWORD); + return baos.toByteArray(); } /** @@ -505,6 +577,138 @@ public class WolfCryptUtilTest { wksStore.size() > 0); } + /** + * Test converting a JKS KeyStore larger than 512kB supplied through + * a mark-limited BufferedInputStream. + */ + @Test + public void testConvertLargeJksToWksFromBufferedStream() + throws Exception { + assumeTestFileExists(TEST_JKS_PATH); + + byte[] jksBytes = buildLargeKeyStore("JKS"); + assertTrue("Test KeyStore should be large", + jksBytes.length > LARGE_KEYSTORE_MIN_SIZE); + + InputStream wksStream = WolfCryptUtil.convertKeyStoreToWKS( + new BufferedInputStream(new ByteArrayInputStream(jksBytes)), + PASSWORD, PASSWORD, true); + + KeyStore wksStore = KeyStore.getInstance("WKS", WKS_PROVIDER); + wksStore.load(wksStream, PASSWORD); + + assertEquals("All entries should be converted", + TEST_ENTRY_COUNT, wksStore.size()); + assertNotNull("Private key should exist", + wksStore.getKey(TEST_ALIAS + "0", PASSWORD)); + assertEquals("Certificate chain length should be preserved", + TEST_CHAIN_LENGTH, + wksStore.getCertificateChain(TEST_ALIAS + "0").length); + } + + /** + * Test converting a WKS KeyStore larger than 512kB supplied through + * a mark-limited BufferedInputStream. + */ + @Test + public void testConvertLargeWksToWksFromBufferedStream() + throws Exception { + assumeTestFileExists(TEST_JKS_PATH); + + byte[] wksBytes = buildLargeKeyStore("WKS"); + assertTrue("Test KeyStore should be large", + wksBytes.length > LARGE_KEYSTORE_MIN_SIZE); + + InputStream wksStream = WolfCryptUtil.convertKeyStoreToWKS( + new BufferedInputStream(new ByteArrayInputStream(wksBytes)), + PASSWORD, PASSWORD, true); + + KeyStore wksStore = KeyStore.getInstance("WKS", WKS_PROVIDER); + wksStore.load(wksStream, PASSWORD); + + assertEquals("All entries should be converted", + TEST_ENTRY_COUNT, wksStore.size()); + } + + /** + * Test that a valid KeyStore opened with the wrong password reports + * the password problem rather than a format detection failure. + */ + @Test + public void testConvertWrongPasswordReportsPasswordError() + throws Exception { + + char[] wrongPassword = "wrongPasswordNotTheRealOne".toCharArray(); + + String[] paths = { TEST_P12_PATH, TEST_JKS_PATH }; + + for (String path : paths) { + /* Skip just this path, not the whole method */ + if (!new File(path).exists()) { + continue; + } + + try { + WolfCryptUtil.convertKeyStoreToWKS(loadKeyStoreFile(path), + wrongPassword, PASSWORD, true); + fail("Conversion should fail with wrong password: " + path); + + } catch (IOException e) { + assertFalse("Wrong password for " + path + " should not be " + + "reported as a format error: " + e.getMessage(), + e.getMessage().contains("neither WKS, JKS nor PKCS12")); + + assertTrue("Expected UnrecoverableKeyException cause for " + + path + ", got: " + e.getCause(), + e.getCause() instanceof UnrecoverableKeyException); + } + } + } + + /** + * Test converting a PKCS12 KeyStore supplied through a mark-limited + * BufferedInputStream, covering the third detection branch. + */ + @Test + public void testConvertP12FromBufferedStream() throws Exception { + assumeTestFileExists(TEST_P12_PATH); + + byte[] p12Bytes = loadKeyStoreBytes(TEST_P12_PATH); + + InputStream wksStream = WolfCryptUtil.convertKeyStoreToWKS( + new BufferedInputStream(new ByteArrayInputStream(p12Bytes)), + PASSWORD, PASSWORD, true); + + KeyStore wksStore = KeyStore.getInstance("WKS", WKS_PROVIDER); + wksStore.load(wksStream, PASSWORD); + + assertTrue("RSA key entry should exist", + wksStore.isKeyEntry("client")); + assertNotNull("RSA private key should exist", + wksStore.getKey("client", PASSWORD)); + } + + /** + * Test that input in no supported KeyStore format fails with a + * descriptive exception. + */ + @Test + public void testConvertInvalidKeyStoreFormat() throws Exception { + + byte[] garbage = new byte[1024]; + Arrays.fill(garbage, (byte)0xAB); + + try { + WolfCryptUtil.convertKeyStoreToWKS( + new ByteArrayInputStream(garbage), PASSWORD, PASSWORD, true); + fail("Conversion should fail for invalid KeyStore data"); + } catch (IOException e) { + assertTrue("Exception should indicate unsupported format: " + + e.getMessage(), + e.getMessage().contains("neither WKS, JKS nor PKCS12")); + } + } + @Test public void testIsAlgorithmDisabledSimple() { String origProperty = Security.getProperty(