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?
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:
The test produces the following output:
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?