Skip to content

PKCS12 setKeyEntry throws an unchecked IllegalStateException instead of KeyStoreException when a certificate in the chain has a null public key #2419

Description

@Arpan0995

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions