Summary
PKCS12KeyStoreSpi.engineSetKeyEntry is declared to throw only KeyStoreException, but when a certificate in the supplied chain returns null from getPublicKey(), it instead throws an unchecked java.lang.IllegalStateException ("error creating key"). A caller that handles the declared KeyStoreException is bypassed by an unchecked exception.
This is independent of the certificate's algorithm: any certificate whose getPublicKey() returns null triggers it. A null public key occurs when the provider has no key-info converter for the certificate's algorithm; a SNOVA_29_6_5 certificate is one concrete current example (reported separately), but the keystore-side defect is general.
Environment
- bcprov 1.86.0.20694 (current 1.86 beta), main at commit b51452f
- JDK 27
Steps to reproduce
Build a certificate whose getPublicKey() is null (here, via SNOVA_29_6_5, which has no registered converter), then store a private key with that certificate as its chain:
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class Pkcs12SetKeyEntryNpe {
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
Provider bcpqc = (Provider) Class.forName(
"org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider").getDeclaredConstructor().newInstance();
Security.addProvider(bcpqc);
// a certificate whose getPublicKey() returns null (any such cert reproduces this)
KeyPair snova = KeyPairGenerator.getInstance("SNOVA_29_6_5_SSK", bcpqc).generateKeyPair();
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(snova.getPublic().getEncoded());
AlgorithmIdentifier sigAlg = spki.getAlgorithm();
V1TBSCertificateGenerator tbs = new V1TBSCertificateGenerator();
tbs.setSerialNumber(new ASN1Integer(BigInteger.ONE));
tbs.setIssuer(new X500Name("CN=t"));
tbs.setSubject(new X500Name("CN=t"));
tbs.setStartDate(new Time(new Date(System.currentTimeMillis() - 100000)));
tbs.setEndDate(new Time(new Date(System.currentTimeMillis() + 100000)));
tbs.setSignature(sigAlg);
tbs.setSubjectPublicKeyInfo(spki);
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(tbs.generateTBSCertificate());
v.add(sigAlg);
v.add(new DERBitString(new byte[]{0, 0, 0, 0}));
byte[] der = Certificate.getInstance(new DERSequence(v)).getEncoded();
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509", "BC")
.generateCertificate(new ByteArrayInputStream(der)); // cert.getPublicKey() == null
KeyPair rsa = KeyPairGenerator.getInstance("RSA", "BC").generateKeyPair();
KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
ks.load(null, null);
ks.setKeyEntry("a", rsa.getPrivate(), "pw".toCharArray(), new java.security.cert.Certificate[]{cert});
}
}
Actual behaviour
java.lang.IllegalStateException: error creating key
at org.bouncycastle.util.Exceptions.illegalStateException(...)
at org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.createSubjectKeyId(...)
at org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi$CertId.<init>(...)
at org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.engineSetKeyEntry(...)
engineSetKeyEntry is declared throws KeyStoreException only, so this unchecked exception escapes the declared contract.
Expected behaviour
A checked KeyStoreException with a clear message (the declared contract), rather than an unchecked exception.
Root cause
engineSetKeyEntry (PKCS12KeyStoreSpi.java:633, declared throws KeyStoreException at line 638) builds a CertId for each chain certificate at line 676: chainCerts.put(new CertId(chain[i].getPublicKey()), chain[i]). When getPublicKey() is null, the CertId constructor (line 238) calls createSubjectKeyId(key) (line 241), which dereferences the null public key at pubKey.getEncoded() (line 317). The resulting NullPointerException is caught by the surrounding catch (Exception e) and rethrown as Exceptions.illegalStateException("error creating key", e) (line 323), an unchecked exception.
The provider already applies the opposite discipline one layer down: BouncyCastleProvider.getPublicKey catches RuntimeException and rethrows it as a checked IOException, with the comment "a converter must not leak a RuntimeException out of the declared ... contract" (lines 656-660). The keystore boundary does not hold the same line.
The sibling PKCS12PBMAC1KeyStoreSpi (RFC 9579 PBMAC1) carries the same code: its engineSetKeyEntry (line 631) builds new CertId(chain[i].getPublicKey()) (line 673) through its own createSubjectKeyId (line 310), so the same guard is needed there.
Suggested direction
Guard for a null public key in engineSetKeyEntry, before new CertId(chain[i].getPublicKey()) (line 676), and throw a checked KeyStoreException there, matching the plain new KeyStoreException(...) style already used in that method (lines 644, 655, 661). That keeps engineSetKeyEntry within its declared contract for any certificate whose public key cannot be resolved, regardless of algorithm. The guard belongs at this call site rather than inside createSubjectKeyId, which is a shared helper reached from callers that do not declare KeyStoreException. The same fix applies to the sibling PKCS12PBMAC1KeyStoreSpi.
The program above is complete and self-contained; it needs only the BouncyCastle provider jar (bcprov) on the classpath.
Summary
PKCS12KeyStoreSpi.engineSetKeyEntryis declared to throw onlyKeyStoreException, but when a certificate in the supplied chain returnsnullfromgetPublicKey(), it instead throws an uncheckedjava.lang.IllegalStateException("error creating key"). A caller that handles the declaredKeyStoreExceptionis bypassed by an unchecked exception.This is independent of the certificate's algorithm: any certificate whose
getPublicKey()returns null triggers it. A null public key occurs when the provider has no key-info converter for the certificate's algorithm; a SNOVA_29_6_5 certificate is one concrete current example (reported separately), but the keystore-side defect is general.Environment
Steps to reproduce
Build a certificate whose
getPublicKey()is null (here, via SNOVA_29_6_5, which has no registered converter), then store a private key with that certificate as its chain:Actual behaviour
engineSetKeyEntryis declaredthrows KeyStoreExceptiononly, so this unchecked exception escapes the declared contract.Expected behaviour
A checked
KeyStoreExceptionwith a clear message (the declared contract), rather than an unchecked exception.Root cause
engineSetKeyEntry(PKCS12KeyStoreSpi.java:633, declaredthrows KeyStoreExceptionat line 638) builds aCertIdfor each chain certificate at line 676:chainCerts.put(new CertId(chain[i].getPublicKey()), chain[i]). WhengetPublicKey()is null, theCertIdconstructor (line 238) callscreateSubjectKeyId(key)(line 241), which dereferences the null public key atpubKey.getEncoded()(line 317). The resultingNullPointerExceptionis caught by the surroundingcatch (Exception e)and rethrown asExceptions.illegalStateException("error creating key", e)(line 323), an unchecked exception.The provider already applies the opposite discipline one layer down:
BouncyCastleProvider.getPublicKeycatchesRuntimeExceptionand rethrows it as a checkedIOException, with the comment "a converter must not leak a RuntimeException out of the declared ... contract" (lines 656-660). The keystore boundary does not hold the same line.The sibling
PKCS12PBMAC1KeyStoreSpi(RFC 9579 PBMAC1) carries the same code: itsengineSetKeyEntry(line 631) buildsnew CertId(chain[i].getPublicKey())(line 673) through its owncreateSubjectKeyId(line 310), so the same guard is needed there.Suggested direction
Guard for a null public key in
engineSetKeyEntry, beforenew CertId(chain[i].getPublicKey())(line 676), and throw a checkedKeyStoreExceptionthere, matching the plainnew KeyStoreException(...)style already used in that method (lines 644, 655, 661). That keepsengineSetKeyEntrywithin its declared contract for any certificate whose public key cannot be resolved, regardless of algorithm. The guard belongs at this call site rather than insidecreateSubjectKeyId, which is a shared helper reached from callers that do not declareKeyStoreException. The same fix applies to the siblingPKCS12PBMAC1KeyStoreSpi.The program above is complete and self-contained; it needs only the BouncyCastle provider jar (bcprov) on the classpath.