JKSKeyStoreSpi.validateStream (JKSKeyStoreSpi.java:364-392) reads the stream into rawStore (:368); on the password branch (:370) it subtracts the SHA-1 digest size from that length unchecked. :373 digests rawStore.length - checksumCalculator.getDigestSize() bytes and :380 copies from rawStore.length - checksum.length. Below 20 bytes both expressions go negative: update clamps its length to zero, and the System.arraycopy gets source index rawStore.length - 20 and throws ArrayIndexOutOfBoundsException. engineLoad (:208) has no catch, so it escapes KeyStore.load, which declares only IOException, NoSuchAlgorithmException and CertificateException.
Lines are origin/main ab16374d37; the class is byte identical at r1rv86 and in 1.87-SNAPSHOT sources, and the jdk1.1 and jdk1.4 copies do the same. AdaptingKeyStoreSpi.engineLoad reaches it through the gate at :150 and the probe at :158. keystore.type.compat=true is stock in JDK 25 (conf/security/java.security:301), so the PKCS12 types take that path as shipped; the FIPS types and PKCS12-PBMAC1 probe regardless.
import java.io.*; import java.security.*; import java.util.HexFormat;
public class JksShort {
static void load(String tag, String type, String prov, int len, char[] pw) {
// magic 0xFEEDFEED + version 1
byte[] s = HexFormat.of().parseHex("feedfeed00000001" + "00".repeat(len - 8));
try { KeyStore.getInstance(type, prov).load(new ByteArrayInputStream(s), pw); }
catch (Throwable t) {
System.out.println(tag + " [" + type + "/" + prov + "] bytes=" + len + " -> "
+ t.getClass().getName() + ": " + t.getMessage() + " declaredByLoad="
+ (t instanceof IOException || t instanceof NoSuchAlgorithmException || t instanceof java.security.cert.CertificateException));
}
}
public static void main(String[] a) {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
String k = "keystore.type.compat";
System.out.println(k + "=" + Security.getProperty(k));
char[] pw = "changeit".toCharArray();
load("8B pw ", "PKCS12", "BC", 8, pw);
load("20B pw ", "PKCS12", "BC", 20, pw);
load("8B null ", "PKCS12", "BC", 8, null);
load("8B pw ", "JKS", "SUN", 8, pw);
}
}
On the 1.86 and 1.87-SNAPSHOT jars, JDK 25:
keystore.type.compat=true
8B pw [PKCS12/BC] bytes=8 -> java.lang.ArrayIndexOutOfBoundsException: arraycopy: source index -12 out of bounds for byte[8] declaredByLoad=false
20B pw [PKCS12/BC] bytes=20 -> java.io.IOException: password incorrect or store tampered with declaredByLoad=true
8B null [PKCS12/BC] bytes=8 -> java.io.EOFException: null declaredByLoad=true
8B pw [JKS/SUN] bytes=8 -> java.io.EOFException: null declaredByLoad=true
A 20-byte store reaches the comparison and the intended IOException; with a null password the same short store gives EOFException, as does SUN's JKS on the same bytes.
Proposed change
A length floor right after the read at :368, throwing that same IOException when rawStore.length is below checksumCalculator.getDigestSize(), would keep this inside the declared contract, with the same change in the other two copies and a truncated-store test. It is a contract defect and no more: the store is rejected either way, only the exception type changes. Happy to put a patch together.
JKSKeyStoreSpi.validateStream(JKSKeyStoreSpi.java:364-392) reads the stream intorawStore(:368); on the password branch (:370) it subtracts the SHA-1 digest size from that length unchecked.:373digestsrawStore.length - checksumCalculator.getDigestSize()bytes and:380copies fromrawStore.length - checksum.length. Below 20 bytes both expressions go negative:updateclamps its length to zero, and theSystem.arraycopygets source indexrawStore.length - 20and throwsArrayIndexOutOfBoundsException.engineLoad(:208) has no catch, so it escapesKeyStore.load, which declares onlyIOException,NoSuchAlgorithmExceptionandCertificateException.Lines are origin/main
ab16374d37; the class is byte identical atr1rv86and in 1.87-SNAPSHOT sources, and thejdk1.1andjdk1.4copies do the same.AdaptingKeyStoreSpi.engineLoadreaches it through the gate at:150and the probe at:158.keystore.type.compat=trueis stock in JDK 25 (conf/security/java.security:301), so the PKCS12 types take that path as shipped; the FIPS types andPKCS12-PBMAC1probe regardless.On the 1.86 and 1.87-SNAPSHOT jars, JDK 25:
A 20-byte store reaches the comparison and the intended
IOException; with a null password the same short store givesEOFException, as does SUN'sJKSon the same bytes.Proposed change
A length floor right after the read at
:368, throwing that sameIOExceptionwhenrawStore.lengthis belowchecksumCalculator.getDigestSize(), would keep this inside the declared contract, with the same change in the other two copies and a truncated-store test. It is a contract defect and no more: the store is rejected either way, only the exception type changes. Happy to put a patch together.