Summary
On 1.86-SNAPSHOT (provider version 1.8599) and current main, the key pairs produced for every composite ML-KEM algorithm (the twelve id-MLKEM768-* / id-MLKEM1024-* OIDs from draft-ietf-lamps-pq-composite-kem, registered as MLKEM768-X25519-SHA3-256, MLKEM1024-ECDH-P384-SHA3-256 and so on) return null from getAlgorithm() on both the public and the private key. java.security.Key.getAlgorithm() is specified to return the algorithm name, so code that keys off it, such as KeyFactory.getInstance(key.getAlgorithm()), fails with a NullPointerException, and an X.509 certificate carrying a composite ML-KEM key reports cert.getPublicKey().getAlgorithm() == null. The composite ML-DSA keys, which use the same key classes, return the expected names.
Reproduction
Security.addProvider(new BouncyCastleProvider());
KeyPair kp = KeyPairGenerator.getInstance("MLKEM768-X25519-SHA3-256", "BC").generateKeyPair();
kp.getPublic().getAlgorithm(); // null
kp.getPrivate().getAlgorithm(); // null
kp.getPublic().getFormat(); // "X.509" (encoding itself is fine)
KeyFactory kf = KeyFactory.getInstance("MLKEM768-X25519-SHA3-256", "BC");
kf.generatePublic(new X509EncodedKeySpec(kp.getPublic().getEncoded())).getAlgorithm(); // null as well
KeyFactory.getInstance(kp.getPublic().getAlgorithm(), "BC"); // NullPointerException: null algorithm name
// contrast
KeyPairGenerator.getInstance("MLDSA65-Ed25519-SHA512", "BC").generateKeyPair()
.getPublic().getAlgorithm(); // "MLDSA65-Ed25519-SHA512"
Checked for all twelve composite KEM algorithms: 12 of 12 null for the generated public key, 12 of 12 for the private key, and 12 of 12 after a KeyFactory round trip through X509EncodedKeySpec / PKCS8EncodedKeySpec. The encodings round-trip byte for byte, so this is only the name. Storing such a private key in a PKCS12, BCFKS or JKS keystore and reading it back works, and the recovered key still reports null.
Root cause
CompositePublicKey and CompositePrivateKey are shared between the composite signature and composite KEM providers, and both resolve the name through the signature side's index:
prov/src/main/java/org/bouncycastle/jcajce/CompositePublicKey.java line 19 imports org.bouncycastle.jcajce.provider.asymmetric.compositesignatures.CompositeIndex, and getAlgorithm() (line 222) is return CompositeIndex.getAlgorithmName(this.algorithmIdentifier.getAlgorithm());
CompositePrivateKey.java line 25 and line 246 do the same.
That getAlgorithmName is a plain map lookup (compositesignatures/CompositeIndex.java lines 126 to 129, return algorithmNames.get(algorithm);) over the composite signature OIDs only, so every composite KEM OID yields null. The KEM side has its own index with the right names, compositekem/CompositeIndex.getAlgorithmName (lines 154 to 157, for example "MLKEM768-X25519-SHA3-256" at line 57), and the KEM KeyPairGeneratorSpi constructs the shared key classes with the KEM OID (lines 103 and 104), so the name is available but never consulted.
Impact
Any caller that dispatches on key.getAlgorithm() or passes it back into a JCA factory, which is the usual way to reconstruct or wrap a key whose type is not known up front, gets a NullPointerException for these keys, and anything that displays or logs the algorithm of a composite KEM certificate shows null. No security impact; the keys encode, decode and decapsulate correctly.
Suggested fix
Have CompositePublicKey.getAlgorithm() and CompositePrivateKey.getAlgorithm() fall back to the KEM index when the signature index has no entry (or look the OID up in both), so the method returns the same name the provider registers the algorithm under. Happy to send a PR if that is useful.
Summary
On 1.86-SNAPSHOT (provider version 1.8599) and current
main, the key pairs produced for every composite ML-KEM algorithm (the twelveid-MLKEM768-*/id-MLKEM1024-*OIDs from draft-ietf-lamps-pq-composite-kem, registered asMLKEM768-X25519-SHA3-256,MLKEM1024-ECDH-P384-SHA3-256and so on) returnnullfromgetAlgorithm()on both the public and the private key.java.security.Key.getAlgorithm()is specified to return the algorithm name, so code that keys off it, such asKeyFactory.getInstance(key.getAlgorithm()), fails with aNullPointerException, and an X.509 certificate carrying a composite ML-KEM key reportscert.getPublicKey().getAlgorithm() == null. The composite ML-DSA keys, which use the same key classes, return the expected names.Reproduction
Checked for all twelve composite KEM algorithms: 12 of 12 null for the generated public key, 12 of 12 for the private key, and 12 of 12 after a KeyFactory round trip through
X509EncodedKeySpec/PKCS8EncodedKeySpec. The encodings round-trip byte for byte, so this is only the name. Storing such a private key in a PKCS12, BCFKS or JKS keystore and reading it back works, and the recovered key still reportsnull.Root cause
CompositePublicKeyandCompositePrivateKeyare shared between the composite signature and composite KEM providers, and both resolve the name through the signature side's index:prov/src/main/java/org/bouncycastle/jcajce/CompositePublicKey.javaline 19 importsorg.bouncycastle.jcajce.provider.asymmetric.compositesignatures.CompositeIndex, andgetAlgorithm()(line 222) isreturn CompositeIndex.getAlgorithmName(this.algorithmIdentifier.getAlgorithm());CompositePrivateKey.javaline 25 and line 246 do the same.That
getAlgorithmNameis a plain map lookup (compositesignatures/CompositeIndex.javalines 126 to 129,return algorithmNames.get(algorithm);) over the composite signature OIDs only, so every composite KEM OID yieldsnull. The KEM side has its own index with the right names,compositekem/CompositeIndex.getAlgorithmName(lines 154 to 157, for example"MLKEM768-X25519-SHA3-256"at line 57), and the KEMKeyPairGeneratorSpiconstructs the shared key classes with the KEM OID (lines 103 and 104), so the name is available but never consulted.Impact
Any caller that dispatches on
key.getAlgorithm()or passes it back into a JCA factory, which is the usual way to reconstruct or wrap a key whose type is not known up front, gets aNullPointerExceptionfor these keys, and anything that displays or logs the algorithm of a composite KEM certificate showsnull. No security impact; the keys encode, decode and decapsulate correctly.Suggested fix
Have
CompositePublicKey.getAlgorithm()andCompositePrivateKey.getAlgorithm()fall back to the KEM index when the signature index has no entry (or look the OID up in both), so the method returns the same name the provider registers the algorithm under. Happy to send a PR if that is useful.