Skip to content

BC ML-DSA PKCS#12 KeyStore interoperability issue starting with JDK 24+ #2467

Description

@bsanchezb

I'm not sure where is the original problem, but it seems like an interoperability issue exists between BouncyCastle (tested with BC v1.85) and JDK (tested with JDK 24 and 26).

The problem occurs on attempt to access the keys from a PKCS#12 KeyStore loaded with the JDK "SUN" provider on the keys created by "BC" provider.

Here is a test to reproduce the error:

private static final String BC = "BC";

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    @Test
    void test() throws Exception {

        // -----------------------------------------------------------------
        // 1. Generate ML-DSA key pair using Bouncy Castle
        // -----------------------------------------------------------------

        KeyPairGenerator keyPairGenerator =
                KeyPairGenerator.getInstance("ML-DSA-65", BC);

        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        PrivateKey originalPrivateKey = keyPair.getPrivate();

        System.out.println("=== Original BC key ===");
        System.out.println("Private key class     : "
                + originalPrivateKey.getClass().getName());
        System.out.println("Private key algorithm : "
                + originalPrivateKey.getAlgorithm());
        System.out.println("Private key format    : "
                + originalPrivateKey.getFormat());

        System.out.println("Public key class      : "
                + keyPair.getPublic().getClass().getName());
        System.out.println("Public key algorithm  : "
                + keyPair.getPublic().getAlgorithm());

        // -----------------------------------------------------------------
        // 2. Create an X.509 certificate using Bouncy Castle
        // -----------------------------------------------------------------

        X500Name subject = new X500Name("CN=ML-DSA Test");

        Date notBefore = new Date();
        Date notAfter = new Date(
                notBefore.getTime() + 365L * 24 * 60 * 60 * 1000);

        JcaX509v3CertificateBuilder certificateBuilder =
                new JcaX509v3CertificateBuilder(
                        subject,
                        BigInteger.valueOf(System.currentTimeMillis()),
                        notBefore,
                        notAfter,
                        subject,
                        keyPair.getPublic());

        ContentSigner contentSigner =
                new JcaContentSignerBuilder("ML-DSA-65")
                        .setProvider(BC)
                        .build(keyPair.getPrivate());

        X509Certificate certificate =
                new JcaX509CertificateConverter()
                        .setProvider(BC)
                        .getCertificate(
                                certificateBuilder.build(contentSigner));

        System.out.println("=== BC certificate ===");
        System.out.println("Public key class      : "
                + certificate.getPublicKey().getClass().getName());
        System.out.println("Public key algorithm  : "
                + certificate.getPublicKey().getAlgorithm());

        // this succeeds  (Expected : ML-DSA-65 ; Actual : ML-DSA-65)
        assertEquals(keyPair.getPrivate().getAlgorithm(), certificate.getPublicKey().getAlgorithm());

        // -----------------------------------------------------------------
        // 3. Store to KeyStore using BouncyCastle
        // -----------------------------------------------------------------

        char[] password = "test".toCharArray();

        KeyStore keyStore = KeyStore.getInstance("PKCS12", BC);

        keyStore.load(null, null);

        keyStore.setKeyEntry(
                "test",
                keyPair.getPrivate(),
                password,
                new Certificate[]{certificate});

        ByteArrayOutputStream output = new ByteArrayOutputStream();

        keyStore.store(output, password);

        byte[] pkcs12 = output.toByteArray();

        // -----------------------------------------------------------------
        // 4. Reload using the JDK SUN implementation
        // -----------------------------------------------------------------

        KeyStore reloaded = KeyStore.getInstance("PKCS12", "SUN");

        reloaded.load(
                new ByteArrayInputStream(pkcs12),
                password);

        // -----------------------------------------------------------------
        // 5. Inspect what the JDK reconstructed
        // -----------------------------------------------------------------

        PrivateKey restoredPrivateKey =
                (PrivateKey) reloaded.getKey("test", password);

        X509Certificate restoredCertificate =
                (X509Certificate) reloaded.getCertificate("test");

        System.out.println("=== After KeyStore reloaded with JDK SUN ===");

        System.out.println("Private key class     : "
                + restoredPrivateKey.getClass().getName());

        System.out.println("Private key algorithm : "
                + restoredPrivateKey.getAlgorithm());

        System.out.println("Private key format    : "
                + restoredPrivateKey.getFormat());

        System.out.println("Public key class      : "
                + restoredCertificate.getPublicKey().getClass().getName());

        System.out.println("Public key algorithm  : "
                + restoredCertificate.getPublicKey().getAlgorithm());

        // This fails (Expected : ML-DSA-65 ; Actual : ML-DSA)
        assertEquals(restoredPrivateKey.getAlgorithm(), restoredCertificate.getPublicKey().getAlgorithm());

    }

The test produces the following output:

=== Original BC key ===
Private key class     : org.bouncycastle.jcajce.provider.asymmetric.mldsa.BCMLDSAPrivateKey
Private key algorithm : ML-DSA-65
Private key format    : PKCS#8
Public key class      : org.bouncycastle.jcajce.provider.asymmetric.mldsa.BCMLDSAPublicKey
Public key algorithm  : ML-DSA-65
=== BC certificate ===
Public key class      : org.bouncycastle.jcajce.provider.asymmetric.mldsa.BCMLDSAPublicKey
Public key algorithm  : ML-DSA-65
=== After KeyStore reloaded with JDK SUN ===
Private key class     : org.bouncycastle.jcajce.provider.asymmetric.mldsa.BCMLDSAPrivateKey
Private key algorithm : ML-DSA-65
Private key format    : PKCS#8
Public key class      : sun.security.x509.NamedX509Key
Public key algorithm  : ML-DSA

And eventually fails in the last assertion, see JDK returns ML-DSA against BC's ML-DSA-65.

When using BC provider on KeyStore re-load, the test succeeds.

What is the issue behind the problem? Can it be BC or JDK bug?

Activity

  1. changed the title [-]BC ML-DSA PKCS#12 KeyStore interoperability issue starting with JDK 25.0.4[/-] [+]BC ML-DSA PKCS#12 KeyStore interoperability issue starting with JDK 24+[/+] on Sep 25, 2026
  2. self-assigned this
    on Sep 26, 2026
  3. dghgit commented on Sep 27, 2026

    @dghgit
    Contributor

    This should now be fixed and up on https://www.bouncycastle.org/betas

    Three caveats:

    1. getAlgorithm() will still fail - we don't use exactly the same convention as the Oracle JCA and never have, you have to compare through through getParams() (which is now implemented).
    2. getParams() objects can't be compared with equals(). NamedParameterSpec doesn't override it, so you have to compare ((NamedParameterSpec)k.getParams()).getName() to get the match.
    3. The Oracle implementation appears to only recognize expanded-only key - this is legitimate, but BC produces what's know as "both" by default - the private key method getPrivateKey(false) will return a private key which produces expanded-only encoding.

    But it should provide a way of dealing with the issue. Let us know how it goes.

  4. bsanchezb commented on Sep 28, 2026

    @bsanchezb
    Author

    Hi @dghgit , thank you for the prompt response.
    I checked the beta builds, but the test still fails. Are the builds up to date?

    In particular, our blocking point is a keystore entry access, e.g. (if you put this line in the end of the unit test, instead of the last failing condition):

    KeyStore.Entry entry = reloaded.getEntry("test", new KeyStore.PasswordProtection(password));
    

    That results to the following exception:

    java.lang.IllegalArgumentException: private key algorithm does not match algorithm of public key in end entity certificate (at index 0)
    
    	at java.base/java.security.KeyStore$PrivateKeyEntry.<init>(KeyStore.java:555)
    	at java.base/sun.security.pkcs12.PKCS12KeyStore.engineGetEntry(PKCS12KeyStore.java:1325)
    	at java.base/sun.security.util.KeyStoreDelegator.engineGetEntry(KeyStoreDelegator.java:172)
    	at java.base/java.security.KeyStore.getEntry(KeyStore.java:1623)
    

    That seems to be originally caused by a different PrivateKey and PublicKey implementation derived from a KeyStore.

  5. dghgit commented on Oct 1, 2026

    @dghgit
    Contributor

    try storing ((MLDSAPrivateKey) priv).getPrivateKey(false) in the keystore. JDK 24/25 should then builds both keys itself, so the names match, my guess is it's falling back to BC when it finds it can't use the JDK provider for reading the BC key.

    I've also rebuilt and uploaded the SNAPSHOT release again (although I'm not sure that was the problem - I've got a feeling we're still chasing the expanded key issue).

  6. dghgit commented on Oct 5, 2026

    @dghgit
    Contributor

    The latest snapshot has some additional compatibility work on the standard JDK PQC implementations, I'm not sure if it relates to this issue as well, but it's worth trying as well. I'd agree this one is a problem, not sure it's a bug, and even if it is, not sure whose either...

    https://www.bouncycastle.org/betas

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions