F-5891: buffer KeyStore stream for type detection in convertKeyStoreToWKS

pull/249/head
Chris Conlon 2026-07-27 15:01:24 -06:00
parent 1feacd6c71
commit a250fc879e
2 changed files with 264 additions and 34 deletions

View File

@ -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

View File

@ -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(