F-5891: buffer KeyStore stream for type detection in convertKeyStoreToWKS
parent
1feacd6c71
commit
a250fc879e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue